From 14b19049a542042614aa6576673c378fa737b5e1 Mon Sep 17 00:00:00 2001 From: mnhock Date: Sat, 5 Oct 2024 11:17:27 +0200 Subject: [PATCH] Add New Rule for Fields Matching Specific Naming Patterns to Have Specific Modifier Closes gh-113 --- docs/USERGUIDE.md | 13 ++++++++++ .../taikai/internal/ArchConditions.java | 25 +++++++++++++++++++ .../enofex/taikai/java/JavaConfigurer.java | 20 +++++++++++++++ .../com/enofex/taikai/ArchitectureTest.java | 5 ++++ src/test/java/com/enofex/taikai/Usage.java | 6 +++++ 5 files changed, 69 insertions(+) diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index af4309d..d3138f0 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -116,6 +116,7 @@ The default mode is `WITHOUT_TESTS`, which excludes test classes from the import | 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. | +| General | `fieldsShouldHaveModifiers` | Fields matching specific naming patterns should have specified modifiers. | | General | `fieldsShouldNotBePublic` | Fields should not be `public`, except constants. | | General | `methodsShouldNotDeclareGenericExceptions` | Methods should not declare generic exceptions, like `Exception` or `RuntimeException`. | | General | `methodsShouldNotDeclareException` | Methods with names matching a specified pattern should not declare a specified exception type. | @@ -372,6 +373,18 @@ Taikai.builder() .check(); ``` + +- **Fields Should Have Modifiers**: Ensure that fields matching a specific naming pattern have the required modifiers. + +```java +Taikai.builder() + .namespace("com.enofex.taikai") + .java(java -> java + .fieldsShouldHaveModifiers("^[A-Z][A-Z0-9_]*$", List.of(STATIC, FINAL))) + .build() + .check(); +``` + - **Fields Should Not Be Public**: Ensure that no fields in your Java classes are declared as `public`, except constants. ```java diff --git a/src/main/java/com/enofex/taikai/internal/ArchConditions.java b/src/main/java/com/enofex/taikai/internal/ArchConditions.java index ff16401..1407f83 100644 --- a/src/main/java/com/enofex/taikai/internal/ArchConditions.java +++ b/src/main/java/com/enofex/taikai/internal/ArchConditions.java @@ -6,9 +6,12 @@ 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; +import java.util.Collection; +import java.util.stream.Collectors; /** * Internal utility class for defining general ArchCondition used in architectural rules. @@ -80,4 +83,26 @@ public void check(JavaClass item, ConditionEvents events) { } }; } + + /** + * Creates a condition that checks if a field contains all the specified modifiers. + * + * @param requiredModifiers the collection of modifiers that the field is required to have + * @return an architectural condition for checking if a field has the required modifiers + */ + public static ArchCondition hasFieldModifiers( + Collection requiredModifiers) { + return new ArchCondition<>("has field modifiers") { + @Override + public void check(JavaField field, ConditionEvents events) { + if (!field.getModifiers().containsAll(requiredModifiers)) { + events.add(SimpleConditionEvent.violated(field, + "Field %s in class %s is missing one of this %s modifier".formatted( + field.getName(), + field.getOwner().getFullName(), + requiredModifiers.stream().map(Enum::name).collect(Collectors.joining(", "))))); + } + } + }; + } } diff --git a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java index cc987d9..8d06be9 100644 --- a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java @@ -1,6 +1,7 @@ package com.enofex.taikai.java; import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; +import static com.enofex.taikai.internal.ArchConditions.hasFieldModifiers; import static com.enofex.taikai.internal.ArchConditions.notBePublicButNotStatic; import static com.enofex.taikai.internal.DescribedPredicates.annotatedWithAll; import static com.enofex.taikai.internal.DescribedPredicates.areFinal; @@ -25,8 +26,10 @@ import com.enofex.taikai.configures.ConfigurerContext; import com.enofex.taikai.configures.Customizer; import com.enofex.taikai.configures.DisableableConfigurer; +import com.tngtech.archunit.core.domain.JavaModifier; import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.stream.Collectors; public class JavaConfigurer extends AbstractConfigurer { @@ -327,6 +330,23 @@ public JavaConfigurer fieldsShouldNotBePublic(Configuration configuration) { .should(notBePublicButNotStatic()), configuration)); } + public JavaConfigurer fieldsShouldHaveModifiers(String regex, + Collection requiredModifiers) { + return fieldsShouldHaveModifiers(regex, requiredModifiers, defaultConfiguration()); + } + + public JavaConfigurer fieldsShouldHaveModifiers(String regex, + Collection requiredModifiers, + Configuration configuration) { + return addRule(TaikaiRule.of(fields() + .that().haveNameMatching(regex) + .should(hasFieldModifiers(requiredModifiers)) + .as("Fields have name matching %s should have all of this modifiers %s".formatted( + regex, + requiredModifiers.stream().map(Enum::name).collect(Collectors.joining(", ")))), + configuration)); + } + public JavaConfigurer noUsageOf(Class clazz) { return noUsageOf(clazz.getName(), null, defaultConfiguration()); } diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index c5410dc..5166b9d 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -1,8 +1,12 @@ package com.enofex.taikai; +import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; +import static com.tngtech.archunit.core.domain.JavaModifier.STATIC; + import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.List; import org.junit.jupiter.api.Test; class ArchitectureTest { @@ -17,6 +21,7 @@ void shouldFulfilConstrains() { .noUsageOf(Date.class) .noUsageOf(Calendar.class) .noUsageOf(SimpleDateFormat.class) + .fieldsShouldHaveModifiers("^[A-Z][A-Z0-9_]*$", List.of(STATIC, FINAL)) .classesShouldImplementHashCodeAndEquals() .finalClassesShouldNotHaveProtectedMembers() .utilityClassesShouldBeFinalAndHavePrivateConstructor() diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index 98e35e4..74744f1 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -33,6 +33,12 @@ public static void main(String[] args) { .noUsageOfDeprecatedAPIs() .noUsageOfDeprecatedAPIs(defaultConfiguration()) + .fieldsShouldNotBePublic() + .fieldsShouldNotBePublic(defaultConfiguration()) + + .fieldsShouldHaveModifiers("regex", List.of(PRIVATE, FINAL)) + .fieldsShouldHaveModifiers("regex", List.of(PRIVATE, FINAL), defaultConfiguration()) + .classesShouldImplementHashCodeAndEquals() .classesShouldImplementHashCodeAndEquals(defaultConfiguration())