Skip to content

SONARJAVA-6411 Implement Visitor for collecting packages scanned by Spring#5662

Open
asya-vorobeva wants to merge 18 commits into
epic-SONARJAVA-6237from
visitor-to-collect-context-packages
Open

SONARJAVA-6411 Implement Visitor for collecting packages scanned by Spring#5662
asya-vorobeva wants to merge 18 commits into
epic-SONARJAVA-6237from
visitor-to-collect-context-packages

Conversation

@asya-vorobeva
Copy link
Copy Markdown
Contributor

@asya-vorobeva asya-vorobeva commented Jun 7, 2026

See SONARJAVA-6411.


Summary by Gitar

  • New feature:
    • Added ComponentScanPackageGatherer to collect packages configured via @ComponentScan and @SpringBootApplication.
    • Integrated the gatherer into SpringContextModel to track Spring configuration across project modules.
  • Refactoring:
    • Moved SpringUtils and UnitTestUtils to java-frontend to centralize model-related utilities.
  • Testing:
    • Implemented ComponentScanPackageGathererTest and extended CheckVerifier to support asserting on SpringContextModel data.
  • Caching:
    • Added per-file caching logic for collected package data to optimize incremental analysis performance.
  • Infrastructure:
    • Enabled skipping of SpringContextModelGatherer visitors when spring-context is missing from the classpath using DependencyVersionAware.

This will update automatically on new commits.

asya-vorobeva and others added 11 commits June 1, 2026 13:09
Introduce the springcontext package with BeanDefinitionHolder,
BeanDefinitionRegistry, BeanLocation, EntityClassToPropertiesIndex,
ProjectPackageScan, SpringContextModel, and TypeToBeanNamesIndex.
All classes include Javadoc documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ONARJAVA-6237

# Conflicts:
#	its/scanner-integration-tests/pom.xml
#	java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModel.java
- Add ComponentScanPackageCollector visitor in spring/context package:
  collects @componentscan and @SpringBootApplication scanned packages
  per module and writes them to ProjectPackageScan in the shared
  SpringContextModel. Supports incremental analysis via cache. This visitor's usage will replace existing SpringBeansShouldBeAccessible visitor in future PRs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-frontend

- Move SpringUtils and UnitTestUtils from java-checks helpers to java-frontend model package
- Rename ComponentScanPackageCollector to ComponentScanPackageGatherer
- Add SpringContextModel provisioning support to CheckVerifier
- Add unit tests for ComponentScanPackageGatherer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add test verifying SpringContextModel is filled with ProjectPackageScan after JavaFrontend.scan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and CheckVerifier#getSpringContextModel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… scan annotations

When a file has no scan annotations, an empty string was written to cache.
Reading it back via String#split(";") incorrectly produced [""] instead of an empty list,
causing a blank package name to be registered in ProjectPackageScan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@hashicorp-vault-sonar-prod
Copy link
Copy Markdown
Contributor

hashicorp-vault-sonar-prod Bot commented Jun 7, 2026

SONARJAVA-6411

asya-vorobeva and others added 6 commits June 7, 2026 14:22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…path

Implement DependencyVersionAware in SpringContextModelGatherer so all gatherers
are automatically skipped on modules without spring-context on the classpath.
Add integration test verifying the gatherer produces no output when spring-context
is removed from the classpath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reimplements all non-caching tests using VisitorsBridge + JParserTestUtils
directly in java-frontend, closing the coverage gap caused by the class living
in java-frontend while its tests previously lived in java-checks.

Test resource files that were only needed for non-caching tests are moved
from java-checks-test-sources to java-frontend/src/test/files/springcontext/.
The java-checks ComponentScanPackageGathererTest retains only caching tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extend SpringContextModelGatherer from IssuableSubscriptionVisitor
  instead of SubscriptionVisitor so that leaveFile is called by
  IssuableSubscriptionVisitorsRunner, enabling per-file cache writes
  and fixing incremental analysis performance (eclipse_jetty_incremental)

- Move all ComponentScanPackageGatherer tests to java-frontend using
  VisitorsBridge + JParserTestUtils; caching tests use mocked
  ReadCache/WriteCache injected via SensorContextTester

- Remove getSpringContextModel() from CheckVerifier interface and both
  implementations — no longer needed now tests live in java-frontend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Anonymous class: visitNode returns early when simpleName() is null
- Blank scanBasePackages entry: resolvePackage falls through to
  Optional.empty() for blank strings, which are silently ignored

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@asya-vorobeva asya-vorobeva force-pushed the visitor-to-collect-context-packages branch from 38722f0 to eb5b5bb Compare June 8, 2026 07:59
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.

Great work, especially with the tests and the cache implementation 🚀
I left a couple of things we should look at before merging

* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks.helpers;
package org.sonar.java.model;
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 feel like this is not the best package to contain this class, maybe we can put it under org.sonar.java.utils

Comment thread java-frontend/src/main/java/org/sonar/java/model/SpringUtils.java Outdated
@asya-vorobeva asya-vorobeva force-pushed the visitor-to-collect-context-packages branch from c8dedf3 to 1bee570 Compare June 8, 2026 10:32
Comment on lines +112 to +114
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), collectedPackages);
}
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: gatherSpringContextData registers empty module entries

gatherSpringContextData always calls springContextModel.getProjectPackageScan().addPackages(moduleKey, collectedPackages), and addPackages uses computeIfAbsent(module, ...).addAll(...). When collectedPackages is empty (a module on which spring-context is in the classpath but no @ComponentScan/@SpringBootApplication scan packages were found), this still creates an empty Set entry for the module. As a result getModules() will return modules that have zero scanned packages, which may mislead downstream consumers that iterate getModules() expecting only modules with actual scan configuration.

Note endOfAnalysis runs twice per module (the same gatherer instance is registered in both the main and test scanners in JavaFrontend), so the entry is created even from the empty test-scope pass. There are no production consumers of getModules() yet, so impact is currently low, but it is worth guarding against. Suggested fix: skip the write when collectedPackages is empty.

Avoid creating empty module entries in ProjectPackageScan.:

@Override
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
  if (collectedPackages.isEmpty()) {
    return;
  }
  springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), collectedPackages);
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@asya-vorobeva asya-vorobeva force-pushed the visitor-to-collect-context-packages branch from 1bee570 to 72affd0 Compare June 8, 2026 11:42
@asya-vorobeva asya-vorobeva force-pushed the visitor-to-collect-context-packages branch from 72affd0 to 52f9e4d Compare June 8, 2026 12:05
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.

Looks good, but I think the its/sources submodule slipped through, if you can remove that, then it's good to go

- Test isCompatibleWithDependencies for all four trigger dependencies
  (spring-context, spring-beans, spring-boot-starter, spring-boot-starter-web)
  and for the none-present case using a parameterized test

- Move SpringUtils and UnitTestUtils from java-frontend model package
  to java-frontend utils package; update all import sites in java-checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@asya-vorobeva asya-vorobeva force-pushed the visitor-to-collect-context-packages branch from 52f9e4d to 7e9f7af Compare June 8, 2026 13:23
@sonarqube-next
Copy link
Copy Markdown

sonarqube-next Bot commented Jun 8, 2026

@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Jun 8, 2026

CI failed: The build failed due to an integration test regression in `JavaRulingTest.eclipse_jetty_incremental`, where the analyzer produced a larger output than the current test baseline.

Overview

A single integration test failure was identified in the it-java-ruling module during the eclipse_jetty_incremental test case, indicating that the PR changes caused a regression in the expected analysis output metrics.

Failures

JavaRulingTest.eclipse_jetty_incremental (confidence: high)

  • Type: test
  • Affected jobs: 80105119115
  • Related to change: yes
  • Root cause: The new ComponentScanPackageGatherer likely increases the volume of data captured by the SpringContextModel, causing the analysis output (198790) to exceed the established baseline (173384) in the incremental ruling test.
  • Suggested fix: Verify if the additional findings are intentional and correct. If they are, update the expected result files in its/ruling/src/test/resources to match the new analysis output. If this increase is unintended, audit the ComponentScanPackageGatherer implementation to ensure it isn't scanning unnecessary files or packages in the Jetty project.

Summary

  • Change-related failures: 1 integration test failure due to changed analysis output metrics.
  • Infrastructure/flaky failures: 0 (Note: Minor Develocity network timeouts were observed but did not block the core execution flow).
  • Recommended action: Review the delta in the eclipse_jetty_incremental test and update the test resources if the increased output size is an expected consequence of the new Spring scanning functionality.
Code Review 👍 Approved with suggestions 6 resolved / 7 findings

Implements ComponentScanPackageGatherer to track Spring configurations and refactors utility classes into java-frontend, resolving several issues with empty package registration and caching. Consider addressing the minor issue where gatherSpringContextData registers empty module entries.

💡 Quality: gatherSpringContextData registers empty module entries

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:112-114 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ProjectPackageScan.java:51-53

gatherSpringContextData always calls springContextModel.getProjectPackageScan().addPackages(moduleKey, collectedPackages), and addPackages uses computeIfAbsent(module, ...).addAll(...). When collectedPackages is empty (a module on which spring-context is in the classpath but no @ComponentScan/``@SpringBootApplication scan packages were found), this still creates an empty Set entry for the module. As a result getModules() will return modules that have zero scanned packages, which may mislead downstream consumers that iterate getModules() expecting only modules with actual scan configuration.

Note endOfAnalysis runs twice per module (the same gatherer instance is registered in both the main and test scanners in JavaFrontend), so the entry is created even from the empty test-scope pass. There are no production consumers of getModules() yet, so impact is currently low, but it is worth guarding against. Suggested fix: skip the write when collectedPackages is empty.

Avoid creating empty module entries in ProjectPackageScan.
@Override
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
  if (collectedPackages.isEmpty()) {
    return;
  }
  springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), collectedPackages);
}
✅ 6 resolved
Edge Case: Unresolved basePackageClasses symbol yields empty-string package

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:135-149
packageNameOf walks symbol.owner() until a package symbol is reached and returns its name (ComponentScanPackageGatherer.java:174-180). For an unresolved type referenced by @ComponentScan(basePackageClasses = ...) / @SpringBootApplication(scanBasePackageClasses = ...), the symbol's owner chain terminates at the root package, whose name is the empty string. The loop terminates safely (no NPE/infinite loop, verified against JSymbol/Symbols), but an empty string "" is then added to the scanned-package set, which silently represents "scan everything". This may broaden downstream scan-coverage decisions unexpectedly when classpath resolution fails. Consider filtering out null/empty package names before adding them, or guarding on symbol.isUnknown().

Quality: @VisibleForTesting on a private field is contradictory

📄 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java:99-100
In JavaCheckVerifier.java the new field is declared @VisibleForTesting private SpringContextModel springContextModel;. @VisibleForTesting documents that a member's visibility was relaxed to enable testing, but the field is private, so the annotation is misleading and serves no purpose here (the field is already exposed via the public getSpringContextModel() accessor). Remove the annotation or the private modifier to match intent.

Edge Case: @SpringBootApplication own-package fallback uses file-level, not class-level state

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:91-105 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:151-155
In collectFromSpringBootApplication, the decision whether to fall back to the annotated class's own package is computed as useOwnPackageAsFallback = packagesCollectedAtFileLevel.isEmpty() (ComponentScanPackageGatherer.java:130). The intent, documented in the comment at lines 152-154, is to only skip the own-package fallback when packages were already collected via @ComponentScan on the same class. However, packagesCollectedAtFileLevel is a file-scoped accumulator that is only cleared in setContext/leaveFile, so it carries over state from previously-visited classes in the same compilation unit.

Consequence: a .java file containing more than one top-level class — e.g. an unrelated @ComponentScan(basePackages="x") class followed by a @SpringBootApplication class with no scan attributes — will incorrectly suppress the SpringBoot class's own-package contribution, because packagesCollectedAtFileLevel is non-empty by the time the SpringBoot class is visited (visit order depends on declaration order). Multi-class files are realistic in this domain (the PR's own test source ComponentScanWithBasePackages.java declares four annotated classes in one file).

Fix: track the component-scan packages collected for the current class only, rather than relying on the file-level set. For example, capture collectedPackages/packagesCollectedAtFileLevel size before processing the class, or have collectFromComponentScan return whether it added anything for this class, and pass that boolean into collectFromSpringBootApplication.

Edge Case: Own-package fallback can register a blank package name

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:135-149 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:147-150 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:182-188
This PR deliberately filters out blank package names everywhere else: @ComponentScan values are filtered in addAnnotationValueToCollectedPackages (!oString.isBlank(), !pkg.isBlank()), and scanBasePackages/scanBasePackageClasses are filtered in resolvePackage. However, the @SpringBootApplication own-package fallback bypasses this check:

return useOwnPackageAsFallback ? Collections.singletonList(classPackageName) : List.of();

classPackageName comes from packageNameOf(classSymbol), which returns the (possibly empty) package symbol name. For a @SpringBootApplication class declared in the default package, packageNameOf returns "", and that empty string is added directly to collectedPackages without a blank check. This is inconsistent with the rest of the gatherer and may pollute ProjectPackageScan with an empty package entry, which downstream prefix/containment matching could treat as matching everything. A @SpringBootApplication in the default package is unusual but legal.

Suggested fix: guard the fallback so a blank own-package name is dropped, mirroring the other paths.

Quality: Javadoc still says gatherers skipped unless spring-context present

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java:35 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java:46-50
The class Javadoc at SpringContextModelGatherer.java:35 still states: "All gatherers are skipped when spring-context is not present in the module classpath." After this commit, isCompatibleWithDependencies also returns true for spring-beans, spring-boot-starter, and spring-boot-starter-web. The documentation is now inaccurate and should be updated to reflect that any of these Spring dependencies enables the gatherers.

...and 1 more resolved from earlier reviews

🤖 Prompt for agents
Code Review: Implements `ComponentScanPackageGatherer` to track Spring configurations and refactors utility classes into `java-frontend`, resolving several issues with empty package registration and caching. Consider addressing the minor issue where `gatherSpringContextData` registers empty module entries.

1. 💡 Quality: gatherSpringContextData registers empty module entries
   Files: java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java:112-114, java-frontend/src/main/java/org/sonar/java/model/springcontext/ProjectPackageScan.java:51-53

   `gatherSpringContextData` always calls `springContextModel.getProjectPackageScan().addPackages(moduleKey, collectedPackages)`, and `addPackages` uses `computeIfAbsent(module, ...).addAll(...)`. When `collectedPackages` is empty (a module on which spring-context is in the classpath but no @ComponentScan/@SpringBootApplication scan packages were found), this still creates an empty `Set` entry for the module. As a result `getModules()` will return modules that have zero scanned packages, which may mislead downstream consumers that iterate `getModules()` expecting only modules with actual scan configuration.
   
   Note `endOfAnalysis` runs twice per module (the same gatherer instance is registered in both the main and test scanners in JavaFrontend), so the entry is created even from the empty test-scope pass. There are no production consumers of `getModules()` yet, so impact is currently low, but it is worth guarding against. Suggested fix: skip the write when `collectedPackages` is empty.

   Fix (Avoid creating empty module entries in ProjectPackageScan.):
   @Override
   public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
     if (collectedPackages.isEmpty()) {
       return;
     }
     springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), collectedPackages);
   }

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:

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

Was this helpful? React with 👍 / 👎 | Gitar

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