Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3351066
Generate rule metadata
NoemieBenard Aug 24, 2026
134d12b
Create test samples
NoemieBenard Aug 26, 2026
048473e
Implement check
NoemieBenard Aug 26, 2026
6017061
Add support for `@Fallback`
NoemieBenard Aug 26, 2026
a9b173b
Fix bug when one resolved injection point hides another ambiguous one
NoemieBenard Aug 26, 2026
56b5849
Fix FP when qualifier is defined on the bean itself
NoemieBenard Aug 27, 2026
44d349a
Fix duplicate issue bug
NoemieBenard Aug 27, 2026
9558a3b
Fix FN when there are two `@Primary` candidates
NoemieBenard Aug 27, 2026
95075b9
Fix test gap and incorrect comments
NoemieBenard Aug 27, 2026
7224507
Update comments
NoemieBenard Aug 27, 2026
a0a90d3
Refactor project sensor to use Strategy design pattern
NoemieBenard Aug 28, 2026
da8696a
Create new structure for storing all collected dependencies
NoemieBenard Aug 31, 2026
14b05cc
Refactor check to use TypeToDependenciesIndex
NoemieBenard Aug 31, 2026
d4085cb
Fix incorrect condition
NoemieBenard Aug 31, 2026
2787db5
Fix test expectations
NoemieBenard Aug 31, 2026
e428ced
Fix FN when there are multiple matching candidates
NoemieBenard Sep 1, 2026
3352b3a
Remove `@Qualifier` annotation on the bean definition
NoemieBenard Sep 1, 2026
dfcb733
Add cross-module tests
NoemieBenard Sep 1, 2026
8fb01c5
Fix Spring dependency
NoemieBenard Sep 1, 2026
c946cf0
Update comments and clean up
NoemieBenard Sep 1, 2026
43ad38d
Add javadoc for execute method
NoemieBenard Sep 2, 2026
15e3ee1
Remove concurrent execution on ScannerIntegrationAbstractTest
NoemieBenard Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
import org.junit.jupiter.api.parallel.ExecutionMode;
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 👍 / 👎

public abstract class ScannerIntegrationAbstractTest {

private static FileLocation javaPluginLocation;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.it.spring;

import com.sonarsource.scanner.integrationtester.dsl.issue.TextRange;
import com.sonarsource.scanner.integrationtester.dsl.issue.TextRangeIssue;
import org.junit.jupiter.api.Test;
import org.sonar.java.it.ScannerIntegrationAbstractTest;

import java.nio.file.Path;

import static org.assertj.core.api.Assertions.assertThat;

class AmbiguousDependencyCrossModuleTest extends ScannerIntegrationAbstractTest {

@Test
void test() {
var issues = analyze(Path.of("ambiguous-dependencies-should-be-resolved"), "S9352");
assertThat(issues)
.hasSize(5)
.contains(new TextRangeIssue(
"app/src/main/java/com/example/app/ReportingConsumer.java",
"java:S9352",
"Multiple beans match this dependency (excelReportingService, pdfReportingService); disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".",
new TextRange(16, 16, 29, 45)),
new TextRangeIssue(
"app/src/main/java/com/example/app/CacheConsumer.java",
"java:S9352",
"Multiple beans match this dependency (diskCacheProvider, inMemoryCacheProvider, redisCacheProvider); disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".",
new TextRange(12, 12, 26, 39)),
new TextRangeIssue(
"app/src/main/java/com/example/app/PaymentConsumer.java",
"java:S9352",
"Multiple beans match this dependency (creditCardPaymentGateway, digitalWalletPaymentGateway); disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".",
new TextRange(15, 15, 27, 41)),
new TextRangeIssue(
"app/src/main/java/com/example/app/NotificationConsumer.java",
"java:S9352",
"Multiple beans match this dependency (emailNotificationService, smsNotificationService); disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".",
new TextRange(16, 16, 32, 51)),
new TextRangeIssue(
"app/src/main/java/com/example/app/InventoryConsumer.java",
"java:S9352",
"Multiple beans match this dependency (storeInventoryService, warehouseInventoryService); disambiguate it with \"@Qualifier\" or mark one bean as \"@Primary\".",
new TextRange(12, 12, 29, 45)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.example</groupId>
<artifactId>ambiguous-dependencies-should-be-resolved</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>app</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-a</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-b</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.app;

import com.example.common.CacheProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// CASE: ambiguity between three @Bean-method-declared beans (not @Component), same module.
@Component
public class CacheConsumer {

@Autowired
private CacheProvider cacheProvider;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.app;

import com.example.common.ShippingCarrier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
* CASE: ambiguity resolved via @Primary, cross-module.
* PrimaryCarrier (@Primary) lives in module-a, SecondaryCarrier in module-b.
*/
@Component
public class CarrierConsumer {

@Autowired
private ShippingCarrier shippingCarrier;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.app;

import com.example.common.DiscountService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
* CASE: ambiguity resolved via @Qualifier on a constructor parameter, cross-module.
* StandardDiscountService lives in module-a, PremiumDiscountService in module-b.
*/
@Component
public class DiscountConsumer {

private final DiscountService discountService;

public DiscountConsumer(@Qualifier("premiumDiscountService") DiscountService discountService) {
this.discountService = discountService;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.app;

import com.example.common.InventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// CASE: ambiguity between two @Bean-method-declared beans, each in its own @Configuration class, cross-module.
@Component
public class InventoryConsumer {

@Autowired
private InventoryService inventoryService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.app;

import com.example.common.NotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
* CASE: unresolved ambiguity, same module.
* EmailNotificationService and SmsNotificationService (module-a) are both
* plain beans with no @Primary/@Qualifier - ambiguous.
*/
@Component
public class NotificationConsumer {

@Autowired
private NotificationService notificationService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.app;

import com.example.common.NotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

// CASE: ambiguity resolved via @Qualifier, same module.
@Component
public class NotificationQualifiedConsumer {

@Autowired
@Qualifier("smsNotificationService")
private NotificationService notificationService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.app;

import com.example.common.PaymentGateway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
* CASE: unresolved ambiguity, cross-module.
* CreditCardPaymentGateway lives in module-a, DigitalWalletPaymentGateway in module-b.
*/
@Component
public class PaymentConsumer {

@Autowired
private PaymentGateway paymentGateway;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.app;

import com.example.common.PricingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// CASE: ambiguity resolved via @Primary, same module.
@Component
public class PricingConsumer {

@Autowired
private PricingService pricingService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.app;

import com.example.common.ReportingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
* CASE: two beans both marked @Primary, same module - still ambiguous.
* Having more than one "primary" candidate is itself unresolved: Spring
* throws NoUniqueBeanDefinitionException rather than picking either one.
*/
@Component
public class ReportingConsumer {

@Autowired
private ReportingService reportingService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.example")
public class SpringExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringExampleApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.example</groupId>
<artifactId>ambiguous-dependencies-should-be-resolved</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>module-a</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.modulea;

import com.example.common.CacheProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

public CacheProvider redisCacheProvider() {
return key -> "redis:" + key;
}

@Bean
public CacheProvider inMemoryCacheProvider() {
return key -> "memory:" + key;
}

@Bean
public CacheProvider diskCacheProvider() {
return key -> "disk:" + key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.modulea;

import com.example.common.PaymentGateway;
import org.springframework.stereotype.Component;

@Component
public class CreditCardPaymentGateway implements PaymentGateway {
@Override
public void charge(double amount) {
System.out.println("Charging credit card: " + amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.modulea;

import com.example.common.NotificationService;
import org.springframework.stereotype.Component;

@Component
public class EmailNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.modulea;

import com.example.common.ReportingService;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
public class ExcelReportingService implements ReportingService {
@Override
public String generate() {
return "excel-report";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.modulea;

import com.example.common.ReportingService;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
public class PdfReportingService implements ReportingService {
@Override
public String generate() {
return "pdf-report";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.modulea;

import com.example.common.ShippingCarrier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
public class PrimaryCarrier implements ShippingCarrier {
@Override
public String track(String trackingId) {
return "primary:" + trackingId;
}
}
Loading
Loading