aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-scanner-engine
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2018-04-18 13:23:37 +0200
committerSonarTech <sonartech@sonarsource.com>2018-04-26 20:20:52 +0200
commit9c04ec39f96d56889bd5a7570d4728f82046b1df (patch)
treec17dd562c1662ac21aed39db2ae3c1daa7b5dc45 /sonar-scanner-engine
parentec13da45f7f3c3e228dafc0478ea52a2900129a5 (diff)
downloadsonarqube-9c04ec39f96d56889bd5a7570d4728f82046b1df.tar.gz
sonarqube-9c04ec39f96d56889bd5a7570d4728f82046b1df.zip
SONAR-10551 Import issues from external rule engines from generic report
Diffstat (limited to 'sonar-scanner-engine')
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java5
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java124
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.java82
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java133
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java5
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java124
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java130
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java2
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json58
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json34
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json33
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json31
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json28
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json25
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json32
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json32
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json32
18 files changed, 909 insertions, 3 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java
index 6c948483948..da7fd41b874 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java
@@ -26,6 +26,7 @@ import org.sonar.core.component.DefaultResourceTypes;
import org.sonar.core.config.CorePropertyDefinitions;
import org.sonar.core.issue.tracking.Tracker;
import org.sonar.scanner.cpd.CpdComponents;
+import org.sonar.scanner.externalissue.ExternalIssuesImportSensor;
import org.sonar.scanner.genericcoverage.GenericCoverageSensor;
import org.sonar.scanner.genericcoverage.GenericTestExecutionSensor;
import org.sonar.scanner.issue.tracking.ServerIssueFromWs;
@@ -70,6 +71,10 @@ public class BatchComponents {
components.add(GenericTestExecutionSensor.class);
components.addAll(GenericTestExecutionSensor.properties());
+ // External issues
+ components.add(ExternalIssuesImportSensor.class);
+ components.add(ExternalIssuesImportSensor.properties());
+
} else {
// Issues tracking
components.add(new Tracker<TrackedIssue, ServerIssueFromWs>());
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java
new file mode 100644
index 00000000000..f484f0bd195
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.scanner.externalissue;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.scanner.externalissue.ReportParser.Issue;
+import org.sonar.scanner.externalissue.ReportParser.Location;
+import org.sonar.scanner.externalissue.ReportParser.Report;
+
+public class ExternalIssueImporter {
+ private static final Logger LOG = Loggers.get(ExternalIssuesImportSensor.class);
+ private static final int MAX_UNKNOWN_FILE_PATHS_TO_PRINT = 5;
+
+ private final SensorContext context;
+ private final Report report;
+ private final Set<String> unknownFiles = new LinkedHashSet<>();
+ private final Set<String> knownFiles = new LinkedHashSet<>();
+
+ public ExternalIssueImporter(SensorContext context, Report report) {
+ this.context = context;
+ this.report = report;
+ }
+
+ public void execute() {
+ int issueCount = 0;
+
+ for (Issue issue : report.issues) {
+ NewExternalIssue externalIssue = context.newExternalIssue()
+ .forRule(RuleKey.of(issue.engineId, issue.ruleId))
+ .severity(Severity.valueOf(issue.severity))
+ .type(RuleType.valueOf(issue.type));
+
+ NewIssueLocation primary = fillLocation(context, externalIssue.newLocation(), issue.primaryLocation);
+ if (primary != null) {
+ knownFiles.add(issue.primaryLocation.filePath);
+ externalIssue.at(primary);
+ if (issue.secondaryLocations != null) {
+ for (Location l : issue.secondaryLocations) {
+ NewIssueLocation secondary = fillLocation(context, externalIssue.newLocation(), l);
+ if (secondary != null) {
+ externalIssue.addLocation(secondary);
+ }
+ }
+ }
+ issueCount++;
+ externalIssue.save();
+ } else {
+ unknownFiles.add(issue.primaryLocation.filePath);
+ }
+ }
+
+ LOG.info("Imported {} {} in {} {}", issueCount, pluralize("issue", issueCount), knownFiles.size(), pluralize("file", knownFiles.size()));
+ int numberOfUnknownFiles = unknownFiles.size();
+ if (numberOfUnknownFiles > 0) {
+ LOG.info("External issues ignored for " + numberOfUnknownFiles + " unknown files, including: "
+ + unknownFiles.stream().limit(MAX_UNKNOWN_FILE_PATHS_TO_PRINT).collect(Collectors.joining(", ")));
+ }
+ }
+
+ private static String pluralize(String msg, int count) {
+ if (count == 1) {
+ return msg;
+ }
+ return msg + "s";
+ }
+
+ @CheckForNull
+ private NewIssueLocation fillLocation(SensorContext context, NewIssueLocation newLocation, Location location) {
+ InputFile file = findFile(context, location.filePath);
+ if (file != null) {
+ newLocation
+ .message(location.message)
+ .on(file);
+
+ if (location.textRange != null) {
+ if (location.textRange.startColumn != null) {
+ TextPointer start = file.newPointer(location.textRange.startLine, location.textRange.startColumn);
+ TextPointer end = file.newPointer(location.textRange.endLine, location.textRange.endColumn);
+ newLocation.at(file.newRange(start, end));
+ } else {
+ newLocation.at(file.selectLine(location.textRange.startLine));
+ }
+ }
+ return newLocation;
+ }
+ return null;
+ }
+
+ @CheckForNull
+ private InputFile findFile(SensorContext context, String filePath) {
+ return context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.java
new file mode 100644
index 00000000000..bbaab97a504
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.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.scanner.externalissue;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.sonar.api.CoreProperties;
+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.config.Configuration;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.scanner.externalissue.ReportParser.Report;
+
+public class ExternalIssuesImportSensor implements Sensor {
+ private static final Logger LOG = Loggers.get(ExternalIssuesImportSensor.class);
+ static final String REPORT_PATHS_PROPERTY_KEY = "sonar.externalIssuesReportPaths";
+
+ private final Configuration config;
+
+ public ExternalIssuesImportSensor(Configuration config) {
+ this.config = config;
+ }
+
+ public static List<PropertyDefinition> properties() {
+ return Collections.singletonList(
+ PropertyDefinition.builder(REPORT_PATHS_PROPERTY_KEY)
+ .name("Issues report paths")
+ .description("List of comma-separated paths (absolute or relative) containing report with issues created by external rule engines.")
+ .category(CoreProperties.CATEGORY_EXTERNAL_ISSUES)
+ .onQualifiers(Qualifiers.PROJECT)
+ .build());
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("Import external issues report")
+ .onlyWhenConfiguration(c -> c.hasKey(REPORT_PATHS_PROPERTY_KEY));
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ Set<String> reportPaths = loadReportPaths();
+ for (String reportPath : reportPaths) {
+ LOG.debug("Importing issues from '{}'", reportPath);
+ Path reportFilePath = context.fileSystem().resolvePath(reportPath).toPath();
+ ReportParser parser = new ReportParser(reportFilePath);
+ Report report = parser.parse();
+ ExternalIssueImporter issueImporter = new ExternalIssueImporter(context, report);
+ issueImporter.execute();
+ }
+ }
+
+ private Set<String> loadReportPaths() {
+ return Arrays.stream(config.getStringArray(REPORT_PATHS_PROPERTY_KEY)).collect(Collectors.toSet());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java
new file mode 100644
index 00000000000..61c35a05eb8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java
@@ -0,0 +1,133 @@
+/*
+ * 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.scanner.externalissue;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+
+public class ReportParser {
+ private Gson gson = new Gson();
+ private Path filePath;
+
+ public ReportParser(Path filePath) {
+ this.filePath = filePath;
+ }
+
+ public Report parse() {
+ try (Reader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
+ return validate(gson.fromJson(reader, Report.class));
+ } catch (JsonIOException | IOException e) {
+ throw new IllegalStateException("Failed to read external issues report '" + filePath + "'", e);
+ } catch (JsonSyntaxException e) {
+ throw new IllegalStateException("Failed to read external issues report '" + filePath + "': invalid JSON syntax", e);
+ }
+ }
+
+ private Report validate(Report report) {
+ for (Issue issue : report.issues) {
+ mandatoryField(issue.primaryLocation, "primaryLocation");
+ mandatoryField(issue.engineId, "engineId");
+ mandatoryField(issue.ruleId, "ruleId");
+ mandatoryField(issue.severity, "severity");
+ mandatoryField(issue.type, "type");
+ mandatoryField(issue.primaryLocation, "primaryLocation");
+ mandatoryFieldPrimaryLocation(issue.primaryLocation.filePath, "filePath");
+ mandatoryFieldPrimaryLocation(issue.primaryLocation.message, "message");
+
+ if (issue.primaryLocation.textRange != null) {
+ mandatoryFieldPrimaryLocation(issue.primaryLocation.textRange.startLine, "startLine of the text range");
+ }
+
+ if (issue.secondaryLocations != null) {
+ for (Location l : issue.secondaryLocations) {
+ mandatoryFieldSecondaryLocation(l.filePath, "filePath");
+ mandatoryFieldSecondaryLocation(l.textRange, "textRange");
+ mandatoryFieldSecondaryLocation(l.textRange.startLine, "startLine of the text range");
+ }
+ }
+ }
+
+ return report;
+ }
+
+ private void mandatoryFieldPrimaryLocation(@Nullable Object value, String fieldName) {
+ if (value == null) {
+ throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in the primary location of the issue.", filePath, fieldName));
+ }
+ }
+
+ private void mandatoryFieldSecondaryLocation(@Nullable Object value, String fieldName) {
+ if (value == null) {
+ throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in a secondary location of the issue.", filePath, fieldName));
+ }
+ }
+
+ private void mandatoryField(@Nullable Object value, String fieldName) {
+ if (value == null) {
+ throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", filePath, fieldName));
+ }
+ }
+
+ private void mandatoryField(@Nullable String value, String fieldName) {
+ if (StringUtils.isBlank(value)) {
+ throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", filePath, fieldName));
+ }
+ }
+
+ static class Report {
+ Issue[] issues;
+ }
+
+ static class Issue {
+ String engineId;
+ String ruleId;
+ String severity;
+ String type;
+ Location primaryLocation;
+ @Nullable
+ Location[] secondaryLocations;
+ }
+
+ static class Location {
+ @Nullable
+ String message;
+ String filePath;
+ @Nullable
+ TextRange textRange;
+ }
+
+ static class TextRange {
+ Integer startLine;
+ @Nullable
+ Integer startColumn;
+ @Nullable
+ Integer endLine;
+ @Nullable
+ Integer endColumn;
+ }
+}
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 f8d72f526e7..1aeefadb7a6 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
@@ -37,6 +37,7 @@ import org.sonar.api.utils.MessageException;
import org.sonar.scanner.protocol.Constants.Severity;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
import org.sonar.scanner.report.ReportPublisher;
/**
@@ -113,12 +114,14 @@ public class ModuleIssues {
private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) {
String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? issue.ruleKey().toString() : issue.primaryLocation().message();
Severity severity = Severity.valueOf(issue.severity().name());
-
+ IssueType issueType = IssueType.valueOf(issue.type().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.setType(issueType);
builder.setRuleRepository(issue.ruleKey().repository());
builder.setRuleKey(issue.ruleKey().rule());
builder.setMsg(primaryMessage);
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java
index 16066a4f617..555bb7f94f9 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java
@@ -41,7 +41,7 @@ public class NoOpNewExternalIssue implements NewExternalIssue {
}
@Override
- public NewExternalIssue remediationEffort(Long effort) {
+ public NewExternalIssue remediationEffortMinutes(Long effort) {
// no op
return this;
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java
new file mode 100644
index 00000000000..3795ac37146
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.scanner.externalissue;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.scanner.externalissue.ReportParser.Report;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportParserTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void parse_sample() {
+ ReportParser parser = new ReportParser(Paths.get("src/test/resources/org/sonar/scanner/externalissue/report.json"));
+
+ System.out.println(Paths.get("org/sonar/scanner/externalissue/report.json").toAbsolutePath());
+ Report report = parser.parse();
+
+ assertThat(report.issues).hasSize(2);
+ assertThat(report.issues[0].engineId).isEqualTo("eslint");
+ assertThat(report.issues[0].ruleId).isEqualTo("rule1");
+ assertThat(report.issues[0].severity).isEqualTo("MAJOR");
+ assertThat(report.issues[0].effortMinutes).isEqualTo(20);
+ assertThat(report.issues[0].type).isEqualTo("CODE_SMELL");
+ assertThat(report.issues[0].primaryLocation.filePath).isEqualTo("file1.js");
+ assertThat(report.issues[0].primaryLocation.message).isEqualTo("fix the issue here");
+ assertThat(report.issues[0].primaryLocation.textRange.startColumn).isNull();
+ assertThat(report.issues[0].primaryLocation.textRange.startLine).isEqualTo(1);
+ assertThat(report.issues[0].primaryLocation.textRange.endColumn).isNull();
+ assertThat(report.issues[0].primaryLocation.textRange.endLine).isEqualTo(2);
+ assertThat(report.issues[0].secondaryLocations).isNull();
+ }
+
+ private Path path(String reportName) {
+ return Paths.get("src/test/resources/org/sonar/scanner/externalissue/" + reportName);
+ }
+
+ @Test
+ public void fail_if_report_doesnt_exist() {
+ ReportParser parser = new ReportParser(Paths.get("unknown.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Failed to read external issues report 'unknown.json'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_report_is_not_valid_json() {
+ ReportParser parser = new ReportParser(path("report_invalid_json.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("invalid JSON syntax");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_primaryLocation_not_set() {
+ ReportParser parser = new ReportParser(path("report_missing_primaryLocation.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'primaryLocation'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_engineId_not_set() {
+ ReportParser parser = new ReportParser(path("report_missing_engineId.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'engineId'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_ruleId_not_set() {
+ ReportParser parser = new ReportParser(path("report_missing_ruleId.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'ruleId'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_severity_not_set() {
+ ReportParser parser = new ReportParser(path("report_missing_severity.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'severity'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_type_not_set() {
+ ReportParser parser = new ReportParser(path("report_missing_type.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'type'");
+ parser.parse();
+ }
+
+ @Test
+ public void fail_if_filePath_not_set_in_primaryLocation() {
+ ReportParser parser = new ReportParser(path("report_missing_filePath.json"));
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("missing mandatory field 'filePath'");
+ parser.parse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java
new file mode 100644
index 00000000000..6d406300c3b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.scanner.mediumtest.issues;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggersTest;
+import org.sonar.scanner.mediumtest.ScannerMediumTester;
+import org.sonar.scanner.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.Constants.Severity;
+import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExternalIssuesMediumTest {
+ @Rule
+ public LogTester logs = new LogTester();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ScannerMediumTester tester = new ScannerMediumTester()
+ .registerPlugin("xoo", new XooPlugin());
+
+ @Test
+ public void testOneIssuePerLine() 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("sonar.oneExternalIssuePerLine.activate", "true")
+ .execute();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(issues).isEmpty();
+
+ List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(externalIssues).hasSize(8 /* lines */);
+
+ ExternalIssue issue = externalIssues.get(0);
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getTextRange().getStartLine());
+ }
+
+ @Test
+ public void testLoadIssuesFromJsonReport() throws URISyntaxException, IOException {
+ 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("sonar.externalIssuesReportPaths", "externalIssues.json")
+ .execute();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(issues).isEmpty();
+
+ List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(externalIssues).hasSize(2);
+
+ // precise issue location
+ ExternalIssue issue = externalIssues.get(0);
+ assertThat(issue.getFlowCount()).isZero();
+ assertThat(issue.getMsg()).isEqualTo("fix the issue here");
+ assertThat(issue.getRuleKey()).isEqualTo("rule1");
+ assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR);
+ assertThat(issue.getType()).isEqualTo(IssueType.CODE_SMELL);
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(5);
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(5);
+ assertThat(issue.getTextRange().getStartOffset()).isEqualTo(3);
+ assertThat(issue.getTextRange().getEndOffset()).isEqualTo(41);
+
+ // location on a line
+ issue = externalIssues.get(1);
+ assertThat(issue.getFlowCount()).isZero();
+ assertThat(issue.getMsg()).isEqualTo("fix the bug here");
+ assertThat(issue.getRuleKey()).isEqualTo("rule2");
+ assertThat(issue.getSeverity()).isEqualTo(Severity.CRITICAL);
+ assertThat(issue.getType()).isEqualTo(IssueType.BUG);
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(3);
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(3);
+ assertThat(issue.getTextRange().getStartOffset()).isEqualTo(0);
+ assertThat(issue.getTextRange().getEndOffset()).isEqualTo(24);
+
+ // One file-level issue in helloscala
+ List<ExternalIssue> externalIssues2 = result.externalIssuesFor(result.inputFile("xources/hello/helloscala.xoo"));
+ assertThat(externalIssues2).hasSize(1);
+
+ issue = externalIssues2.iterator().next();
+ assertThat(issue.getFlowCount()).isZero();
+ assertThat(issue.getMsg()).isEqualTo("fix the bug here");
+ assertThat(issue.getRuleKey()).isEqualTo("rule3");
+ assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR);
+ assertThat(issue.getType()).isEqualTo(IssueType.BUG);
+ assertThat(issue.hasTextRange()).isFalse();
+
+ // one issue is located in a non-existing file
+ assertThat(logs.logs()).contains("External issues ignored for 1 unknown files, including: invalidFile");
+
+ }
+}
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 babe6f078ba..0226398fdd9 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
@@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.tuple;
public class IssuesMediumTest {
- @org.junit.Rule
+ @Rule
public TemporaryFolder temp = new TemporaryFolder();
@Rule
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json
new file mode 100644
index 00000000000..4ababc77edb
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json
@@ -0,0 +1,58 @@
+{
+"issues" : [
+ {
+ "engineId": "externalXoo",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "xources/hello/HelloJava.xoo",
+ "textRange": {
+ "startLine": 5,
+ "startColumn": 3,
+ "endLine": 5,
+ "endColumn": 41
+ }
+ }
+ },
+ {
+ "engineId": "externalXoo",
+ "ruleId": "rule2",
+ "severity": "CRITICAL",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "xources/hello/HelloJava.xoo",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 3
+ }
+ }
+ },
+ {
+ "engineId": "externalXoo",
+ "ruleId": "rule2",
+ "severity": "CRITICAL",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "invalidFile",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 3
+ }
+ }
+ },
+ {
+ "engineId": "externalXoo",
+ "ruleId": "rule3",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "xources/hello/helloscala.xoo"
+ }
+ }
+]
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json
new file mode 100644
index 00000000000..6eb1024206d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json
@@ -0,0 +1,34 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "effortMinutes": 20,
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 4
+ }
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json
new file mode 100644
index 00000000000..55bf64cf8a3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json
@@ -0,0 +1,33 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2,
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 4
+ }
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json
new file mode 100644
index 00000000000..990fa88ce16
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json
@@ -0,0 +1,31 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3
+ }
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json
new file mode 100644
index 00000000000..aa2edd26357
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json
@@ -0,0 +1,28 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here"
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json
new file mode 100644
index 00000000000..e86cd2b60fd
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json
@@ -0,0 +1,25 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "type": "BUG"
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json
new file mode 100644
index 00000000000..bbd5b85fbf3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json
@@ -0,0 +1,32 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "severity": "MAJOR",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 4
+ }
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json
new file mode 100644
index 00000000000..b69d9fb4bc6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json
@@ -0,0 +1,32 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "type": "BUG",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 4
+ }
+ }
+ }
+]
+}
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json
new file mode 100644
index 00000000000..7321073a6bb
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json
@@ -0,0 +1,32 @@
+{
+"issues" : [
+ {
+ "engineId": "eslint",
+ "ruleId": "rule1",
+ "severity": "MAJOR",
+ "type": "CODE_SMELL",
+ "primaryLocation": {
+ "message": "fix the issue here",
+ "filePath": "file1.js",
+ "textRange": {
+ "startLine": 1,
+ "endLine": 2
+ }
+ }
+ },
+ {
+ "engineId": "eslint",
+ "ruleId": "rule2",
+ "severity": "MAJOR",
+ "primaryLocation": {
+ "message": "fix the bug here",
+ "filePath": "file2.js",
+ "textRange": {
+ "startLine": 3,
+ "endLine": 4
+ }
+ }
+ }
+]
+}
+