Skip to content

Conversation

@chadongmin
Copy link

@chadongmin chadongmin commented Nov 15, 2025

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?

  1. Synthetic classes: Compiler-generated (e.g., enum switch maps defined in JLS 13.1.7)

    • Marked with ACC_SYNTHETIC flag in bytecode
    • Examples: MyClass$1 from enum switches
  2. Anonymous classes: Explicitly created in source but may violate naming rules

Changes Made

Core Implementation (ArchConditions.java):

  • Added haveWithHint() method that creates custom ArchCondition with 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.java to handle hint messages
  • Updated ExamplesIntegrationTest.java to 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.SYNTHETIC available 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 SUCCESSFUL with 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

  • Code follows project style guidelines
  • Imports organized per CONTRIBUTING.md (java., javax., others, static, no wildcards)
  • All tests pass (./gradlew clean build -PallTests)
  • Commit message follows conventions (lowercase, imperative, <70 chars, with body)
  • DCO signed (git commit -s)
  • Unit tests added for new functionality
  • Integration tests updated
  • No regressions in existing tests

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: TNG#1509
Signed-off-by: chadongmin <[email protected]>
@chadongmin
Copy link
Author

@hankem
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! 😃

@hankem
Copy link
Member

hankem commented Nov 15, 2025

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.

The synthetic class hint message was using hardcoded \n line separators,
which caused test failures on Windows CI. Windows uses \r\n as line separator,
but the test framework splits violation messages using System.lineSeparator().
This mismatch prevented proper message parsing and matching in integration tests.

Changed SYNTHETIC_CLASS_HINT_MESSAGE from a static constant with hardcoded \n
to a dynamic method getSyntheticClassHintMessage() that uses System.lineSeparator().
This ensures the hint message uses the correct platform-specific line separator,
allowing tests to pass on all platforms (macOS, Linux, and Windows).

Signed-off-by: chadongmin <[email protected]>
@chadongmin
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Improve Error Message for Failures on Synthetic/Anonymous Classes

2 participants