diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index a224025da5..500858cafd 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -1391,6 +1391,11 @@ Stream NamingConventionTest() { .by(simpleNameOf(WronglyAnnotated.class).notEndingWith("Controller")) .by(simpleNameOf(SomeEnum.class).notEndingWith("Controller")) .by(simpleNameOfAnonymousClassOf(UseCaseOneThreeController.class).notEndingWith("Controller")) + .by(violation("")) + .by(violation("Hint: The failing class appears to be a synthetic or anonymous class generated by the compiler (e.g., from lambdas, switch expressions, or inner classes). To exclude these from your rule, consider adding:")) + .by(violation(" .that().doNotHaveModifier(JavaModifier.SYNTHETIC)")) + .by(violation("or:")) + .by(violation(" .that().areNotAnonymousClasses()")) .ofRule("classes that have simple name containing 'Controller' should reside in a package '..controller..'") .by(javaClass(AbstractController.class).notResidingIn("..controller..")) diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedNaming.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedNaming.java index 698000927c..47f385e84b 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedNaming.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedNaming.java @@ -10,12 +10,26 @@ public static Creator simpleNameOfAnonymousClassOf(Class clazz) { } public static class Creator { + private static final String SYNTHETIC_CLASS_HINT = + "\n\nHint: The failing class appears to be a synthetic or anonymous class " + + "generated by the compiler (e.g., from lambdas, switch expressions, or inner classes). " + + "To exclude these from your rule, consider adding:\n" + + " .that().doNotHaveModifier(JavaModifier.SYNTHETIC)\n" + + "or:\n" + + " .that().areNotAnonymousClasses()"; + private final String className; private final String simpleName; + private final boolean isAnonymous; private Creator(String className, String simpleName) { + this(className, simpleName, className.contains("$")); + } + + private Creator(String className, String simpleName, boolean isAnonymous) { this.className = className; this.simpleName = simpleName; + this.isAnonymous = isAnonymous; } public ExpectedMessage notStartingWith(String prefix) { @@ -31,8 +45,11 @@ public ExpectedMessage containing(String infix) { } private ExpectedMessage expectedClassViolation(String description) { - return new ExpectedMessage(String.format("Class <%s> %s in (%s.java:0)", - className, description, simpleName)); + String message = String.format("Class <%s> %s in (%s.java:0)", + className, description, simpleName); + // Note: Hint message is not included in expected message for integration tests + // The hint feature is tested separately in unit tests (ClassesShouldTest) + return new ExpectedMessage(message); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java index b0be5bdf02..1109352f93 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java @@ -530,7 +530,7 @@ public static ArchCondition notHaveSimpleName(String name) { @PublicAPI(usage = ACCESS) public static ArchCondition haveSimpleNameStartingWith(String prefix) { - return have(simpleNameStartingWith(prefix)); + return haveWithHint(simpleNameStartingWith(prefix)); } @PublicAPI(usage = ACCESS) @@ -540,7 +540,7 @@ public static ArchCondition haveSimpleNameNotStartingWith(String pref @PublicAPI(usage = ACCESS) public static ArchCondition haveSimpleNameContaining(String infix) { - return have(simpleNameContaining(infix)); + return haveWithHint(simpleNameContaining(infix)); } @PublicAPI(usage = ACCESS) @@ -550,7 +550,7 @@ public static ArchCondition haveSimpleNameNotContaining(String infix) @PublicAPI(usage = ACCESS) public static ArchCondition haveSimpleNameEndingWith(String suffix) { - return have(simpleNameEndingWith(suffix)); + return haveWithHint(simpleNameEndingWith(suffix)); } @PublicAPI(usage = ACCESS) @@ -1308,6 +1308,47 @@ public static ConditionByPred .describeEventsBy((predicateDescription, satisfied) -> (satisfied ? "has " : "does not have ") + predicateDescription); } + private static String getSyntheticClassHintMessage() { + String lineSeparator = System.lineSeparator(); + return lineSeparator + lineSeparator + "Hint: The failing class appears to be a synthetic or anonymous class " + + "generated by the compiler (e.g., from lambdas, switch expressions, or inner classes). " + + "To exclude these from your rule, consider adding:" + lineSeparator + + " .that().doNotHaveModifier(JavaModifier.SYNTHETIC)" + lineSeparator + + "or:" + lineSeparator + + " .that().areNotAnonymousClasses()"; + } + + /** + * Like {@link #have(DescribedPredicate)}, but adds a helpful hint when the condition fails on synthetic or anonymous classes. + * This helps new users understand that compiler-generated classes can be excluded from rules. + * @param predicate The predicate determining which objects satisfy/violate the condition + * @return An {@link ArchCondition} that provides hints for failures on synthetic/anonymous classes + */ + private static ArchCondition haveWithHint(DescribedPredicate predicate) { + return new ArchCondition(ArchPredicates.have(predicate).getDescription()) { + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + boolean satisfied = predicate.test(javaClass); + String baseMessage = (satisfied ? "has " : "does not have ") + predicate.getDescription(); + + String message; + if (!satisfied && isSyntheticOrAnonymous(javaClass)) { + message = javaClass.getDescription() + " " + baseMessage + + " in " + javaClass.getSourceCodeLocation() + getSyntheticClassHintMessage(); + } else { + message = javaClass.getDescription() + " " + baseMessage + + " in " + javaClass.getSourceCodeLocation(); + } + + events.add(new SimpleConditionEvent(javaClass, satisfied, message)); + } + }; + } + + private static boolean isSyntheticOrAnonymous(JavaClass javaClass) { + return javaClass.getModifiers().contains(JavaModifier.SYNTHETIC) || javaClass.isAnonymousClass(); + } + /** * Derives an {@link ArchCondition} from a {@link DescribedPredicate}. Similar to {@link ArchCondition#from(DescribedPredicate)}, * but more conveniently creates a message to be used within a 'be'-sentence. diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java index db74bdfae1..8f5524214f 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java @@ -372,6 +372,35 @@ public void haveSimpleNameNotEndingWith(ArchRule rule, String suffix) { .doesNotContain(SomeClass.class.getName()); } + @Test + public void haveSimpleNameEndingWith_should_show_hint_for_anonymous_classes() { + Class anonymousClass = NestedClassWithSomeMoreClasses.getAnonymousClass(); + + ArchRule rule = classes() + .should().haveSimpleNameEndingWith("SomethingElse"); + + EvaluationResult result = rule.evaluate(importClasses(anonymousClass)); + + assertThat(singleLineFailureReportOf(result)) + .contains("does not have simple name ending with 'SomethingElse'") + .contains("Hint:") + .contains("synthetic or anonymous") + .contains("doNotHaveModifier(JavaModifier.SYNTHETIC)") + .contains("areNotAnonymousClasses()"); + } + + @Test + public void haveSimpleNameEndingWith_should_NOT_show_hint_for_regular_classes() { + ArchRule rule = classes() + .should().haveSimpleNameEndingWith("ValidSuffix"); + + EvaluationResult result = rule.evaluate(importClasses(WrongNamedClass.class)); + + assertThat(singleLineFailureReportOf(result)) + .contains("does not have simple name ending with 'ValidSuffix'") + .doesNotContain("Hint:"); + } + @DataProvider public static Object[][] resideInAPackage_rules() { String thePackage = ArchRule.class.getPackage().getName();