From 8fbaace76960fe43a82c3c3c3e5e659416e2fd7c Mon Sep 17 00:00:00 2001 From: mnhock Date: Sun, 16 Jun 2024 19:58:55 +0200 Subject: [PATCH] Provide a new Java Rule checking if final classes do not have protected members Closes gh-31 --- docs/USERGUIDE.md | 94 +++++++++++-------- .../enofex/taikai/AnnotationPredicates.java | 21 ----- .../com/enofex/taikai/JavaPredicates.java | 77 +++++++++++++++ .../enofex/taikai/java/ConstantNaming.java | 31 ++++++ .../taikai/java/FieldsShouldNotBePublic.java | 28 ------ .../enofex/taikai/java/JavaConfigurer.java | 17 +++- .../enofex/taikai/java/NamingConfigurer.java | 21 +---- .../enofex/taikai/java/ProtectedMembers.java | 37 ++++++++ .../enofex/taikai/java/UtilityClasses.java | 11 --- .../taikai/spring/SpringPredicates.java | 2 +- .../enofex/taikai/test/JUnit5Configurer.java | 18 +--- .../enofex/taikai/test/JUnit5Predicates.java | 2 +- .../com/enofex/taikai/ArchitectureTest.java | 1 + src/test/java/com/enofex/taikai/Usage.java | 6 +- 14 files changed, 223 insertions(+), 143 deletions(-) delete mode 100644 src/main/java/com/enofex/taikai/AnnotationPredicates.java create mode 100644 src/main/java/com/enofex/taikai/JavaPredicates.java create mode 100644 src/main/java/com/enofex/taikai/java/ConstantNaming.java delete mode 100644 src/main/java/com/enofex/taikai/java/FieldsShouldNotBePublic.java create mode 100644 src/main/java/com/enofex/taikai/java/ProtectedMembers.java diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index f246afb..716ef6f 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -19,47 +19,48 @@ Architecture rules are defined using Taikai's fluent API, allowing developers to ## 3. Usage -| Category | Subcategory | Method Name | Rule Description | Import Options | -|------------|----------------|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|-------------------------| -| **Java** | General | `classesShouldImplementHashCodeAndEquals` | Classes should implement `hashCode` and `equals` | Default (WITHOUT_TESTS) | -| | General | `fieldsShouldNotBePublic` | Fields should not be `public` (except constants) | Default (WITHOUT_TESTS) | -| | General | `methodsShouldNotThrowGenericException` | Methods should not throw generic exceptions (`Exception`, `RuntimeException`) | Default (WITHOUT_TESTS) | -| | General | `noUsageOf` | Disallow usage of specific classes | Default (WITHOUT_TESTS) | -| | General | `noUsageOf` | Disallow usage of specific classes by class reference | Default (WITHOUT_TESTS) | -| | General | `noUsageOfDeprecatedAPIs` | No usage of deprecated APIs annotated with `Deprecated` | Default (WITHOUT_TESTS) | -| | General | `noUsageOfSystemOutOrErr` | Disallow usage of `System.out` or `System.err` | Default (WITHOUT_TESTS) | -| | General | `utilityClassesShouldBeFinalAndHavePrivateConstructor` | Utility classes should be `final` and have a private constructor | Default (WITHOUT_TESTS) | -| | Imports | `shouldHaveNoCycles` | No cyclic dependencies in imports | Default (WITHOUT_TESTS) | -| | Imports | `shouldNotImport` | Disallow specific imports (e.g., `..shaded..`) | Default (WITHOUT_TESTS) | -| | Naming | `classesShouldNotMatch` | Classes should not match specific naming patterns (e.g., `.*Impl`) | Default (WITHOUT_TESTS) | -| | Naming | `methodsShouldNotMatch` | Methods should not match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `fieldsShouldNotMatch` | Fields should not match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `classesAnnotatedWithShouldMatch` | Classes annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `methodsAnnotatedWithShouldMatch` | Methods annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `fieldsAnnotatedWithShouldMatch` | Fields annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `constantsShouldFollowConvention` | Constants should follow naming conventions | Default (WITHOUT_TESTS) | -| | Naming | `interfacesShouldNotHavePrefixI` | Interfaces should not have the prefix `I` | Default (WITHOUT_TESTS) | -| **Test** | JUnit 5 | `classesShouldNotBeAnnotatedWithDisabled` | Ensure classes are not annotated with `@Disabled` | Default (ONLY_TESTS) | -| | JUnit 5 | `methodsShouldNotBeAnnotatedWithDisabled` | Ensure methods are not annotated with `@Disabled` | Default (ONLY_TESTS) | -| | JUnit 5 | `methodsShouldBePackagePrivate` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are package-private. | Default (ONLY_TESTS) | -| | JUnit 5 | `methodsShouldBeAnnotatedWithDisplayName` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are annotated with `@DisplayName`. | Default (ONLY_TESTS) | -| | JUnit 5 | `methodsShouldMatch` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` have names matching a specific regex pattern. | Default (ONLY_TESTS) | -| | JUnit 5 | `methodsShouldNotDeclareThrownExceptions` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` do not declare any thrown exceptions. | Default (ONLY_TESTS) | -| **Spring** | General | `noAutowiredFields` | Fields should not be annotated with `@Autowired` (prefer constructor injection) | Default (WITHOUT_TESTS) | -| | Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | Default (WITHOUT_TESTS) | -| | Configurations | `namesShouldEndWithConfiguration` | Configuration classes should end with "Configuration" | Default (WITHOUT_TESTS) | -| | Configurations | `namesShouldMatch` | Configuration classes should match a regex pattern | Default (WITHOUT_TESTS) | -| | Controllers | `namesShouldEndWithController` | Controllers should end with "Controller" | Default (WITHOUT_TESTS) | -| | Controllers | `namesShouldMatch` | Controllers should match a regex pattern | Default (WITHOUT_TESTS) | -| | Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | Default (WITHOUT_TESTS) | -| | Controllers | `shouldBePackagePrivate` | Controllers should be package-private | Default (WITHOUT_TESTS) | -| | Controllers | `shouldNotDependOnOtherControllers` | Controllers should not depend on other controllers | Default (WITHOUT_TESTS) | -| | Repositories | `namesShouldEndWithRepository` | Repositories should end with "Repository" | Default (WITHOUT_TESTS) | -| | Repositories | `namesShouldMatch` | Repositories should match a regex pattern | Default (WITHOUT_TESTS) | -| | Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | Default (WITHOUT_TESTS) | -| | Services | `namesShouldEndWithService` | Services should end with "Service" | Default (WITHOUT_TESTS) | -| | Services | `namesShouldMatch` | Services should match a regex pattern | Default (WITHOUT_TESTS) | -| | Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | Default (WITHOUT_TESTS) | +| Category | Subcategory | Method Name | Rule Description | Import Options | +|------------|----------------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-------------------------| +| **Java** | General | `classesShouldImplementHashCodeAndEquals` | Classes should implement `hashCode` and `equals` | Default (WITHOUT_TESTS) | +| | General | `fieldsShouldNotBePublic` | Fields should not be `public` (except constants) | Default (WITHOUT_TESTS) | +| | General | `methodsShouldNotThrowGenericException` | Methods should not throw generic exceptions (`Exception`, `RuntimeException`) | Default (WITHOUT_TESTS) | +| | General | `noUsageOf` | Disallow usage of specific classes | Default (WITHOUT_TESTS) | +| | General | `noUsageOf` | Disallow usage of specific classes by class reference | Default (WITHOUT_TESTS) | +| | General | `noUsageOfDeprecatedAPIs` | No usage of deprecated APIs annotated with `Deprecated` | Default (WITHOUT_TESTS) | +| | General | `noUsageOfSystemOutOrErr` | Disallow usage of `System.out` or `System.err` | Default (WITHOUT_TESTS) | +| | General | `utilityClassesShouldBeFinalAndHavePrivateConstructor` | Utility classes should be `final` and have a private constructor | Default (WITHOUT_TESTS) | +| | General | `finalClassesShouldNotHaveProtectedMembers` | Ensures that classes declared as `final` do not contain any `protected` members | Default (WITHOUT_TESTS) | +| | Imports | `shouldHaveNoCycles` | No cyclic dependencies in imports | Default (WITHOUT_TESTS) | +| | Imports | `shouldNotImport` | Disallow specific imports (e.g., `..shaded..`) | Default (WITHOUT_TESTS) | +| | Naming | `classesShouldNotMatch` | Classes should not match specific naming patterns (e.g., `.*Impl`) | Default (WITHOUT_TESTS) | +| | Naming | `methodsShouldNotMatch` | Methods should not match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `fieldsShouldNotMatch` | Fields should not match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `classesAnnotatedWithShouldMatch` | Classes annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `methodsAnnotatedWithShouldMatch` | Methods annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `fieldsAnnotatedWithShouldMatch` | Fields annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `constantsShouldFollowConvention` | Constants should follow naming conventions | Default (WITHOUT_TESTS) | +| | Naming | `interfacesShouldNotHavePrefixI` | Interfaces should not have the prefix `I` | Default (WITHOUT_TESTS) | +| **Test** | JUnit 5 | `classesShouldNotBeAnnotatedWithDisabled` | Ensure classes are not annotated with `@Disabled` | Default (ONLY_TESTS) | +| | JUnit 5 | `methodsShouldNotBeAnnotatedWithDisabled` | Ensure methods are not annotated with `@Disabled` | Default (ONLY_TESTS) | +| | JUnit 5 | `methodsShouldBePackagePrivate` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are package-private. | Default (ONLY_TESTS) | +| | JUnit 5 | `methodsShouldBeAnnotatedWithDisplayName` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are annotated with `@DisplayName`. | Default (ONLY_TESTS) | +| | JUnit 5 | `methodsShouldMatch` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` have names matching a specific regex pattern. | Default (ONLY_TESTS) | +| | JUnit 5 | `methodsShouldNotDeclareThrownExceptions` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` do not declare any thrown exceptions. | Default (ONLY_TESTS) | +| **Spring** | General | `noAutowiredFields` | Fields should not be annotated with `@Autowired` (prefer constructor injection) | Default (WITHOUT_TESTS) | +| | Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | Default (WITHOUT_TESTS) | +| | Configurations | `namesShouldEndWithConfiguration` | Configuration classes should end with "Configuration" | Default (WITHOUT_TESTS) | +| | Configurations | `namesShouldMatch` | Configuration classes should match a regex pattern | Default (WITHOUT_TESTS) | +| | Controllers | `namesShouldEndWithController` | Controllers should end with "Controller" | Default (WITHOUT_TESTS) | +| | Controllers | `namesShouldMatch` | Controllers should match a regex pattern | Default (WITHOUT_TESTS) | +| | Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | Default (WITHOUT_TESTS) | +| | Controllers | `shouldBePackagePrivate` | Controllers should be package-private | Default (WITHOUT_TESTS) | +| | Controllers | `shouldNotDependOnOtherControllers` | Controllers should not depend on other controllers | Default (WITHOUT_TESTS) | +| | Repositories | `namesShouldEndWithRepository` | Repositories should end with "Repository" | Default (WITHOUT_TESTS) | +| | Repositories | `namesShouldMatch` | Repositories should match a regex pattern | Default (WITHOUT_TESTS) | +| | Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | Default (WITHOUT_TESTS) | +| | Services | `namesShouldEndWithService` | Services should end with "Service" | Default (WITHOUT_TESTS) | +| | Services | `namesShouldMatch` | Services should match a regex pattern | Default (WITHOUT_TESTS) | +| | Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | Default (WITHOUT_TESTS) | ### Java Configuration @@ -186,6 +187,17 @@ Taikai.builder() .check(); ``` +- **Ensure Final Classes Do Not Have Protected Members**: Ensures that classes declared as `final` do not contain any `protected` members. Since `final` classes cannot be subclassed, having `protected` members is unnecessary. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .java(java -> java + .finalClassesShouldNotHaveProtectedMembers()) + .build() + .check(); +``` + ### Test Configuration Test configuration involves specifying constraints related to testing frameworks and practices. diff --git a/src/main/java/com/enofex/taikai/AnnotationPredicates.java b/src/main/java/com/enofex/taikai/AnnotationPredicates.java deleted file mode 100644 index ad1d83c..0000000 --- a/src/main/java/com/enofex/taikai/AnnotationPredicates.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.enofex.taikai; - -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; - -public final class AnnotationPredicates { - - private AnnotationPredicates() { - } - - public static DescribedPredicate annotatedWith(String annotation, - boolean isMetaAnnotated) { - return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { - @Override - public boolean test(CanBeAnnotated canBeAnnotated) { - return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) : - canBeAnnotated.isAnnotatedWith(annotation); - } - }; - } -} diff --git a/src/main/java/com/enofex/taikai/JavaPredicates.java b/src/main/java/com/enofex/taikai/JavaPredicates.java new file mode 100644 index 0000000..1f45542 --- /dev/null +++ b/src/main/java/com/enofex/taikai/JavaPredicates.java @@ -0,0 +1,77 @@ +package com.enofex.taikai; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +public final class JavaPredicates { + + private JavaPredicates() { + } + + public static DescribedPredicate annotatedWith(String annotation, + boolean isMetaAnnotated) { + return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { + @Override + public boolean test(CanBeAnnotated canBeAnnotated) { + return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) : + canBeAnnotated.isAnnotatedWith(annotation); + } + }; + } + + public static ArchCondition notDeclareThrownExceptions() { + return new ArchCondition<>("not declare thrown exceptions") { + @Override + public void check(JavaMethod method, ConditionEvents events) { + if (!method.getThrowsClause().isEmpty()) { + String message = String.format("Method %s declares thrown exceptions", + method.getFullName()); + events.add(SimpleConditionEvent.violated(method, message)); + } + } + }; + } + + public static ArchCondition notBePublic() { + return new ArchCondition<>("not be public") { + @Override + public void check(JavaField field, ConditionEvents events) { + if (!field.getModifiers().contains(JavaModifier.STATIC) + && field.getModifiers().contains(JavaModifier.PUBLIC)) { + String message = String.format("Field %s in class %s is public", + field.getName(), + field.getOwner().getFullName()); + events.add(SimpleConditionEvent.violated(field, message)); + } + } + }; + } + + public static DescribedPredicate areFinal() { + return new DescribedPredicate<>("are final") { + @Override + public boolean test(JavaClass javaClass) { + return javaClass.getModifiers().contains(JavaModifier.FINAL); + } + }; + } + + + public static ArchCondition beFinal() { + return new ArchCondition<>("be final") { + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + boolean isFinal = javaClass.getModifiers().contains(JavaModifier.FINAL); + String message = String.format("Class %s is not final", javaClass.getName()); + events.add(new SimpleConditionEvent(javaClass, isFinal, message)); + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/java/ConstantNaming.java b/src/main/java/com/enofex/taikai/java/ConstantNaming.java new file mode 100644 index 0000000..96cf4ec --- /dev/null +++ b/src/main/java/com/enofex/taikai/java/ConstantNaming.java @@ -0,0 +1,31 @@ +package com.enofex.taikai.java; + +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import java.util.regex.Pattern; + +final class ConstantNaming { + + private ConstantNaming() { + } + + static ArchCondition shouldFollowConstantNamingConvention() { + return new ArchCondition<>("follow constant naming convention") { + + private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]*$"); + + @Override + public void check(JavaField field, ConditionEvents events) { + if (!field.getOwner().isEnum() && !CONSTANT_NAME_PATTERN.matcher(field.getName()) + .matches()) { + String message = String.format( + "Constant %s in class %s does not follow the naming convention", field.getName(), + field.getOwner().getName()); + events.add(SimpleConditionEvent.violated(field, message)); + } + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/java/FieldsShouldNotBePublic.java b/src/main/java/com/enofex/taikai/java/FieldsShouldNotBePublic.java deleted file mode 100644 index 3fdcb1b..0000000 --- a/src/main/java/com/enofex/taikai/java/FieldsShouldNotBePublic.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.enofex.taikai.java; - -import com.tngtech.archunit.core.domain.JavaField; -import com.tngtech.archunit.core.domain.JavaModifier; -import com.tngtech.archunit.lang.ArchCondition; -import com.tngtech.archunit.lang.ConditionEvents; -import com.tngtech.archunit.lang.SimpleConditionEvent; - -final class FieldsShouldNotBePublic { - - private FieldsShouldNotBePublic() { - } - - static ArchCondition notBePublic() { - return new ArchCondition<>("not be public") { - @Override - public void check(JavaField field, ConditionEvents events) { - if (!field.getModifiers().contains(JavaModifier.STATIC) - && field.getModifiers().contains(JavaModifier.PUBLIC)) { - String message = String.format("Field %s in class %s is public", - field.getName(), - field.getOwner().getFullName()); - events.add(SimpleConditionEvent.violated(field, message)); - } - } - }; - } -} diff --git a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java index bbed6fc..559a1b1 100644 --- a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java @@ -1,10 +1,12 @@ package com.enofex.taikai.java; +import static com.enofex.taikai.JavaPredicates.areFinal; +import static com.enofex.taikai.JavaPredicates.beFinal; +import static com.enofex.taikai.JavaPredicates.notBePublic; import static com.enofex.taikai.java.Deprecations.notUseDeprecatedAPIs; -import static com.enofex.taikai.java.FieldsShouldNotBePublic.notBePublic; import static com.enofex.taikai.java.HashCodeAndEquals.implementHashCodeAndEquals; import static com.enofex.taikai.java.NoSystemOutOrErr.notUseSystemOutOrErr; -import static com.enofex.taikai.java.UtilityClasses.beFinal; +import static com.enofex.taikai.java.ProtectedMembers.notHaveProtectedMembers; import static com.enofex.taikai.java.UtilityClasses.havePrivateConstructor; import static com.enofex.taikai.java.UtilityClasses.utilityClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -104,6 +106,17 @@ public JavaConfigurer noUsageOfSystemOutOrErr(Configuration configuration) { return addRule(TaikaiRule.of(classes().should(notUseSystemOutOrErr()), configuration)); } + public JavaConfigurer finalClassesShouldNotHaveProtectedMembers() { + return finalClassesShouldNotHaveProtectedMembers(null); + } + + public JavaConfigurer finalClassesShouldNotHaveProtectedMembers(Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that(areFinal()) + .should(notHaveProtectedMembers()) + .as("Final classes should not have protected members"), configuration)); + } + @Override public void disable() { disable(ImportsConfigurer.class); diff --git a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java index b3c23f2..446f693 100644 --- a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java @@ -1,5 +1,6 @@ package com.enofex.taikai.java; +import static com.enofex.taikai.java.ConstantNaming.shouldFollowConstantNamingConvention; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; @@ -12,12 +13,10 @@ import com.enofex.taikai.configures.AbstractConfigurer; import com.enofex.taikai.configures.ConfigurerContext; import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaField; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import java.lang.annotation.Annotation; -import java.util.regex.Pattern; public final class NamingConfigurer extends AbstractConfigurer { @@ -160,22 +159,4 @@ public NamingConfigurer constantsShouldFollowConvention(Configuration configurat fields().that().areFinal().and().areStatic().should(shouldFollowConstantNamingConvention()) .as("Constants should follow constant naming convention"), configuration)); } - - private static ArchCondition shouldFollowConstantNamingConvention() { - return new ArchCondition<>("follow constant naming convention") { - - private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]*$"); - - @Override - public void check(JavaField field, ConditionEvents events) { - if (!field.getOwner().isEnum() && !CONSTANT_NAME_PATTERN.matcher(field.getName()) - .matches()) { - String message = String.format( - "Constant %s in class %s does not follow the naming convention", field.getName(), - field.getOwner().getName()); - events.add(SimpleConditionEvent.violated(field, message)); - } - } - }; - } } diff --git a/src/main/java/com/enofex/taikai/java/ProtectedMembers.java b/src/main/java/com/enofex/taikai/java/ProtectedMembers.java new file mode 100644 index 0000000..6e43dff --- /dev/null +++ b/src/main/java/com/enofex/taikai/java/ProtectedMembers.java @@ -0,0 +1,37 @@ +package com.enofex.taikai.java; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +final class ProtectedMembers { + + private ProtectedMembers() { + } + + static ArchCondition notHaveProtectedMembers() { + return new ArchCondition<>("not have protected members") { + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + for (JavaField field : javaClass.getFields()) { + if (field.getModifiers().contains(JavaModifier.PROTECTED)) { + String message = String.format("Field %s in final class %s is protected", + field.getName(), javaClass.getName()); + events.add(SimpleConditionEvent.violated(field, message)); + } + } + for (JavaMethod method : javaClass.getMethods()) { + if (method.getModifiers().contains(JavaModifier.PROTECTED)) { + String message = String.format("Method %s in final class %s is protected", + method.getName(), javaClass.getName()); + events.add(SimpleConditionEvent.violated(method, message)); + } + } + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/java/UtilityClasses.java b/src/main/java/com/enofex/taikai/java/UtilityClasses.java index 13c0358..8d2db67 100644 --- a/src/main/java/com/enofex/taikai/java/UtilityClasses.java +++ b/src/main/java/com/enofex/taikai/java/UtilityClasses.java @@ -30,17 +30,6 @@ public boolean test(JavaClass javaClass) { }; } - static ArchCondition beFinal() { - return new ArchCondition<>("be final") { - @Override - public void check(JavaClass javaClass, ConditionEvents events) { - boolean isFinal = javaClass.getModifiers().contains(JavaModifier.FINAL); - String message = String.format("Class %s is not final", javaClass.getName()); - events.add(new SimpleConditionEvent(javaClass, isFinal, message)); - } - }; - } - static ArchCondition havePrivateConstructor() { return new ArchCondition<>("have a private constructor") { @Override diff --git a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java index 6382f11..8bef12e 100644 --- a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java +++ b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java @@ -1,6 +1,6 @@ package com.enofex.taikai.spring; -import static com.enofex.taikai.AnnotationPredicates.annotatedWith; +import static com.enofex.taikai.JavaPredicates.annotatedWith; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java index b0e907c..b346e3e 100644 --- a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java +++ b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java @@ -1,5 +1,6 @@ package com.enofex.taikai.test; +import static com.enofex.taikai.JavaPredicates.notDeclareThrownExceptions; import static com.enofex.taikai.test.JUnit5Predicates.ANNOTATION_DISABLED; import static com.enofex.taikai.test.JUnit5Predicates.ANNOTATION_DISPLAY_NAME; import static com.enofex.taikai.test.JUnit5Predicates.ANNOTATION_PARAMETRIZED_TEST; @@ -15,10 +16,6 @@ import com.enofex.taikai.TaikaiRule.Configuration; import com.enofex.taikai.configures.AbstractConfigurer; import com.enofex.taikai.configures.ConfigurerContext; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.lang.ArchCondition; -import com.tngtech.archunit.lang.ConditionEvents; -import com.tngtech.archunit.lang.SimpleConditionEvent; public final class JUnit5Configurer extends AbstractConfigurer { @@ -101,17 +98,4 @@ public JUnit5Configurer classesShouldNotBeAnnotatedWithDisabled(Configuration co .as("Classes should not be annotated with %s".formatted(ANNOTATION_DISABLED)), configuration)); } - - private ArchCondition notDeclareThrownExceptions() { - return new ArchCondition<>("not declare thrown exceptions") { - @Override - public void check(JavaMethod method, ConditionEvents events) { - if (!method.getThrowsClause().isEmpty()) { - String message = String.format("Method %s declares thrown exceptions", - method.getFullName()); - events.add(SimpleConditionEvent.violated(method, message)); - } - } - }; - } } \ No newline at end of file diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java b/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java index 91eda74..173ae9e 100644 --- a/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java +++ b/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java @@ -1,6 +1,6 @@ package com.enofex.taikai.test; -import static com.enofex.taikai.AnnotationPredicates.annotatedWith; +import static com.enofex.taikai.JavaPredicates.annotatedWith; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index cc80b73..0dec1bd 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -23,6 +23,7 @@ void shouldFulfilConstrains() { .methodsShouldNotBeAnnotatedWithDisabled())) .java(java -> java .noUsageOfDeprecatedAPIs() + .finalClassesShouldNotHaveProtectedMembers() .classesShouldImplementHashCodeAndEquals() .methodsShouldNotThrowGenericException() .utilityClassesShouldBeFinalAndHavePrivateConstructor() diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index 7622b26..7d9ecb4 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -3,7 +3,10 @@ import java.util.Calendar; import java.util.Date; -class Usage { +final class Usage { + + private Usage() { + } public static void main(String[] args) { Taikai taikai = Taikai.builder() @@ -45,6 +48,7 @@ public static void main(String[] args) { .noUsageOfDeprecatedAPIs() .classesShouldImplementHashCodeAndEquals() .methodsShouldNotThrowGenericException() + .finalClassesShouldNotHaveProtectedMembers() .utilityClassesShouldBeFinalAndHavePrivateConstructor() .imports(imports -> imports .shouldHaveNoCycles()