Skip to content

Commit

Permalink
Add new java rules
Browse files Browse the repository at this point in the history
  • Loading branch information
mnhock authored and mnhock committed Jun 7, 2024
1 parent c90a221 commit cdf5b88
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 41 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img height="20" src="https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg">
# 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
-------------------
Expand All @@ -16,15 +16,15 @@ Maven Usage
</dependency>
```

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
-------------------

```java
@Test
void shouldFulfilConstrains() {
Taikai taikai = Taikai.builder()
Taikai.builder()
.namespace("com.enofex.taikai")
.spring(spring -> spring
.noAutowiredFields()
Expand Down Expand Up @@ -52,14 +52,18 @@ void shouldFulfilConstrains() {
.classesShouldNotBeAnnotatedWithDisabled()
.methodsShouldNotBeAnnotatedWithDisabled()))
.java(java -> java
.noUsageOfDeprecatedAPIs()
.methodsShouldNotThrowGenericException()
.utilityClassesShouldBeFinalAndHavePrivateConstructor()
.imports(imports -> imports
.shouldHaveNoCycles()
.shouldNotImport("..shaded..")
.shouldNotImport("org.junit.."))
.naming(naming -> naming
.classesShouldNotMatch(".*Impl")
.interfacesShouldNotHavePrefixI()))
.build();
.build()
.check();
}
```

Expand Down
13 changes: 5 additions & 8 deletions src/main/java/com/enofex/taikai/Namespace.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 1 addition & 3 deletions src/main/java/com/enofex/taikai/TaikaiRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/com/enofex/taikai/java/Deprecations.java
Original file line number Diff line number Diff line change
@@ -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<JavaClass> notUseDeprecatedAPIs() {
return new ArchCondition<JavaClass>("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");
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/enofex/taikai/java/JavaConfigurer.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +27,35 @@ public JavaConfigurer naming(Customizer<NamingConfigurer> 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);
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/com/enofex/taikai/java/UtilityClasses.java
Original file line number Diff line number Diff line change
@@ -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<JavaClass> 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<JavaClass> 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<JavaClass> 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));
}
};
}
}
34 changes: 12 additions & 22 deletions src/main/java/com/enofex/taikai/spring/SpringPredicates.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,53 +19,43 @@ private SpringPredicates() {
static DescribedPredicate<CanBeAnnotated> 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<CanBeAnnotated> annotatedWithConfiguration(
boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_CONFIGURATION));
return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated);
}

static DescribedPredicate<CanBeAnnotated> annotatedWithRestController(
boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_REST_CONTROLLER));
static DescribedPredicate<CanBeAnnotated> annotatedWithRestController(boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated);
}

static DescribedPredicate<CanBeAnnotated> annotatedWithController(boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_CONTROLLER));
return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated);
}

static DescribedPredicate<CanBeAnnotated> annotatedWithService(boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_SERVICE));
return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated);
}

static DescribedPredicate<CanBeAnnotated> annotatedWithRepository(boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_REPOSITORY));
return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated);
}

static DescribedPredicate<CanBeAnnotated> 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<CanBeAnnotated> annotatedAutowired(boolean isMetaAnnotated) {
return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated,
"annotated with %s".formatted(ANNOTATION_AUTOWIRED));
return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated);
}

private static DescribedPredicate<CanBeAnnotated> 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) :
Expand Down
10 changes: 6 additions & 4 deletions src/test/java/com/enofex/taikai/ArchitectureTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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..")
Expand All @@ -21,8 +24,7 @@ void shouldFulfilConstrains() {
.naming(naming -> naming
.classesShouldNotMatch(".*Impl")
.interfacesShouldNotHavePrefixI()))
.build();

taikai.check();
.build()
.check();
}
}

0 comments on commit cdf5b88

Please sign in to comment.