Skip to content

Commit feab27c

Browse files
SONARJAVA-6187 S4605: improve scanning detection (#5631)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a0307a commit feab27c

7 files changed

Lines changed: 68 additions & 19 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package checks.spring.s4605.springBootApplication.fifthApp;
2+
3+
import checks.spring.s4605.springBootApplication.fifthApp.service.ServiceMarker;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication(
7+
scanBasePackages = "checks.spring.s4605.springBootApplication.fifthApp.extra",
8+
scanBasePackageClasses = ServiceMarker.class)
9+
public class SpringBootApp5 {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package checks.spring.s4605.springBootApplication.fifthApp.controller;
2+
3+
import org.springframework.web.bind.annotation.RestController;
4+
5+
@RestController
6+
public class MyController {} // Noncompliant {{'MyController' is not reachable by @ComponentScan or @SpringBootApplication. Either move it to a package configured in @ComponentScan or update your @ComponentScan configuration.}}
7+
// ^^^^^^^^^^^^
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package checks.spring.s4605.springBootApplication.fifthApp.extra;
2+
3+
import org.springframework.stereotype.Service;
4+
5+
@Service
6+
public class ExtraService {} // Compliant - in the package referenced by scanBasePackages
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package checks.spring.s4605.springBootApplication.fifthApp.service;
2+
3+
import org.springframework.stereotype.Service;
4+
5+
@Service
6+
public class MyService {} // Compliant - in the package referenced by scanBasePackageClasses
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package checks.spring.s4605.springBootApplication.fifthApp.service;
2+
3+
public class ServiceMarker {}

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class SpringBeansShouldBeAccessibleCheck extends IssuableSubscriptionVisi
6666

6767
private static final String COMPONENT_SCAN_ANNOTATION = "org.springframework.context.annotation.ComponentScan";
6868
private static final Set<String> COMPONENT_SCAN_BASE_ARGUMENTS = SetUtils.immutableSetOf("basePackages", "basePackageClasses", "value");
69+
private static final Set<String> SCAN_BASE_ANNOTATIONS = SetUtils.immutableSetOf("scanBasePackages", "scanBasePackageClasses");
6970

7071
private static final String CACHE_KEY_PREFIX = "java:S4605:targeted:";
7172

@@ -187,25 +188,24 @@ private static Optional<List<String>> readFromCache(InputFileScannerContext cont
187188

188189
private static List<String> targetedPackages(String classPackageName, SymbolMetadata classSymbolMetadata) {
189190
// annotation is necessarily there already
190-
return Objects.requireNonNull(classSymbolMetadata.valuesForAnnotation(SpringUtils.SPRING_BOOT_APP_ANNOTATION))
191-
.stream()
192-
.filter(v -> "scanBasePackages".equals(v.name()))
193-
.map(SymbolMetadata.AnnotationValue::value)
194-
.findFirst()
195-
// list of packages to scan
196-
.filter(Object[].class::isInstance)
197-
.map(Object[].class::cast)
198-
.map(SpringBeansShouldBeAccessibleCheck::asStringList)
199-
// Using this annotation without arguments tells Spring to scan the current package and all of its sub-packages.
200-
.orElse(Collections.singletonList(classPackageName));
201-
}
202-
203-
private static List<String> asStringList(Object[] array) {
204-
return Arrays.asList(array)
205-
.stream()
206-
.filter(String.class::isInstance)
207-
.map(String.class::cast)
191+
var scanBaseValues = Objects.requireNonNull(classSymbolMetadata.valuesForAnnotation(SpringUtils.SPRING_BOOT_APP_ANNOTATION)).stream()
192+
.filter(v -> SCAN_BASE_ANNOTATIONS.contains(v.name()) && v.value() instanceof Object[])
208193
.toList();
194+
195+
List<String> packages = new ArrayList<>();
196+
for (SymbolMetadata.AnnotationValue value : scanBaseValues) {
197+
boolean isClassBased = "scanBasePackageClasses".equals(value.name());
198+
for (Object element : (Object[]) value.value()) {
199+
if (!isClassBased && element instanceof String s) {
200+
packages.add(s);
201+
} else if (isClassBased && element instanceof Symbol s) {
202+
packages.add(packageNameOf(s));
203+
}
204+
}
205+
}
206+
207+
// Using this annotation without arguments tells Spring to scan the current package and all of its sub-packages.
208+
return scanBaseValues.isEmpty() ? Collections.singletonList(classPackageName) : packages;
209209
}
210210

211211
private void addMessageToMap(String classPackageName, IdentifierTree classNameTree) {

java-checks/src/test/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheckTest.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ void testSpringBootApplication() {
131131
}
132132

133133
@Test
134-
void testSpringBootApplicationWithAnnotation() {
134+
void testSpringBootApplicationWithScanBasePackages() {
135+
//test case when using explicit package names
135136
final String testFolderThirdApp = BASE_PATH + "springBootApplication/thirdApp/";
136137
List<String> thirdAppTestFiles = Arrays.asList(
137138
mainCodeSourcesPath(testFolderThirdApp + "SpringBootApp3.java"),
@@ -143,6 +144,7 @@ void testSpringBootApplicationWithAnnotation() {
143144
.withCheck(new SpringBeansShouldBeAccessibleCheck())
144145
.verifyIssues();
145146

147+
//test case when using string constants for package names
146148
final String testFolderFourthApp = BASE_PATH + "springBootApplication/fourthApp/";
147149
List<String> fourthAppTestFiles = Arrays.asList(
148150
mainCodeSourcesPath(testFolderFourthApp + "SpringBootApp4.java"),
@@ -157,6 +159,22 @@ void testSpringBootApplicationWithAnnotation() {
157159
.verifyIssues();
158160
}
159161

162+
@Test
163+
void testSpringBootApplicationWithMixedScanBasePackageAttributes() {
164+
final String testFolder = BASE_PATH + "springBootApplication/fifthApp/";
165+
List<String> files = List.of(
166+
mainCodeSourcesPath(testFolder + "SpringBootApp5.java"),
167+
mainCodeSourcesPath(testFolder + "service/ServiceMarker.java"),
168+
mainCodeSourcesPath(testFolder + "service/MyService.java"),
169+
mainCodeSourcesPath(testFolder + "extra/ExtraService.java"),
170+
mainCodeSourcesPath(testFolder + "controller/MyController.java"));
171+
172+
CheckVerifier.newVerifier()
173+
.onFiles(files)
174+
.withCheck(new SpringBeansShouldBeAccessibleCheck())
175+
.verifyIssues();
176+
}
177+
160178
@Test
161179
void testBothAnnotationsTogether() {
162180
final String folderApp = BASE_PATH + "mixed/app1/";

0 commit comments

Comments
 (0)