]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10551 Import issues from external rule engines from generic report
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 18 Apr 2018 11:23:37 +0000 (13:23 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 26 Apr 2018 18:20:52 +0000 (20:20 +0200)
21 files changed:
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java

index 1e27f1369e09489f7c4dec433f190c5366367839..51ff7cab643ac63f58b341eb3184e2b0d89d6669 100644 (file)
@@ -74,6 +74,12 @@ public interface CoreProperties {
    */
   String SUBCATEGORY_L10N = "localization";
 
+  /**
+   * @since 7.2
+   */
+  String CATEGORY_EXTERNAL_ISSUES = "externalIssues";
+
+  
   /**
    * @since 2.11
    */
index a0bd77b53c72a6590d8c0ab4d03bd67e7cf07e5c..03b65866bc81410d28b6a398c07839f10187d572 100644 (file)
@@ -52,7 +52,7 @@ public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIs
   }
 
   @Override
-  public DefaultExternalIssue severity(@Nullable Severity severity) {
+  public DefaultExternalIssue severity(Severity severity) {
     this.severity = severity;
     return this;
   }
index 6c94848394880a8c267af0450fa72102615e8d88..da7fd41b87407639b6b887e286d54ac556cd4f77 100644 (file)
@@ -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 (file)
index 0000000..f484f0b
--- /dev/null
@@ -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 (file)
index 0000000..bbaab97
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.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 (file)
index 0000000..61c35a0
--- /dev/null
@@ -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;
+  }
+}
index f8d72f526e7fb72e3efc98a612978b8794314a54..1aeefadb7a670e13464828ea176082a3b82ebf84 100644 (file)
@@ -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);
index 16066a4f617bdfc6041fec7ca15992b94fdf66c1..555bb7f94f9951ac123b2e43e706ae62423dd276 100644 (file)
@@ -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 (file)
index 0000000..3795ac3
--- /dev/null
@@ -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 (file)
index 0000000..6d40630
--- /dev/null
@@ -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");
+
+  }
+}
index babe6f078ba580bdc79dfd2edcac65390b8a7e2d..0226398fdd9cefaea5fab15d763d36c1b5cb0d2d 100644 (file)
@@ -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 (file)
index 0000000..4ababc7
--- /dev/null
@@ -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 (file)
index 0000000..6eb1024
--- /dev/null
@@ -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 (file)
index 0000000..55bf64c
--- /dev/null
@@ -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 (file)
index 0000000..990fa88
--- /dev/null
@@ -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 (file)
index 0000000..aa2edd2
--- /dev/null
@@ -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 (file)
index 0000000..e86cd2b
--- /dev/null
@@ -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 (file)
index 0000000..bbd5b85
--- /dev/null
@@ -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 (file)
index 0000000..b69d9fb
--- /dev/null
@@ -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 (file)
index 0000000..7321073
--- /dev/null
@@ -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
+      }
+    }     
+  }
+]
+}
+   
index cfa467775a74f88d962d47409d5ab228441d6ae2..d8d4677560ca8507c4785d509f8e357671e7ac83 100644 (file)
@@ -64,7 +64,7 @@ public class ExternalIssueTest extends AbstractIssueTest {
     assertThat(issuesList).allMatch(issue -> RuleType.CODE_SMELL.equals(issue.getType()));
     assertThat(issuesList).allMatch(issue -> "sample:src/main/xoo/sample/Sample.xoo".equals(issue.getComponent()));
     assertThat(issuesList).allMatch(issue -> "OPEN".equals(issue.getStatus()));
-    assertThat(issuesList).allMatch(issue -> Boolean.TRUE.equals(issue.getFromExternalRule()));
+    assertThat(issuesList).allMatch(issue -> issue.getExternalRuleEngine().equals("xoo"));
 
     List<org.sonarqube.ws.Rules.Rule> rulesList = tester.wsClient().rules()
       .search(new org.sonarqube.ws.client.rules.SearchRequest().setIsExternal(Boolean.toString(true))).getRulesList();