diff --git a/README.md b/README.md index 1ca7a58..733fb4a 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,14 @@ void shouldFulfillConstraints() { .methodsShouldNotMatch("foo") .fieldsShouldNotMatch("bar") .fieldsShouldMatch("com.awesome.Foo", "foo") - .constantsShouldFollowConvention() + .constantsShouldFollowConventions() .interfacesShouldNotHavePrefixI())) + .logging(logging -> logging + .loggersShouldFollowConventions(Logger.class, "logger", EnumSet.of(PRIVATE, FINAL))) .test(test -> test .junit5(junit5 -> junit5 .classesShouldNotBeAnnotatedWithDisabled() - .methodsShouldNotBeAnnotatedWithDisabled())) + .methodsShouldNotBeAnnotatedWithDisabled())) .spring(spring -> spring .noAutowiredFields() .boot(boot -> boot diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index fea21c2..a3acf1d 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -19,7 +19,32 @@ To use Taikai, include it as a dependency in your Maven `pom.xml`: Ensure to configure `${taikai.version}` to the latest stable version compatible with your project's ArchUnit version. -## 3. Rules Overview +## 3. Usage + +### 3.1 Setting the Namespace + +The `namespace` setting specifies the base package of your project. Taikai will analyze all classes within this namespace. The default mode is `WITHOUT_TESTS`, which excludes test classes from the import check. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .build() + .check(); +``` + +### 3.2 Enforcing Rules on Empty Sets + +The `failOnEmpty` setting determines whether the build should fail if no classes match a given rule. This is useful to ensure that your rules are applied consistently and to avoid false positives. The default is `false`. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .failOnEmpty(true) + .build() + .check(); +``` + +## 4. Rules Overview Taikai's architecture rules cover a wide range of categories to enforce best practices and maintain consistency. @@ -48,9 +73,17 @@ The default mode is `WITHOUT_TESTS`, which excludes test classes from the import | Naming | `fieldsShouldNotMatch` | Fields should not match specific naming patterns | | Naming | `fieldsShouldMatch` | Fields should match specific naming patterns for specific classes | | Naming | `fieldsAnnotatedWithShouldMatch` | Fields annotated with should match specific naming patterns | -| Naming | `constantsShouldFollowConvention` | Constants should follow naming conventions, except `serialVersionUID` | +| Naming | `constantsShouldFollowConventions` | Constants should follow naming conventions, except `serialVersionUID` | | Naming | `interfacesShouldNotHavePrefixI` | Interfaces should not have the prefix `I` | +### Logging Rules + +The default mode is `WITHOUT_TESTS`, which checks only test classes. + +| Category | Method Name | Rule Description | +|----------|-------------------|----------------------------------------------------------------------------------------------------| +| General | `loggersShouldFollowConventions` | Ensure that the specified logger follow a specific naming pattern and have the required modifiers | + ### Test Rules The default mode is `ONLY_TESTS`, which checks only test classes. @@ -90,7 +123,7 @@ The default mode is `WITHOUT_TESTS`, which excludes test classes from the import | Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | | Services | `shouldNotDependOnControllers` | Services annotated with `@Service.` should not depend on controllers annotated with `@Controller` or `@RestController` | -## 4. Java Rules +## 5. Java Rules Java configuration involves defining constraints related to Java language features, coding standards, and architectural patterns. @@ -178,7 +211,7 @@ Taikai.builder() .fieldsShouldMatch("com.awesome.Foo", "foo") .fieldsShouldMatch(Foo.class, "foo") .fieldsAnnotatedWithShouldMatch(Annotation.class, "coolField") - .constantsShouldFollowConvention() + .constantsShouldFollowConventions() .interfacesShouldNotHavePrefixI()))) .build() .check(); @@ -206,7 +239,7 @@ Taikai.builder() .check(); ``` -- **No Usage of System.out or System.err**: Enforce disallowing the use of `System.out` and `System.err` for logging, encouraging the use of proper logging frameworks instead. +- **No Usage of `System.out` or `System.err`**: Enforce disallowing the use of `System.out` and `System.err` for logging, encouraging the use of proper logging frameworks instead. ```java Taikai.builder() @@ -228,7 +261,22 @@ Taikai.builder() .check(); ``` -## 5. Test Rules +## 6. Logging Rules + +Logging configuration involves specifying constraints related to logging frameworks and practices. + +- **Ensure Logger Field Conforms to Standards**: Ensure that classes use a logger field of the specified type, with the correct name and modifiers. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .logging(logging -> logging + .loggersShouldFollowConventions(org.slf4j.Logger.class, "logger", EnumSet.of(PRIVATE, FINAL))) + .build() + .check(); +``` + +## 7. Test Rules Test configuration involves specifying constraints related to testing frameworks and practices. @@ -317,7 +365,7 @@ Taikai.builder() .check(); ``` -## 6. Spring Rules +## 8. Spring Rules Spring configuration involves defining constraints specific to Spring Framework usage. @@ -403,7 +451,7 @@ Taikai.builder() .check(); ``` -## 7. Customization +## 9. Customization ### Custom Configuration for Import Rules @@ -439,7 +487,7 @@ Taikai.builder() ``` By using the `addRule()` method and providing a custom ArchUnit rule, you can extend Taikai's capabilities to enforce additional architectural constraints that are not covered by the predefined rules. This flexibility allows you to adapt Taikai to suit the unique architectural needs of your Java project. -## 8. Examples +## 10. Examples Below are some examples demonstrating the usage of Taikai to define and enforce architectural rules in Java projects, including Spring-specific configurations: @@ -478,6 +526,8 @@ class ArchitectureTest { .namesShouldMatch("regex") .shouldNotDependOnOtherControllers() .shouldBePackagePrivate())) + .logging(logging -> logging + .loggersShouldFollowConventions(Logger.class, "logger", EnumSet.of(PRIVATE, FINAL))) .build() .check(); } diff --git a/src/main/java/com/enofex/taikai/Taikai.java b/src/main/java/com/enofex/taikai/Taikai.java index 4314804..fcae95c 100644 --- a/src/main/java/com/enofex/taikai/Taikai.java +++ b/src/main/java/com/enofex/taikai/Taikai.java @@ -5,6 +5,7 @@ import com.enofex.taikai.configures.Configurers; import com.enofex.taikai.configures.Customizer; import com.enofex.taikai.java.JavaConfigurer; +import com.enofex.taikai.logging.LoggingConfigurer; import com.enofex.taikai.spring.SpringConfigurer; import com.enofex.taikai.test.TestConfigurer; import com.tngtech.archunit.ArchConfiguration; @@ -100,14 +101,18 @@ public Builder java(Customizer customizer) { return configure(customizer, JavaConfigurer::new); } - public Builder spring(Customizer customizer) { - return configure(customizer, SpringConfigurer::new); + public Builder logging(Customizer customizer) { + return configure(customizer, LoggingConfigurer::new); } public Builder test(Customizer customizer) { return configure(customizer, TestConfigurer::new); } + public Builder spring(Customizer customizer) { + return configure(customizer, SpringConfigurer::new); + } + private Builder configure(Customizer customizer, Function supplier) { Objects.requireNonNull(customizer); diff --git a/src/main/java/com/enofex/taikai/java/ConstantNaming.java b/src/main/java/com/enofex/taikai/java/ConstantNaming.java index b9f338b..c73abcc 100644 --- a/src/main/java/com/enofex/taikai/java/ConstantNaming.java +++ b/src/main/java/com/enofex/taikai/java/ConstantNaming.java @@ -15,7 +15,7 @@ final class ConstantNaming { private ConstantNaming() { } - static ArchCondition shouldFollowConstantNamingConvention() { + static ArchCondition shouldFollowConstantNamingConventions() { return new ArchCondition<>("follow constant naming convention") { @Override public void check(JavaField field, ConditionEvents events) { diff --git a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java index 1cfed58..de7b110 100644 --- a/src/main/java/com/enofex/taikai/java/NamingConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/NamingConfigurer.java @@ -1,6 +1,6 @@ package com.enofex.taikai.java; -import static com.enofex.taikai.java.ConstantNaming.shouldFollowConstantNamingConvention; +import static com.enofex.taikai.java.ConstantNaming.shouldFollowConstantNamingConventions; 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; @@ -185,14 +185,14 @@ public void check(JavaClass javaClass, ConditionEvents events) { }; } - public NamingConfigurer constantsShouldFollowConvention() { - return constantsShouldFollowConvention(null); + public NamingConfigurer constantsShouldFollowConventions() { + return constantsShouldFollowConventions(null); } - public NamingConfigurer constantsShouldFollowConvention(Configuration configuration) { + public NamingConfigurer constantsShouldFollowConventions(Configuration configuration) { return addRule(TaikaiRule.of(fields() .that().areFinal().and().areStatic() - .should(shouldFollowConstantNamingConvention()) - .as("Constants should follow constant naming convention"), configuration)); + .should(shouldFollowConstantNamingConventions()) + .as("Constants should follow constant naming conventions"), configuration)); } } diff --git a/src/main/java/com/enofex/taikai/logging/LoggerConventions.java b/src/main/java/com/enofex/taikai/logging/LoggerConventions.java new file mode 100644 index 0000000..ab89faa --- /dev/null +++ b/src/main/java/com/enofex/taikai/logging/LoggerConventions.java @@ -0,0 +1,44 @@ +package com.enofex.taikai.logging; + +import com.tngtech.archunit.core.domain.JavaClass; +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; +import java.util.Set; + +final class LoggerConventions { + + private LoggerConventions() { + } + + static ArchCondition followLoggerConventions(String typeName, String regex, + Set requiredModifiers) { + return new ArchCondition<>( + "have a logger field of type %s with name pattern %s and modifiers %s".formatted( + typeName, regex, requiredModifiers)) { + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + for (JavaField field : javaClass.getAllFields()) { + if (field.getRawType().isAssignableTo(typeName)) { + if (!field.getName().matches(regex)) { + events.add(SimpleConditionEvent.violated(field, + "Field '%s' in class %s does not match the naming pattern '%s'".formatted( + field.getName(), + javaClass.getName(), regex))); + } + + if (!field.getModifiers().containsAll(requiredModifiers)) { + events.add(SimpleConditionEvent.violated(field, + "Field '%s' in class %s does not have the required modifiers %s".formatted( + field.getName(), + javaClass.getName(), + requiredModifiers))); + } + } + } + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/logging/LoggingConfigurer.java b/src/main/java/com/enofex/taikai/logging/LoggingConfigurer.java new file mode 100644 index 0000000..84e1316 --- /dev/null +++ b/src/main/java/com/enofex/taikai/logging/LoggingConfigurer.java @@ -0,0 +1,42 @@ +package com.enofex.taikai.logging; + +import static com.enofex.taikai.logging.LoggerConventions.followLoggerConventions; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +import com.enofex.taikai.TaikaiRule; +import com.enofex.taikai.TaikaiRule.Configuration; +import com.enofex.taikai.configures.AbstractConfigurer; +import com.enofex.taikai.configures.ConfigurerContext; +import com.tngtech.archunit.core.domain.JavaModifier; +import java.util.Set; + +public final class LoggingConfigurer extends AbstractConfigurer { + + public LoggingConfigurer(ConfigurerContext configurerContext) { + super(configurerContext); + } + + public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex, + Set requiredModifiers) { + return loggersShouldFollowConventions(typeName, regex, requiredModifiers, null); + } + + public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex, + Set requiredModifiers, Configuration configuration) { + return addRule(TaikaiRule.of(classes().should( + followLoggerConventions(typeName, regex, requiredModifiers)), + configuration)); + } + + public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex, + Set requiredModifiers) { + return loggersShouldFollowConventions(clazz, regex, requiredModifiers, null); + } + + public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex, + Set requiredModifiers, Configuration configuration) { + return addRule(TaikaiRule.of(classes().should( + followLoggerConventions(clazz.getName(), regex, requiredModifiers)), + configuration)); + } +} \ No newline at end of file diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java index 1cd843e..bf25e49 100644 --- a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java +++ b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java @@ -6,8 +6,6 @@ import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_PARAMETRIZED_TEST; import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_TEST; import static com.enofex.taikai.test.JUnit5DescribedPredicates.annotatedWithTestOrParameterizedTest; -import static com.tngtech.archunit.base.DescribedPredicate.not; -import static com.tngtech.archunit.lang.conditions.ArchConditions.beInterfaces; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index 1714767..58c8962 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -11,14 +11,6 @@ class ArchitectureTest { void shouldFulfilConstrains() { Taikai.builder() .namespace("com.enofex.taikai") - .test(test -> test - .junit5(junit5 -> junit5 - .classesShouldNotBeAnnotatedWithDisabled() - .classesShouldBePackagePrivate(".*Test") - .methodsShouldNotBeAnnotatedWithDisabled() - .methodsShouldMatch("should.*") - .methodsShouldBePackagePrivate() - .methodsShouldNotDeclareExceptions())) .java(java -> java .noUsageOfDeprecatedAPIs() .noUsageOfSystemOutOrErr() @@ -39,7 +31,15 @@ void shouldFulfilConstrains() { .naming(naming -> naming .classesShouldNotMatch(".*Impl") .interfacesShouldNotHavePrefixI() - .constantsShouldFollowConvention())) + .constantsShouldFollowConventions())) + .test(test -> test + .junit5(junit5 -> junit5 + .classesShouldNotBeAnnotatedWithDisabled() + .classesShouldBePackagePrivate(".*Test") + .methodsShouldNotBeAnnotatedWithDisabled() + .methodsShouldMatch("should.*") + .methodsShouldBePackagePrivate() + .methodsShouldNotDeclareExceptions())) .build() .check(); } diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index f9cf296..85bc108 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -1,13 +1,52 @@ package com.enofex.taikai; +import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; +import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE; + import java.util.Calendar; import java.util.Date; +import java.util.EnumSet; +import java.util.logging.Logger; class Usage { public static void main(String[] args) { Taikai.builder() .namespace("com.enofex.taikai") + .java(java -> java + .noUsageOf(Date.class) + .noUsageOf(Calendar.class) + .noUsageOf("java.text.SimpleDateFormat") + .noUsageOfSystemOutOrErr() + .noUsageOfDeprecatedAPIs() + .classesShouldImplementHashCodeAndEquals() + .methodsShouldNotDeclareGenericExceptions() + .finalClassesShouldNotHaveProtectedMembers() + .utilityClassesShouldBeFinalAndHavePrivateConstructor() + .serialVersionUIDFieldsShouldBeStaticFinalLong() + .imports(imports -> imports + .shouldHaveNoCycles() + .shouldNotImport("..shaded..") + .shouldNotImport("..lombok..") + .shouldNotImport("org.junit..")) + .naming(naming -> naming + .classesShouldNotMatch(".*Impl") + .methodsShouldNotMatch("foo") + .fieldsShouldNotMatch("bar") + .fieldsShouldMatch("com.awesome.Foo", "foo") + .constantsShouldFollowConventions() + .interfacesShouldNotHavePrefixI())) + .logging(logging -> logging + .loggersShouldFollowConventions(Logger.class, "logger", EnumSet.of(PRIVATE, FINAL))) + .test(test -> test + .junit5(junit5 -> junit5 + .methodsShouldNotDeclareExceptions() + .methodsShouldMatch("should.*") + .methodsShouldBePackagePrivate() + .methodsShouldBeAnnotatedWithDisplayName() + .methodsShouldNotBeAnnotatedWithDisabled() + .classesShouldBePackagePrivate(".*Test") + .classesShouldNotBeAnnotatedWithDisabled())) .spring(spring -> spring .noAutowiredFields() .boot(boot -> boot @@ -31,38 +70,6 @@ public static void main(String[] args) { .shouldBeAnnotatedWithRepository() .namesShouldMatch("regex") .namesShouldEndWithRepository())) - .test(test -> test - .junit5(junit5 -> junit5 - .methodsShouldNotDeclareExceptions() - .methodsShouldMatch("should.*") - .methodsShouldBePackagePrivate() - .methodsShouldBeAnnotatedWithDisplayName() - .methodsShouldNotBeAnnotatedWithDisabled() - .classesShouldBePackagePrivate(".*Test") - .classesShouldNotBeAnnotatedWithDisabled())) - .java(java -> java - .noUsageOf(Date.class) - .noUsageOf(Calendar.class) - .noUsageOf("java.text.SimpleDateFormat") - .noUsageOfSystemOutOrErr() - .noUsageOfDeprecatedAPIs() - .classesShouldImplementHashCodeAndEquals() - .methodsShouldNotDeclareGenericExceptions() - .finalClassesShouldNotHaveProtectedMembers() - .utilityClassesShouldBeFinalAndHavePrivateConstructor() - .serialVersionUIDFieldsShouldBeStaticFinalLong() - .imports(imports -> imports - .shouldHaveNoCycles() - .shouldNotImport("..shaded..") - .shouldNotImport("..lombok..") - .shouldNotImport("org.junit..")) - .naming(naming -> naming - .classesShouldNotMatch(".*Impl") - .methodsShouldNotMatch("foo") - .fieldsShouldNotMatch("bar") - .fieldsShouldMatch("com.awesome.Foo", "foo") - .constantsShouldFollowConvention() - .interfacesShouldNotHavePrefixI())) .build() .check(); }