From 7ee64aab0b90ffd6de6576b2b6c8267008c512e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 1 Oct 2026 11:49:29 +0200 Subject: [PATCH 1/2] Use a centralized, per-file Spring context model in gatherers --- .../spring/AmbiguousDependencyCheckTest.java | 3 +- .../java/org/sonar/java/JavaFrontend.java | 5 +- .../springcontext/BeanDefinitionGatherer.java | 17 +++-- .../model/springcontext/BeanLocation.java | 3 +- .../ComponentScanPackageGatherer.java | 22 ++---- .../SpringContextGatheringModel.java | 74 +++++++++++++++++++ .../SpringContextModelGatherer.java | 8 +- .../SpringContextModelGatherers.java | 8 +- .../java/org/sonar/java/JavaFrontendTest.java | 33 ++++++++- .../BeanDefinitionGathererTest.java | 8 +- .../ComponentScanPackageGathererTest.java | 14 +++- .../SpringContextModelGathererTest.java | 20 ++--- 12 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextGatheringModel.java diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java index ed95c8856a9..bbb8fe0e511 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java @@ -38,6 +38,7 @@ import org.sonar.java.model.springcontext.BeanDefinitionHolder; import org.sonar.java.model.springcontext.BeanLocation; import org.sonar.java.model.springcontext.ProfileExpression; +import org.sonar.java.model.springcontext.SpringContextGatheringModel; import org.sonar.java.model.springcontext.SpringContextModel; import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.telemetry.NoOpTelemetry; @@ -375,7 +376,7 @@ private static SpringContextModel buildModel(List filePaths) { SpringContextModel model = new SpringContextModel(); sonarComponents.setSpringContextModel(model); - BeanDefinitionGatherer gatherer = new BeanDefinitionGatherer(new NoOpTelemetry()); + BeanDefinitionGatherer gatherer = new BeanDefinitionGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()); VisitorsBridge visitorsBridge = new VisitorsBridge(List.of((JavaCheck) gatherer), classpath, sonarComponents); for (String filePath : filePaths) { File file = new File(filePath); diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java index e817b0b5f1f..2eb9106a2d9 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java @@ -42,6 +42,7 @@ import org.sonar.java.filters.SonarJavaIssueFilter; import org.sonar.java.model.JParserConfig; import org.sonar.java.model.VisitorsBridge; +import org.sonar.java.model.springcontext.SpringContextGatheringModel; import org.sonar.java.model.springcontext.SpringContextModelGatherers; import org.sonar.java.telemetry.Telemetry; import org.sonar.java.telemetry.TelemetryKey; @@ -98,8 +99,8 @@ public JavaFrontend(JavaVersion javaVersion, SonarComponents sonarComponents, Me if (sonarComponents.getSpringContextModel() != null) { // Call SpringContextModelGatherers.getAllGatherers twice to have separate gatherer // instances between the main and test scanners to avoid duplicating the issues - codeVisitors.addAll(SpringContextModelGatherers.getAllGatherers(telemetry)); - testCodeVisitors.addAll(SpringContextModelGatherers.getAllGatherers(telemetry)); + codeVisitors.addAll(SpringContextModelGatherers.getAllGatherers(new SpringContextGatheringModel(), telemetry)); + testCodeVisitors.addAll(SpringContextModelGatherers.getAllGatherers(new SpringContextGatheringModel(), telemetry)); } if (!sonarComponents.isSonarLintContext()) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java index 0ec65f05bb8..651e40b4658 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java @@ -76,15 +76,13 @@ public class BeanDefinitionGatherer extends SpringContextModelGatherer private static final String PRIMARY_ANNOTATION = "org.springframework.context.annotation.Primary"; private static final String CACHE_KEY_PREFIX = "java:spring:bean-definitions:"; - private final Map> beansCollectedByFile = new LinkedHashMap<>(); - /** * Beans found in the file currently being scanned, used for per-file cache writes. */ private final List beansCollectedAtFileLevel = new ArrayList<>(); - public BeanDefinitionGatherer(Telemetry telemetry) { - super(telemetry); + public BeanDefinitionGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) { + super(springContextGatheringModel, telemetry); } @Override @@ -142,7 +140,8 @@ protected void visitSpringNode(Tree tree) { @Override protected void leaveSpringFile(JavaFileScannerContext context) { var beans = List.copyOf(beansCollectedAtFileLevel); - beansCollectedByFile.put(context.getInputFile(), beans); + InputFile currentFile = context.getInputFile(); + springContextGatheringModel.collectBeans(context.getModuleKey(), currentFile.key(), currentFile, beans); writeToCache(context, beans); beansCollectedAtFileLevel.clear(); } @@ -164,7 +163,8 @@ public List deserialize(byte[] data) { @Override public void restore(InputFileScannerContext context, List beans) { - beansCollectedByFile.put(context.getInputFile(), List.copyOf(beans)); + InputFile currentFile = context.getInputFile(); + springContextGatheringModel.collectBeans(context.getModuleKey(), currentFile.key(), currentFile, beans); } /** @@ -180,8 +180,9 @@ public void restore(InputFileScannerContext context, List { - for (BeanDefinitionHolder.InputFileData data : beans) { + springContextGatheringModel.getInputFilesData(context.getModuleKey()).forEach(inputFileData -> { + InputFile inputFile = inputFileData.inputFile(); + for (BeanDefinitionHolder.InputFileData data : inputFileData.beans()) { var location = new BeanLocation(inputFile, data.textSpan()); var holderBuilder = new BeanDefinitionHolder.Builder( data.type(), context.getModuleKey(), data.beanPackage(), location) diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanLocation.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanLocation.java index 186c0a926b7..5baf07fa937 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanLocation.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanLocation.java @@ -16,6 +16,7 @@ */ package org.sonar.java.model.springcontext; +import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.telemetry.SizeEstimable; @@ -27,7 +28,7 @@ * @param inputFile the file in which the bean is declared * @param mainLocation the precise text span of the bean declaration, used for reporting issues */ -public record BeanLocation(InputFile inputFile, AnalyzerMessage.TextSpan mainLocation) implements SizeEstimable { +public record BeanLocation(@Nullable InputFile inputFile, AnalyzerMessage.TextSpan mainLocation) implements SizeEstimable { @Override public long estimateSize(SizeEstimator estimator) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java index 7323328d893..5146693c52e 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/ComponentScanPackageGatherer.java @@ -18,13 +18,12 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import org.sonar.api.batch.fs.InputFile; import org.sonar.java.caching.FileCachingCheck; import org.sonar.java.telemetry.Telemetry; import org.sonar.java.utils.PackageUtils; @@ -61,18 +60,13 @@ public class ComponentScanPackageGatherer extends SpringContextModelGatherer imp private static final Set COMPONENT_SCAN_BASE_ARGUMENTS = SetUtils.immutableSetOf("basePackages", "basePackageClasses", "value"); private static final Set SCAN_BASE_ANNOTATIONS = SetUtils.immutableSetOf("scanBasePackages", "scanBasePackageClasses"); - /** - * Packages accumulated across all files in the current module, mapped by input file key. - */ - private final Map> collectedPackagesByFile = new HashMap<>(); - /** * Packages found in the file currently being scanned, used for per-file cache writes. */ private final Set packagesCollectedAtFileLevel = new HashSet<>(); - public ComponentScanPackageGatherer(Telemetry telemetry) { - super(telemetry); + public ComponentScanPackageGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) { + super(springContextGatheringModel, telemetry); } @Override @@ -102,7 +96,8 @@ public Set deserialize(byte[] data) { @Override public void restore(InputFileScannerContext context, Set packages) { - collectedPackagesByFile.put(context.getInputFile().key(), Set.copyOf(packages)); + InputFile currentFile = context.getInputFile(); + springContextGatheringModel.collectPackages(context.getModuleKey(), currentFile.key(), currentFile, Set.copyOf(packages)); } @Override @@ -126,16 +121,15 @@ protected void visitSpringNode(Tree tree) { @Override protected void leaveSpringFile(JavaFileScannerContext context) { var packages = Set.copyOf(packagesCollectedAtFileLevel); - collectedPackagesByFile.put(context.getInputFile().key(), packages); + InputFile currentFile = context.getInputFile(); + springContextGatheringModel.collectPackages(context.getModuleKey(), currentFile.key(), currentFile, packages); writeToCache(context, packages); packagesCollectedAtFileLevel.clear(); } @Override public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) { - Set collectedPackages = new HashSet<>(); - collectedPackagesByFile.values().forEach(collectedPackages::addAll); - springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), collectedPackages); + springContextModel.getProjectPackageScan().addPackages(context.getModuleKey(), springContextGatheringModel.getPackages(context.getModuleKey())); } private void collectFromComponentScan(SymbolMetadata metadata) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextGatheringModel.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextGatheringModel.java new file mode 100644 index 00000000000..e763f7c9c34 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextGatheringModel.java @@ -0,0 +1,74 @@ +/* + * 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.model.springcontext; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; + +/** + * Stores the data collected per-module, per-file during Spring context collection. + */ +public class SpringContextGatheringModel { + + /** + * The Spring data collected for a given file in a module. + * + * @param beans The bean definitions gathered in the file. + * @param packages The packages covered by the file. + */ + public record InputFileData(@Nullable InputFile inputFile, List beans, Set packages) { + } + + /** + * The Spring data collected per-module, per-file. + */ + private final Map> filesData = new HashMap<>(); + + public void collectBeans(String moduleKey, String fileKey, @Nullable InputFile inputFile, List beans) { + filesData + .computeIfAbsent(moduleKey, k -> new HashMap<>()) + .computeIfAbsent(fileKey, k -> new InputFileData(inputFile, new ArrayList<>(), new HashSet<>())) + .beans.clear(); + filesData.get(moduleKey).get(fileKey).beans.addAll(beans); + } + + public void collectPackages(String moduleKey, String fileKey, @Nullable InputFile inputFile, Set packages) { + filesData + .computeIfAbsent(moduleKey, k -> new HashMap<>()) + .computeIfAbsent(fileKey, k -> new InputFileData(inputFile, new ArrayList<>(), new HashSet<>())) + .packages.clear(); + filesData.get(moduleKey).get(fileKey).packages.addAll(packages); + } + + public Collection getInputFilesData(String moduleKey) { + return filesData.getOrDefault(moduleKey, Map.of()).values(); + } + + public Set getPackages(String moduleKey) { + Set packages = new HashSet<>(); + getInputFilesData(moduleKey).forEach(inputFileData -> packages.addAll(inputFileData.packages)); + return packages; + } + +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java index 253a6e326af..3af82f14701 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherer.java @@ -54,16 +54,18 @@ */ public abstract class SpringContextModelGatherer extends IssuableSubscriptionVisitor implements EndOfAnalysis, DependencyVersionAware { + protected final SpringContextGatheringModel springContextGatheringModel; private final Telemetry telemetry; private final LongSupplier nanoTime; private long gatheringTimeNanos; - protected SpringContextModelGatherer(Telemetry telemetry) { - this(telemetry, System::nanoTime); + protected SpringContextModelGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) { + this(springContextGatheringModel, telemetry, System::nanoTime); } @VisibleForTesting - SpringContextModelGatherer(Telemetry telemetry, LongSupplier nanoTime) { + SpringContextModelGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry, LongSupplier nanoTime) { + this.springContextGatheringModel = springContextGatheringModel; this.telemetry = telemetry; this.nanoTime = nanoTime; } diff --git a/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherers.java b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherers.java index 67540bcfee3..173a69f3e51 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherers.java +++ b/java-frontend/src/main/java/org/sonar/java/model/springcontext/SpringContextModelGatherers.java @@ -24,7 +24,7 @@ * Registry of all {@link SpringContextModelGatherer} visitors that populate the {@link SpringContextModel} * during a module analysis. * - *

Use {@link #getAllGatherers(Telemetry)} to obtain the full list of gatherers to be registered with the scanner. + *

Use {@link #getAllGatherers(SpringContextGatheringModel, Telemetry)} to obtain the full list of gatherers to be registered with the scanner. * New gatherers should be added here as the set of Spring context data we collect grows. */ public class SpringContextModelGatherers { @@ -38,10 +38,10 @@ private SpringContextModelGatherers() { * * @return a list of {@link JavaCheck} instances, each implementing {@link SpringContextModelGatherer} */ - public static List getAllGatherers(Telemetry telemetry) { + public static List getAllGatherers(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) { return List.of( - new ComponentScanPackageGatherer(telemetry), - new BeanDefinitionGatherer(telemetry) + new ComponentScanPackageGatherer(springContextGatheringModel, telemetry), + new BeanDefinitionGatherer(springContextGatheringModel, telemetry) ); } diff --git a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java index b90475ed428..3f5b7bb9baf 100644 --- a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java +++ b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java @@ -55,7 +55,9 @@ import org.sonar.java.exceptions.ApiMismatchException; import org.sonar.java.filters.SonarJavaIssueFilter; import org.sonar.java.model.JavaVersionImpl; +import org.sonar.java.model.springcontext.SpringContextModel; import org.sonar.java.telemetry.NoOpTelemetry; +import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaFileScanner; @@ -180,7 +182,8 @@ void scanning_empty_project_should_be_logged_in_file_by_file_sonarlint() { @Test void scanning_empty_project_should_be_logged_in_batch() { - JavaFrontend frontend = new JavaFrontend(new JavaVersionImpl(), mockSonarComponents(), new Measurer(sensorContext, mock(NoSonarFilter.class)), new NoOpTelemetry(), mock(JavaResourceLocator.class), mainCodeIssueScannerAndFilter); + JavaFrontend frontend = new JavaFrontend(new JavaVersionImpl(), mockSonarComponents(), new Measurer(sensorContext, mock(NoSonarFilter.class)), new NoOpTelemetry(), + mock(JavaResourceLocator.class), mainCodeIssueScannerAndFilter); frontend.scan(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); assertThat(filterOutAnalysisProgress(logTester.logs(Level.INFO))).containsExactly( @@ -235,6 +238,34 @@ void scanning_empty_project_should_be_logged_in_autoscan() { ); } + @Test + void spring_bean_is_registered_once_when_main_and_test_scanners_finish() throws IOException { + sensorContext = SensorContextTester.create(temp.toFile().getAbsoluteFile()); + sensorContext.setRuntime(SONARQUBE_RUNTIME); + InputFile mainFile = addFile(temp, "@org.springframework.stereotype.Component class MainBean {}", sensorContext); + InputFile testFile = addFile(temp, "@org.springframework.stereotype.Component class HelperTest {}", sensorContext); + + FileLinesContextFactory fileLinesContextFactory = mock(FileLinesContextFactory.class); + when(fileLinesContextFactory.createFor(any(InputFile.class))).thenReturn(mock(FileLinesContext.class)); + javaClasspath = mock(ClasspathForMain.class); + javaTestClasspath = mock(ClasspathForTest.class); + var classpath = TestClasspathUtils.DEFAULT_MODULE.getClassPath(); + when(javaClasspath.getElements()).thenReturn(classpath); + when(javaTestClasspath.getElements()).thenReturn(classpath); + + sonarComponents = new SonarComponents(fileLinesContextFactory, sensorContext.fileSystem(), javaClasspath, javaTestClasspath, mock(CheckFactory.class), mock(ActiveRules.class)); + sonarComponents.setSensorContext(sensorContext); + var springContextModel = new SpringContextModel(); + sonarComponents.setSpringContextModel(springContextModel); + + JavaFrontend frontend = new JavaFrontend(new JavaVersionImpl(), sonarComponents, new Measurer(sensorContext, mock(NoSonarFilter.class)), new NoOpTelemetry(), + mock(JavaResourceLocator.class), null); + frontend.scan(List.of(mainFile), List.of(testFile), List.of()); + + assertThat(springContextModel.getBeanDefinitionRegistry().getByName("mainBean")).hasSize(1); + assertThat(springContextModel.getBeanDefinitionRegistry().getByName("helperTest")).hasSize(1); + } + @Test void test_scan_logs_when_caching_is_enabled_and_can_skip_unchanged_files() throws ApiMismatchException { File baseDir = temp.toFile().getAbsoluteFile(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java index db0f37b5179..ad0df1d3643 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java @@ -51,7 +51,7 @@ class BeanDefinitionGathererTest extends SpringContextGathererTest { @BeforeEach void setUp() { - gatherer = new BeanDefinitionGatherer(new NoOpTelemetry()); + gatherer = new BeanDefinitionGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()); model = new SpringContextModel(); } @@ -315,8 +315,9 @@ void scanWithoutParsing_restores_beans_from_cache() { InputFileScannerContext context = mock(InputFileScannerContext.class); when(context.getInputFile()).thenReturn(inputFile); when(context.getCacheContext()).thenReturn(cacheContext); + when(context.getModuleKey()).thenReturn(""); - gatherer = new BeanDefinitionGatherer(new NoOpTelemetry()); + gatherer = new BeanDefinitionGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()); model = new SpringContextModel(); assertThat(gatherer.scanWithoutParsing(context)).isTrue(); verify(writeCache).copyFromPrevious(entry.key()); @@ -351,8 +352,9 @@ void parsed_beans_replace_beans_restored_for_the_same_file() { InputFileScannerContext context = mock(InputFileScannerContext.class); when(context.getInputFile()).thenReturn(inputFile); when(context.getCacheContext()).thenReturn(cacheContext); + when(context.getModuleKey()).thenReturn(""); - gatherer = new BeanDefinitionGatherer(new NoOpTelemetry()); + gatherer = new BeanDefinitionGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()); model = new SpringContextModel(); assertThat(gatherer.scanWithoutParsing(context)).isTrue(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java index 81ac09f60d0..e10ac5712b9 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java @@ -46,7 +46,7 @@ class ComponentScanPackageGathererTest extends SpringContextGathererTest { @BeforeEach void setUp() { - gatherer = new ComponentScanPackageGatherer(new NoOpTelemetry()); + gatherer = new ComponentScanPackageGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()); model = new SpringContextModel(); } @@ -135,6 +135,16 @@ void no_scan_annotations_collects_nothing() { assertThat(model.getProjectPackageScan().getPackagesForModule(MODULE_KEY)).isEmpty(); } + @Test + void module_without_scanned_files_collects_nothing() { + ModuleScannerContext context = mock(ModuleScannerContext.class); + when(context.getModuleKey()).thenReturn(MODULE_KEY); + + gatherer.gatherSpringContextData(context, model); + + assertThat(model.getProjectPackageScan().getPackagesForModule(MODULE_KEY)).isEmpty(); + } + // ---- Multiple files ------------------------------------------------------- @Test @@ -177,6 +187,7 @@ void scanWithoutParsing_restores_packages_from_cache() { InputFileScannerContext context = mock(InputFileScannerContext.class); when(context.getInputFile()).thenReturn(inputFile); when(context.getCacheContext()).thenReturn(cacheContext); + when(context.getModuleKey()).thenReturn(MODULE_KEY); assertThat(gatherer.scanWithoutParsing(context)).isTrue(); @@ -201,6 +212,7 @@ void parsed_packages_replace_packages_restored_for_the_same_file() { InputFileScannerContext context = mock(InputFileScannerContext.class); when(context.getInputFile()).thenReturn(inputFile); when(context.getCacheContext()).thenReturn(cacheContext); + when(context.getModuleKey()).thenReturn(MODULE_KEY); assertThat(gatherer.scanWithoutParsing(context)).isTrue(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java index 7c48060527c..3938ddb9fa2 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java @@ -63,7 +63,7 @@ class SpringContextModelGathererTest { @Test void gathering_time_is_reported() { var telemetry = new DefaultTelemetry(); - scanFile(SIMPLE_CLASS, new SampleGatherer(telemetry), CLASSPATH); + scanFile(SIMPLE_CLASS, new SampleGatherer(new SpringContextGatheringModel(), telemetry), CLASSPATH); assertThat(model.getTypeToBeansIndex().getNamesForType("com.example.MyService", "", Set.of())).containsExactly("myServiceBean"); assertThat(telemetry.toMap().get(GATHERING_TIME_KEY)).matches("\\d+"); } @@ -71,14 +71,14 @@ void gathering_time_is_reported() { @Test void every_phase_of_a_parsed_file_is_measured() { var telemetry = new DefaultTelemetry(); - scanFile(SIMPLE_CLASS, new SampleGatherer(telemetry, new IncrementingNanoTime(ONE_MILLISECOND_IN_NANOS)), CLASSPATH); + scanFile(SIMPLE_CLASS, new SampleGatherer(new SpringContextGatheringModel(), telemetry, new IncrementingNanoTime(ONE_MILLISECOND_IN_NANOS)), CLASSPATH); assertThat(telemetry.toMap()).containsEntry(GATHERING_TIME_KEY, String.valueOf(PHASES_OF_A_PARSED_FILE)); } @Test void restoring_a_file_from_the_cache_is_measured() { var telemetry = new DefaultTelemetry(); - var gatherer = new SampleGatherer(telemetry, new IncrementingNanoTime(ONE_MILLISECOND_IN_NANOS)); + var gatherer = new SampleGatherer(new SpringContextGatheringModel(), telemetry, new IncrementingNanoTime(ONE_MILLISECOND_IN_NANOS)); gatherer.scanWithoutParsing(mock(InputFileScannerContext.class)); bridgeFor(gatherer, CLASSPATH).endOfAnalysis(); assertThat(telemetry.toMap()).containsEntry(GATHERING_TIME_KEY, "2"); @@ -91,7 +91,7 @@ void restoring_a_file_from_the_cache_is_measured() { @Test void sub_millisecond_phases_are_summed_before_being_converted_to_millis() { var telemetry = new DefaultTelemetry(); - scanFile(SIMPLE_CLASS, new SampleGatherer(telemetry, new IncrementingNanoTime(SUB_MILLISECOND_IN_NANOS)), CLASSPATH); + scanFile(SIMPLE_CLASS, new SampleGatherer(new SpringContextGatheringModel(), telemetry, new IncrementingNanoTime(SUB_MILLISECOND_IN_NANOS)), CLASSPATH); assertThat(telemetry.toMap()).containsEntry(GATHERING_TIME_KEY, "1"); } @@ -100,12 +100,12 @@ void sub_millisecond_phases_are_summed_before_being_converted_to_millis() { @ParameterizedTest @ValueSource(strings = {"spring-context", "spring-beans", "spring-boot-starter", "spring-boot-starter-web"}) void isCompatibleWithDependencies_true_when_spring_dependency_is_present(String dependency) { - assertThat(new SampleGatherer(new NoOpTelemetry()).isCompatibleWithDependencies(finderFor(dependency))).isTrue(); + assertThat(new SampleGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()).isCompatibleWithDependencies(finderFor(dependency))).isTrue(); } @Test void isCompatibleWithDependencies_false_when_no_spring_dependency_is_present() { - assertThat(new SampleGatherer(new NoOpTelemetry()).isCompatibleWithDependencies(finderFor())).isFalse(); + assertThat(new SampleGatherer(new SpringContextGatheringModel(), new NoOpTelemetry()).isCompatibleWithDependencies(finderFor())).isFalse(); } // ---- Helpers -------------------------------------------------------------- @@ -136,12 +136,12 @@ private static Function> finderFor(String... presentDe static class SampleGatherer extends SpringContextModelGatherer { - SampleGatherer(Telemetry telemetry) { - super(telemetry); + SampleGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) { + super(springContextGatheringModel, telemetry); } - SampleGatherer(Telemetry telemetry, LongSupplier nanoTime) { - super(telemetry, nanoTime); + SampleGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry, LongSupplier nanoTime) { + super(springContextGatheringModel, telemetry, nanoTime); } @Override From 4231b6913ded1d06e6160bbb053c3cc5141806da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 1 Oct 2026 13:36:33 +0200 Subject: [PATCH 2/2] Fix QG --- .../src/test/java/org/sonar/java/JavaFrontendTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java index 3f5b7bb9baf..b06e36c7f8e 100644 --- a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java +++ b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java @@ -246,7 +246,7 @@ void spring_bean_is_registered_once_when_main_and_test_scanners_finish() throws InputFile testFile = addFile(temp, "@org.springframework.stereotype.Component class HelperTest {}", sensorContext); FileLinesContextFactory fileLinesContextFactory = mock(FileLinesContextFactory.class); - when(fileLinesContextFactory.createFor(any(InputFile.class))).thenReturn(mock(FileLinesContext.class)); + when(fileLinesContextFactory.createFor(any(InputFile.class))).thenReturn(fileLinesContext); javaClasspath = mock(ClasspathForMain.class); javaTestClasspath = mock(ClasspathForTest.class); var classpath = TestClasspathUtils.DEFAULT_MODULE.getClassPath();