diff --git a/README.md b/README.md index ffea0f7..a121162 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Taikai -Taikai is a wrapper around the awesome ArchUnit and provides a set of common rules for different technologies. +Taikai is a powerful extension of the popular ArchUnit library, offering a comprehensive suite of predefined rules tailored for various technologies. It simplifies the process of enforcing architectural constraints and best practices in your codebase, ensuring consistency and quality across your projects. Maven Usage ------------------- @@ -16,7 +16,7 @@ Maven Usage ``` -The `${taikai.version}` property should be defined as a property in your Maven project to specify the version. The library requires that the necessary dependencies are already declared. +The `${taikai.version}` property should be defined as a property in your Maven project to specify the version. The library requires that the necessary dependencies in this case ArchUnit is already declared. JUnit 5 Example test ------------------- @@ -24,7 +24,7 @@ JUnit 5 Example test ```java @Test void shouldFulfilConstrains() { - Taikai taikai = Taikai.builder() + Taikai.builder() .namespace("com.enofex.taikai") .spring(spring -> spring .noAutowiredFields() @@ -52,6 +52,9 @@ void shouldFulfilConstrains() { .classesShouldNotBeAnnotatedWithDisabled() .methodsShouldNotBeAnnotatedWithDisabled())) .java(java -> java + .noUsageOfDeprecatedAPIs() + .methodsShouldNotThrowGenericException() + .utilityClassesShouldBeFinalAndHavePrivateConstructor() .imports(imports -> imports .shouldHaveNoCycles() .shouldNotImport("..shaded..") @@ -59,7 +62,8 @@ void shouldFulfilConstrains() { .naming(naming -> naming .classesShouldNotMatch(".*Impl") .interfacesShouldNotHavePrefixI())) - .build(); + .build() + .check(); } ``` diff --git a/src/main/java/com/enofex/taikai/Namespace.java b/src/main/java/com/enofex/taikai/Namespace.java index d087619..90616d5 100644 --- a/src/main/java/com/enofex/taikai/Namespace.java +++ b/src/main/java/com/enofex/taikai/Namespace.java @@ -20,14 +20,11 @@ public static JavaClasses from(String namespace, IMPORT importOption) { Objects.requireNonNull(namespace); Objects.requireNonNull(importOption); - switch (importOption) { - case WITH_TESTS: - return withTests(namespace); - case ONLY_TESTS: - return onlyTests(namespace); - default: - return withoutTests(namespace); - } + return switch (importOption) { + case WITH_TESTS -> withTests(namespace); + case ONLY_TESTS -> onlyTests(namespace); + default -> withoutTests(namespace); + }; } public static JavaClasses withoutTests(String namespace) { diff --git a/src/main/java/com/enofex/taikai/TaikaiRule.java b/src/main/java/com/enofex/taikai/TaikaiRule.java index 8b8f7fa..3383cf9 100644 --- a/src/main/java/com/enofex/taikai/TaikaiRule.java +++ b/src/main/java/com/enofex/taikai/TaikaiRule.java @@ -37,9 +37,7 @@ public void check(String globalNamespace) { } else { String namespace = this.configuration.namespace() != null ? this.configuration.namespace() - : globalNamespace != null - ? globalNamespace - : null; + : globalNamespace; if (namespace == null) { throw new TaikaiException("Namespace is not provided"); diff --git a/src/main/java/com/enofex/taikai/java/Deprecations.java b/src/main/java/com/enofex/taikai/java/Deprecations.java new file mode 100644 index 0000000..aaacd5e --- /dev/null +++ b/src/main/java/com/enofex/taikai/java/Deprecations.java @@ -0,0 +1,51 @@ +package com.enofex.taikai.java; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +final class Deprecations { + + private Deprecations() { + } + + static ArchCondition notUseDeprecatedAPIs() { + return new ArchCondition("not use deprecated APIs") { + @Override + public void check(JavaClass item, ConditionEvents events) { + if (item.isAnnotatedWith(Deprecated.class)) { + events.add(SimpleConditionEvent.violated(item, + String.format("Class %s is deprecated", item.getName()))); + } + + item.getAllFields().stream() + .filter(field -> field.isAnnotatedWith(Deprecated.class)) + .forEach(field -> events.add(SimpleConditionEvent.violated(field, + String.format("Field %s in class %s is deprecated", field.getName(), + item.getName())))); + + item.getAllMethods().stream() + .filter(method -> !method.getOwner().getName().equals(Object.class.getName())) + .filter(method -> !method.getOwner().getName().equals(Enum.class.getName())) + .filter(method -> method.isAnnotatedWith(Deprecated.class)) + .forEach(method -> events.add(SimpleConditionEvent.violated(method, + String.format("Method %s in class %s is deprecated", method.getName(), + item.getName())))); + + item.getAllConstructors().stream() + .filter(constructor -> constructor.isAnnotatedWith(Deprecated.class)) + .forEach(constructor -> events.add(SimpleConditionEvent.violated(constructor, + String.format("Constructor %s in class %s is deprecated", + constructor.getFullName(), item.getName())))); + + item.getDirectDependenciesFromSelf().stream() + .filter(dependency -> dependency.getTargetClass().isAnnotatedWith(Deprecated.class)) + .forEach(dependency -> events.add( + SimpleConditionEvent.violated(dependency.getTargetClass(), + String.format("Class %s references deprecated class %s", item.getName(), + dependency.getTargetClass().getName())))); + } + }.as("No usage of deprecated APIs"); + } +} diff --git a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java index c0a0ce1..166a51c 100644 --- a/src/main/java/com/enofex/taikai/java/JavaConfigurer.java +++ b/src/main/java/com/enofex/taikai/java/JavaConfigurer.java @@ -1,5 +1,14 @@ package com.enofex.taikai.java; +import static com.enofex.taikai.java.Deprecations.notUseDeprecatedAPIs; +import static com.enofex.taikai.java.UtilityClasses.beFinal; +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; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; + +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.enofex.taikai.configures.Customizer; @@ -18,6 +27,35 @@ public JavaConfigurer naming(Customizer customizer) { return customizer(customizer, () -> new NamingConfigurer(configurerContext())); } + public JavaConfigurer utilityClassesShouldBeFinalAndHavePrivateConstructor() { + return utilityClassesShouldBeFinalAndHavePrivateConstructor(null); + } + + public JavaConfigurer utilityClassesShouldBeFinalAndHavePrivateConstructor( + Configuration configuration) { + return addRule(TaikaiRule.of(utilityClasses() + .should(beFinal()) + .andShould(havePrivateConstructor()), configuration)); + } + + public JavaConfigurer methodsShouldNotThrowGenericException() { + return methodsShouldNotThrowGenericException(null); + } + + public JavaConfigurer methodsShouldNotThrowGenericException(Configuration configuration) { + return addRule(TaikaiRule.of(methods() + .should().notDeclareThrowableOfType(Exception.class) + .as("Methods should not throw generic Exception"), configuration)); + } + + public JavaConfigurer noUsageOfDeprecatedAPIs() { + return noUsageOfDeprecatedAPIs(null); + } + + public JavaConfigurer noUsageOfDeprecatedAPIs(Configuration configuration) { + return addRule(TaikaiRule.of(classes().should(notUseDeprecatedAPIs()), configuration)); + } + @Override public void disable() { disable(ImportsConfigurer.class); diff --git a/src/main/java/com/enofex/taikai/java/UtilityClasses.java b/src/main/java/com/enofex/taikai/java/UtilityClasses.java new file mode 100644 index 0000000..13c0358 --- /dev/null +++ b/src/main/java/com/enofex/taikai/java/UtilityClasses.java @@ -0,0 +1,56 @@ +package com.enofex.taikai.java; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +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 com.tngtech.archunit.lang.syntax.elements.GivenClassesConjunction; + +final class UtilityClasses { + + private UtilityClasses() { + } + + static GivenClassesConjunction utilityClasses() { + return classes().that(haveOnlyStaticMethods()); + } + + static DescribedPredicate haveOnlyStaticMethods() { + return new DescribedPredicate<>("have only static methods") { + @Override + public boolean test(JavaClass javaClass) { + return !javaClass.getMethods().isEmpty() && javaClass.getMethods().stream() + .allMatch(method -> method.getModifiers().contains(JavaModifier.STATIC) + && !"main".equals(method.getName())); + } + }; + } + + 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 + public void check(JavaClass javaClass, ConditionEvents events) { + boolean hasPrivateConstructor = javaClass.getConstructors().stream() + .anyMatch(constructor -> constructor.getModifiers().contains(JavaModifier.PRIVATE)); + String message = String.format("Class %s does not have a private constructor", + javaClass.getName()); + events.add(new SimpleConditionEvent(javaClass, hasPrivateConstructor, message)); + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java index 8ceab94..6fca310 100644 --- a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java +++ b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java @@ -19,53 +19,43 @@ private SpringPredicates() { static DescribedPredicate annotatedWithControllerOrRestController( boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_CONTROLLER)) - .or(annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_REST_CONTROLLER))); + return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated) + .or(annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated)); } static DescribedPredicate annotatedWithConfiguration( boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_CONFIGURATION)); + return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated); } - static DescribedPredicate annotatedWithRestController( - boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_REST_CONTROLLER)); + static DescribedPredicate annotatedWithRestController(boolean isMetaAnnotated) { + return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated); } static DescribedPredicate annotatedWithController(boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_CONTROLLER)); + return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated); } static DescribedPredicate annotatedWithService(boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_SERVICE)); + return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated); } static DescribedPredicate annotatedWithRepository(boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_REPOSITORY)); + return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated); } static DescribedPredicate annotatedWithSpringBootApplication( boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_SPRING_BOOT_APPLICATION, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_SPRING_BOOT_APPLICATION)); + return annotatedWith(ANNOTATION_SPRING_BOOT_APPLICATION, isMetaAnnotated); } static DescribedPredicate annotatedAutowired(boolean isMetaAnnotated) { - return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated, - "annotated with %s".formatted(ANNOTATION_AUTOWIRED)); + return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated); } private static DescribedPredicate annotatedWith(String annotation, - boolean isMetaAnnotated, String description) { - return new DescribedPredicate<>(description) { + boolean isMetaAnnotated) { + return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { @Override public boolean test(CanBeAnnotated canBeAnnotated) { return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) : diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index 8484545..f062291 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -6,13 +6,16 @@ class ArchitectureTest { @Test void shouldFulfilConstrains() { - Taikai taikai = Taikai.builder() + Taikai.builder() .namespace("com.enofex.taikai") .test(test -> test .junit5(junit5 -> junit5 .classesShouldNotBeAnnotatedWithDisabled() .methodsShouldNotBeAnnotatedWithDisabled())) .java(java -> java + .noUsageOfDeprecatedAPIs() + .methodsShouldNotThrowGenericException() + .utilityClassesShouldBeFinalAndHavePrivateConstructor() .imports(imports -> imports .shouldHaveNoCycles() .shouldNotImport("..shaded..") @@ -21,8 +24,7 @@ void shouldFulfilConstrains() { .naming(naming -> naming .classesShouldNotMatch(".*Impl") .interfacesShouldNotHavePrefixI())) - .build(); - - taikai.check(); + .build() + .check(); } } \ No newline at end of file