Skip to content

SONARJAVA-6791 Handle @Qualifier annotations on autowired dependencies in BeanDefinitionGatherer - #5936

Open
NoemieBenard wants to merge 7 commits into
epic-SONARJAVA-6237from
nb/sonarjava-6791-qualifier-handling
Open

SONARJAVA-6791 Handle @Qualifier annotations on autowired dependencies in BeanDefinitionGatherer#5936
NoemieBenard wants to merge 7 commits into
epic-SONARJAVA-6237from
nb/sonarjava-6791-qualifier-handling

Conversation

@NoemieBenard

@NoemieBenard NoemieBenard commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary by Gitar

  • Spring context model updates:
    • Introduced BeanDependency record to capture dependency type and optional @Qualifier value.
    • Updated BeanDefinitionGatherer to extract @Qualifier annotations on fields, constructors, and @Bean methods.

This will update automatically on new commits.

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6791

@NoemieBenard
NoemieBenard marked this pull request as ready for review August 18, 2026 12:11
@NoemieBenard
NoemieBenard force-pushed the nb/sonarjava-6791-qualifier-handling branch from cf0c655 to ef6cdf8 Compare August 18, 2026 15:42
NoemieBenard and others added 2 commits August 18, 2026 17:49
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
/** Names of other beans this bean depends on. */
private List<String> dependingBeans;
/** Dependencies this bean requires, each capturing the required type and an optional {@code @Qualifier} name. */
private List<BeanDependency> dependingBeans;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list can be rather large, and we need to have fast look-up to be able to define needed dependency. I'd suggest to think about using Map / Set, the key should be the bean's name. In Spring framework this should be qualifier or type.

Comment on lines +295 to 309
private static Map<String, String> collectAutowiredDependencies(ClassTree classTree) {
Map<String, String> deps = new LinkedHashMap<>();
for (Tree member : classTree.members()) {
if (member.is(Tree.Kind.VARIABLE)) {
VariableTree field = (VariableTree) member;
if (member instanceof VariableTree field) {
if (field.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) {
deps.add(field.symbol().type().fullyQualifiedName());
String typeFqn = field.symbol().type().fullyQualifiedName();
deps.put(dependencyKey(field.simpleName().name(), extractQualifier(field.symbol().metadata())), typeFqn);
}
} else if (member.is(Tree.Kind.CONSTRUCTOR, Tree.Kind.METHOD)) {
MethodTree method = (MethodTree) member;
if (method.symbol().metadata().isAnnotatedWith(SpringUtils.AUTOWIRED_ANNOTATION)) {
method.parameters().stream()
.map(p -> p.symbol().type().fullyQualifiedName())
.forEach(deps::add);
deps.putAll(parameterDependencies(method));
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Edge Case: Map keyed by field/param name silently drops colliding dependencies

Switching dependingBeans from a List to a Map keyed by qualifier-or-field/param-name means two distinct dependencies that share a key collapse into one, silently losing a dependency the old list preserved. Collisions are reachable: e.g. an @Autowired field named service plus an @Autowired setter/constructor param also named service, or a @Qualifier("x") dependency alongside another dependency whose field name is x (putAll/put overwrite). Consider keying by a guaranteed-unique value or keeping a collection of dependencies per key if multiple injection points must be retained.

Allow multiple dependency types per key to avoid silently dropping colliding entries.:

// If multiple deps can legitimately share a name, keep them all, e.g.:
Map<String, List<String>> deps = new LinkedHashMap<>();
...
deps.computeIfAbsent(dependencyKey(name, qualifier), k -> new ArrayList<>()).add(typeFqn);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@sonarqube-next

Copy link
Copy Markdown
Contributor

@gitar-bot

gitar-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
CI failed: Maven build failures in the integration test module (it-java-ruling) due to the SonarQube Maven plugin requiring Java 21+ while executing on Java 17.

Overview

1 unique failure pattern found across 3 analyzed logs. The integration tests fail during the SonarQube analysis goal because the scanner plugin requires Java 21 or newer, but the build environment is running on Java 17.

Failures

SonarQube Plugin Java Version Mismatch (confidence: high)

  • Type: build
  • Affected jobs: 96448103642, 95633740800
  • Related to change: yes
  • Root cause: The SonarQube Maven plugin (sonar-maven-plugin:4.0.0.4121:sonar) failed because it requires Java 21+ but was invoked with Java 17 during the it-java-ruling integration tests.
  • Suggested fix: Update the CI environment or Maven toolchain configuration for the integration tests (its/ruling) to use Java 21 or newer.

Summary

  • Change-related failures: 1 build failure related to Java runtime version incompatibility with the SonarQube scanner plugin.
  • Infrastructure/flaky failures: 0
  • Recommended action: Ensure Java 21+ is available and configured as the runtime for the integration test modules in the CI pipeline.
Code Review ⚠️ Changes requested 4 resolved / 5 findings

Adds @Qualifier annotation handling to BeanDefinitionGatherer and refactors dependency tracking with a Map. The map implementation uses field or parameter names as keys, which silently drops colliding dependencies and loses data previously preserved by the list.

⚠️ Edge Case: Map keyed by field/param name silently drops colliding dependencies

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:295-309 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:322-324

Switching dependingBeans from a List to a Map keyed by qualifier-or-field/param-name means two distinct dependencies that share a key collapse into one, silently losing a dependency the old list preserved. Collisions are reachable: e.g. an @Autowired field named service plus an @Autowired setter/constructor param also named service, or a @Qualifier("x") dependency alongside another dependency whose field name is x (putAll/put overwrite). Consider keying by a guaranteed-unique value or keeping a collection of dependencies per key if multiple injection points must be retained.

Allow multiple dependency types per key to avoid silently dropping colliding entries.
// If multiple deps can legitimately share a name, keep them all, e.g.:
Map<String, List<String>> deps = new LinkedHashMap<>();
...
deps.computeIfAbsent(dependencyKey(name, qualifier), k -> new ArrayList<>()).add(typeFqn);
✅ 4 resolved
Quality: Redundant same-package import breaks import ordering

📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:20
import org.sonar.java.model.springcontext.BeanDependency; is redundant because the test class is in the same package (org.sonar.java.model.springcontext), and it is inserted between the two java.util.* imports, breaking alphabetical ordering. Remove this import line.

Quality: Duplicated BeanData construction into two parallel lists

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:116-127 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:304-315
In both visitNode and collectBeanMethod an identical new BeanData(...) is built and added to collectedBeans and again to beansCollectedAtFileLevel. The two lists must stay in sync; a future edit to one construction site that forgets the other would silently make the cache write diverge from the in-memory module data. Build the BeanData once into a local variable and add the same instance to both lists to remove the duplication and the sync hazard.

Bug: Qualifier values written raw to cache break serialization

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:158-171 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:231-237 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:322-336
The removed serializeDependency/deserializeDependency Base64-encoded the qualifier before writing it to the cache. The new format writes key:value with the key (a @Qualifier value, i.e. an arbitrary annotation string) unencoded. If a qualifier contains : (DEP_KEY_VALUE_SEPARATOR), , (DEP_SEPARATOR), | (FIELD_SEPARATOR) or a newline (BEAN_SEPARATOR), serialization/deserialization corrupts: e.g. @Qualifier("a:b") deserializes with key a and a truncated value, and @Qualifier("a,b") makes entry.indexOf(...) return -1 and throws in substring, discarding the whole file's cache. Restore Base64 encoding for the key (and value if needed) so arbitrary qualifier strings round-trip safely.

Quality: Doc comment says decapitalized class name, code uses field name

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionHolder.java:54-58 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:301 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:317 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:322-324
The new Javadoc states the key is "the default Spring bean name (simple class name, decapitalized)" when no @Qualifier is present, but dependencyKey actually uses field.simpleName().name() / parameter name. These differ whenever a field/param is named differently from its decapitalized type (e.g. ApplicationContext ctx). Align the comment with the implementation (or vice versa) to avoid confusion for downstream consumers.

🤖 Prompt for agents
Code Review: Adds `@Qualifier` annotation handling to `BeanDefinitionGatherer` and refactors dependency tracking with a `Map`. The map implementation uses field or parameter names as keys, which silently drops colliding dependencies and loses data previously preserved by the list.

1. ⚠️ Edge Case: Map keyed by field/param name silently drops colliding dependencies
   Files: java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:295-309, java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:322-324

   Switching `dependingBeans` from a `List` to a `Map` keyed by qualifier-or-field/param-name means two distinct dependencies that share a key collapse into one, silently losing a dependency the old list preserved. Collisions are reachable: e.g. an `@Autowired` field named `service` plus an `@Autowired` setter/constructor param also named `service`, or a `@Qualifier("x")` dependency alongside another dependency whose field name is `x` (`putAll`/`put` overwrite). Consider keying by a guaranteed-unique value or keeping a collection of dependencies per key if multiple injection points must be retained.

   Fix (Allow multiple dependency types per key to avoid silently dropping colliding entries.):
   // If multiple deps can legitimately share a name, keep them all, e.g.:
   Map<String, List<String>> deps = new LinkedHashMap<>();
   ...
   deps.computeIfAbsent(dependencyKey(name, qualifier), k -> new ArrayList<>()).add(typeFqn);

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.
Unblock → Override a blocking verdict and allow merging.

Comment with these commands to change the behavior for this request:

Auto-apply Compact Unblock
gitar auto-apply:on         
gitar display:verbose         
gitar unblock         

Was this helpful? React with 👍 / 👎 | Gitar

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.

2 participants