From a087eb237d62b3280f1fc5d3540b1b57afccd4bd Mon Sep 17 00:00:00 2001
From: Steve Marion <steve.marion@sonarsource.com>
Date: Fri, 28 Feb 2025 16:38:04 +0100
Subject: SONAR-24521 Implements sonar plugin API addAnalysisData method in the
 scanner-engine. Add Xoo plugin sensor that utilizes the analysisData method.

---
 .../mediumtest/analysisdata/AnalysisDataIT.java    | 68 ++++++++++++++++++++++
 .../scanner/bootstrap/ScannerPluginRepository.java | 12 ++--
 .../org/sonar/scanner/mediumtest/LocalPlugin.java  |  8 ++-
 .../sonar/scanner/sensor/DefaultSensorStorage.java | 36 ++++++++++--
 .../sonar/scanner/sensor/ProjectSensorContext.java | 10 ++++
 .../scanner/sensor/DefaultSensorStorageTest.java   | 25 ++++----
 .../sonar/scanner/mediumtest/AnalysisResult.java   |  4 ++
 .../scanner/mediumtest/FakePluginInstaller.java    |  7 ++-
 .../scanner/mediumtest/ScannerMediumTester.java    | 10 +++-
 9 files changed, 151 insertions(+), 29 deletions(-)
 create mode 100644 sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/analysisdata/AnalysisDataIT.java

(limited to 'sonar-scanner-engine')

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;
@@ -232,6 +233,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;
-- 
cgit v1.2.3