]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10543 Sensor Java API should allow to add external rule engine issues
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Fri, 6 Apr 2018 09:33:23 +0000 (11:33 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 26 Apr 2018 18:20:49 +0000 (20:20 +0200)
36 files changed:
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java [new file with mode: 0644]
plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java

index 97e59ef3156e6611d33b534d282f1021425c150d..3c036b1d566bd6ea39fe3d16967ee650d35fbe06 100644 (file)
@@ -48,6 +48,7 @@ import org.sonar.xoo.rule.NoSonarSensor;
 import org.sonar.xoo.rule.OneBlockerIssuePerFileSensor;
 import org.sonar.xoo.rule.OneBugIssuePerLineSensor;
 import org.sonar.xoo.rule.OneDayDebtPerFileSensor;
+import org.sonar.xoo.rule.OneExternalIssuePerLineSensor;
 import org.sonar.xoo.rule.OneIssueOnDirPerFileSensor;
 import org.sonar.xoo.rule.OneIssuePerDirectorySensor;
 import org.sonar.xoo.rule.OneIssuePerFileSensor;
@@ -164,9 +165,13 @@ public class XooPlugin implements Plugin {
     if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(5, 5))) {
       context.addExtension(CpdTokenizerSensor.class);
     }
-    if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(6,6))) {
+    if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(6, 6))) {
       context.addExtension(XooBuiltInQualityProfilesDefinition.class);
     }
+    // TODO change to v7.2 once version of SQ was updated
+    if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 1))) {
+      context.addExtension(OneExternalIssuePerLineSensor.class);
+    }
   }
 
 }
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java
new file mode 100644 (file)
index 0000000..4dbf744
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.rule;
+
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.xoo.Xoo;
+import org.sonar.xoo.Xoo2;
+
+public class OneExternalIssuePerLineSensor implements Sensor {
+  public static final String RULE_KEY = "OneExternalIssuePerLine";
+  public static final String ENGINE_KEY = "XooEngine";
+  public static final String SEVERITY = "MAJOR";
+  public static final Long EFFORT = 10l;
+  public static final RuleType type = RuleType.BUG;
+  public static final String ACTIVATE_EXTERNAL_ISSUES = "sonar.oneExternalIssuePerLine.activate";
+
+  @Override
+  public void describe(SensorDescriptor descriptor) {
+    descriptor
+      .name("One External Issue Per Line")
+      .onlyOnLanguages(Xoo.KEY, Xoo2.KEY)
+      .onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE_EXTERNAL_ISSUES).orElse(false));
+  }
+
+  @Override
+  public void execute(SensorContext context) {
+    analyse(context, Xoo.KEY, XooRulesDefinition.XOO_REPOSITORY);
+    analyse(context, Xoo2.KEY, XooRulesDefinition.XOO2_REPOSITORY);
+  }
+
+  private void analyse(SensorContext context, String language, String repo) {
+    FileSystem fs = context.fileSystem();
+    FilePredicates p = fs.predicates();
+    for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(language), p.hasType(Type.MAIN)))) {
+      createIssues(file, context, repo);
+    }
+  }
+
+  private void createIssues(InputFile file, SensorContext context, String repo) {
+    RuleKey ruleKey = RuleKey.of(repo, RULE_KEY);
+    for (int line = 1; line <= file.lines(); line++) {
+      NewExternalIssue newIssue = context.newExternalIssue();
+      newIssue
+        .forRule(ruleKey)
+        .at(newIssue.newLocation()
+          .on(file)
+          .at(file.selectLine(line))
+          .message("This issue is generated on each line"))
+        .severity(Severity.valueOf(SEVERITY))
+        .remediationEffort(EFFORT)
+        .type(type)
+        .save();
+    }
+  }
+}
index 70f8d253b460bc37a61d53b2488c2ea4aa69e48d..6384ffa58c692ba0f62f81d693d9aa1c26d32475 100644 (file)
@@ -55,4 +55,12 @@ public class XooPluginTest {
     new XooPlugin().define(context);
     assertThat(context.getExtensions()).hasSize(51).contains(CpdTokenizerSensor.class);
   }
+  
+  @Test
+  public void provide_extensions_for_7_2() {
+    SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("7.2"), SonarQubeSide.SCANNER);
+    Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
+    new XooPlugin().define(context);
+    assertThat(context.getExtensions()).hasSize(52).contains(CpdTokenizerSensor.class);
+  }
 }
index 2ae66410abcd57d8fe0bb4bfc6e28e4ce23c835c..9557e969e2c10e23af77d5826303cdb0940fa4ac 100644 (file)
@@ -39,6 +39,8 @@ public interface BatchReportReader {
   ScannerReport.Component readComponent(int componentRef);
 
   CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef);
+  
+  CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef);
 
   CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef);
 
index 9ef182d94fa50c61ea5b0e9c8e53183682c7dcb7..b7dc446fe28734c7138e558c62c84a6ca0adb9fc 100644 (file)
@@ -108,6 +108,12 @@ public class BatchReportReaderImpl implements BatchReportReader {
     ensureInitialized();
     return delegate.readComponentIssues(componentRef);
   }
+  
+  @Override
+  public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
+    ensureInitialized();
+    return delegate.readComponentExternalIssues(componentRef);
+  }
 
   @Override
   public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) {
index b92d04c2a99ec11c191943241961c90251352211..bd2e6f6d396cd4486ea4317d96538873274d1ed4 100644 (file)
@@ -22,8 +22,11 @@ package org.sonar.server.computation.task.projectanalysis.issue;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import org.apache.commons.lang.StringUtils;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.tracking.Input;
@@ -34,6 +37,7 @@ import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.scanner.protocol.Constants.Severity;
 import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
 import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReader;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
@@ -111,6 +115,16 @@ public class TrackerRawInputFactory {
         }
       }
 
+      try (CloseableIterator<ScannerReport.ExternalIssue> reportIssues = reportReader.readComponentExternalIssues(component.getReportAttributes().getRef())) {
+        // optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed
+        // as late as possible
+        while (reportIssues.hasNext()) {
+          ScannerReport.ExternalIssue reportExternalIssue = reportIssues.next();
+          DefaultIssue issue = toIssue(getLineHashSequence(), reportExternalIssue);
+          result.add(issue);
+        }
+      }
+
       return result;
     }
 
@@ -157,6 +171,59 @@ public class TrackerRawInputFactory {
       return issue;
     }
 
+    private DefaultIssue toIssue(LineHashSequence lineHashSeq, ScannerReport.ExternalIssue reportIssue) {
+      DefaultIssue issue = new DefaultIssue();
+      init(issue);
+      issue.setRuleKey(RuleKey.of(reportIssue.getRuleRepository(), reportIssue.getRuleKey()));
+      if (reportIssue.hasTextRange()) {
+        int startLine = reportIssue.getTextRange().getStartLine();
+        issue.setLine(startLine);
+        issue.setChecksum(lineHashSeq.getHashForLine(startLine));
+      } else {
+        issue.setChecksum("");
+      }
+      if (isNotEmpty(reportIssue.getMsg())) {
+        issue.setMessage(reportIssue.getMsg());
+      }
+      if (reportIssue.getSeverity() != Severity.UNSET_SEVERITY) {
+        issue.setSeverity(reportIssue.getSeverity().name());
+      }
+      if (reportIssue.getEffort() != 0) {
+        issue.setEffort(Duration.create(reportIssue.getEffort()));
+      }
+      DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder();
+      if (reportIssue.hasTextRange()) {
+        dbLocationsBuilder.setTextRange(convertTextRange(reportIssue.getTextRange()));
+      }
+      for (ScannerReport.Flow flow : reportIssue.getFlowList()) {
+        if (flow.getLocationCount() > 0) {
+          DbIssues.Flow.Builder dbFlowBuilder = DbIssues.Flow.newBuilder();
+          for (ScannerReport.IssueLocation location : flow.getLocationList()) {
+            dbFlowBuilder.addLocation(convertLocation(location));
+          }
+          dbLocationsBuilder.addFlow(dbFlowBuilder);
+        }
+      }
+      issue.setLocations(dbLocationsBuilder.build());
+      issue.setType(toRuleType(reportIssue.getType()));
+      issue.setDescriptionUrl(StringUtils.stripToNull(reportIssue.getDescriptionUrl()));
+      return issue;
+    }
+
+    private RuleType toRuleType(IssueType type) {
+      switch (type) {
+        case BUG:
+          return RuleType.BUG;
+        case CODE_SMELL:
+          return RuleType.CODE_SMELL;
+        case VULNERABILITY:
+          return RuleType.VULNERABILITY;
+        case UNRECOGNIZED:
+        default:
+          throw new IllegalStateException("Invalid issue type: " + type);
+      }
+    }
+
     private DefaultIssue init(DefaultIssue issue) {
       issue.setResolution(null);
       issue.setStatus(Issue.STATUS_OPEN);
index caaf43ada0e6694375009211e5ceddfe7d46d35e..cfeaa8ab89ce023cc0c8d9fbfe892ab11ee59720 100644 (file)
@@ -44,6 +44,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader {
   private Map<Integer, ScannerReport.Changesets> changesets = new HashMap<>();
   private Map<Integer, ScannerReport.Component> components = new HashMap<>();
   private Map<Integer, List<ScannerReport.Issue>> issues = new HashMap<>();
+  private Map<Integer, List<ScannerReport.ExternalIssue>> externalIssues = new HashMap<>();
   private Map<Integer, List<ScannerReport.Duplication>> duplications = new HashMap<>();
   private Map<Integer, List<ScannerReport.CpdTextBlock>> duplicationBlocks = new HashMap<>();
   private Map<Integer, List<ScannerReport.Symbol>> symbols = new HashMap<>();
@@ -172,6 +173,11 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader {
   public CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef) {
     return closeableIterator(issues.get(componentRef));
   }
+  
+  @Override
+  public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
+    return closeableIterator(externalIssues.get(componentRef));
+  }
 
   public BatchReportReaderRule putIssues(int componentRef, List<ScannerReport.Issue> issue) {
     this.issues.put(componentRef, issue);
index e1a6f6cdf5729b0df16b69ec85123850abb002fd..287fc2f89b338ac97007977d0c2bbd083bed40f8 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.api.batch.sensor;
 
 import java.io.Serializable;
+
 import org.sonar.api.SonarRuntime;
 import org.sonar.api.batch.fs.FileSystem;
 import org.sonar.api.batch.fs.InputFile;
@@ -30,7 +31,9 @@ import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
 import org.sonar.api.batch.sensor.error.NewAnalysisError;
 import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
 import org.sonar.api.batch.sensor.internal.SensorContextTester;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
 import org.sonar.api.batch.sensor.issue.NewIssue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
@@ -112,6 +115,12 @@ public interface SensorContext {
    */
   NewIssue newIssue();
 
+  /**
+   * Fluent builder to create a new {@link ExternalIssue}. Don't forget to call {@link NewExternalIssue#save()} once all parameters are provided.
+   * @since 7.2
+   */
+  NewExternalIssue newExternalIssue();
+
   // ------------ HIGHLIGHTING ------------
 
   /**
index b8e8c96916013a3ab96ce392dee1e74ffbd7e290..ef225532139ce05b723248acf23959fe770d5b77 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
 import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
 import org.sonar.api.batch.sensor.error.AnalysisError;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable;
@@ -43,6 +44,7 @@ class InMemorySensorStorage implements SensorStorage {
   Table<String, String, Measure> measuresByComponentAndMetric = HashBasedTable.create();
 
   Collection<Issue> allIssues = new ArrayList<>();
+  Collection<ExternalIssue> allExternalIssues = new ArrayList<>();
   Collection<AnalysisError> allAnalysisErrors = new ArrayList<>();
 
   Map<String, DefaultHighlighting> highlightingByComponent = new HashMap<>();
@@ -114,4 +116,9 @@ class InMemorySensorStorage implements SensorStorage {
     checkArgument(value != null, "Value of context property must not be null");
     contextProperties.put(key, value);
   }
+
+  @Override
+  public void store(ExternalIssue issue) {
+    allExternalIssues.add(issue);
+  }
 }
index b6968768d17dc068cb2d6b327e87656b24f621af..b13340fb1c1b61f4daea3e26b04c15ad81a51bba 100644 (file)
@@ -58,8 +58,11 @@ import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
 import org.sonar.api.batch.sensor.highlighting.TypeOfText;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
 import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
 import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
@@ -219,6 +222,15 @@ public class SensorContextTester implements SensorContext {
     return sensorStorage.allIssues;
   }
 
+  @Override
+  public NewExternalIssue newExternalIssue() {
+    return new DefaultExternalIssue(sensorStorage);
+  }
+
+  public Collection<ExternalIssue> allExternalIssues() {
+    return sensorStorage.allExternalIssues;
+  }
+
   public Collection<AnalysisError> allAnalysisErrors() {
     return sensorStorage.allAnalysisErrors;
   }
index d2dbcb416daa1dc7e256c0afc3aa0b63c2320926..d8c7cb5648f2170cd4d4d03c053e3e896730197b 100644 (file)
@@ -24,6 +24,7 @@ import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
 import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
 import org.sonar.api.batch.sensor.error.AnalysisError;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable;
@@ -39,6 +40,8 @@ public interface SensorStorage {
 
   void store(Issue issue);
 
+  void store(ExternalIssue issue);
+
   void store(DefaultHighlighting highlighting);
 
   /**
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java
new file mode 100644 (file)
index 0000000..cdc7d6d
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.rules.RuleType;
+
+/**
+ * Represents an issue imported from an external rule engine by a {@link Sensor}.
+ * @since 7.2
+ */
+public interface ExternalIssue extends IIssue {
+
+  Severity severity();
+
+  /**
+   * Link to a page describing more details about the rule that triggered this issue.
+   */
+  @CheckForNull
+  String descriptionUrl();
+
+  /**
+   * Effort to fix the issue, in minutes.
+   */
+  @CheckForNull
+  Long remediationEffort();
+
+  /**
+   * Type of the issue.
+   */
+  RuleType type();
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java
new file mode 100644 (file)
index 0000000..32ac7ac
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue;
+
+import java.util.List;
+import org.sonar.api.batch.sensor.issue.Issue.Flow;
+import org.sonar.api.rule.RuleKey;
+
+/**
+ * @since 7.2
+ */
+public interface IIssue {
+  /**
+   * The {@link RuleKey} of this issue.
+   */
+  RuleKey ruleKey();
+  
+  /**
+   * Primary locations for this issue.
+   */
+  IssueLocation primaryLocation();
+
+  /**
+   * List of flows for this issue. Can be empty.
+   */
+  List<Flow> flows();
+}
index 5bb21addabb6dac94cd5ede892cdb1bc44287f48..0f752778775ece9899ddca4e888268f19f4bb5a8 100644 (file)
@@ -23,27 +23,20 @@ import java.util.List;
 import javax.annotation.CheckForNull;
 import org.sonar.api.batch.rule.Severity;
 import org.sonar.api.batch.sensor.Sensor;
-import org.sonar.api.rule.RuleKey;
 
 /**
  * Represents an issue detected by a {@link Sensor}.
  *
  * @since 5.1
  */
-public interface Issue {
-
+public interface Issue extends IIssue {
   interface Flow {
     /**
      * @return Ordered list of locations for the execution flow
      */
     List<IssueLocation> locations();
   }
-
-  /**
-   * The {@link RuleKey} of this issue.
-   */
-  RuleKey ruleKey();
-
+  
   /**
    * Effort to fix the issue. Used by technical debt model.
    * @deprecated since 5.5 use {@link #gap()}
@@ -58,23 +51,24 @@ public interface Issue {
    */
   @CheckForNull
   Double gap();
-
+  
   /**
    * Overridden severity.
    */
   @CheckForNull
   Severity overriddenSeverity();
-
+  
   /**
    * Primary locations for this issue.
    * @since 5.2
    */
+  @Override
   IssueLocation primaryLocation();
 
   /**
    * List of flows for this issue. Can be empty.
    * @since 5.2
    */
+  @Override
   List<Flow> flows();
-
 }
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java
new file mode 100644 (file)
index 0000000..2a3f41b
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue;
+
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+
+/**
+ * Builder for an issue imported from an external rule engine by a {@link Sensor}.
+ * Don't forget to {@link #save()} after setting the fields.
+ * 
+ * @since 7.2 
+ */
+public interface NewExternalIssue {
+  /**
+   * The {@link RuleKey} of the issue.
+   */
+  NewExternalIssue forRule(RuleKey ruleKey);
+
+  /**
+   * Type of issue.
+   */
+  NewExternalIssue type(RuleType type);
+
+  /**
+   * Effort to fix the issue, in minutes.
+   */
+  NewExternalIssue remediationEffort(@Nullable Long effort);
+
+  /**
+   * Set the severity of the issue.
+   */
+  NewExternalIssue severity(Severity severity);
+
+  /**
+   * Primary location for this issue.
+   */
+  NewExternalIssue at(NewIssueLocation primaryLocation);
+
+  /**
+   * Add a secondary location for this issue. Several secondary locations can be registered.
+   */
+  NewExternalIssue addLocation(NewIssueLocation secondaryLocation);
+
+  /**
+   * Add a URL pointing to more information about the rule triggering this issue. 
+   */
+  NewExternalIssue descriptionUrl(String url);
+
+  /**
+   * Register a flow for this issue. A flow is an ordered list of issue locations that help to understand the issue.
+   * It should be a <b>path that backtracks the issue from its primary location to the start of the flow</b>. 
+   * Several flows can be registered.
+   */
+  NewExternalIssue addFlow(Iterable<NewIssueLocation> flowLocations);
+
+  /**
+   * Create a new location for this issue. First registered location is considered as primary location.
+   */
+  NewIssueLocation newLocation();
+
+  /**
+   * Save the issue. If rule key is unknown or rule not enabled in the current quality profile then a warning is logged but no exception
+   * is thrown.
+   */
+  void save();
+
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
new file mode 100644 (file)
index 0000000..6949429
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue.internal;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.sonar.api.batch.sensor.internal.DefaultStorable;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.batch.sensor.issue.Issue.Flow;
+import org.sonar.api.rule.RuleKey;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Collections.unmodifiableList;
+import static java.util.stream.Collectors.toList;
+
+public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue>  extends DefaultStorable {
+  protected RuleKey ruleKey;
+  protected IssueLocation primaryLocation;
+  protected List<List<IssueLocation>> flows = new ArrayList<>();
+  
+  protected AbstractDefaultIssue() {
+    super(null);
+  }
+  
+  public AbstractDefaultIssue(SensorStorage storage) {
+    super(storage);
+  }
+  
+  public T forRule(RuleKey ruleKey) {
+    this.ruleKey = ruleKey;
+    return (T) this;
+  }
+  
+  public RuleKey ruleKey() {
+    return this.ruleKey;
+  }
+  
+  public IssueLocation primaryLocation() {
+    return primaryLocation;
+  }
+
+  public List<Flow> flows() {
+    return this.flows.stream()
+      .<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l)))
+      .collect(toList());
+  }
+  
+  public NewIssueLocation newLocation() {
+    return new DefaultIssueLocation();
+  }
+
+  public T at(NewIssueLocation primaryLocation) {
+    Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null");
+    checkState(this.primaryLocation == null, "at() already called");
+    this.primaryLocation = (DefaultIssueLocation) primaryLocation;
+    Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component");
+    return (T) this;
+  }
+
+  public T addLocation(NewIssueLocation secondaryLocation) {
+    flows.add(Arrays.asList((IssueLocation) secondaryLocation));
+    return (T) this;
+  }
+
+  public T addFlow(Iterable<NewIssueLocation> locations) {
+    List<IssueLocation> flowAsList = new ArrayList<>();
+    for (NewIssueLocation issueLocation : locations) {
+      flowAsList.add((DefaultIssueLocation) issueLocation);
+    }
+    flows.add(flowAsList);
+    return (T) this;
+  }
+  
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java
new file mode 100644 (file)
index 0000000..de3eed8
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue.internal;
+
+import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.rules.RuleType;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIssue> implements ExternalIssue, NewExternalIssue {
+  private Long effort;
+  private Severity severity;
+  private String url;
+  private RuleType type;
+
+  public DefaultExternalIssue() {
+    super(null);
+  }
+
+  public DefaultExternalIssue(SensorStorage storage) {
+    super(storage);
+  }
+
+  @Override
+  public DefaultExternalIssue remediationEffort(@Nullable Long effort) {
+    Preconditions.checkArgument(effort == null || effort >= 0, format("effort must be greater than or equal 0 (got %s)", effort));
+    this.effort = effort;
+    return this;
+  }
+
+  @Override
+  public DefaultExternalIssue severity(@Nullable Severity severity) {
+    this.severity = severity;
+    return this;
+  }
+
+  @Override
+  public Severity severity() {
+    return this.severity;
+  }
+
+  @Override
+  public Long remediationEffort() {
+    return this.effort;
+  }
+
+  @Override
+  public void doSave() {
+    requireNonNull(this.ruleKey, "ruleKey is mandatory on external issue");
+    checkState(primaryLocation != null, "Primary location is mandatory on every external issue");
+    checkState(primaryLocation.inputComponent().isFile(), "External issues must be located in files");
+    checkState(type != null, "Type is mandatory on every external issue");
+    checkState(severity != null, "Severity is mandatory on every external issue");
+    checkState(severity != null, "Severity is mandatory on every external issue");
+    storage.store(this);
+  }
+
+  @Override
+  public DefaultExternalIssue descriptionUrl(String url) {
+    this.url = url;
+    return this;
+  }
+
+  @Override
+  public String descriptionUrl() {
+    return url;
+  }
+
+  @Override
+  public RuleType type() {
+    return type;
+  }
+
+  @Override
+  public DefaultExternalIssue type(RuleType type) {
+    this.type = type;
+    return this;
+  }
+}
index 8b1f8bcb2f0d331b3384676f866f550bdff1fc03..30095731169b236e2c7731aa336f78d556bbf8f9 100644 (file)
 package org.sonar.api.batch.sensor.issue.internal;
 
 import com.google.common.base.Preconditions;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import javax.annotation.Nullable;
 import org.sonar.api.batch.rule.Severity;
-import org.sonar.api.batch.sensor.internal.DefaultStorable;
 import org.sonar.api.batch.sensor.internal.SensorStorage;
 import org.sonar.api.batch.sensor.issue.Issue;
 import org.sonar.api.batch.sensor.issue.IssueLocation;
 import org.sonar.api.batch.sensor.issue.NewIssue;
-import org.sonar.api.batch.sensor.issue.NewIssueLocation;
-import org.sonar.api.rule.RuleKey;
 
 import static com.google.common.base.Preconditions.checkState;
 import static java.lang.String.format;
-import static java.util.Collections.unmodifiableList;
 import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
 
-public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
-
-  private RuleKey ruleKey;
+public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements Issue, NewIssue {
   private Double gap;
   private Severity overriddenSeverity;
-  private IssueLocation primaryLocation;
-  private List<List<IssueLocation>> flows = new ArrayList<>();
 
   public DefaultIssue() {
     super(null);
@@ -55,12 +43,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
     super(storage);
   }
 
-  @Override
-  public DefaultIssue forRule(RuleKey ruleKey) {
-    this.ruleKey = ruleKey;
-    return this;
-  }
-
   @Override
   public DefaultIssue effortToFix(@Nullable Double effortToFix) {
     return gap(effortToFix);
@@ -79,41 +61,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
     return this;
   }
 
-  @Override
-  public NewIssueLocation newLocation() {
-    return new DefaultIssueLocation();
-  }
-
-  @Override
-  public DefaultIssue at(NewIssueLocation primaryLocation) {
-    Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null");
-    checkState(this.primaryLocation == null, "at() already called");
-    this.primaryLocation = (DefaultIssueLocation) primaryLocation;
-    Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component");
-    return this;
-  }
-
-  @Override
-  public NewIssue addLocation(NewIssueLocation secondaryLocation) {
-    flows.add(Arrays.asList((IssueLocation) secondaryLocation));
-    return this;
-  }
-
-  @Override
-  public DefaultIssue addFlow(Iterable<NewIssueLocation> locations) {
-    List<IssueLocation> flowAsList = new ArrayList<>();
-    for (NewIssueLocation issueLocation : locations) {
-      flowAsList.add((DefaultIssueLocation) issueLocation);
-    }
-    flows.add(flowAsList);
-    return this;
-  }
-
-  @Override
-  public RuleKey ruleKey() {
-    return this.ruleKey;
-  }
-
   @Override
   public Severity overriddenSeverity() {
     return this.overriddenSeverity;
@@ -134,13 +81,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
     return primaryLocation;
   }
 
-  @Override
-  public List<Flow> flows() {
-    return this.flows.stream()
-      .<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l)))
-      .collect(toList());
-  }
-
   @Override
   public void doSave() {
     requireNonNull(this.ruleKey, "ruleKey is mandatory on issue");
index 25378ef02e602d705f68ce520cf13e26177cb406..d206d512fbd95b512813efd55c65814b0c2f85a8 100644 (file)
@@ -39,6 +39,7 @@ public class RuleKey implements Serializable, Comparable<RuleKey> {
    */
   @Deprecated
   public static final String MANUAL_REPOSITORY_KEY = "manual";
+  public static final String EXTERNAL_RULE_REPO_PREFIX = "external_";
 
   private final String repository;
   private final String rule;
index a8f57a13ef85ed956645f0439068fec27dc62dfe..0cee9bc2a96fe806ac03f2ac78e160107e16c73e 100644 (file)
@@ -34,16 +34,19 @@ import org.sonar.api.batch.fs.internal.DefaultInputModule;
 import org.sonar.api.batch.fs.internal.DefaultTextPointer;
 import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
 import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.Severity;
 import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
 import org.sonar.api.batch.sensor.error.AnalysisError;
 import org.sonar.api.batch.sensor.error.NewAnalysisError;
 import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
 import org.sonar.api.batch.sensor.issue.NewIssue;
 import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
 import org.sonar.api.config.Settings;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.SonarException;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -105,6 +108,26 @@ public class SensorContextTesterTest {
     assertThat(tester.allIssues()).hasSize(2);
   }
 
+  @Test
+  public void testExternalIssues() {
+    assertThat(tester.allExternalIssues()).isEmpty();
+    NewExternalIssue newExternalIssue = tester.newExternalIssue();
+    newExternalIssue
+      .at(newExternalIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build()))
+      .forRule(RuleKey.of("repo", "rule"))
+      .type(RuleType.BUG)
+      .severity(Severity.BLOCKER)
+      .save();
+    newExternalIssue = tester.newExternalIssue();
+    newExternalIssue
+      .at(newExternalIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build()))
+      .type(RuleType.BUG)
+      .severity(Severity.BLOCKER)
+      .forRule(RuleKey.of("repo", "rule"))
+      .save();
+    assertThat(tester.allExternalIssues()).hasSize(2);
+  }
+
   @Test
   public void testAnalysisErrors() {
     assertThat(tester.allAnalysisErrors()).isEmpty();
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java
new file mode 100644 (file)
index 0000000..d219cef
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.api.batch.sensor.issue.internal;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class DefaultExternalIssueTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
+  private DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.php")
+    .initMetadata("Foo\nBar\n")
+    .build();
+
+  @Test
+  public void build_file_issue() {
+    SensorStorage storage = mock(SensorStorage.class);
+    DefaultExternalIssue issue = new DefaultExternalIssue(storage)
+      .at(new DefaultIssueLocation()
+        .on(inputFile)
+        .at(inputFile.selectLine(1))
+        .message("Wrong way!"))
+      .forRule(RuleKey.of("repo", "rule"))
+      .remediationEffort(10l)
+      .descriptionUrl("url")
+      .type(RuleType.BUG)
+      .severity(Severity.BLOCKER);
+
+    assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputFile);
+    assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule"));
+    assertThat(issue.primaryLocation().textRange().start().line()).isEqualTo(1);
+    assertThat(issue.remediationEffort()).isEqualTo(10l);
+    assertThat(issue.descriptionUrl()).isEqualTo("url");
+    assertThat(issue.type()).isEqualTo(RuleType.BUG);
+    assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
+    assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!");
+
+    issue.save();
+
+    verify(storage).store(issue);
+  }
+
+  @Test
+  public void fail_to_store_if_no_type() {
+    SensorStorage storage = mock(SensorStorage.class);
+    DefaultExternalIssue issue = new DefaultExternalIssue(storage)
+      .at(new DefaultIssueLocation()
+        .on(inputFile)
+        .at(inputFile.selectLine(1))
+        .message("Wrong way!"))
+      .forRule(RuleKey.of("repo", "rule"))
+      .remediationEffort(10l)
+      .descriptionUrl("url")
+      .severity(Severity.BLOCKER);
+
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("Type is mandatory");
+    issue.save();
+  }
+
+  @Test
+  public void fail_to_store_if_primary_location_is_not_a_file() {
+    SensorStorage storage = mock(SensorStorage.class);
+    DefaultExternalIssue issue = new DefaultExternalIssue(storage)
+      .at(new DefaultIssueLocation()
+        .on(mock(InputComponent.class))
+        .message("Wrong way!"))
+      .forRule(RuleKey.of("repo", "rule"))
+      .remediationEffort(10l)
+      .descriptionUrl("url")
+      .severity(Severity.BLOCKER);
+
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("External issues must be located in files");
+    issue.save();
+  }
+
+  @Test
+  public void fail_to_store_if_no_severity() {
+    SensorStorage storage = mock(SensorStorage.class);
+    DefaultExternalIssue issue = new DefaultExternalIssue(storage)
+      .at(new DefaultIssueLocation()
+        .on(inputFile)
+        .at(inputFile.selectLine(1))
+        .message("Wrong way!"))
+      .forRule(RuleKey.of("repo", "rule"))
+      .remediationEffort(10l)
+      .descriptionUrl("url")
+      .type(RuleType.BUG);
+
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("Severity is mandatory");
+    issue.save();
+  }
+
+}
index ef7bda53f1668c0e7125835d8407de7ea1eb9b5b..40167c3c7696f2d4b833ebd32924104dde52e45a 100644 (file)
@@ -20,6 +20,8 @@
 package org.sonar.scanner.issue;
 
 import com.google.common.base.Strings;
+import java.util.Collection;
+import java.util.function.Consumer;
 import javax.annotation.concurrent.ThreadSafe;
 import org.sonar.api.batch.fs.TextRange;
 import org.sonar.api.batch.fs.internal.DefaultInputComponent;
@@ -27,6 +29,7 @@ import org.sonar.api.batch.rule.ActiveRule;
 import org.sonar.api.batch.rule.ActiveRules;
 import org.sonar.api.batch.rule.Rule;
 import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
 import org.sonar.api.batch.sensor.issue.Issue.Flow;
 import org.sonar.api.rule.RuleKey;
@@ -73,6 +76,12 @@ public class ModuleIssues {
     return false;
   }
 
+  public void initAndAddExternalIssue(ExternalIssue issue) {
+    DefaultInputComponent inputComponent = (DefaultInputComponent) issue.primaryLocation().inputComponent();
+    ScannerReport.ExternalIssue rawExternalIssue = createReportExternalIssue(issue, inputComponent.batchId());
+    write(inputComponent.batchId(), rawExternalIssue);
+  }
+
   private static ScannerReport.Issue createReportIssue(Issue issue, int componentRef, String ruleName, String activeRuleSeverity) {
     String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? ruleName : issue.primaryLocation().message();
     org.sonar.api.batch.rule.Severity overriddenSeverity = issue.overriddenSeverity();
@@ -97,14 +106,41 @@ public class ModuleIssues {
     if (gap != null) {
       builder.setGap(gap);
     }
-    applyFlows(componentRef, builder, locationBuilder, textRangeBuilder, issue);
+    applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows());
     return builder.build();
   }
 
-  private static void applyFlows(int componentRef, ScannerReport.Issue.Builder builder, ScannerReport.IssueLocation.Builder locationBuilder,
-    ScannerReport.TextRange.Builder textRangeBuilder, Issue issue) {
+  private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) {
+    String primaryMessage = issue.primaryLocation().message();
+    Severity severity = Severity.valueOf(issue.severity().name());
+
+    ScannerReport.ExternalIssue.Builder builder = ScannerReport.ExternalIssue.newBuilder();
+    ScannerReport.IssueLocation.Builder locationBuilder = IssueLocation.newBuilder();
+    ScannerReport.TextRange.Builder textRangeBuilder = ScannerReport.TextRange.newBuilder();
+    // non-null fields
+    builder.setSeverity(severity);
+    builder.setRuleRepository(issue.ruleKey().repository());
+    builder.setRuleKey(issue.ruleKey().rule());
+    builder.setMsg(primaryMessage);
+    locationBuilder.setMsg(primaryMessage);
+
+    locationBuilder.setComponentRef(componentRef);
+    TextRange primaryTextRange = issue.primaryLocation().textRange();
+    if (primaryTextRange != null) {
+      builder.setTextRange(toProtobufTextRange(textRangeBuilder, primaryTextRange));
+    }
+    Long effort = issue.remediationEffort();
+    if (effort != null) {
+      builder.setEffort(effort);
+    }
+    applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows());
+    return builder.build();
+  }
+
+  private static void applyFlows(Consumer<ScannerReport.Flow> consumer, ScannerReport.IssueLocation.Builder locationBuilder,
+    ScannerReport.TextRange.Builder textRangeBuilder, Collection<Flow> flows) {
     ScannerReport.Flow.Builder flowBuilder = ScannerReport.Flow.newBuilder();
-    for (Flow flow : issue.flows()) {
+    for (Flow flow : flows) {
       if (flow.locations().isEmpty()) {
         return;
       }
@@ -123,7 +159,7 @@ public class ModuleIssues {
         }
         flowBuilder.addLocation(locationBuilder.build());
       }
-      builder.addFlow(flowBuilder.build());
+      consumer.accept(flowBuilder.build());
     }
   }
 
@@ -152,4 +188,7 @@ public class ModuleIssues {
     reportPublisher.getWriter().appendComponentIssue(batchId, rawIssue);
   }
 
+  public void write(int batchId, ScannerReport.ExternalIssue rawIssue) {
+    reportPublisher.getWriter().appendComponentExternalIssue(batchId, rawIssue);
+  }
 }
index 232fbc88c0677ed055801312657b3d68f4dcdc04..80a33078927a2626113d731908fcc125d21ec21e 100644 (file)
@@ -123,6 +123,11 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver
     int ref = reportComponents.get(inputComponent.key()).getRef();
     return issuesFor(ref);
   }
+  
+  public List<ScannerReport.ExternalIssue> externalIssuesFor(InputComponent inputComponent) {
+    int ref = reportComponents.get(inputComponent.key()).getRef();
+    return externalIssuesFor(ref);
+  }
 
   public List<ScannerReport.Issue> issuesFor(Component reportComponent) {
     int ref = reportComponent.getRef();
@@ -138,6 +143,16 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver
     }
     return result;
   }
+  
+  private List<ScannerReport.ExternalIssue> externalIssuesFor(int ref) {
+    List<ScannerReport.ExternalIssue> result = Lists.newArrayList();
+    try (CloseableIterator<ScannerReport.ExternalIssue> it = reader.readComponentExternalIssues(ref)) {
+      while (it.hasNext()) {
+        result.add(it.next());
+      }
+    }
+    return result;
+  }
 
   public Collection<InputFile> inputFiles() {
     return inputFiles.values();
index 6972e241500712b4028de5f587a0338208b97511..ff9a7ba1685c2e909f51e35f5b967a0dff8370b0 100644 (file)
@@ -37,7 +37,9 @@ import org.sonar.api.batch.sensor.error.NewAnalysisError;
 import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
 import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
 import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
 import org.sonar.api.batch.sensor.measure.NewMeasure;
 import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -130,6 +132,11 @@ public class DefaultSensorContext implements SensorContext {
     return new DefaultIssue(sensorStorage);
   }
 
+  @Override
+  public NewExternalIssue newExternalIssue() {
+    return new DefaultExternalIssue(sensorStorage);
+  }
+
   @Override
   public NewHighlighting newHighlighting() {
     if (analysisMode.isIssues()) {
@@ -182,4 +189,5 @@ public class DefaultSensorContext implements SensorContext {
     DefaultInputFile file = (DefaultInputFile) inputFile;
     file.setPublished(true);
   }
+
 }
index 6b5a7137d94a396c295c47a74de9245f07ad88b1..1ccd219c600630e0ac4eaa34256af58a289a07b5 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
 import org.sonar.api.batch.sensor.error.AnalysisError;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
 import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
 import org.sonar.api.batch.sensor.measure.Measure;
 import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -373,6 +374,18 @@ public class DefaultSensorStorage implements SensorStorage {
     moduleIssues.initAndAddIssue(issue);
   }
 
+  /**
+   * Thread safe assuming that each issues for each file are only written once.
+   */
+  @Override
+  public void store(ExternalIssue externalIssue) {
+    if (externalIssue.primaryLocation().inputComponent() instanceof DefaultInputFile) {
+      DefaultInputFile defaultInputFile = (DefaultInputFile) externalIssue.primaryLocation().inputComponent();
+      defaultInputFile.setPublished(true);
+    }
+    moduleIssues.initAndAddExternalIssue(externalIssue);
+  }
+
   @Override
   public void store(DefaultHighlighting highlighting) {
     ScannerReportWriter writer = reportPublisher.getWriter();
@@ -493,4 +506,5 @@ public class DefaultSensorStorage implements SensorStorage {
   public void storeProperty(String key, String value) {
     contextPropertiesCache.put(key, value);
   }
+
 }
index 07f627c8423d8dc1cdbe54a984f5a2857224c277..6309720339dd19d71488b9bb9c448b8a2bb8d48d 100644 (file)
@@ -28,6 +28,7 @@ import org.sonar.api.batch.fs.internal.DefaultInputFile;
 import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
 import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
 import org.sonar.api.batch.rule.internal.RulesBuilder;
+import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
 import org.sonar.api.rule.RuleKey;
@@ -146,6 +147,23 @@ public class ModuleIssuesTest {
     assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
   }
 
+  @Test
+  public void add_external_issue_to_cache() {
+    ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+    initModuleIssues();
+
+    DefaultExternalIssue issue = new DefaultExternalIssue()
+      .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+      .forRule(SQUID_RULE_KEY)
+      .severity(org.sonar.api.batch.rule.Severity.CRITICAL);
+
+    moduleIssues.initAndAddExternalIssue(issue);
+
+    ArgumentCaptor<ScannerReport.ExternalIssue> argument = ArgumentCaptor.forClass(ScannerReport.ExternalIssue.class);
+    verify(reportPublisher.getWriter()).appendComponentExternalIssue(eq(file.batchId()), argument.capture());
+    assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
+  }
+
   @Test
   public void use_severity_from_active_rule_if_no_severity_on_issue() {
     ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
index 64c8b66cb84dfa30a1799146e5088132cb5db44d..babe6f078ba580bdc79dfd2edcac65390b8a7e2d 100644 (file)
@@ -29,8 +29,10 @@ import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.scanner.mediumtest.ScannerMediumTester;
 import org.sonar.scanner.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue;
 import org.sonar.scanner.protocol.output.ScannerReport.Issue;
 import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.OneExternalIssuePerLineSensor;
 import org.sonar.xoo.rule.XooRulesDefinition;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -60,10 +62,31 @@ public class IssuesMediumTest {
 
     List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
     assertThat(issues).hasSize(8 /* lines */);
+    
+    List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+    assertThat(externalIssues).isEmpty();
 
     Issue issue = issues.get(0);
     assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getTextRange().getStartLine());
   }
+  
+  @Test
+  public void testOneExternalIssuePerLine() throws Exception {
+    File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+    File tmpDir = temp.newFolder();
+    FileUtils.copyDirectory(projectDir, tmpDir);
+
+    TaskResult result = tester
+      .newScanTask(new File(tmpDir, "sonar-project.properties"))
+      .property(OneExternalIssuePerLineSensor.ACTIVATE_EXTERNAL_ISSUES, "true")
+      .execute();
+
+    List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+    assertThat(externalIssues).hasSize(8 /* lines */);
+
+    ExternalIssue externalIssue = externalIssues.get(0);
+    assertThat(externalIssue.getTextRange().getStartLine()).isEqualTo(externalIssue.getTextRange().getStartLine());
+  }
 
   @Test
   public void findActiveRuleByInternalKey() throws Exception {
index 68771fe50598aa88cb8ae09db8a2ede3cb22e9f1..24cc70506bee7a65b0bdab96848e165db0ceabef 100644 (file)
@@ -84,6 +84,7 @@ public class DefaultSensorContextTest {
     assertThat(adaptor.runtime()).isEqualTo(runtime);
 
     assertThat(adaptor.newIssue()).isNotNull();
+    assertThat(adaptor.newExternalIssue()).isNotNull();
     assertThat(adaptor.newMeasure()).isNotNull();
 
     assertThat(adaptor.isCancelled()).isFalse();
index 843e4730b450616f9b558547529e7ba97f9f97bb..bb039e2da18b2ff7682812839be33ff25bcb21d5 100644 (file)
@@ -36,7 +36,9 @@ import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
 import org.sonar.api.batch.measure.MetricFinder;
 import org.sonar.api.batch.sensor.highlighting.TypeOfText;
 import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
 import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
 import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
 import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -124,6 +126,18 @@ public class DefaultSensorStorageTest {
     assertThat(argumentCaptor.getValue()).isEqualTo(issue);
   }
 
+  @Test
+  public void should_save_external_issue() {
+    InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build();
+
+    DefaultExternalIssue externalIssue = new DefaultExternalIssue().at(new DefaultIssueLocation().on(file));
+    underTest.store(externalIssue);
+
+    ArgumentCaptor<ExternalIssue> argumentCaptor = ArgumentCaptor.forClass(ExternalIssue.class);
+    verify(moduleIssues).initAndAddExternalIssue(argumentCaptor.capture());
+    assertThat(argumentCaptor.getValue()).isEqualTo(externalIssue);
+  }
+
   @Test
   public void should_skip_issue_on_short_branch_when_file_status_is_SAME() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
index bf750bd0722f0347afc83f89663852d74e9d5427..019e64d169d177486fd9d4e58d957e13839af7ae 100644 (file)
@@ -31,6 +31,7 @@ public class FileStructure {
 
   public enum Domain {
     ISSUES("issues-", Domain.PB),
+    EXTERNAL_ISSUES("external-issues-", Domain.PB),
     COMPONENT("component-", Domain.PB),
     MEASURES("measures-", Domain.PB),
     DUPLICATIONS("duplications-", Domain.PB),
index c6e3c94fa84ad33c03b70ee69bc0cf40c6e35358..2fb837b2558de2161fb88bc008ac7b0e7a1443ec 100644 (file)
@@ -83,6 +83,14 @@ public class ScannerReportReader {
     return emptyCloseableIterator();
   }
 
+  public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
+    File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
+    if (fileExists(file)) {
+      return Protobuf.readStream(file, ScannerReport.ExternalIssue.parser());
+    }
+    return emptyCloseableIterator();
+  }
+
   public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) {
     File file = fileStructure.fileFor(FileStructure.Domain.DUPLICATIONS, componentRef);
     if (fileExists(file)) {
index 8b3b9c8202b8ec05b10c4e887932c653a95a4879..ebb6fe925e27fdc0c22f17a90831df691007d018 100644 (file)
@@ -82,6 +82,21 @@ public class ScannerReportWriter {
     }
   }
 
+  public File writeComponentExternalIssues(int componentRef, Iterable<ScannerReport.ExternalIssue> issues) {
+    File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
+    Protobuf.writeStream(issues, file, false);
+    return file;
+  }
+
+  public void appendComponentExternalIssue(int componentRef, ScannerReport.ExternalIssue issue) {
+    File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
+    try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true))) {
+      issue.writeDelimitedTo(out);
+    } catch (Exception e) {
+      throw ContextException.of("Unable to write external issue", e).addContext("file", file);
+    }
+  }
+
   public File writeComponentMeasures(int componentRef, Iterable<ScannerReport.Measure> measures) {
     File file = fileStructure.fileFor(FileStructure.Domain.MEASURES, componentRef);
     Protobuf.writeStream(measures, file, false);
index c99b9c9882e796809709c3ae19e8ccb206ea5798..9bdb00bd4cf1fa5ba338a1b31019cb071618b4ed 100644 (file)
@@ -186,6 +186,30 @@ message Issue {
   repeated Flow flow = 7;
 }
 
+message ExternalIssue {
+  string rule_repository = 1;
+  string rule_key = 2;
+  // Only when issue component is a file. Can also be empty for a file if this is an issue global to the file.
+  string msg = 3;
+  Severity severity = 4;
+  int64 effort = 5;
+  // Only when issue component is a file. Can also be empty for a file if this is an issue global to the file.
+  // Will be identical to the first location of the first flow
+  TextRange text_range = 6;
+  repeated Flow flow = 7;
+  // Can be empty as the field is optional
+  string descriptionUrl = 8;
+  IssueType type = 9;
+  string rule_title = 10;
+  string rule_name = 11;
+}
+
+enum IssueType {
+  CODE_SMELL = 0;
+  BUG = 1;
+  VULNERABILITY = 2;
+}
+
 message IssueLocation {
   int32 component_ref = 1;
   // Only when component is a file. Can be empty for a file if this is an issue global to the file.
index 07b8d46e1f52193df36c07e7176e37292c5388a5..9a46a5ba1d694f5728502dcd9d310592e10fe0ef 100644 (file)
@@ -61,7 +61,8 @@ public class FileStructureTest {
   public void locate_files() throws Exception {
     File dir = temp.newFolder();
     FileUtils.write(new File(dir, "metadata.pb"), "metadata content");
-    FileUtils.write(new File(dir, "issues-3.pb"), "issues of component 3");
+    FileUtils.write(new File(dir, "issues-3.pb"), "external issues of component 3");
+    FileUtils.write(new File(dir, "external-issues-3.pb"), "issues of component 3");
     FileUtils.write(new File(dir, "component-42.pb"), "details of component 42");
 
     FileStructure structure = new FileStructure(dir);
@@ -69,6 +70,8 @@ public class FileStructureTest {
     assertThat(structure.fileFor(FileStructure.Domain.COMPONENT, 42)).exists().isFile();
     assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 3)).exists().isFile();
     assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 42)).doesNotExist();
+    assertThat(structure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 3)).exists().isFile();
+    assertThat(structure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 42)).doesNotExist();
   }
 
   @Test
index 6430f8866e9b0b2e48765026fa7289ddc024a2ad..6791f83a5f463154a860b8465832d8dc8898547f 100644 (file)
@@ -104,6 +104,17 @@ public class ScannerReportReaderTest {
     assertThat(underTest.readComponentIssues(200)).isEmpty();
   }
 
+  @Test
+  public void read_external_issues() {
+    ScannerReportWriter writer = new ScannerReportWriter(dir);
+    ScannerReport.ExternalIssue issue = ScannerReport.ExternalIssue.newBuilder()
+      .build();
+    writer.writeComponentExternalIssues(1, asList(issue));
+
+    assertThat(underTest.readComponentExternalIssues(1)).hasSize(1);
+    assertThat(underTest.readComponentExternalIssues(200)).isEmpty();
+  }
+
   @Test
   public void empty_list_if_no_issue_found() {
     assertThat(underTest.readComponentIssues(UNKNOWN_COMPONENT_REF)).isEmpty();
index 647ed49a40ecf29a30de439e56d96fef42e5d0a6..f4ee4dc3ed30b55e9cf10efee1a0339329a56ed7 100644 (file)
@@ -116,6 +116,26 @@ public class ScannerReportWriterTest {
     }
   }
 
+  @Test
+  public void write_external_issues() {
+    // no data yet
+    assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isFalse();
+
+    // write data
+    ScannerReport.ExternalIssue issue = ScannerReport.ExternalIssue.newBuilder()
+      .setMsg("the message")
+      .build();
+
+    underTest.writeComponentExternalIssues(1, asList(issue));
+
+    assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isTrue();
+    File file = underTest.getFileStructure().fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 1);
+    assertThat(file).exists().isFile();
+    try (CloseableIterator<ScannerReport.ExternalIssue> read = Protobuf.readStream(file, ScannerReport.ExternalIssue.parser())) {
+      assertThat(Iterators.size(read)).isEqualTo(1);
+    }
+  }
+
   @Test
   public void write_measures() {
     assertThat(underTest.hasComponentData(FileStructure.Domain.MEASURES, 1)).isFalse();