Skip to content

Commit fc28063

Browse files
committed
improve error messages for synthetic/anonymous class violations
When ArchUnit rules fail on synthetic or anonymous classes generated by the compiler (e.g., from lambdas or enum switches), new users are often confused by error messages pointing to classes they didn't write (MyService$1). This commit enhances naming convention error messages to detect synthetic/ anonymous classes and provide a helpful hint directing users to exclude these classes using .doNotHaveModifier(JavaModifier.SYNTHETIC) or .areNotAnonymousClasses(). The implementation uses a custom ArchCondition that checks the failing object and appends the hint only when the condition fails on a synthetic or anonymous class, ensuring regular violations remain unchanged. Implementation details: - Checks both SYNTHETIC (compiler-generated, e.g., enum switch maps per JLS 13.1.7) and anonymous classes (both can cause unexpected naming violations) - Applies to haveSimpleNameStartingWith, haveSimpleNameContaining, and haveSimpleNameEndingWith methods - Fully compatible with Java 8-21 (all APIs available since Java 1.5) - Includes unit tests for anonymous classes and integration tests for synthetic classes Resolves: #1509 Signed-off-by: chadongmin <cdm2883@naver.com>
1 parent 687323f commit fc28063

4 files changed

Lines changed: 95 additions & 5 deletions

File tree

archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,11 @@ Stream<DynamicTest> NamingConventionTest() {
13911391
.by(simpleNameOf(WronglyAnnotated.class).notEndingWith("Controller"))
13921392
.by(simpleNameOf(SomeEnum.class).notEndingWith("Controller"))
13931393
.by(simpleNameOfAnonymousClassOf(UseCaseOneThreeController.class).notEndingWith("Controller"))
1394+
.by(violation(""))
1395+
.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:"))
1396+
.by(violation(" .that().doNotHaveModifier(JavaModifier.SYNTHETIC)"))
1397+
.by(violation("or:"))
1398+
.by(violation(" .that().areNotAnonymousClasses()"))
13941399

13951400
.ofRule("classes that have simple name containing 'Controller' should reside in a package '..controller..'")
13961401
.by(javaClass(AbstractController.class).notResidingIn("..controller.."))

archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedNaming.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,26 @@ public static Creator simpleNameOfAnonymousClassOf(Class<?> clazz) {
1010
}
1111

1212
public static class Creator {
13+
private static final String SYNTHETIC_CLASS_HINT =
14+
"\n\nHint: The failing class appears to be a synthetic or anonymous class " +
15+
"generated by the compiler (e.g., from lambdas, switch expressions, or inner classes). " +
16+
"To exclude these from your rule, consider adding:\n" +
17+
" .that().doNotHaveModifier(JavaModifier.SYNTHETIC)\n" +
18+
"or:\n" +
19+
" .that().areNotAnonymousClasses()";
20+
1321
private final String className;
1422
private final String simpleName;
23+
private final boolean isAnonymous;
1524

1625
private Creator(String className, String simpleName) {
26+
this(className, simpleName, className.contains("$"));
27+
}
28+
29+
private Creator(String className, String simpleName, boolean isAnonymous) {
1730
this.className = className;
1831
this.simpleName = simpleName;
32+
this.isAnonymous = isAnonymous;
1933
}
2034

2135
public ExpectedMessage notStartingWith(String prefix) {
@@ -31,8 +45,11 @@ public ExpectedMessage containing(String infix) {
3145
}
3246

3347
private ExpectedMessage expectedClassViolation(String description) {
34-
return new ExpectedMessage(String.format("Class <%s> %s in (%s.java:0)",
35-
className, description, simpleName));
48+
String message = String.format("Class <%s> %s in (%s.java:0)",
49+
className, description, simpleName);
50+
// Note: Hint message is not included in expected message for integration tests
51+
// The hint feature is tested separately in unit tests (ClassesShouldTest)
52+
return new ExpectedMessage(message);
3653
}
3754
}
3855
}

archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ public static ArchCondition<JavaClass> notHaveSimpleName(String name) {
530530

531531
@PublicAPI(usage = ACCESS)
532532
public static ArchCondition<JavaClass> haveSimpleNameStartingWith(String prefix) {
533-
return have(simpleNameStartingWith(prefix));
533+
return haveWithHint(simpleNameStartingWith(prefix));
534534
}
535535

536536
@PublicAPI(usage = ACCESS)
@@ -540,7 +540,7 @@ public static ArchCondition<JavaClass> haveSimpleNameNotStartingWith(String pref
540540

541541
@PublicAPI(usage = ACCESS)
542542
public static ArchCondition<JavaClass> haveSimpleNameContaining(String infix) {
543-
return have(simpleNameContaining(infix));
543+
return haveWithHint(simpleNameContaining(infix));
544544
}
545545

546546
@PublicAPI(usage = ACCESS)
@@ -550,7 +550,7 @@ public static ArchCondition<JavaClass> haveSimpleNameNotContaining(String infix)
550550

551551
@PublicAPI(usage = ACCESS)
552552
public static ArchCondition<JavaClass> haveSimpleNameEndingWith(String suffix) {
553-
return have(simpleNameEndingWith(suffix));
553+
return haveWithHint(simpleNameEndingWith(suffix));
554554
}
555555

556556
@PublicAPI(usage = ACCESS)
@@ -1308,6 +1308,45 @@ public static <T extends HasDescription & HasSourceCodeLocation> ConditionByPred
13081308
.describeEventsBy((predicateDescription, satisfied) -> (satisfied ? "has " : "does not have ") + predicateDescription);
13091309
}
13101310

1311+
private static final String SYNTHETIC_CLASS_HINT_MESSAGE =
1312+
"\n\nHint: The failing class appears to be a synthetic or anonymous class " +
1313+
"generated by the compiler (e.g., from lambdas, switch expressions, or inner classes). " +
1314+
"To exclude these from your rule, consider adding:\n" +
1315+
" .that().doNotHaveModifier(JavaModifier.SYNTHETIC)\n" +
1316+
"or:\n" +
1317+
" .that().areNotAnonymousClasses()";
1318+
1319+
/**
1320+
* Like {@link #have(DescribedPredicate)}, but adds a helpful hint when the condition fails on synthetic or anonymous classes.
1321+
* This helps new users understand that compiler-generated classes can be excluded from rules.
1322+
* @param predicate The predicate determining which objects satisfy/violate the condition
1323+
* @return An {@link ArchCondition} that provides hints for failures on synthetic/anonymous classes
1324+
*/
1325+
private static ArchCondition<JavaClass> haveWithHint(DescribedPredicate<? super JavaClass> predicate) {
1326+
return new ArchCondition<JavaClass>(ArchPredicates.have(predicate).getDescription()) {
1327+
@Override
1328+
public void check(JavaClass javaClass, ConditionEvents events) {
1329+
boolean satisfied = predicate.test(javaClass);
1330+
String baseMessage = (satisfied ? "has " : "does not have ") + predicate.getDescription();
1331+
1332+
String message;
1333+
if (!satisfied && isSyntheticOrAnonymous(javaClass)) {
1334+
message = javaClass.getDescription() + " " + baseMessage +
1335+
" in " + javaClass.getSourceCodeLocation() + SYNTHETIC_CLASS_HINT_MESSAGE;
1336+
} else {
1337+
message = javaClass.getDescription() + " " + baseMessage +
1338+
" in " + javaClass.getSourceCodeLocation();
1339+
}
1340+
1341+
events.add(new SimpleConditionEvent(javaClass, satisfied, message));
1342+
}
1343+
};
1344+
}
1345+
1346+
private static boolean isSyntheticOrAnonymous(JavaClass javaClass) {
1347+
return javaClass.getModifiers().contains(JavaModifier.SYNTHETIC) || javaClass.isAnonymousClass();
1348+
}
1349+
13111350
/**
13121351
* Derives an {@link ArchCondition} from a {@link DescribedPredicate}. Similar to {@link ArchCondition#from(DescribedPredicate)},
13131352
* but more conveniently creates a message to be used within a 'be'-sentence.

archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,35 @@ public void haveSimpleNameNotEndingWith(ArchRule rule, String suffix) {
372372
.doesNotContain(SomeClass.class.getName());
373373
}
374374

375+
@Test
376+
public void haveSimpleNameEndingWith_should_show_hint_for_anonymous_classes() {
377+
Class<?> anonymousClass = NestedClassWithSomeMoreClasses.getAnonymousClass();
378+
379+
ArchRule rule = classes()
380+
.should().haveSimpleNameEndingWith("SomethingElse");
381+
382+
EvaluationResult result = rule.evaluate(importClasses(anonymousClass));
383+
384+
assertThat(singleLineFailureReportOf(result))
385+
.contains("does not have simple name ending with 'SomethingElse'")
386+
.contains("Hint:")
387+
.contains("synthetic or anonymous")
388+
.contains("doNotHaveModifier(JavaModifier.SYNTHETIC)")
389+
.contains("areNotAnonymousClasses()");
390+
}
391+
392+
@Test
393+
public void haveSimpleNameEndingWith_should_NOT_show_hint_for_regular_classes() {
394+
ArchRule rule = classes()
395+
.should().haveSimpleNameEndingWith("ValidSuffix");
396+
397+
EvaluationResult result = rule.evaluate(importClasses(WrongNamedClass.class));
398+
399+
assertThat(singleLineFailureReportOf(result))
400+
.contains("does not have simple name ending with 'ValidSuffix'")
401+
.doesNotContain("Hint:");
402+
}
403+
375404
@DataProvider
376405
public static Object[][] resideInAPackage_rules() {
377406
String thePackage = ArchRule.class.getPackage().getName();

0 commit comments

Comments
 (0)