Skip to content

Commit 271f514

Browse files
Check and test project for ambiguous resolution of @Autowired'd @component
1 parent fcebe57 commit 271f514

File tree

7 files changed

+218
-1
lines changed

7 files changed

+218
-1
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package checks.spring.innovation;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
@Component
6+
public class A implements Named {
7+
@Override
8+
public String getName() {
9+
return "A";
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package checks.spring.innovation;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
@Component
6+
public class B implements Named {
7+
@Override
8+
public String getName() {
9+
return "B";
10+
}
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package checks.spring.innovation;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.boot.CommandLineRunner;
5+
import org.springframework.boot.SpringApplication;
6+
import org.springframework.boot.autoconfigure.SpringBootApplication;
7+
8+
@SpringBootApplication
9+
public class DemoApplication implements CommandLineRunner {
10+
11+
@Autowired
12+
Named nameSource; // Noncompliant
13+
14+
@Autowired
15+
A a;
16+
17+
@Autowired
18+
Named second; // Noncompliant
19+
20+
public static void main(String[] args) {
21+
SpringApplication.run(DemoApplication.class, args);
22+
}
23+
24+
@Override
25+
public void run(String... args) throws Exception {
26+
System.out.println("Hello, " + nameSource.getName() + "!");
27+
}
28+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package checks.spring.innovation;
2+
3+
public interface Named {
4+
String getName();
5+
}

java-checks/src/main/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import org.sonar.plugins.java.api.tree.Tree;
4848
import org.sonarsource.analyzer.commons.collections.SetUtils;
4949

50-
@Rule(key = "S4605")
50+
//@Rule(key = "S4605")
5151
public class SpringBeansShouldBeAccessibleCheck extends IssuableSubscriptionVisitor implements EndOfAnalysis {
5252

5353
private static final Logger LOG = LoggerFactory.getLogger(SpringBeansShouldBeAccessibleCheck.class);
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks.spring;
18+
19+
import java.util.Arrays;
20+
import java.util.HashMap;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import org.sonar.check.Rule;
26+
import org.sonar.java.model.DefaultJavaFileScannerContext;
27+
import org.sonar.java.model.DefaultModuleScannerContext;
28+
import org.sonar.java.reporting.AnalyzerMessage;
29+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
30+
import org.sonar.plugins.java.api.ModuleScannerContext;
31+
import org.sonar.plugins.java.api.internal.EndOfAnalysis;
32+
import org.sonar.plugins.java.api.semantic.Symbol;
33+
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
34+
import org.sonar.plugins.java.api.semantic.Type;
35+
import org.sonar.plugins.java.api.tree.ClassTree;
36+
import org.sonar.plugins.java.api.tree.Tree;
37+
import org.sonar.plugins.java.api.tree.VariableTree;
38+
39+
@Rule(key = "S4605")
40+
public class SpringInnovationCheck extends IssuableSubscriptionVisitor implements EndOfAnalysis {
41+
42+
private static final String[] SPRING_BEAN_ANNOTATIONS = {
43+
"org.springframework.stereotype.Component"
44+
};
45+
46+
private static final String[] SPRING_INJECTION_ANNOTATIONS = {
47+
"org.springframework.beans.factory.annotation.Autowired"
48+
};
49+
50+
record Location(AnalyzerMessage analyzerMessage) {}
51+
52+
Map<String, Set<Location>> injections = new HashMap<>();
53+
Map<String, Set<String>> availableImpls = new HashMap<>();
54+
55+
@Override
56+
public List<Tree.Kind> nodesToVisit() {
57+
return List.of(Tree.Kind.CLASS);
58+
}
59+
60+
@Override
61+
public void visitNode(Tree tree) {
62+
if (tree instanceof ClassTree classTree) {
63+
SymbolMetadata classSymbolMetadata = classTree.symbol().metadata();
64+
if (hasAnnotation(classSymbolMetadata, SPRING_BEAN_ANNOTATIONS)) {
65+
Symbol.TypeSymbol symbol = classTree.symbol();
66+
Set<String> types = getTypes(symbol);
67+
for (String type : types) {
68+
Set<String> impls = availableImpls.computeIfAbsent(type, k -> new HashSet<>());
69+
impls.add(symbol.type().fullyQualifiedName());
70+
}
71+
}
72+
73+
DefaultJavaFileScannerContext defaultContext = (DefaultJavaFileScannerContext) context;
74+
75+
for (Tree member: classTree.members()) {
76+
if (member instanceof VariableTree variableTree) {
77+
if (hasAnnotation(variableTree.symbol().metadata(), SPRING_INJECTION_ANNOTATIONS)) {
78+
String typeFqn = variableTree.symbol().type().fullyQualifiedName();
79+
80+
Set<Location> locations = injections.computeIfAbsent(typeFqn, k -> new HashSet<>());
81+
82+
// Just on the `simpleName()`, because for simplicity we don't want to include the annotation.
83+
AnalyzerMessage message = defaultContext.createAnalyzerMessage(this, variableTree.simpleName(), "More than one candidate implementation");
84+
locations.add(new Location(message));
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
private static boolean hasAnnotation(SymbolMetadata classSymbolMetadata, String... annotationName) {
92+
return Arrays.stream(annotationName).anyMatch(classSymbolMetadata::isAnnotatedWith);
93+
}
94+
95+
private static Set<String> getTypes(Symbol.TypeSymbol symbol) {
96+
var result = new HashSet<String>();
97+
result.add(symbol.type().fullyQualifiedName());
98+
for (Type iface: symbol.interfaces()) {
99+
result.add(iface.fullyQualifiedName());
100+
}
101+
return result;
102+
}
103+
104+
@Override
105+
public void endOfAnalysis(ModuleScannerContext context) {
106+
var defaultContext = (DefaultModuleScannerContext) context;
107+
108+
for(Map.Entry<String,Set<Location>> entry : injections.entrySet()) {
109+
String typeFqn = entry.getKey();
110+
Set<Location> locations = entry.getValue();
111+
if (availableImpls.get(typeFqn).size() > 1) {
112+
for(Location location :locations) {
113+
defaultContext.reportIssue(location.analyzerMessage());
114+
}
115+
}
116+
}
117+
}
118+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks.spring;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import org.junit.jupiter.api.Test;
22+
import org.sonar.java.checks.verifier.CheckVerifier;
23+
24+
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
25+
26+
class SpringInnovationCheckTest {
27+
28+
private static final String BASE_PATH = "checks/spring/innovation/";
29+
30+
@Test
31+
void testComponentScan() {
32+
final String testFolder = BASE_PATH;
33+
List<String> files = Arrays.asList(
34+
mainCodeSourcesPath(testFolder + "A.java"),
35+
mainCodeSourcesPath(testFolder + "B.java"),
36+
mainCodeSourcesPath(testFolder + "Named.java"),
37+
mainCodeSourcesPath(testFolder + "DemoApplication.java"));
38+
39+
CheckVerifier.newVerifier()
40+
.onFiles(files)
41+
.withCheck(new SpringInnovationCheck())
42+
.verifyIssues();
43+
}
44+
}

0 commit comments

Comments
 (0)