From: Duarte Meneses Date: Wed, 18 Apr 2018 11:23:37 +0000 (+0200) Subject: SONAR-10551 Import issues from external rule engines from generic report X-Git-Tag: 7.5~1310 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9c04ec39f96d56889bd5a7570d4728f82046b1df;p=sonarqube.git SONAR-10551 Import issues from external rule engines from generic report --- diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java index 1e27f1369e0..51ff7cab643 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java @@ -74,6 +74,12 @@ public interface CoreProperties { */ String SUBCATEGORY_L10N = "localization"; + /** + * @since 7.2 + */ + String CATEGORY_EXTERNAL_ISSUES = "externalIssues"; + + /** * @since 2.11 */ 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 index a0bd77b53c7..03b65866bc8 100644 --- 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 @@ -52,7 +52,7 @@ public class DefaultExternalIssue extends AbstractDefaultIssue()); 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 unknownFiles = new LinkedHashSet<>(); + private final Set 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 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 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 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 issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).isEmpty(); + + List 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 issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).isEmpty(); + + List 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 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 + } + } + } +] +} + diff --git a/tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java b/tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java index cfa467775a7..d8d4677560c 100644 --- a/tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java +++ b/tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java @@ -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 rulesList = tester.wsClient().rules() .search(new org.sonarqube.ws.client.rules.SearchRequest().setIsExternal(Boolean.toString(true))).getRulesList();