Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,11 @@ Stream<DynamicTest> 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.."))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ public static ArchCondition<JavaClass> notHaveSimpleName(String name) {

@PublicAPI(usage = ACCESS)
public static ArchCondition<JavaClass> haveSimpleNameStartingWith(String prefix) {
return have(simpleNameStartingWith(prefix));
return haveWithHint(simpleNameStartingWith(prefix));
}

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

@PublicAPI(usage = ACCESS)
public static ArchCondition<JavaClass> haveSimpleNameContaining(String infix) {
return have(simpleNameContaining(infix));
return haveWithHint(simpleNameContaining(infix));
}

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

@PublicAPI(usage = ACCESS)
public static ArchCondition<JavaClass> haveSimpleNameEndingWith(String suffix) {
return have(simpleNameEndingWith(suffix));
return haveWithHint(simpleNameEndingWith(suffix));
}

@PublicAPI(usage = ACCESS)
Expand Down Expand Up @@ -1308,6 +1308,47 @@ public static <T extends HasDescription & HasSourceCodeLocation> 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<JavaClass> haveWithHint(DescribedPredicate<? super JavaClass> predicate) {
return new ArchCondition<JavaClass>(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down