Repository: jhipster/jhipster-lite
Pull request #10399 (2024-07-26 08:50:09)

feat(archUnit): adding equals/hashcode contract check
This feature adds a new ArchUnit test that is designed to make a simple equals/hashcode validation.

Every class implementing equals(Object) should also implement hashCode().
You have to implement both or none in each class for this new test to pass.


Patch:
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactory.java
index fdb529748..4350484c7 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactory.java
@@ -27,6 +27,7 @@ public class ArchUnitModuleFactory {
         .add(SOURCE.template("archunit.properties"), to("src/test/resources/archunit.properties"))
         .add(SOURCE.template("AnnotationArchTest.java"), testDestination.append("AnnotationArchTest.java"))
         .add(SOURCE.template("HexagonalArchTest.java"), testDestination.append("HexagonalArchTest.java"))
+        .add(SOURCE.template("EqualsHashcodeArchTest.java"), testDestination.append("EqualsHashcodeArchTest.java"))
         .and()
       .javaDependencies()
         .addDependency(archUnitDependency())


Test patch:
diff --git a/src/main/resources/generator/server/javatool/archunit/test/EqualsHashcodeArchTest.java.mustache b/src/main/resources/generator/server/javatool/archunit/test/EqualsHashcodeArchTest.java.mustache
new file mode 100644
index 000000000..c7dc65aaf
--- /dev/null
+++ b/src/main/resources/generator/server/javatool/archunit/test/EqualsHashcodeArchTest.java.mustache
@@ -0,0 +1,58 @@
+package {{packageName}};
+
+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.JavaClasses;
+import com.tngtech.archunit.core.domain.JavaMethod;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.lang.ArchCondition;
+import org.junit.jupiter.api.Test;
+
+@UnitTest
+class EqualsHashcodeArchTest {
+
+  private static final String ROOT_PACKAGE = "{{packageName}}..";
+
+  private static final JavaClasses classes = new ClassFileImporter()
+    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
+    .importPackages(ROOT_PACKAGE);
+
+  @Test
+  void shouldHaveValidEqualsHashcodeContract() {
+    classes().that().resideInAnyPackage(ROOT_PACKAGE).and().areNotInterfaces().should(implementBothOrNone()).check(classes);
+  }
+
+  private ArchCondition.ConditionByPredicate<JavaClass> implementBothOrNone() {
+    return ArchCondition.from(
+      new DescribedPredicate<>("Class should implement none or both method equals and hashcode") {
+        @Override
+        public boolean test(JavaClass clazz) {
+          return hasEquals(clazz) == hasHashCode(clazz);
+        }
+
+        private boolean hasHashCode(JavaClass clazz) {
+          return clazz.getMethods().stream().anyMatch(this::isMethodHashCode);
+        }
+
+        private boolean isMethodHashCode(JavaMethod method) {
+          return method.getName().equals("hashCode") && method.getParameters().isEmpty();
+        }
+
+        private boolean hasEquals(JavaClass clazz) {
+          return clazz.getMethods().stream().anyMatch(this::isMethodEquals);
+        }
+
+        private boolean isMethodEquals(JavaMethod method) {
+          return method.getName().equals("equals") && hasOneObjectParameter(method);
+        }
+
+        private boolean hasOneObjectParameter(JavaMethod method) {
+          return method.getParameters().size() == 1 && method.getParameters().getFirst().getRawType().isAssignableFrom(Object.class);
+        }
+      }
+    );
+  }
+}
diff --git a/src/test/features/server/javatool/arch-unit.feature b/src/test/features/server/javatool/arch-unit.feature
index fd01ab2b3..5d79e3a37 100644
--- a/src/test/features/server/javatool/arch-unit.feature
+++ b/src/test/features/server/javatool/arch-unit.feature
@@ -4,4 +4,6 @@ Feature: Arch Unit
     When I apply "java-archunit" module to default project with maven file
       | packageName | tech.jhipster.chips |
     Then I should have files in "src/test/java/tech/jhipster/chips"
-      | HexagonalArchTest.java |
+      | HexagonalArchTest.java      |
+      | AnnotationArchTest.java     |
+      | EqualsHashcodeArchTest.java |
diff --git a/src/test/java/tech/jhipster/lite/EqualsHashcodeArchTest.java b/src/test/java/tech/jhipster/lite/EqualsHashcodeArchTest.java
new file mode 100644
index 000000000..75d7b3887
--- /dev/null
+++ b/src/test/java/tech/jhipster/lite/EqualsHashcodeArchTest.java
@@ -0,0 +1,58 @@
+package tech.jhipster.lite;
+
+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.JavaClasses;
+import com.tngtech.archunit.core.domain.JavaMethod;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.lang.ArchCondition;
+import org.junit.jupiter.api.Test;
+
+@UnitTest
+class EqualsHashcodeArchTest {
+
+  private static final String ROOT_PACKAGE = "tech.jhipster.lite..";
+
+  private static final JavaClasses classes = new ClassFileImporter()
+    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
+    .importPackages(ROOT_PACKAGE);
+
+  @Test
+  void shouldHaveValidEqualsHashcodeContract() {
+    classes().that().resideInAnyPackage(ROOT_PACKAGE).and().areNotInterfaces().should(implementBothOrNone()).check(classes);
+  }
+
+  private ArchCondition.ConditionByPredicate<JavaClass> implementBothOrNone() {
+    return ArchCondition.from(
+      new DescribedPredicate<>("Class should implement none or both method equals and hashcode") {
+        @Override
+        public boolean test(JavaClass clazz) {
+          return hasEquals(clazz) == hasHashCode(clazz);
+        }
+
+        private boolean hasHashCode(JavaClass clazz) {
+          return clazz.getMethods().stream().anyMatch(this::isMethodHashCode);
+        }
+
+        private boolean isMethodHashCode(JavaMethod method) {
+          return method.getName().equals("hashCode") && method.getParameters().isEmpty();
+        }
+
+        private boolean hasEquals(JavaClass clazz) {
+          return clazz.getMethods().stream().anyMatch(this::isMethodEquals);
+        }
+
+        private boolean isMethodEquals(JavaMethod method) {
+          return method.getName().equals("equals") && hasOneObjectParameter(method);
+        }
+
+        private boolean hasOneObjectParameter(JavaMethod method) {
+          return method.getParameters().size() == 1 && method.getParameters().getFirst().getRawType().isAssignableFrom(Object.class);
+        }
+      }
+    );
+  }
+}
diff --git a/src/test/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactoryTest.java
index d3e12ce69..571ee4df4 100644
--- a/src/test/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactoryTest.java
+++ b/src/test/java/tech/jhipster/lite/generator/server/javatool/archunit/domain/ArchUnitModuleFactoryTest.java
@@ -23,7 +23,12 @@ class ArchUnitModuleFactoryTest {
     JHipsterModule module = factory.buildModule(properties);
 
     assertThatModuleWithFiles(module, pomFile(), testLogbackFile())
-      .hasFiles("src/test/resources/archunit.properties", "src/test/java/tech/jhipster/jhlitest/AnnotationArchTest.java")
+      .hasFiles(
+        "src/test/resources/archunit.properties",
+        "src/test/java/tech/jhipster/jhlitest/AnnotationArchTest.java",
+        "src/test/java/tech/jhipster/jhlitest/HexagonalArchTest.java",
+        "src/test/java/tech/jhipster/jhlitest/EqualsHashcodeArchTest.java"
+      )
       .hasFile("pom.xml")
       .containing("<artifactId>archunit-junit5-api</artifactId>")
       .and()