aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java7
-rw-r--r--plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java82
-rw-r--r--plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java8
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java67
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java6
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java9
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java7
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java12
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java3
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java51
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java44
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java18
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java88
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java95
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java103
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java62
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java1
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java23
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java126
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java15
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java8
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java14
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java18
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java23
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java1
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java14
-rw-r--r--sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java1
-rw-r--r--sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java8
-rw-r--r--sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java15
-rw-r--r--sonar-scanner-protocol/src/main/protobuf/scanner_report.proto24
-rw-r--r--sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java5
-rw-r--r--sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java11
-rw-r--r--sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java20
36 files changed, 966 insertions, 80 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 97e59ef3156..3c036b1d566 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
@@ -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
index 00000000000..4dbf744243b
--- /dev/null
+++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java
@@ -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();
+ }
+ }
+}
diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
index 70f8d253b46..6384ffa58c6 100644
--- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
+++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
@@ -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);
+ }
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
index 2ae66410abc..9557e969e2c 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
@@ -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);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
index 9ef182d94fa..b7dc446fe28 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
@@ -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) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
index b92d04c2a99..bd2e6f6d396 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
@@ -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);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
index caaf43ada0e..cfeaa8ab89c 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
@@ -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);
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
index e1a6f6cdf57..287fc2f89b3 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
@@ -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 ------------
/**
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
index b8e8c969160..ef225532139 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
@@ -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);
+ }
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
index b6968768d17..b13340fb1c1 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
@@ -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;
}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
index d2dbcb416da..d8c7cb5648f 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
@@ -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
index 00000000000..cdc7d6df2f2
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java
@@ -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
index 00000000000..32ac7acf9e2
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java
@@ -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();
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java
index 5bb21addabb..0f752778775 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java
@@ -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
index 00000000000..2a3f41b4f60
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java
@@ -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
index 00000000000..694942984c2
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
@@ -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
index 00000000000..de3eed8b4e4
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java
@@ -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;
+ }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
index 8b1f8bcb2f0..30095731169 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
@@ -20,32 +20,20 @@
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);
@@ -56,12 +44,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
}
@Override
- public DefaultIssue forRule(RuleKey ruleKey) {
- this.ruleKey = ruleKey;
- return this;
- }
-
- @Override
public DefaultIssue effortToFix(@Nullable Double effortToFix) {
return gap(effortToFix);
}
@@ -80,41 +62,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
}
@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;
}
@@ -135,13 +82,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
}
@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");
checkState(primaryLocation != null, "Primary location is mandatory on every issue");
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java
index 25378ef02e6..d206d512fbd 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java
@@ -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;
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
index a8f57a13ef8..0cee9bc2a96 100644
--- a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
@@ -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;
@@ -106,6 +109,26 @@ public class SensorContextTesterTest {
}
@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();
NewAnalysisError newAnalysisError = tester.newAnalysisError();
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
index 00000000000..d219cefc8ed
--- /dev/null
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java
@@ -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();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
index ef7bda53f16..40167c3c769 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
@@ -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);
+ }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java
index 232fbc88c06..80a33078927 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java
@@ -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();
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
index 6972e241500..ff9a7ba1685 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
@@ -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;
@@ -131,6 +133,11 @@ public class DefaultSensorContext implements SensorContext {
}
@Override
+ public NewExternalIssue newExternalIssue() {
+ return new DefaultExternalIssue(sensorStorage);
+ }
+
+ @Override
public NewHighlighting newHighlighting() {
if (analysisMode.isIssues()) {
return NO_OP_NEW_HIGHLIGHTING;
@@ -182,4 +189,5 @@ public class DefaultSensorContext implements SensorContext {
DefaultInputFile file = (DefaultInputFile) inputFile;
file.setPublished(true);
}
+
}
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 6b5a7137d94..1ccd219c600 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
@@ -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);
}
+
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java
index 07f627c8423..6309720339d 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java
@@ -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;
@@ -147,6 +148,23 @@ public class ModuleIssuesTest {
}
@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);
activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).activate();
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
index 64c8b66cb84..babe6f078ba 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
@@ -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 {
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
index 68771fe5059..24cc70506be 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
@@ -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();
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 843e4730b45..bb039e2da18 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
@@ -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;
@@ -125,6 +127,18 @@ public class DefaultSensorStorageTest {
}
@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();
when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
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 bf750bd0722..019e64d169d 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
@@ -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),
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 c6e3c94fa84..2fb837b2558 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
@@ -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)) {
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 8b3b9c8202b..ebb6fe925e2 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
@@ -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);
diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
index c99b9c9882e..9bdb00bd4cf 100644
--- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
+++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
@@ -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.
diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java
index 07b8d46e1f5..9a46a5ba1d6 100644
--- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java
+++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java
@@ -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
diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java
index 6430f8866e9..6791f83a5f4 100644
--- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java
+++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java
@@ -105,6 +105,17 @@ public class ScannerReportReaderTest {
}
@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();
}
diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java
index 647ed49a40e..f4ee4dc3ed3 100644
--- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java
+++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java
@@ -117,6 +117,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();