From fd350f61576ae29aaba3f22e36368f6c609d8c54 Mon Sep 17 00:00:00 2001 From: mnhock Date: Mon, 23 Sep 2024 08:43:54 +0200 Subject: [PATCH] Ensure Required Annotations Based on Presence of Specified Annotation on a Class Closes gb-101 --- docs/USERGUIDE.md | 13 ++++++++++ .../taikai/internal/DescribedPredicates.java | 20 ++++++++++++++ .../enofex/taikai/java/JavaConfigurer.java | 26 +++++++++++++++++++ .../enofex/taikai/java/NamingConfigurer.java | 8 +++++- 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index ec5cf91..482c587 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -111,6 +111,7 @@ The default mode is `WITHOUT_TESTS`, which excludes test classes from the import | General | `classesAnnotatedWithShouldResideInPackage` | Classes annotated with a specific annotation should reside in a specified package. | | General | `classesShouldResideOutsidePackage` | Classes matching specific naming patterns should reside outside a specified package. | | General | `classesShouldBeAnnotatedWith` | Classes matching specific naming patterns should be annotated with a specified annotation. | +| General | `classesShouldBeAnnotatedWithAll` | Classes annotated with a specific annotation should be annotated with a specified annotations. | | General | `classesShouldNotBeAnnotatedWith` | Classes matching specific naming patterns should not be annotated with a specified annotation. | | General | `classesShouldBeAssignableTo` | Classes matching specific naming patterns should be assignable to a certain type. | | General | `classesShouldImplement` | Classes matching specific naming patterns should implement to a interface. | @@ -261,6 +262,18 @@ Taikai.builder() .check(); ``` +- **Classes Annotated with a Specified Annotation Should Be Annotated with Specified Annotations**: Ensure that classes annotated with a specific annotations should be annotated with the specified annotations. + +```java +Taikai.builder() + .namespace("com.company.project") + .java(java -> java + .classesShouldNotBeAnnotatedWithAll(Modifying.class, List.of(Transactional.class, Query.class)) + .classesShouldNotBeAnnotatedWithAll("org.springframework.data.jpa.repository.Modifying", List.of("org.springframework.transaction.annotation.Transactional", "org.springframework.data.jpa.repository.Query")) + .build() + .check(); +``` + - **Classes Should Not Be Annotated with Specified Annotation**: Ensure that classes matching a specific regex pattern are not annotated with the specified annotation. ```java diff --git a/src/main/java/com/enofex/taikai/internal/DescribedPredicates.java b/src/main/java/com/enofex/taikai/internal/DescribedPredicates.java index 03f5422..89a89ee 100644 --- a/src/main/java/com/enofex/taikai/internal/DescribedPredicates.java +++ b/src/main/java/com/enofex/taikai/internal/DescribedPredicates.java @@ -5,6 +5,7 @@ import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import java.util.Collection; /** * Internal utility class for defining general DescribedPredicate used in architectural rules. @@ -35,6 +36,25 @@ public boolean test(CanBeAnnotated canBeAnnotated) { }; } + /** + * Creates a predicate that checks if an element is annotated with all the specified annotations. + * + * @param annotations the collection of annotations to check for + * @param isMetaAnnotated true if the annotations should be meta-annotated, false otherwise + * @return a described predicate for the annotation check + */ + public static DescribedPredicate annotatedWithAll(Collection annotations, + boolean isMetaAnnotated) { + return new DescribedPredicate<>("annotated with all of %s".formatted(annotations)) { + @Override + public boolean test(CanBeAnnotated canBeAnnotated) { + return annotations.stream().allMatch(annotation -> + isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) + : canBeAnnotated.isAnnotatedWith(annotation)); + } + }; + } + /** * Creates a predicate that checks if a class is final. * diff --git a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java index fd9b6f7..47399a9 100644 --- a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java @@ -2,6 +2,7 @@ import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; import static com.enofex.taikai.internal.ArchConditions.notBePublicButNotStatic; +import static com.enofex.taikai.internal.DescribedPredicates.annotatedWithAll; import static com.enofex.taikai.internal.DescribedPredicates.areFinal; import static com.enofex.taikai.java.Deprecations.notUseDeprecatedAPIs; import static com.enofex.taikai.java.HashCodeAndEquals.implementHashCodeAndEquals; @@ -11,6 +12,7 @@ import static com.enofex.taikai.java.SerialVersionUID.namedSerialVersionUID; import static com.enofex.taikai.java.UtilityClasses.havePrivateConstructor; import static com.enofex.taikai.java.UtilityClasses.utilityClasses; +import static com.tngtech.archunit.lang.conditions.ArchConditions.be; import static com.tngtech.archunit.lang.conditions.ArchConditions.beFinal; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; @@ -24,6 +26,7 @@ import com.enofex.taikai.configures.Customizer; import com.enofex.taikai.configures.DisableableConfigurer; import java.lang.annotation.Annotation; +import java.util.Collection; public class JavaConfigurer extends AbstractConfigurer { @@ -113,6 +116,29 @@ public JavaConfigurer classesShouldResideOutsidePackage(String regex, String pac regex, packageIdentifier)), configuration)); } + + public JavaConfigurer classesShouldBeAnnotatedWithAll(Class annotationType, + Collection> requiredAnnotationTypes) { + return classesShouldBeAnnotatedWithAll(annotationType.getName(), + requiredAnnotationTypes.stream().map(Class::getName).toList(), defaultConfiguration()); + } + + public JavaConfigurer classesShouldBeAnnotatedWithAll(String annotationType, + Collection requiredAnnotationTypes) { + return classesShouldBeAnnotatedWithAll(annotationType, requiredAnnotationTypes, + defaultConfiguration()); + } + + public JavaConfigurer classesShouldBeAnnotatedWithAll(String annotationType, + Collection requiredAnnotationTypes, Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that().areMetaAnnotatedWith(annotationType) + .should(be(annotatedWithAll(requiredAnnotationTypes, true))) + .as("Classes annotated with %s should be annotated with %s".formatted( + annotationType, String.join(", ", requiredAnnotationTypes))), + configuration)); + } + public JavaConfigurer classesShouldBeAnnotatedWith(String regex, Class annotationType) { return classesShouldBeAnnotatedWith(regex, annotationType.getName(), defaultConfiguration()); diff --git a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java index 57370ee..22648ff 100644 --- a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java @@ -30,7 +30,8 @@ public NamingConfigurer packagesShouldMatch(String packageIdentifier) { return packagesShouldMatch(packageIdentifier, defaultConfiguration()); } - public NamingConfigurer packagesShouldMatch(String packageIdentifier, Configuration configuration) { + public NamingConfigurer packagesShouldMatch(String packageIdentifier, + Configuration configuration) { return addRule(TaikaiRule.of(classes() .should().resideInAPackage(packageIdentifier) .as("Package names should match %s".formatted(packageIdentifier)), @@ -52,6 +53,11 @@ public NamingConfigurer classesAnnotatedWithShouldMatch( return classesAnnotatedWithShouldMatch(annotationType.getName(), regex, defaultConfiguration()); } + public NamingConfigurer classesAnnotatedWithShouldMatch( + Class annotationType, String regex, Configuration configuration) { + return classesAnnotatedWithShouldMatch(annotationType.getName(), regex, configuration); + } + public NamingConfigurer classesAnnotatedWithShouldMatch(String annotationType, String regex) { return classesAnnotatedWithShouldMatch(annotationType, regex, defaultConfiguration()); }