Skip to content

SONARJAVA-6421 Implement S9352: Bean autowiring ambiguity should be resolved using "@Qualifier" or "@Primary" - #6044

Closed
NoemieBenard wants to merge 22 commits into
epic-SONARJAVA-6237from
nb/sonarjava-6421-ambiguous-dependency-rule
Closed

SONARJAVA-6421 Implement S9352: Bean autowiring ambiguity should be resolved using "@Qualifier" or "@Primary"#6044
NoemieBenard wants to merge 22 commits into
epic-SONARJAVA-6237from
nb/sonarjava-6421-ambiguous-dependency-rule

Conversation

@NoemieBenard

@NoemieBenard NoemieBenard commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary by Gitar

  • New checks:
    • Implemented Spring rule S9352 to detect ambiguous bean autowiring without @Qualifier or @Primary
  • Model & Index updates:
    • Added TypeToDependenciesIndex and integrated it into BeanDefinitionGatherer to track injection points

This will update automatically on new commits.

@hashicorp-vault-sonar-prod

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

Copy link
Copy Markdown
Contributor

SONARJAVA-6421

@datadog-sonarsource

This comment has been minimized.

TypeToBeanNamesIndex typeToBeanNamesIndex = model.getTypeToBeanNamesIndex();

List<AmbiguousDependency> ambiguousDependencies = new ArrayList<>();
for (BeanDefinitionHolder bean : registry.getAll()) {

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.

Here makes sense to iterate in outer loop through TypeToBeanNamesIndex map to inspect types, then for each type iterate through specific beans using BeanDefinitionRegistry.

// itself), otherwise at least one injection point remains ambiguous.
return candidates.size() <= 1
|| injectionPointNames.stream().allMatch(name -> matchesCandidate(name, candidates, registry))
|| hasExactlyOnePrimaryCandidate(candidates, registry);

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.

Here it's very important to check for having Profile set for the candidates. Candidates having configured profile should be excluded from consideration as possibly mutually exclusive.

if (!isResolved(candidates, dependency.getValue(), registry)) {
// A @Fallback candidate is only a real contender when it is the sole remaining one; otherwise it is
// ignored by Spring, so the effective candidates are whichever bean(s) are not marked @Fallback.
Set<String> effectiveCandidates = excludeFallbackCandidates(candidates, registry);

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.

Fallback annotation is modern (introduced in Spring Framework 6.2) and thus currently low used. We can add it later in follow-up ticket.

// ignored by Spring, so the effective candidates are whichever bean(s) are not marked @Fallback.
Set<String> effectiveCandidates = excludeFallbackCandidates(candidates, registry);
if (effectiveCandidates.size() > 1) {
ambiguousDependencies.add(new AmbiguousDependency(bean.getLocation(), message(requiredType, effectiveCandidates)));

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.

As we've discussed, another alternative is to raise issues on project level instead. Drawback is that we won't see it in SonarLint. So the current approach looks appropriate.

@Override
public void execute(SensorContext context) {
// Nothing to do for now
reportAmbiguousDependencies(context);

@asya-vorobeva asya-vorobeva Aug 28, 2026

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.

Here we have in mind that we'll implement multiple checks which will be called here. So I'd recommend to use Strategy design pattern.
For it we need to create some common interface with some execute method which all the needed checks will implement, and common record with information needed to create an issue (generalize AmbiguousDependency).
Then we can inject all of them into this sensor (injection mechanism is on you) and run execute method for all of them in a loop.

@asya-vorobeva asya-vorobeva left a comment

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.

To properly test such checks, please use scanner-integration-framework capabilities provided in this ticket.

@NoemieBenard
NoemieBenard marked this pull request as ready for review September 1, 2026 12:27
@NoemieBenard
NoemieBenard force-pushed the nb/sonarjava-6421-ambiguous-dependency-rule branch from 64fade8 to c946cf0 Compare September 1, 2026 12:43
@Configuration
public class CacheConfig {

@Bean

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.

Let's configure these beans in different modules / files to be sure that it recognizes such cases.

+ " disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".";

@Override
public List<SpringContextIssue> execute(SpringContextModel model) {

@asya-vorobeva asya-vorobeva Sep 1, 2026

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 method is super-hard to read. Let's add Javadoc comment describing how it acts. If you will fill that it's not enough, add additional internal comments in the method.

@asya-vorobeva asya-vorobeva left a comment

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 PR became too huge. Would be great to separate it into several ones: one with SpringModel refactoring, another with rule implementation (including tests). Or at least to re-group-squash commits to have clear history and when merging not do squash.

}

private static boolean hasProfile(BeanDefinitionRegistry registry, String beanName) {
return registry.getByName(beanName).stream().anyMatch(bean -> bean.getProfiles() != null);

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.

I couldn't find a place where we add profiles information to the bean holder. Probably we missed it? Should be done in BeanDefinitionGatherer.

import org.sonar.java.test.classpath.TestClasspathUtils;

@Execution(ExecutionMode.CONCURRENT)
@Execution(ExecutionMode.SAME_THREAD)

@gitar-bot gitar-bot Bot Sep 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: junit-platform.properties still documents/enables removed CONCURRENT mode

ScannerIntegrationAbstractTest is now @Execution(ExecutionMode.SAME_THREAD), so no test class in its/scanner-integration-tests runs concurrently, yet junit-platform.properties still enables parallel execution and its comment explicitly states "Subclasses of ScannerIntegrationAbstractTest are annotated with @execution(CONCURRENT); these properties activate it" — the only two subclasses (AmbiguousDependencyCrossModuleTest, SpringBeansShouldBeAccessibleCrossModuleTest) inherit SAME_THREAD. A maintainer reading the properties file will believe these ITs run in parallel and may "restore" concurrency, re-introducing whatever flakiness this commit removed. Update the comment (and drop the now-inert parallel properties, or note why they are kept) so config and code agree.

Align the properties file with the SAME_THREAD annotation and explain why.:

# Parallel execution is intentionally disabled: ScannerIntegrationAbstractTest is
# annotated with @Execution(SAME_THREAD) because the scanner runs share static state
# (plugin location, runner config) and must not overlap.
junit.jupiter.execution.parallel.enabled=false

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown
CI failed: All CI workflows completed successfully with no actual build or test errors detected.

Overview

All 25 analyzed CI job logs show successful execution of the workflow, including the autoscan diff report generation and artifact upload. Only standard runner deprecation warnings and informational log messages were present.

Failures

None Detected (confidence: high)

  • Type: other
  • Affected jobs: none
  • Related to change: no
  • Root cause: No error or failure occurred during the CI execution.
  • Suggested fix: No action required.

Summary

  • Change-related failures: 0 failures
  • Infrastructure/flaky failures: 0 failures
  • Recommended action: None. The builds and tests passed successfully.
Code Review 👍 Approved with suggestions 13 resolved / 14 findings

Implements Spring rule S9352 to detect ambiguous bean autowiring without @Qualifier or @Primary, with TypeToDependenciesIndex integration for tracking injection points. Multiple ambiguity detection edge cases were resolved during review.

Consider updating junit-platform.properties to reflect that ScannerIntegrationAbstractTest and its subclasses now run in SAME_THREAD mode — the file currently documents parallel execution that no longer occurs, which may mislead maintainers into re-enabling concurrency.

💡 Quality: junit-platform.properties still documents/enables removed CONCURRENT mode

📄 its/scanner-integration-tests/src/test/java/org/sonar/java/it/ScannerIntegrationAbstractTest.java:56

ScannerIntegrationAbstractTest is now @Execution(ExecutionMode.SAME_THREAD), so no test class in its/scanner-integration-tests runs concurrently, yet junit-platform.properties still enables parallel execution and its comment explicitly states "Subclasses of ScannerIntegrationAbstractTest are annotated with @Execution(CONCURRENT); these properties activate it" — the only two subclasses (AmbiguousDependencyCrossModuleTest, SpringBeansShouldBeAccessibleCrossModuleTest) inherit SAME_THREAD. A maintainer reading the properties file will believe these ITs run in parallel and may "restore" concurrency, re-introducing whatever flakiness this commit removed. Update the comment (and drop the now-inert parallel properties, or note why they are kept) so config and code agree.

Align the properties file with the SAME_THREAD annotation and explain why.
# Parallel execution is intentionally disabled: ScannerIntegrationAbstractTest is
# annotated with @Execution(SAME_THREAD) because the scanner runs share static state
# (plugin location, runner config) and must not overlap.
junit.jupiter.execution.parallel.enabled=false
✅ 13 resolved
Bug: Rule flags its own documented @fallback compliant example

📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9352.html:122-136 📄 java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/s9352/FallbackRegularComponent.java:7-10 📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json:541-542
The shipped description presents @Fallback as a third valid fix (diff-id 3 "Compliant solution"), but the implementation ignores @Fallback entirely — only @Primary, qualifier and name matches are consulted — as the new non-compiling samples acknowledge. Running the rule on the doc's own compliant snippet (two DataSource beans, one @Fallback, field dataSource matching neither bean name) raises an issue, so the rule contradicts its documentation on a default-profile Critical rule. Either treat @Fallback candidates as excluded from the candidate pool before enabling the rule in Sonar way, or remove the @Fallback fix section from the description.

Bug: One resolved injection point hides other ambiguous ones

📄 java-checks/src/main/java/org/sonar/java/checks/spring/AmbiguousDependencyCheck.java:56-70
collectAutowiredDependencies merges every injection point of the same type into a single name set per type, and isAmbiguous clears the whole type as soon as one of those names matches a candidate (candidates.stream().noneMatch(injectionPointNames::contains)). For @Service class C { @Autowired @Qualifier("componentOne") ApplicationContextAware a; @Autowired ApplicationContextAware b; } the set is {componentOne, b}, componentOne matches a candidate, so the genuinely ambiguous field b is never reported even though Spring fails to start. Require every recorded name to resolve instead of any one of them.

Bug: False positive when @qualifier is declared on the bean itself

📄 java-checks/src/main/java/org/sonar/java/checks/spring/AmbiguousDependencyCheck.java:67-71
The check compares injection-point qualifier values against bean names only (typeToBeanNamesIndex.getNamesForType, populated from extractBeanName/defaultBeanName). Spring also resolves @Autowired @Qualifier("main") Foo f against a bean declared @Component @Qualifier("main") (or a @Bean method annotated with @Qualifier), which the gatherer never records; such code is reported as ambiguous although the context starts fine. Since S9352 is enabled in Sonar way with Critical/HIGH reliability, this is a user-visible false positive — index bean-side qualifier values in TypeToBeanNamesIndex (or skip injection points whose qualifier matches no known bean name at all).

Edge Case: Two @primary candidates silently treated as resolved

📄 java-checks/src/main/java/org/sonar/java/checks/spring/AmbiguousDependencyCheck.java:67-75
isAmbiguous only asks whether any candidate is @Primary; when two beans of the same type are both annotated @Primary, Spring still throws NoUniqueBeanDefinitionException, yet the check reports nothing. Count the primary candidates and keep the dependency ambiguous unless exactly one is primary.

Quality: Two-candidate fallback test passes even if exclusion is broken

📄 java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java:86-92
fallback_candidate_does_not_resolve_ambiguity_with_two_other_candidates only asserts hasSize(1), which holds whether the @Fallback bean is excluded (2 candidates reported) or not (3 candidates reported) — so it does not actually exercise excludeFallbackCandidates. This matters because detection relies on matching the fully-qualified unresolved annotation name in non-compiling sources (spring-context 5.3.31 has no Fallback), which is exactly what the assertion should pin down. Assert the issue message so that the fallback bean is verified to be absent from the reported candidate list.

...and 8 more resolved from earlier reviews

🤖 Prompt for agents
Code Review: Implements Spring rule S9352 to detect ambiguous bean autowiring without `@Qualifier` or `@Primary`, with `TypeToDependenciesIndex` integration for tracking injection points. Multiple ambiguity detection edge cases were resolved during review.
  
  Consider updating `junit-platform.properties` to reflect that `ScannerIntegrationAbstractTest` and its subclasses now run in `SAME_THREAD` mode — the file currently documents parallel execution that no longer occurs, which may mislead maintainers into re-enabling concurrency.

1. 💡 Quality: junit-platform.properties still documents/enables removed CONCURRENT mode
   Files: its/scanner-integration-tests/src/test/java/org/sonar/java/it/ScannerIntegrationAbstractTest.java:56

   `ScannerIntegrationAbstractTest` is now `@Execution(ExecutionMode.SAME_THREAD)`, so no test class in `its/scanner-integration-tests` runs concurrently, yet `junit-platform.properties` still enables parallel execution and its comment explicitly states "Subclasses of ScannerIntegrationAbstractTest are annotated with @Execution(CONCURRENT); these properties activate it" — the only two subclasses (AmbiguousDependencyCrossModuleTest, SpringBeansShouldBeAccessibleCrossModuleTest) inherit SAME_THREAD. A maintainer reading the properties file will believe these ITs run in parallel and may "restore" concurrency, re-introducing whatever flakiness this commit removed. Update the comment (and drop the now-inert parallel properties, or note why they are kept) so config and code agree.

   Fix (Align the properties file with the SAME_THREAD annotation and explain why.):
   # Parallel execution is intentionally disabled: ScannerIntegrationAbstractTest is
   # annotated with @Execution(SAME_THREAD) because the scanner runs share static state
   # (plugin location, runner config) and must not overlap.
   junit.jupiter.execution.parallel.enabled=false

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.

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

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

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqube-next

sonarqube-next Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Quality Gate failed Quality Gate failed

Failed conditions
Vulnerability dependency risks too severe (required < 'medium' severity)

See analysis details on SonarQube

@NoemieBenard

NoemieBenard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing this PR after splitting it into smaller ones: #6068, #6071, #6073

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