Skip to content

SONARJAVA-7166 S9352: Don't register beans declared in test scope - #6320

Merged
asya-vorobeva merged 3 commits into
masterfrom
asya/fix-test-scope-fps-s9352
Oct 14, 2026
Merged

asya-vorobeva merged 3 commits into
masterfrom
asya/fix-test-scope-fps-s9352

Conversation

@asya-vorobeva

Copy link
Copy Markdown
Contributor

Spring Boot's TestTypeExcludeFilter keeps test classes, @TestConfiguration classes, and anything nested inside them out of component scanning, so the beans and injection points they declare are never part of the application context the Spring context model describes. Collecting them let test beans compete with main ones and raised false positives such as the nested test @configuration in Spring Initializr's InitializrAutoConfigurationTests.

JavaFileTypeClassifier gains the two signals that filter keys on: @TestConfiguration and @testable among the class-level annotations, and declaresTestMethods, which defers to UnitTestUtils.isTestClass for the method-level markers the annotation set deliberately omits.

The gatherers are gated in SpringContextModelGatherer rather than in BeanDefinitionGatherer alone, so a @componentscan declared on a test class no longer widens the production scan scope either. leaveSpringFile still runs for a test file, which publishes an empty result and keeps a valid cache entry so unchanged files are not re-parsed.

The scanner integration test harness now builds with test-compile and declares sonar.tests along with the test binaries and libraries when a module has test sources, which is what lets the new fixture in AmbiguousDependencyCrossModuleTest cover the real scanner path: platform test-type assignment and the MAIN/TEST gatherer registration in JavaSensor.

Spring Boot's TestTypeExcludeFilter keeps test classes, @TestConfiguration
classes, and anything nested inside them out of component scanning, so the
beans and injection points they declare are never part of the application
context the Spring context model describes. Collecting them let test beans
compete with main ones and raised false positives such as the nested test
@configuration in Spring Initializr's InitializrAutoConfigurationTests.

JavaFileTypeClassifier gains the two signals that filter keys on:
@TestConfiguration and @testable among the class-level annotations, and
declaresTestMethods, which defers to UnitTestUtils.isTestClass for the
method-level markers the annotation set deliberately omits.

The gatherers are gated in SpringContextModelGatherer rather than in
BeanDefinitionGatherer alone, so a @componentscan declared on a test class
no longer widens the production scan scope either. leaveSpringFile still
runs for a test file, which publishes an empty result and keeps a valid
cache entry so unchanged files are not re-parsed.

The scanner integration test harness now builds with test-compile and
declares sonar.tests along with the test binaries and libraries when a
module has test sources, which is what lets the new fixture in
AmbiguousDependencyCrossModuleTest cover the real scanner path: platform
test-type assignment and the MAIN/TEST gatherer registration in JavaSensor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

SONARJAVA-7166

@datadog-sonarsource

This comment has been minimized.

JavaFrontendTest and SpringContextModelTest both asserted that a bean
declared in a test file is registered, which is exactly what the exclusion
changes. Their expectations now state the new behaviour, and the main-scope
assertions they also carry are left untouched.
Change SpringContextModelTest to have a Configuration: its main fixture
lives under src/test/files/, which the path heuristic reads as test scope, so
it declares sonar.tests the way a real analysis does.

JavaSensor handed the gatherers to RuleScope.TEST as well, which is now a
guaranteed no-op: getTestFiles collects InputFile.Type.TEST files, those feed
testChecks and astScannerForTests, and the test-scope exclusion short-circuits
every one of them before anything is gathered. Dropping the registration also
spares each test file a setContext and an empty cache write, and stops the
gatherers from reporting a scanWithoutParsing miss for test files on a first
run, which would have forced unchanged ones to be parsed.

The exclusion itself stays: RuleScope.TEST is a strict subset of what
JavaFileTypeClassifier covers, so a main-scope file that is test code by its
path, its annotations or its test methods still reaches the gatherers and is
still excluded by them.

JavaFrontendTest keeps registering both scopes on purpose, so that the
exclusion is pinned on its own rather than only through this wiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asya-vorobeva
asya-vorobeva force-pushed the asya/fix-test-scope-fps-s9352 branch from e50cdc1 to 229385a Compare October 12, 2026 13:05
The AST signals inspected only the top-level types of a compilation unit, so a
test class that is itself nested went unnoticed whenever the outer class
carried no test marker of its own:

  class Outer {                      // nothing marks this as test code
    static class InnerTest {
      @test void probe() {}
      @configuration static class Cfg {
        @bean ApplicationContext bean() { return null; }
      }
    }
  }

Spring excludes Cfg: TestTypeExcludeFilter resolves a candidate's enclosing
class and re-applies itself recursively, reaching InnerTest and its test
methods. We collected the bean instead. Walking down from the compilation unit
into nested types reaches the same set as walking up from each candidate.

UnitTestUtils.isTestClass only answers for top-level classes, and spots a
nested test class solely through an @nested member, so the method signal now
also asks whether a type declares test methods itself. That additionally
covers an abstract base test class, which isTestClass rejects outright while
Spring excludes it along with anything nested inside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Code Review ✅ Approved 3 closed / 3 findings

🟡 Medium risk · New test-scope detection suppresses Spring bean and scan gathering, which could hide dependency issues if production files are misclassified.

Excludes test-scope classes, @TestConfiguration classes, and their nested members from Spring bean registration and component scanning, matching Spring Boot's TestTypeExcludeFilter so test beans no longer compete with main beans. No open issues found.

✅ 3 closed
Closed: Quality: Null guard misses the no-SonarComponents case its Javadoc describes

📄 java-frontend/src/main/java/org/sonar/java/utils/JavaFileTypeClassifier.java:164-175
The Javadoc on looksLikeTestPath says a context built without SonarComponents has no Configuration, and that the guard skips the heuristic in that case instead of failing. That isn't what happens. DefaultModuleScannerContext.getConfiguration() calls sonarComponents.getConfiguration() directly, so when sonarComponents is null (for example new VisitorsBridge(null) or new JavaAstScanner(null, ...)) it throws an NPE before the configuration != null check is reached. A real SonarComponents with no sensor context also NPEs inside context.config(). The guard only covers a mocked SonarComponents that returns null. In the case the comment names, the code hits the exact failure the comment warns about: the exception gets swallowed and the file is silently treated as main code. To fix this, make DefaultModuleScannerContext.getConfiguration() return null when sonarComponents is null, as sonarProduct() already does. Otherwise, reword the Javadoc to describe only the case the guard really handles.

Closed: Bug: Test-scope gate also drops main beans that only match path/name heuristic

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java:49-53 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java:88-98 📄 java-frontend/src/main/java/org/sonar/java/utils/JavaFileTypeClassifier.java:157-162
SpringContextModelGatherer.setContext decides test scope with JavaFileTypeClassifier.isTestFile. When sonar.tests is not configured, that method also applies looksLikeTestFile, which matches main sources such as src/main/java/com/acme/testing/FooConfig.java, .../test/... packages, or files ending in *Spec.java/*IT.java. This happens for modules with no test sources (the Maven/Gradle scanners only set sonar.tests when a test directory exists) and in IDE-style analyses. For these files, @Component/@Bean definitions, injection points and @ComponentScan/@SpringBootApplication scan packages are silently thrown away. Spring's TestTypeExcludeFilter does not do this: it only looks at test annotations and test methods (plus enclosing classes), never at paths or file names. So the model stops matching the real application context, and S9352 misses real ambiguities, which contradicts the class Javadoc saying the filter is mirrored. Suggested fix: gate the gatherers on the platform type plus the annotation and test-method signals, and leave out the path/naming heuristic.

Closed: Bug: Deep nested-test detection excludes the whole file, not just the subtree

📄 java-frontend/src/main/java/org/sonar/java/utils/JavaFileTypeClassifier.java:52-55 📄 java-frontend/src/main/java/org/sonar/java/utils/JavaFileTypeClassifier.java:215-224 📄 java-frontend/src/main/java/org/sonar/java/utils/JavaFileTypeClassifier.java:227-241 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java:91-98
anyTypeMatches now goes down into nested types at any depth. The result is still a single file-wide flag: SpringContextModelGatherer.setContext stores it in currentFileIsInTestScope, and StringLiteralDuplicatedCheck.scanFile returns early on it. So when a nested class, however deep, has one @Test method or a class-level test annotation, the whole file is treated as test code. That includes the outer class and any sibling types.

Spring's TestTypeExcludeFilter works the other way. It starts at each candidate and walks up through its enclosing classes, so it excludes only the test class and what is nested inside it. In a MAIN file like @Configuration class AppConfig { @Bean Foo foo() {...} static class SelfCheck { @Test void t() {} } }, Spring still registers AppConfig and foo. With this commit the gatherer drops them, and S1192 stops reporting anywhere in the file.

The new Javadoc says "Walking down from the compilation unit reaches the same set as walking up from each candidate". That holds only if the gate is applied per type, not per file. Possible fixes: let gatherers skip just the matching test subtree (track depth in visitNode/leaveNode for test-scope ClassTrees), or keep whole-file escalation for top-level types only and fix the Javadoc.

Verification Repos 4 inspected

Functional validation No results

Rules No rules evaluated

Cross-repo 4 repositories inspected 0 open impacts

SonarSource/sonarlint-eclipse Inspected
SonarSource/sonarlint-vscode Inspected
SonarSource/sonarlint-intellij Inspected
SonarSource/sonar-java-symbolic-execution Inspected

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing compact verification results.

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

Copy link
Copy Markdown
Contributor

@aurelien-coet-sonarsource aurelien-coet-sonarsource 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.

LGTM 👍

@asya-vorobeva
asya-vorobeva merged commit b750cc8 into master Oct 14, 2026
23 checks passed
@asya-vorobeva
asya-vorobeva deleted the asya/fix-test-scope-fps-s9352 branch October 14, 2026 08:08
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