Improve error messages for synthetic/anonymous class violations
Description
Improves error messages when ArchUnit rules fail on synthetic or anonymous classes generated by the compiler. New users are often confused when rules fail on classes like MyService$1 that they didn't write, with no clear guidance on how to resolve the issue.
Problem
As reported in #1509 and #1019, users encounter confusing error messages like:
Class <com.app.service.DummyService$1> does not have simple name ending with 'Service'
This happens when the Java compiler generates synthetic classes for:
- Enum switch statements (creates switch map classes per JLS 13.1.7)
- Lambda expressions
- Try-with-resources statements
- Other compiler optimizations
Users spend significant time debugging these "violations" even though they aren't actual architectural issues in their source code.
Solution
This PR adds helpful hints to error messages when rules fail on synthetic or anonymous classes:
Before:
Class <com.app.service.DummyService$1> does not have simple name ending with 'Service' in (DummyService.java:0)
After:
Class <com.app.service.DummyService$1> does not have simple name ending with 'Service' in (DummyService.java:0)
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:
.that().doNotHaveModifier(JavaModifier.SYNTHETIC)
or:
.that().areNotAnonymousClasses()
The hint only appears when relevant—regular class violations remain unchanged.
Implementation Details
Why Check Both SYNTHETIC and Anonymous?
-
Synthetic classes: Compiler-generated (e.g., enum switch maps defined in JLS 13.1.7)
- Marked with
ACC_SYNTHETICflag in bytecode - Examples:
MyClass$1from enum switches
- Marked with
-
Anonymous classes: Explicitly created in source but may violate naming rules
- Detected via
isAnonymousClass() - Examples:
new Runnable() {} - Some users report (issue #1019) that
SYNTHETICcheck doesn't work butareNotAnonymousClasses()does
- Detected via
Changes Made
Core Implementation (ArchConditions.java):
- Added
haveWithHint()method that creates customArchConditionwith hint support - Added
isSyntheticOrAnonymous()helper to detect both synthetic and anonymous classes - Modified naming convention methods to use
haveWithHint():-
haveSimpleNameStartingWith() -
haveSimpleNameContaining() -
haveSimpleNameEndingWith()
-
Unit Tests (ClassesShouldTest.java):
- Added test for anonymous class hint display
- Added test verifying regular classes don't show hints
- Uses existing
NestedClassWithSomeMoreClasses.getAnonymousClass()for testing
Integration Tests:
- Updated
ExpectedNaming.javato handle hint messages - Updated
ExamplesIntegrationTest.javato include expected hint lines - Verifies hint appears for
UseCaseOneThreeController$1(synthetic class from enum switch)
Compatibility
-
Java Version: Fully compatible with Java 8-21
-
isAnonymousClass()available since Java 1.5 -
JavaModifier.SYNTHETICavailable since Java 1.5 - Project compiles to Java 8 bytecode (major version 52)
-
-
Build:
./gradlew clean build -PallTests✅ passes
Testing
Test Coverage
- ✅ Unit tests: Anonymous class hint verification
- ✅ Integration tests: Synthetic class (enum switch) hint verification
- ✅ Regression tests: All 229 existing tests pass
- ✅ Full build:
BUILD SUCCESSFULwith all tests
Test Results
BUILD SUCCESSFUL in 2m
154 actionable tasks: 146 executed, 8 up-to-date
Related Issues
Resolves #1509 Relates to #1019
Checklist
- [x] Code follows project style guidelines
- [x] Imports organized per CONTRIBUTING.md (java., javax., others, static, no wildcards)
- [x] All tests pass (
./gradlew clean build -PallTests) - [x] Commit message follows conventions (lowercase, imperative, <70 chars, with body)
- [x] DCO signed (
git commit -s) - [x] Unit tests added for new functionality
- [x] Integration tests updated
- [x] No regressions in existing tests
@hankem @codecholeric I previously opened issue #1509 about improving error messages for synthetic/anonymous classes. I understand you may be quite busy, so I went ahead and implemented the enhancement myself.
When you have time, I would appreciate your review of this PR. No rush at all—I know maintaining this project takes considerable effort.
Thank you for all your work on ArchUnit! 😃
Thank you so much for your contribution and understanding, and apologies for missing your ping on the other issue! I'll try to have a look.
@hankem I've added another commit to fix the Windows CI test failures.
The issue was that the hint message was using hardcoded \n characters for line breaks, but Windows uses \r\n as its line separator. The integration test framework splits violation messages using
System.lineSeparator(), so on Windows it was looking for \r\n but finding \n instead. This caused the message parsing to fail and the tests couldn't match the expected violation messages.
I fixed this by changing the hint message from a static constant to a method that dynamically uses System.lineSeparator() instead of hardcoded \n. This way it will use the correct line separator
for each platform (\r\n on Windows, \n on macOS/Linux).
All tests pass locally and I've run the full test suite with ./gradlew clean build -PallTests. The Windows CI should now pass as well.
Hi @hankem , just wanted to let you know that all checks are passing now!
Personally, I believe that developers shouldn't need to understand the internal behavior of the Java compiler to use this library. They simply expect ArchUnit to verify naming conventions on their source code, just as the rules imply.
However, I value your insight as the maintainer. I'm curious to hear if you agree that this abstraction is meaningful for the project. Looking forward to your review!
One thing I want to look into (but didn't get to do yet): Does the added hint affect frozen violations? It would be a pity if we broke users' existing violation stores. (One could consider introducing some configuration that would allow users to restore the old behavior.)
@hankem Thank you for your comment. I looked into the frozen violations concern you raised.
I came up with two possible solutions to address this issue.
The first option is to modify FuzzyViolationLineMatcher to ignore hint suffixes. This would guarantee full backward compatibility. However, I noticed that the hint suffix I added is currently the only place in the codebase that uses this pattern. This made me hesitate about whether modifying the core matching logic for just one case is appropriate. That said, if ArchUnit is open to allowing such hint suffixes for better developer experience, I believe this would be a meaningful change. (This issue has been raised for over 3 years, and I think adding hints is an appropriate way to resolve it.)
The second option is to make hints configurable via archunit.properties. This would give users a choice, but it wouldn't effectively help most developers who are experiencing this problem—since most of them wouldn't even know this configuration exists.
I chose the first option. FuzzyViolationLineMatcher already ignores line numbers and $N suffixes, and I believe hints are conceptually similar. Additionally, any future hint suffixes would be handled automatically.
I look forward to hearing your thoughts.