Skip to content
Merged
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
7 changes: 0 additions & 7 deletions its/plugin/tests/src/test/java/com/sonar/it/java/JspTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,6 @@ public void should_transpile_jsp() throws Exception {
Telemetry java.language.version: 8
Telemetry java.module_count: 1
Telemetry java.scanner_app: ScannerMaven
Telemetry java.spring.bean_count: \\d+
Telemetry java.spring.bean_name_count: \\d+
Telemetry java.spring.component_scan_package_count: \\d+
Telemetry java.spring.context_checks_time_ms: \\d+
Telemetry java.spring.context_model_gathering_time_ms: \\d+
Telemetry java.spring.context_model_size_bytes: \\d+
Telemetry java.spring.injection_point_count: \\d+
"""));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public void foo() {

List<RaisedIssueDto> issues = analyzeFileAndGetIssues(inputFileDto.getUri());

assertThat(baseDir.resolve(".sonar/spring-context-model.json")).doesNotExist();

assertThat(issues)
.extracting(
RaisedIssueDto::getRuleKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.sonarsource.scanner.integrationtester.dsl.ScannerOutputReader;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.sonar.java.it.ScannerIntegrationAbstractTest;

Expand Down Expand Up @@ -91,6 +92,11 @@ class SpringTelemetryTest extends ScannerIntegrationAbstractTest {
void spring_context_model_is_reported_as_telemetry() {
var project = analyzeProject(Path.of("ambiguous-dependencies-should-be-resolved"), "S9352").getProject();

assertThat(projectBaseDir.resolve(".sonar/spring-context-model.json")).exists();

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.

Let's also add checks that such files aren't created for sub-modules.

for (String module : List.of("module-common", "module-a", "module-b", "app")) {
assertThat(projectBaseDir.resolve(module).resolve(".sonar/spring-context-model.json")).doesNotExist();
}

assertThat(telemetryValue(project, "java.spring.bean_count"))
.isEqualTo(Integer.toString(EXPECTED_BEAN_COUNT));
assertThat(telemetryValue(project, "java.spring.bean_name_count"))
Expand All @@ -109,6 +115,16 @@ void spring_context_model_is_reported_as_telemetry() {
.isLessThan(MAXIMUM_EXPECTED_CONTEXT_MODEL_SIZE_BYTES);
}

@Test
void spring_context_is_not_collected_or_saved_without_spring_dependencies() {
var project = analyzeProject(Path.of("no-spring-dependencies"), "S9352").getProject();

assertThat(projectBaseDir.resolve(".sonar/spring-context-model.json")).doesNotExist();
assertThat(project.getTelemetryEntry("java.spring.bean_count")).isNull();
assertThat(project.getTelemetryEntry("java.spring.context_model_gathering_time_ms")).isNull();
assertThat(project.getTelemetryEntry("java.spring.context_checks_time_ms")).isNull();
}

private static String telemetryValue(ScannerOutputReader.AnalyzedProject project, String key) {
ScannerOutputReader.TelemetryEntry entry = project.getTelemetryEntry(key);
assertThat(entry).as("telemetry entry %s", key).isNotNull();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>no-spring-dependencies</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app</artifactId>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package example;

public class Service {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>no-spring-dependencies</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
</modules>
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
* <p>The structure is serialized to JSON and saved to a file at the end of the analysis, so it can be restored and
* reused in later analyses when {@code sonar.java.springContext.model.path} is explicitly set. Restored data for
* unvisited files is retained to provide whole-project context during partial analyses.</p>
*
* <p>Collection is enabled when Spring dependencies are detected in any analyzed module outside SonarLint.
* This state belongs to the current analysis and is neither persisted nor restored.</p>
*/
@ScannerSide
@SonarLintSide
Expand Down Expand Up @@ -76,6 +79,15 @@ public Set<String> packages() {
*/
private final Map<String, Map<String, InputFileData>> filesData = new HashMap<>();
private boolean restored;
private boolean enabled;

public boolean isEnabled() {
return enabled;
}

public void enable() {
enabled = true;
}

/**
* Returns the file data indexed by module key and file key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,7 @@ protected SpringContextModelGatherer(SpringContextGatheringModel springContextGa

@Override
public boolean isCompatibleWithDependencies(Function<String, Optional<Version>> dependencyFinder) {
return dependencyFinder.apply("spring-context")
.or(() -> dependencyFinder.apply("spring-beans"))
.or(() -> dependencyFinder.apply("spring-boot-starter"))
.or(() -> dependencyFinder.apply("spring-boot-starter-web"))
.isPresent();
return SpringContextModelGatherers.hasSpringDependency(dependencyFinder);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package org.sonar.java.model.springcontext;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.sonar.java.telemetry.Telemetry;
import org.sonar.plugins.java.api.Version;

/**
* Registry of all {@link SpringContextModelGatherer} visitors that collect data for the {@link SpringContextModel}
Expand All @@ -32,6 +35,14 @@ private SpringContextModelGatherers() {
// utility class, should not be instantiated
}

public static boolean hasSpringDependency(Function<String, Optional<Version>> dependencyFinder) {
return dependencyFinder.apply("spring-context")
.or(() -> dependencyFinder.apply("spring-beans"))
.or(() -> dependencyFinder.apply("spring-boot-starter"))
.or(() -> dependencyFinder.apply("spring-boot-starter-web"))
.isPresent();
}

/**
* Returns all gatherers that contribute data to the {@link SpringContextModel}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ void round_trips_module_and_file_data_without_serializing_input_files() {
var bean = new BeanDefinitionHolder.InputFileData("bean", "example.Bean", BeanDefinitionKind.STEREOTYPE, "example", new TextSpan(1, 0, 1, 4),
false, ProfileExpression.UNCONDITIONAL, null, Map.of(), Set.of("example.Bean"));
var model = new SpringContextGatheringModel();
model.enable();
model.collectBeans("module-a", "file-a", INPUT_FILE, List.of(bean));
model.collectPackages("module-a", "file-a", INPUT_FILE, Set.of("example"));
model.collectBeans("module-a", "file-b", INPUT_FILE, List.of());
model.collectPackages("module-b", "file-c", INPUT_FILE, Set.of("other"));

String json = GSON.toJson(model);

assertThat(JsonParser.parseString(json).getAsJsonObject().keySet()).containsExactly("filesData");

var filesData = JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("filesData");
assertThat(filesData.keySet()).containsExactlyInAnyOrder("module-a", "module-b");
var file = filesData.getAsJsonObject("module-a").getAsJsonObject("file-a");
Expand All @@ -59,6 +62,7 @@ void round_trips_module_and_file_data_without_serializing_input_files() {
assertThat(file.getAsJsonArray("packages").get(0).getAsString()).isEqualTo("example");

var restored = GSON.fromJson(json, SpringContextGatheringModel.class);
assertThat(restored.isEnabled()).isFalse();
assertThat(restored.filesData().get("module-a")).containsOnlyKeys("file-a", "file-b");
assertThat(restored.filesData().get("module-b")).containsOnlyKeys("file-c");
assertThat(restored.filesData().get("module-a").get("file-a").beans()).containsExactly(bean);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public void define(Context context) {
} else {
list.add(DefaultTelemetry.class);
list.add(ProjectEndOfAnalysisSensor.class);
list.add(SpringContextModelSensor.class);
list.addAll(SurefireExtensions.getExtensions());
list.add(DroppedPropertiesSensor.class);
list.add(JavaSonarWayProfile.class);
Expand Down Expand Up @@ -125,7 +126,6 @@ public void define(Context context) {
.defaultValue("False")
.build(),
JavaSensor.class,
SpringContextModelSensor.class,
PostAnalysisIssueFilter.class));

list.add(AnalysisWarningsWrapper.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.sonar.plugins.java;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
Expand All @@ -34,11 +35,11 @@
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.issue.NoSonarFilter;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleScope;
import org.sonar.java.GeneratedCheckList;
import org.sonar.java.JavaFrontend;
import org.sonar.java.Measurer;
import org.sonar.java.SonarComponents;
import org.sonar.java.classpath.DependencyVersionInference;
import org.sonar.java.filters.PostAnalysisIssueFilter;
import org.sonar.java.jsp.Jasper;
import org.sonar.java.model.GeneratedFile;
Expand Down Expand Up @@ -91,11 +92,6 @@ public JavaSensor(SonarComponents sonarComponents, JavaResourceLocator javaResou
this.springContextGatheringModel = springContextGatheringModel;
this.sonarComponents.registerMainChecks(GeneratedCheckList.REPOSITORY_KEY, GeneratedCheckList.getJavaChecks());
this.sonarComponents.registerTestChecks(GeneratedCheckList.REPOSITORY_KEY, GeneratedCheckList.getJavaTestChecks());
// Main scope only: the Spring context model describes the application context, which never contains the beans
// declared by test code. Gatherers registered for RuleScope.TEST would be handed exactly the files
// JavaFileTypeClassifier already excludes, and collect nothing.
SpringContextModelGatherers.getAllGatherers(springContextGatheringModel, telemetry)
.forEach(gatherer -> this.sonarComponents.registerCustomFileScanner(RuleScope.MAIN, gatherer));
}

@Override
Expand All @@ -108,13 +104,20 @@ public void execute(SensorContext context) {
PerformanceMeasure.Duration sensorDuration = createPerformanceMeasureReport(context);

sonarComponents.setSensorContext(context);
// Restore the Spring context gathering model from the previous analysis, if it hasn't been restored already
// (i.e. the model is only restored the first time the sensor is run on a module and is reused between modules).
if (context.config().hasKey(SPRING_CONTEXT_MODEL_PATH_PROPERTY) && !springContextGatheringModel.isRestored()) {
var path = SpringContextModelPersistence.modelPath(context, sonarComponents.projectLevelBaseDir());
springContextGatheringModel.restoreFrom(SpringContextModelPersistence.load(path));
}
sonarComponents.setCheckFilter(createCheckFilter(sonarComponents.isAutoScanCheckFiltering()));
List<JavaCheck> mainChecks = new ArrayList<>(sonarComponents.mainChecks());
if (!sonarComponents.isSonarLintContext()) {
List<File> classpath = sonarComponents.getJavaClasspath();
var dependencyService = new DependencyVersionInference();
if (SpringContextModelGatherers.hasSpringDependency(artifactId -> dependencyService.infer(artifactId, classpath))) {
springContextGatheringModel.enable();
if (context.config().hasKey(SPRING_CONTEXT_MODEL_PATH_PROPERTY) && !springContextGatheringModel.isRestored()) {
var path = SpringContextModelPersistence.modelPath(context, sonarComponents.projectLevelBaseDir());
springContextGatheringModel.restoreFrom(SpringContextModelPersistence.load(path));
}
mainChecks.addAll(SpringContextModelGatherers.getAllGatherers(springContextGatheringModel, telemetry));
}
}

Measurer measurer = new Measurer(context, noSonarFilter);

Expand All @@ -131,7 +134,7 @@ public void execute(SensorContext context) {
telemetry,
javaResourceLocator,
postAnalysisIssueFilter,
sonarComponents.mainChecks().toArray(new JavaCheck[0]));
mainChecks.toArray(new JavaCheck[0]));
frontend.scan(getSourceFiles(context.fileSystem()), getTestFiles(context.fileSystem()), runJasper(context));

sensorDuration.stop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.sonar.plugins.java;

import java.util.concurrent.TimeUnit;
import org.sonar.api.SonarProduct;
import org.sonar.api.batch.DependedUpon;
import org.sonar.api.batch.Phase;
import org.sonar.api.batch.sensor.SensorContext;
Expand Down Expand Up @@ -72,6 +73,9 @@ public void describe(SensorDescriptor descriptor) {
*/
@Override
public void execute(SensorContext context) {
if (context.runtime().getProduct() == SonarProduct.SONARLINT || !gatheringModel.isEnabled()) {
return;
}
long buildingStartTime = System.nanoTime();
SpringContextModel springContextModel = SpringContextModel.of(gatheringModel);
telemetry.aggregateAsCounter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ void sonarLint_9_9_extensions() {
Plugin.Context context = new Plugin.Context(runtime);
javaPlugin.define(context);
assertThat(context.getExtensions())
.hasSize(23)
.contains(SonarLintCache.class, SpringContextGatheringModel.class);
.hasSize(22)
.contains(SonarLintCache.class, SpringContextGatheringModel.class)
.doesNotContain(SpringContextModelSensor.class);
}


Expand All @@ -53,7 +54,7 @@ void sonarqube_9_9_extensions() {
javaPlugin.define(context);
assertThat(context.getExtensions())
.hasSize(40)
.contains(SpringContextGatheringModel.class)
.contains(SpringContextGatheringModel.class, SpringContextModelSensor.class)
.doesNotContain(Jasper.class);
}

Expand All @@ -64,7 +65,7 @@ void sonarqube_9_9_commercial_extensions() {
javaPlugin.define(context);
assertThat(context.getExtensions())
.hasSize(41)
.contains(Jasper.class, SpringContextGatheringModel.class);
.contains(Jasper.class, SpringContextGatheringModel.class, SpringContextModelSensor.class);
}

}
Loading
Loading