SONARJAVA-6489: Implement rule S8909 - Quarkus CacheKeyGenerator should be instantiable#5699
SONARJAVA-6489: Implement rule S8909 - Quarkus CacheKeyGenerator should be instantiable#5699romainbrenguier wants to merge 16 commits into
Conversation
This commit contains partial work that failed to complete. Continuing with SA-CI and PR creation.
- Fixed CacheKeyGeneratorInstantiableCheck implementation to properly detect issues - Added mock-up CacheKeyGenerator interface for testing instead of external dependency - Moved test sample to compiling sources with proper mock-ups - Test now passes successfully Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Generated HTML and JSON rule metadata from RSPEC - Added S8909 to Sonar way profile - Rule detects non-instantiable CacheKeyGenerator implementations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Comment: <details open> <summary><b>Code Review</b> <kbd>⚠️ Changes requested</kbd> <kbd>1 resolved / 3 findings</kbd></summary> Implements rule S8909 for Quarkus CacheKeyGenerator instantiability, but introduces a redundant dependency conflict and incorrectly evaluates implicit package-private constructors as public. <details> <summary>⚠️ <b>Quality:</b> Redundant quarkus-cache dependency conflicts with local mock interface</summary> <kbd>📄 <a href="https://github.com/SonarSource/sonar-java/pull/5699/files#diff-65b481d7948308268695c9b6ced91981e76b2f17492fa537fa94e0e1acc6057dR189-R194">java-checks-test-sources/default/pom.xml:189-194</a></kbd> <kbd>📄 <a href="https://github.com/SonarSource/sonar-java/pull/5699/files#diff-8eba708ee0e692d479690a9177582d8d3295836ae8036bddc835e583fd5f26c9R8-R9">java-checks-test-sources/default/src/main/java/io/quarkus/cache/CacheKeyGenerator.java:8-9</a></kbd> The PR adds the real `io.quarkus:quarkus-cache:3.2.0.Final` dependency in pom.xml AND also adds a hand-written mock `io/quarkus/cache/CacheKeyGenerator.java` with the same fully-qualified name. The PR description states the mock was created "avoiding external dependency", which directly contradicts also adding the dependency. Having both a local source file and a jar that define `io.quarkus.cache.CacheKeyGenerator` on the same compile path is at best redundant and can cause type-resolution ambiguity / duplicate-type issues in the test-sources module. Pick one approach: either rely on the real dependency and delete the mock, or keep the mock and drop the new dependency from pom.xml (consistent with the stated intent). Note the real Quarkus `CacheKeyGenerator` also declares a default `boolean generationGroup()` method, so the mock is not a faithful copy. </details> <details> <summary>💡 <b>Edge Case:</b> Implicit constructor of package-private class treated as public</summary> <kbd>📄 <a href="https://github.com/SonarSource/sonar-java/pull/5699/files#diff-163bcab034b13aa1549ddbee8f2fe2cde6ae0fa4bc77bce4da28044ef134cdf5R86-R98">java-checks/src/main/java/org/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java:86-98</a></kbd> <kbd>📄 <a href="https://github.com/SonarSource/sonar-java/pull/5699/files#diff-163bcab034b13aa1549ddbee8f2fe2cde6ae0fa4bc77bce4da28044ef134cdf5R49-R51">java-checks/src/main/java/org/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java:49-51</a></kbd> `hasPublicNoArgsConstructor` returns true when the no-arg constructor's `declaration() == null` (implicit default constructor), regardless of visibility. An implicit default constructor inherits the class's access level, so for a package-private (or private nested) class implementing `CacheKeyGenerator` the implicit constructor is NOT public. The issue message explicitly tells users to add a "public no-args constructor", yet such a class is reported compliant — an inconsistency that can produce a false negative if Quarkus requires a public constructor for non-CDI instantiation. Consider also checking the effective visibility of the implicit constructor (e.g. require the class itself to be public), or align the message with the actual check. Also note records/enums implementing the interface are not covered since `nodesToVisit()` only registers `Tree.Kind.CLASS`. </details> <details> <summary><kbd>✅ 1 resolved</kbd></summary> <details> <summary>✅ <b>Quality:</b> Missing S8909 rule metadata (HTML + JSON) breaks build</summary> > <kbd>📄 java-checks/src/main/java/org/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java:30</kbd> > The new rule `CacheKeyGeneratorInstantiableCheck` is annotated with `@Rule(key = "S8909")` and is auto-registered via `CheckListGenerator`, but no metadata files exist for it. `GeneratedCheckListTest` asserts that both `org/sonar/l10n/java/rules/java/S8909.html` and `S8909.json` exist for every registered rule (see GeneratedCheckListTest.java:137-142), and a Glob confirms neither file is present. Without these the rule has no description, no severity/type/profile assignment, and the test suite (hence the build) will fail. Add `S8909.html` (rule description) and `S8909.json` (metadata: title, type, status, tags, remediation, default severity, and SonarWay profile membership) under `java-checks/src/main/resources/org/sonar/l10n/java/rules/java/`. </details> </details> <details> <summary>🤖 <b>Prompt for agents</b></summary> ```` Code Review: Implements rule S8909 for Quarkus CacheKeyGenerator instantiability, but introduces a redundant dependency conflict and incorrectly evaluates implicit package-private constructors as public. 1.⚠️ Quality: Redundant quarkus-cache dependency conflicts with local mock interface Files: java-checks-test-sources/default/pom.xml:189-194, java-checks-test-sources/default/src/main/java/io/quarkus/cache/CacheKeyGenerator.java:8-9 The PR adds the real `io.quarkus:quarkus-cache:3.2.0.Final` dependency in pom.xml AND also adds a hand-written mock `io/quarkus/cache/CacheKeyGenerator.java` with the same fully-qualified name. The PR description states the mock was created "avoiding external dependency", which directly contradicts also adding the dependency. Having both a local source file and a jar that define `io.quarkus.cache.CacheKeyGenerator` on the same compile path is at best redundant and can cause type-resolution ambiguity / duplicate-type issues in the test-sources module. Pick one approach: either rely on the real dependency and delete the mock, or keep the mock and drop the new dependency from pom.xml (consistent with the stated intent). Note the real Quarkus `CacheKeyGenerator` also declares a default `boolean generationGroup()` method, so the mock is not a faithful copy. 2. 💡 Edge Case: Implicit constructor of package-private class treated as public Files: java-checks/src/main/java/org/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java:86-98, java-checks/src/main/java/org/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java:49-51 `hasPublicNoArgsConstructor` returns true when the no-arg constructor's `declaration() == null` (implicit default constructor), regardless of visibility. An implicit default constructor inherits the class's access level, so for a package-private (or private nested) class implementing `CacheKeyGenerator` the implicit constructor is NOT public. The issue message explicitly tells users to add a "public no-args constructor", yet such a class is reported compliant — an inconsistency that can produce a false negative if Quarkus requires a public constructor for non-CDI instantiation. Consider also checking the effective visibility of the implicit constructor (e.g. require the class itself to be public), or align the message with the actual check. Also note records/enums implementing the interface are not covered since `nodesToVisit()` only registers `Tree.Kind.CLASS`. ```` </details> </details> <details> <summary><b>Options</b> </summary> <kbd>Auto-apply is off</kbd> → Gitar will not commit updates to this branch.<br><kbd>Display: compact</kbd> → Showing less information.<br><kbd>Unblock</kbd> → Override a blocking verdict and allow merging. Comment with these commands to change: <table> <tr> <td><kbd>Auto-apply</kbd></td> <td><kbd>Compact</kbd></td> <td><kbd>Unblock</kbd></td> </tr> <tr> <td> ``` gitar auto-apply:on ``` </td> <td> ``` gitar display:verbose ``` </td> <td> ``` gitar unblock ``` </td> </tr> </table> </details> <sub>Was this helpful? React with 👍 / 👎 | [Gitar](https://gitar.ai)</sub>
- Fix Noncompliant marker length in CacheKeyGeneratorInstantiableCheckSample (50 carets -> 45 carets to match class name)
Add expected autoscan diff for new rule S8909 (3 false positives in autoscan mode) and update S6813 false negatives count (65 -> 68). Update test to expect 11 rules causing FPs instead of 10. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… account for rule S8909 causing false positives
…6489-S8909 # Conflicts: # its/autoscan/src/test/resources/autoscan/diffs/diff_S6813.json # sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json
joke1196
left a comment
There was a problem hiding this comment.
This looks good to me. As I am not familiar with the analyzer I am not fully sure what the pitfalls could be, but here the implementation and the test make sense
| softly.assertThat(newDiffs).containsExactlyInAnyOrderElementsOf(knownDiffs.values()); | ||
| softly.assertThat(newTotal).isEqualTo(knownTotal); | ||
| softly.assertThat(rulesCausingFPs).hasSize(11); | ||
| softly.assertThat(rulesCausingFPs).hasSize(12); |
There was a problem hiding this comment.
Why? I never had to bump this one so im curious to know the reason
There was a problem hiding this comment.
The new rule S8909 introduced in this PR causes 3 false positives in AutoScan mode (analysis without full bytecode/dependencies), as documented in its/autoscan/src/test/resources/autoscan/diffs/diff_S8909.json. This makes it the 12th rule that produces false positives in AutoScan mode, so the assertion needed to be updated from 11 to 12.
The AutoScan test compares issues found with complete semantic analysis (Maven build with bytecode) versus limited semantic analysis (AutoScan mode), and tracks which rules cause false positives without full bytecode information.
There was a problem hiding this comment.
Apparently the new rule has some false positives in auto-scan mode. I'll try to fix them.
|
|
||
| private static final String CACHE_KEY_GENERATOR = "io.quarkus.cache.CacheKeyGenerator"; | ||
|
|
||
| private static final List<String> CDI_SCOPE_ANNOTATIONS = Arrays.asList( |
|
|
||
| @Override | ||
| public List<Tree.Kind> nodesToVisit() { | ||
| return Collections.singletonList(Tree.Kind.CLASS); |
| if (!isApplicableClass(classTree)) { | ||
| return; | ||
| } | ||
| if (hasCdiScopeAnnotation(classTree) || hasPublicNoArgsConstructor(classTree)) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Why dont we merge those ifs
| private static boolean hasPublicNoArgsConstructor(ClassTree classTree) { | ||
| Collection<Symbol> constructors = classTree.symbol().lookupSymbols("<init>"); | ||
| var noArgConstructor = constructors.stream() | ||
| .filter(CacheKeyGeneratorInstantiableCheck::isNoArgConstructor) | ||
| .findFirst(); | ||
|
|
||
| if (noArgConstructor.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| Symbol constructor = noArgConstructor.get(); | ||
| if (constructor.declaration() == null) { | ||
| return classTree.symbol().isPublic(); | ||
| } | ||
| return constructor.isPublic(); | ||
| } | ||
|
|
||
| private static boolean isNoArgConstructor(Symbol constructor) { | ||
| return ((Symbol.MethodSymbol) constructor).parameterTypes().isEmpty(); | ||
| } |
There was a problem hiding this comment.
A bit verbose, I propose
| private static boolean hasPublicNoArgsConstructor(ClassTree classTree) { | |
| Collection<Symbol> constructors = classTree.symbol().lookupSymbols("<init>"); | |
| var noArgConstructor = constructors.stream() | |
| .filter(CacheKeyGeneratorInstantiableCheck::isNoArgConstructor) | |
| .findFirst(); | |
| if (noArgConstructor.isEmpty()) { | |
| return false; | |
| } | |
| Symbol constructor = noArgConstructor.get(); | |
| if (constructor.declaration() == null) { | |
| return classTree.symbol().isPublic(); | |
| } | |
| return constructor.isPublic(); | |
| } | |
| private static boolean isNoArgConstructor(Symbol constructor) { | |
| return ((Symbol.MethodSymbol) constructor).parameterTypes().isEmpty(); | |
| } | |
| private static boolean hasPublicNoArgsConstructor(ClassTree classTree) { | |
| Collection<Symbol> constructors = classTree.symbol().lookupSymbols("<init>"); | |
| return constructors.stream() | |
| .map(Symbol.MethodSymbol.class::cast) | |
| .filter(CacheKeyGeneratorInstantiableCheck::isNoArgConstructor) | |
| .findFirst() | |
| .map(Symbol::isPublic) | |
| .orElse(classTree.symbol().isPublic()); | |
| } | |
| private static boolean isNoArgConstructor(Symbol.MethodSymbol constructor) { | |
| return constructor.parameterTypes().isEmpty(); | |
| } |
…/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java Comment: List.of
…/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java Comment: List.of
…/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java Comment: Why dont we merge those `if`s
…/sonar/java/checks/quarkus/CacheKeyGeneratorInstantiableCheck.java
Comment: A bit verbose, I propose
```suggestion
private static boolean hasPublicNoArgsConstructor(ClassTree classTree) {
Collection<Symbol> constructors = classTree.symbol().lookupSymbols("<init>");
return constructors.stream()
.map(Symbol.MethodSymbol.class::cast)
.filter(CacheKeyGeneratorInstantiableCheck::isNoArgConstructor)
.findFirst()
.map(Symbol::isPublic)
.orElse(classTree.symbol().isPublic());
}
private static boolean isNoArgConstructor(Symbol.MethodSymbol constructor) {
return constructor.parameterTypes().isEmpty();
}
```
| private static boolean matchesAnnotation(AnnotationTree annotationTree, String fullyQualifiedName) { | ||
| String simpleName = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.') + 1); | ||
| Tree annotationType = annotationTree.annotationType(); | ||
| return switch (annotationType.kind()) { | ||
| case IDENTIFIER -> ((IdentifierTree) annotationType).name().equals(simpleName); | ||
| case MEMBER_SELECT -> { | ||
| MemberSelectExpressionTree memberSelect = (MemberSelectExpressionTree) annotationType; | ||
| yield memberSelect.identifier().name().equals(simpleName) || memberSelect.expression().symbolType().is(fullyQualifiedName); | ||
| } | ||
| default -> false; | ||
| }; | ||
| } |
There was a problem hiding this comment.
💡 Edge Case: Simple-name annotation match may suppress issues for unrelated annotations
matchesAnnotation falls back to matching only the annotation's simple name for the IDENTIFIER case (and as one OR branch for MEMBER_SELECT). Because all entries in CDI_SCOPE_ANNOTATIONS share common simple names (e.g. ApplicationScoped, Dependent, RequestScoped), any annotation with one of those simple names — regardless of its actual package — will be treated as a CDI scope annotation and suppress the issue. This is a reasonable tradeoff to avoid autoscan false positives when semantics are unresolved, but in fully-resolved compilations it could produce a false negative if a class uses an unrelated @Dependent/@RequestScoped from a different library and still lacks a real CDI scope. Consider preferring the resolved symbolType().is(fqn) check and only relying on the simple-name match when the type cannot be resolved (unknown type), to keep the fully-resolved path precise.
Was this helpful? React with 👍 / 👎
Code Review 👍 Approved with suggestions 3 resolved / 4 findingsImplements rule S8909 to detect non-instantiable Quarkus CacheKeyGenerator implementations, resolving metadata and dependency issues. Ensure the simple-name annotation matcher is narrowed to prevent false suppressions from unrelated annotations. 💡 Edge Case: Simple-name annotation match may suppress issues for unrelated annotations
✅ 3 resolved✅ Quality: Missing S8909 rule metadata (HTML + JSON) breaks build
✅ Quality: Redundant quarkus-cache dependency conflicts with local mock interface
✅ Edge Case: Implicit constructor of package-private class treated as public
🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|


Summary
Implements rule S8909 to detect Quarkus
CacheKeyGeneratorimplementations that cannot be instantiated by the CDI container.Changes
CacheKeyGeneratorInstantiableCheckto detect classes implementingCacheKeyGeneratorthat:@ApplicationScoped,@Dependent, etc.)CacheKeyGeneratorinterface for testing (avoiding external dependency)Test Coverage
The rule detects:
The rule correctly ignores:
🤖 Generated with Claude Code