Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -375,7 +376,7 @@ private static SpringContextModel buildModel(List<String> 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);
Expand Down
5 changes: 3 additions & 2 deletions java-frontend/src/main/java/org/sonar/java/JavaFrontend.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InputFile, List<BeanDefinitionHolder.InputFileData>> beansCollectedByFile = new LinkedHashMap<>();

/**
* Beans found in the file currently being scanned, used for per-file cache writes.
*/
private final List<BeanDefinitionHolder.InputFileData> beansCollectedAtFileLevel = new ArrayList<>();

public BeanDefinitionGatherer(Telemetry telemetry) {
super(telemetry);
public BeanDefinitionGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) {
super(springContextGatheringModel, telemetry);
}

@Override
Expand Down Expand Up @@ -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();
}
Expand All @@ -164,7 +163,8 @@ public List<BeanDefinitionHolder.InputFileData> deserialize(byte[] data) {

@Override
public void restore(InputFileScannerContext context, List<BeanDefinitionHolder.InputFileData> beans) {
beansCollectedByFile.put(context.getInputFile(), List.copyOf(beans));
InputFile currentFile = context.getInputFile();
springContextGatheringModel.collectBeans(context.getModuleKey(), currentFile.key(), currentFile, beans);
}

/**
Expand All @@ -180,8 +180,9 @@ public void restore(InputFileScannerContext context, List<BeanDefinitionHolder.I
*/
@Override
public void gatherSpringContextData(ModuleScannerContext context, SpringContextModel springContextModel) {
beansCollectedByFile.forEach((inputFile, beans) -> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,18 +60,13 @@ public class ComponentScanPackageGatherer extends SpringContextModelGatherer imp
private static final Set<String> COMPONENT_SCAN_BASE_ARGUMENTS = SetUtils.immutableSetOf("basePackages", "basePackageClasses", "value");
private static final Set<String> SCAN_BASE_ANNOTATIONS = SetUtils.immutableSetOf("scanBasePackages", "scanBasePackageClasses");

/**
* Packages accumulated across all files in the current module, mapped by input file key.
*/
private final Map<String, Set<String>> collectedPackagesByFile = new HashMap<>();

/**
* Packages found in the file currently being scanned, used for per-file cache writes.
*/
private final Set<String> packagesCollectedAtFileLevel = new HashSet<>();

public ComponentScanPackageGatherer(Telemetry telemetry) {
super(telemetry);
public ComponentScanPackageGatherer(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) {
super(springContextGatheringModel, telemetry);
}

@Override
Expand Down Expand Up @@ -102,7 +96,8 @@ public Set<String> deserialize(byte[] data) {

@Override
public void restore(InputFileScannerContext context, Set<String> packages) {
collectedPackagesByFile.put(context.getInputFile().key(), Set.copyOf(packages));
InputFile currentFile = context.getInputFile();
springContextGatheringModel.collectPackages(context.getModuleKey(), currentFile.key(), currentFile, Set.copyOf(packages));
}

@Override
Expand All @@ -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<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BeanDefinitionHolder.InputFileData> beans, Set<String> packages) {
}

/**
* The Spring data collected per-module, per-file.
*/
private final Map<String, Map<String, InputFileData>> filesData = new HashMap<>();

public void collectBeans(String moduleKey, String fileKey, @Nullable InputFile inputFile, List<BeanDefinitionHolder.InputFileData> 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<String> 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<InputFileData> getInputFilesData(String moduleKey) {
return filesData.getOrDefault(moduleKey, Map.of()).values();
}

public Set<String> getPackages(String moduleKey) {
Set<String> packages = new HashSet<>();
getInputFilesData(moduleKey).forEach(inputFileData -> packages.addAll(inputFileData.packages));
return packages;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* Registry of all {@link SpringContextModelGatherer} visitors that populate the {@link SpringContextModel}
* during a module analysis.
*
* <p>Use {@link #getAllGatherers(Telemetry)} to obtain the full list of gatherers to be registered with the scanner.
* <p>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 {
Expand All @@ -38,10 +38,10 @@ private SpringContextModelGatherers() {
*
* @return a list of {@link JavaCheck} instances, each implementing {@link SpringContextModelGatherer}
*/
public static List<JavaCheck> getAllGatherers(Telemetry telemetry) {
public static List<JavaCheck> getAllGatherers(SpringContextGatheringModel springContextGatheringModel, Telemetry telemetry) {
return List.of(
new ComponentScanPackageGatherer(telemetry),
new BeanDefinitionGatherer(telemetry)
new ComponentScanPackageGatherer(springContextGatheringModel, telemetry),
new BeanDefinitionGatherer(springContextGatheringModel, telemetry)
);
}

Expand Down
33 changes: 32 additions & 1 deletion java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(fileLinesContext);
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();

Expand Down
Loading
Loading