diff options
author | Steve Marion <steve.marion@sonarsource.com> | 2025-02-28 16:38:04 +0100 |
---|---|---|
committer | Matteo Mara <matteo.mara@sonarsource.com> | 2025-03-17 22:23:55 +0100 |
commit | a087eb237d62b3280f1fc5d3540b1b57afccd4bd (patch) | |
tree | e7cbc5dfa9581be63b0cdb9d8e1a2657a0f187f5 | |
parent | e496fb60dd1fbf1fd90b6ca08b9499ba8035a5c0 (diff) | |
download | sonarqube-a087eb237d62b3280f1fc5d3540b1b57afccd4bd.tar.gz sonarqube-a087eb237d62b3280f1fc5d3540b1b57afccd4bd.zip |
SONAR-24521 Implements sonar plugin API addAnalysisData method in the scanner-engine.
Add Xoo plugin sensor that utilizes the analysisData method.
20 files changed, 429 insertions, 38 deletions
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index 7eed410abd7..b32bca06bda 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -24,6 +24,7 @@ import org.sonar.api.PropertyType; import org.sonar.api.SonarProduct; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinition.ConfigScope; +import org.sonar.xoo.architecture.ArchitectureSensor; import org.sonar.xoo.coverage.ItCoverageSensor; import org.sonar.xoo.coverage.OverallCoverageSensor; import org.sonar.xoo.coverage.UtCoverageSensor; @@ -197,6 +198,8 @@ public class XooPlugin implements Plugin { HotspotWithSingleContextSensor.class, HotspotWithCodeVariantsSensor.class, + ArchitectureSensor.class, + // Coverage UtCoverageSensor.class, ItCoverageSensor.class, diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java new file mode 100644 index 00000000000..e8fbc623c87 --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/ArchitectureSensor.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.architecture; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.stream.StreamSupport; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.scanner.sensor.ProjectSensor; +import org.sonar.xoo.Xoo; + +public class ArchitectureSensor implements ProjectSensor { + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("architecture-xoo-sensor") + .onlyOnLanguage(Xoo.KEY); + } + + @Override + public void execute(SensorContext context) { + final String mimeType = "application/file-graph+json;version=1.0;source=xoo"; + + long count = StreamSupport.stream( + context.fileSystem().inputFiles( + context.fileSystem().predicates().hasLanguage(Xoo.KEY)).spliterator(), false) + .count(); + + context.addAnalysisData( + Xoo.NAME + ".class_file_graph", + mimeType, + new ByteArrayInputStream(("{graph:\"data\", \"classCount\":" + count + "}") + .getBytes(StandardCharsets.UTF_8)) + ); + + context.addAnalysisData( + Xoo.NAME + ".file_graph", + mimeType, + new ByteArrayInputStream(("{graph:\"data\", \"fileCount\":" + count + "}") + .getBytes(StandardCharsets.UTF_8)) + ); + } +} diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/package-info.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/package-info.java new file mode 100644 index 00000000000..45a09887dc7 --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/architecture/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.xoo.architecture; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java new file mode 100644 index 00000000000..09e46c9a21d --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/architecture/ArchitectureSensorTest.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.architecture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.xoo.Xoo; + +public class ArchitectureSensorTest { + + @Test + public void whenDescribeCalled_thenNameAndLanguageAreSet() { + // given + SensorDescriptor descriptor = mock(SensorDescriptor.class); + when(descriptor.name(anyString())).thenReturn(descriptor); + when(descriptor.onlyOnLanguage(anyString())).thenReturn(descriptor); + + ArchitectureSensor sensor = new ArchitectureSensor(); + + // when + sensor.describe(descriptor); + + // then + verify(descriptor).onlyOnLanguage(Xoo.KEY); + verify(descriptor).name(anyString()); + } + + @Test + public void whenExecuteCalled_thenArchitectureDataIsSaved() { + // given + final int nbFileSensor = 5; + SensorContext context = mock(SensorContext.class, RETURNS_DEEP_STUBS); + + when(context.fileSystem().inputFiles(any())).thenReturn( + Stream.generate(() -> mock(InputFile.class)).limit(nbFileSensor).toList() + ); + + ArchitectureSensor sensor = new ArchitectureSensor(); + + // when + sensor.execute(context); + + // then + ArgumentCaptor<InputStream> inputStreamCaptor = ArgumentCaptor.forClass(InputStream.class); + verify(context).addAnalysisData(eq(Xoo.NAME + ".file_graph"), contains("application/file-graph+json"), inputStreamCaptor.capture()); + try { + String capturedData = new String(inputStreamCaptor.getValue().readAllBytes(), StandardCharsets.UTF_8); + assertThat(capturedData).contains("\"fileCount\":" + nbFileSensor); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java index b62b77831b4..09157ef9420 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java @@ -19,12 +19,17 @@ */ package org.sonar.api.batch.sensor.internal; +import static org.sonar.api.utils.Preconditions.checkArgument; + +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.sonar.api.batch.sensor.code.NewSignificantCode; import org.sonar.api.batch.sensor.code.internal.DefaultSignificantCode; import org.sonar.api.batch.sensor.coverage.NewCoverage; @@ -41,8 +46,6 @@ import org.sonar.api.batch.sensor.rule.AdHocRule; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; -import static org.sonar.api.utils.Preconditions.checkArgument; - class InMemorySensorStorage implements SensorStorage { Map<String, Map<String, Measure>> measuresByComponentAndMetric = new HashMap<>(); @@ -58,6 +61,7 @@ class InMemorySensorStorage implements SensorStorage { Map<String, DefaultSymbolTable> symbolsPerComponent = new HashMap<>(); Map<String, String> contextProperties = new HashMap<>(); Map<String, String> telemetryEntries = new HashMap<>(); + Map<String, AnalysisData> analysisDataEntries = new HashMap<>(); Map<String, DefaultSignificantCode> significantCodePerComponent = new HashMap<>(); @Override @@ -157,4 +161,21 @@ class InMemorySensorStorage implements SensorStorage { } significantCodePerComponent.put(fileKey, significantCode); } + + public void storeAnalysisData(String key, String mimeType, InputStream data) { + checkArgument(!StringUtils.isBlank(key), "Key must not be null"); + checkArgument(!StringUtils.isBlank(mimeType), "MimeType must not be null"); + checkArgument(data != null, "Data must not be null"); + if (analysisDataEntries.containsKey(key)) { + throw new UnsupportedOperationException("Trying to save analysis data twice for the same key is not supported: " + key); + } + try (data) { + byte[] bytes = data.readAllBytes(); + analysisDataEntries.put(key, new AnalysisData(key, mimeType, bytes)); + } catch (IOException e) { + throw new IllegalStateException("Failed to read data from InputStream", e); + } + } + + record AnalysisData(String key, String mimeType, byte[] data) { } } diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index 02c5fa5191f..b4fd5512335 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -19,7 +19,10 @@ */ package org.sonar.api.batch.sensor.internal; +import static java.util.Collections.unmodifiableMap; + import java.io.File; +import java.io.InputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.file.Path; @@ -87,8 +90,6 @@ import org.sonar.api.scanner.fs.InputProject; import org.sonar.api.utils.System2; import org.sonar.api.utils.Version; -import static java.util.Collections.unmodifiableMap; - /** * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve. * <p> @@ -447,6 +448,12 @@ public class SensorContextTester implements SensorContext { sensorStorage.storeTelemetry(key, value); } + @Override + public void addAnalysisData(String key, String mimeType, InputStream data) { + //No Need to check the source of the plugin in the tester + sensorStorage.storeAnalysisData(key,mimeType, data); + } + public void setCacheEnabled(boolean enabled) { this.cacheEnabled = enabled; } diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java index d4ca8ee5865..85b073639b6 100644 --- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java @@ -19,12 +19,16 @@ */ package org.sonar.api.batch.sensor.internal; -import org.junit.Test; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.data.MapEntry.entry; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Test; + public class InMemorySensorStorageTest { InMemorySensorStorage underTest = new InMemorySensorStorage(); @@ -50,4 +54,45 @@ public class InMemorySensorStorageTest { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Value of context property must not be null"); } + + @Test + public void test_storeAnalysisData() { + // Given + String key = "analysisKey"; + String mimeType = "mimeType"; + String dataString = "analysisData"; + ByteArrayInputStream dataStream = new ByteArrayInputStream(dataString.getBytes(StandardCharsets.UTF_8)); + + // When + underTest.storeAnalysisData(key, mimeType, dataStream); + + // Then + assertThat(underTest.analysisDataEntries).containsKey(key); + assertThat(new String(underTest.analysisDataEntries.get(key).data(), StandardCharsets.UTF_8)).isEqualTo(dataString); + } + + @Test + public void storeAnalysisData_throws_UOE_if_operation_not_supported() { + underTest.storeAnalysisData("unsupportedKey", "mimeType", new ByteArrayInputStream("dummyData".getBytes(StandardCharsets.UTF_8))); + ByteArrayInputStream dataStream = new ByteArrayInputStream("newData".getBytes(StandardCharsets.UTF_8)); + + assertThatThrownBy(() -> underTest.storeAnalysisData("unsupportedKey", "mimeType", dataStream)) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + public void storeAnalysisData_throws_IOE_on_data_handling_error() { + InputStream faultyStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated IO Exception"); + } + }; + + assertThatThrownBy(() -> underTest.storeAnalysisData("validKey", "mimeType", faultyStream)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to read data from InputStream"); + } + + } diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java new file mode 100644 index 00000000000..732d759264c --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.scanner.mediumtest.analysisdata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.testfixtures.log.LogTester; +import org.sonar.core.platform.PluginInfo; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.xoo.Xoo; +import org.sonar.xoo.XooPlugin; + +public class AnalysisDataIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin(new PluginInfo("xoo").setOrganizationName("SonarSource"), new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void whenScanningWithXoo_thenArchitectureGraphIsInReport() throws Exception { + // given + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + // when + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .execute(); + + // then + List<ScannerReport.AnalysisData> analysisData = result.analysisData(); + assertThat(analysisData) + .anyMatch(data -> data.getKey().equals(Xoo.NAME + ".file_graph")); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java index 3187c2e9aa3..194c1e17ffa 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java @@ -19,6 +19,10 @@ */ package org.sonar.scanner.bootstrap; +import static java.util.stream.Collectors.toMap; +import static org.sonar.api.utils.Preconditions.checkState; +import static org.sonar.core.config.ScannerProperties.PLUGIN_LOADING_OPTIMIZATION_KEY; + import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -38,10 +42,6 @@ import org.sonar.core.platform.PluginRepository; import org.sonar.core.plugin.PluginType; import org.sonar.scanner.mediumtest.LocalPlugin; -import static java.util.stream.Collectors.toMap; -import static org.sonar.api.utils.Preconditions.checkState; -import static org.sonar.core.config.ScannerProperties.PLUGIN_LOADING_OPTIMIZATION_KEY; - /** * Orchestrates the installation and loading of plugins */ @@ -83,7 +83,7 @@ public class ScannerPluginRepository implements PluginRepository, Startable { // this part is only used by medium tests for (LocalPlugin localPlugin : installer.installLocals()) { ScannerPlugin scannerPlugin = localPlugin.toScannerPlugin(); - String pluginKey = localPlugin.pluginKey(); + String pluginKey = localPlugin.pluginInfo().getKey(); pluginsByKeys.put(pluginKey, scannerPlugin); pluginInstancesByKeys.put(pluginKey, localPlugin.pluginInstance()); } @@ -112,7 +112,7 @@ public class ScannerPluginRepository implements PluginRepository, Startable { // this part is only used by medium tests for (LocalPlugin localPlugin : installer.installOptionalLocals(languageKeys)) { ScannerPlugin scannerPlugin = localPlugin.toScannerPlugin(); - String pluginKey = localPlugin.pluginKey(); + String pluginKey = localPlugin.pluginInfo().getKey(); languagePluginsByKeys.put(pluginKey, scannerPlugin); pluginsByKeys.put(pluginKey, scannerPlugin); pluginInstancesByKeys.put(pluginKey, localPlugin.pluginInstance()); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/LocalPlugin.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/LocalPlugin.java index 0a3d42979f7..70df99a4f3d 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/LocalPlugin.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/LocalPlugin.java @@ -25,9 +25,13 @@ import org.sonar.core.platform.PluginInfo; import org.sonar.core.plugin.PluginType; import org.sonar.scanner.bootstrap.ScannerPlugin; -public record LocalPlugin(String pluginKey, Plugin pluginInstance, Set<String> requiredForLanguages) { +public record LocalPlugin(PluginInfo pluginInfo, Plugin pluginInstance, Set<String> requiredForLanguages) { + + public LocalPlugin(String pluginKey, Plugin pluginInstance, Set<String> requiredForLanguages) { + this(new PluginInfo(pluginKey).setOrganizationName("SonarSource"), pluginInstance, requiredForLanguages); + } public ScannerPlugin toScannerPlugin() { - return new ScannerPlugin(pluginKey, 1L, PluginType.BUNDLED, new PluginInfo(pluginKey)); + return new ScannerPlugin(pluginInfo.getKey(), 1L, PluginType.BUNDLED, pluginInfo); } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java index 5fa6e33aac6..dfc6df5050c 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java @@ -19,6 +19,16 @@ */ package org.sonar.scanner.sensor; +import static java.lang.Math.max; +import static org.sonar.api.measures.CoreMetrics.COMMENT_LINES_DATA_KEY; +import static org.sonar.api.measures.CoreMetrics.LINES_KEY; +import static org.sonar.api.measures.CoreMetrics.PUBLIC_DOCUMENTED_API_DENSITY_KEY; +import static org.sonar.api.measures.CoreMetrics.TEST_SUCCESS_DENSITY_KEY; +import static org.sonar.api.utils.Preconditions.checkArgument; + +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.util.HashSet; import java.util.List; @@ -28,6 +38,7 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputComponent; @@ -80,12 +91,6 @@ import org.sonar.scanner.repository.ContextPropertiesCache; import org.sonar.scanner.repository.TelemetryCache; import org.sonar.scanner.scan.branch.BranchConfiguration; -import static java.lang.Math.max; -import static org.sonar.api.measures.CoreMetrics.COMMENT_LINES_DATA_KEY; -import static org.sonar.api.measures.CoreMetrics.LINES_KEY; -import static org.sonar.api.measures.CoreMetrics.PUBLIC_DOCUMENTED_API_DENSITY_KEY; -import static org.sonar.api.measures.CoreMetrics.TEST_SUCCESS_DENSITY_KEY; - public class DefaultSensorStorage implements SensorStorage { private static final Logger LOG = LoggerFactory.getLogger(DefaultSensorStorage.class); @@ -122,6 +127,7 @@ public class DefaultSensorStorage implements SensorStorage { private final ScannerMetrics scannerMetrics; private final BranchConfiguration branchConfiguration; private final Set<String> alreadyLogged = new HashSet<>(); + private final Set<String> alreadyAddedData = new HashSet<>(); public DefaultSensorStorage(MetricFinder metricFinder, IssuePublisher moduleIssues, Configuration settings, ReportPublisher reportPublisher, SonarCpdBlockIndex index, ContextPropertiesCache contextPropertiesCache, TelemetryCache telemetryCache, ScannerMetrics scannerMetrics, BranchConfiguration branchConfiguration) { @@ -472,4 +478,22 @@ public class DefaultSensorStorage implements SensorStorage { writer.writeComponentSignificantCode(componentRef, protobuf); } + + public void storeAnalysisData(String key, String mimeType, InputStream data) { + checkArgument(!StringUtils.isBlank(key), "Key must not be null"); + checkArgument(!alreadyAddedData.contains(key), "A data with this key already exists"); + checkArgument(!StringUtils.isBlank(mimeType), "MimeType must not be null"); + checkArgument(data != null, "Data must not be null"); + alreadyAddedData.add(key); + try (data) { + ScannerReport.AnalysisData analysisData = ScannerReport.AnalysisData.newBuilder() + .setKey(key) + .setData(ByteString.readFrom(data)) + .build(); + ScannerReportWriter writer = reportPublisher.getWriter(); + writer.appendAnalysisData(analysisData); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to read data InputStream", e); + } + } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java index bac06a38645..855fd945109 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java @@ -19,6 +19,7 @@ */ package org.sonar.scanner.sensor; +import java.io.InputStream; import java.io.Serializable; import javax.annotation.concurrent.ThreadSafe; import org.sonar.api.SonarRuntime; @@ -233,6 +234,15 @@ public class ProjectSensorContext implements SensorContext { } @Override + public void addAnalysisData(String key, String mimeType, InputStream data) { + if (isSonarSourcePlugin()) { + this.sensorStorage.storeAnalysisData(key, mimeType, data); + } else { + throw new IllegalStateException("Analysis data can only be added by SonarSource plugins"); + } + } + + @Override public NewSignificantCode newSignificantCode() { return new DefaultSignificantCode(sensorStorage); } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java index a6cabe242fb..e769e0d6451 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java @@ -19,6 +19,16 @@ */ package org.sonar.scanner.sensor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.data.MapEntry.entry; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -67,16 +77,6 @@ import org.sonar.scanner.repository.ContextPropertiesCache; import org.sonar.scanner.repository.TelemetryCache; import org.sonar.scanner.scan.branch.BranchConfiguration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.data.MapEntry.entry; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - class DefaultSensorStorageTest { @TempDir @@ -112,8 +112,9 @@ class DefaultSensorStorageTest { branchConfiguration = mock(BranchConfiguration.class); - underTest = new DefaultSensorStorage(metricFinder, - moduleIssues, settings.asConfig(), reportPublisher, mock(SonarCpdBlockIndex.class), contextPropertiesCache, telemetryCache, new ScannerMetrics(), branchConfiguration); + underTest = new DefaultSensorStorage( + metricFinder, moduleIssues, settings.asConfig(), reportPublisher, mock(SonarCpdBlockIndex.class), contextPropertiesCache, telemetryCache, new ScannerMetrics(), + branchConfiguration); project = new DefaultInputProject(ProjectDefinition.create() .setKey("foo") diff --git a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/AnalysisResult.java b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/AnalysisResult.java index 25b27dbb5ae..57843c8abd7 100644 --- a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/AnalysisResult.java +++ b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/AnalysisResult.java @@ -199,6 +199,10 @@ public class AnalysisResult implements AnalysisObserver { return readFromReport(ScannerReportReader::readAdHocRules); } + public List<ScannerReport.AnalysisData> analysisData() { + return readFromReport(ScannerReportReader::readAnalysisData); + } + @NotNull private <G> List<G> readFromReport(InputComponent component, BiFunction<ScannerReportReader, Integer, CloseableIterator<G>> readerMethod) { int ref = ((DefaultInputComponent) component).scannerId(); diff --git a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/FakePluginInstaller.java b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/FakePluginInstaller.java index 60a8348f778..5934c9da0bc 100644 --- a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/FakePluginInstaller.java +++ b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/FakePluginInstaller.java @@ -19,13 +19,13 @@ */ package org.sonar.scanner.mediumtest; +import jakarta.annotation.Priority; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import jakarta.annotation.Priority; import org.sonar.api.Plugin; import org.sonar.core.platform.PluginInfo; import org.sonar.core.plugin.PluginType; @@ -49,6 +49,11 @@ public class FakePluginInstaller implements PluginInstaller { return this; } + public FakePluginInstaller add(PluginInfo pluginInfo, Plugin instance) { + mediumTestPlugins.add(new LocalPlugin(pluginInfo, instance, Set.of())); + return this; + } + public FakePluginInstaller addOptional(String pluginKey, Set<String> requiredForLanguages, Plugin instance) { optionalMediumTestPlugins.add(new LocalPlugin(pluginKey, instance, requiredForLanguages)); return this; diff --git a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java index 1f6738ec99c..f2edfe609d4 100644 --- a/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java +++ b/sonar-scanner-engine/src/testFixtures/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java @@ -19,6 +19,8 @@ */ package org.sonar.scanner.mediumtest; +import static java.util.Collections.emptySet; + import jakarta.annotation.Priority; import java.io.File; import java.io.FileInputStream; @@ -60,6 +62,7 @@ import org.sonar.api.utils.Version; import org.sonar.batch.bootstrapper.Batch; import org.sonar.batch.bootstrapper.EnvironmentInformation; import org.sonar.batch.bootstrapper.LogOutput; +import org.sonar.core.platform.PluginInfo; import org.sonar.scanner.bootstrap.GlobalAnalysisMode; import org.sonar.scanner.cache.AnalysisCacheLoader; import org.sonar.scanner.protocol.internal.SensorCacheData; @@ -86,8 +89,6 @@ import org.sonarqube.ws.NewCodePeriods; import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile; import org.sonarqube.ws.Rules.Rule; -import static java.util.Collections.emptySet; - /** * Main utility class for writing scanner medium tests. */ @@ -146,6 +147,11 @@ public class ScannerMediumTester extends ExternalResource implements BeforeTestE return this; } + public ScannerMediumTester registerPlugin(PluginInfo pluginInfo, Plugin instance) { + pluginInstaller.add(pluginInfo, instance); + return this; + } + public ScannerMediumTester registerOptionalPlugin(String pluginKey, Set<String> requiredForLanguages, Plugin instance) { pluginInstaller.addOptional(pluginKey, requiredForLanguages, instance); return this; diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java index 3c0375698b7..fb10916aeac 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java @@ -98,6 +98,10 @@ public class FileStructure { return new File(dir, "analysis-warnings.pb"); } + public File analysisData() { + return new File(dir, "analysis-data.pb"); + } + public File root() { return dir; } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java index cdb7edb2f96..797c6ecae74 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java @@ -19,6 +19,8 @@ */ package org.sonar.scanner.protocol.output; +import static org.sonar.core.util.CloseableIterator.emptyCloseableIterator; + import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -28,8 +30,6 @@ import javax.annotation.CheckForNull; import org.sonar.core.util.CloseableIterator; import org.sonar.core.util.Protobuf; -import static org.sonar.core.util.CloseableIterator.emptyCloseableIterator; - public class ScannerReportReader { private final FileStructure fileStructure; @@ -241,4 +241,12 @@ public class ScannerReportReader { } return Protobuf.readStream(file, ScannerReport.TelemetryEntry.parser()); } + + public CloseableIterator<ScannerReport.AnalysisData> readAnalysisData() { + File file = fileStructure.analysisData(); + if (!fileExists(file)) { + return emptyCloseableIterator(); + } + return Protobuf.readStream(file, ScannerReport.AnalysisData.parser()); + } } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java index 2a27e0b2b3f..bbdb6d753fc 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java @@ -26,7 +26,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import javax.annotation.concurrent.Immutable; - import org.apache.commons.io.FileUtils; import org.sonar.core.util.ContextException; import org.sonar.core.util.Protobuf; @@ -167,6 +166,12 @@ public class ScannerReportWriter { return file; } + public File appendAnalysisData(ScannerReport.AnalysisData analysisData) { + File file = fileStructure.analysisData(); + appendDelimitedTo(file, analysisData, "analysis data for " + analysisData.getKey()); + return file; + } + public File getSourceFile(int componentRef) { return fileStructure.fileFor(FileStructure.Domain.SOURCE, componentRef); } diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index 1357ba53499..aaf5f025ee9 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -92,6 +92,12 @@ message TelemetryEntry { string value = 2; } +message AnalysisData { + string key = 1; + string mime_type = 2; + bytes data = 3; +} + message ActiveRule { string rule_repository = 1; string rule_key = 2; |