From a3e3f905dc3231d48beb5b9abb5ae27120ef7b61 Mon Sep 17 00:00:00 2001 From: Alain Kermis Date: Wed, 27 Sep 2023 16:41:05 +0200 Subject: [PATCH] SONAR-20552 Introduce new generic issue import format in scanner --- .../OneExternalIssueCctPerLineSensor.java | 87 ++++ ...inedRuleExternalIssueCctPerLineSensor.java | 70 ++++ .../sonar/xoo/rule/XooRulesDefinition.java | 13 + .../xoo/rule/XooRulesDefinitionTest.java | 2 +- .../rule/internal/DefaultAdHocRule.java | 4 +- .../scanner/bootstrap/BatchComponents.java | 4 + .../externalissue/ExternalIssueImporter.java | 119 ++++-- .../externalissue/ExternalIssueReport.java | 77 ++++ .../ExternalIssueReportParser.java | 54 +++ .../ExternalIssueReportValidator.java | 144 +++++++ .../ExternalIssuesImportSensor.java | 17 +- .../scanner/externalissue/ReportParser.java | 151 ------- .../ExternalIssueImporterTest.java | 238 +++++++++-- .../ExternalIssueReportParserTest.java | 170 ++++++++ .../ExternalIssueReportValidatorTest.java | 379 ++++++++++++++++++ .../externalissue/ReportParserTest.java | 150 ------- .../sensor/DefaultSensorStorageTest.java | 17 +- .../externalissue/cct/invalid_report.json | 3 + .../scanner/externalissue/cct/report.json | 136 +++++++ .../report_missing_engineId.json | 31 -- .../report_missing_filePath.json | 28 -- .../externalissue/report_missing_message.json | 28 -- .../report_missing_primaryLocation.json | 25 -- .../externalissue/report_missing_ruleId.json | 32 -- .../report_missing_severity.json | 32 -- .../externalissue/report_missing_type.json | 32 -- 26 files changed, 1459 insertions(+), 584 deletions(-) create mode 100644 plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssueCctPerLineSensor.java create mode 100644 plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedRuleExternalIssueCctPerLineSensor.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReport.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportParser.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportValidator.java delete mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportParserTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportValidatorTest.java delete mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json create mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/report.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json delete mode 100644 sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssueCctPerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssueCctPerLineSensor.java new file mode 100644 index 00000000000..d1ea092440f --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssueCctPerLineSensor.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.rule; + +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.xoo.Xoo; + +import static org.sonar.api.issue.impact.Severity.LOW; +import static org.sonar.api.issue.impact.Severity.MEDIUM; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; +import static org.sonar.api.rules.CleanCodeAttribute.CLEAR; + +public class OneExternalIssueCctPerLineSensor implements Sensor { + public static final String RULE_ID = "OneExternalIssueWithPerLineSensor"; + public static final String ENGINE_ID = "XooEngine"; + public static final Long EFFORT = 10L; + public static final String ACTIVATE = "sonar.oneExternalIssueWithPerLineSensor.activate"; + public static final String REGISTER_AD_HOC_RULE = "sonar.oneExternalIssueWithPerLineSensor.adhocRule"; + private static final String NAME = "One External Issue Per Line in Clean Code Taxonomy format"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name(NAME) + .onlyOnLanguages(Xoo.KEY) + .onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE).orElse(false)); + } + + @Override + public void execute(SensorContext context) { + FileSystem fs = context.fileSystem(); + FilePredicates p = fs.predicates(); + for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(InputFile.Type.MAIN)))) { + createIssues(file, context); + } + if (context.config().getBoolean(REGISTER_AD_HOC_RULE).orElse(false)) { + context.newAdHocRule() + .engineId(ENGINE_ID) + .ruleId(RULE_ID) + .name("An ad hoc rule") + .description("blah blah") + .cleanCodeAttribute(CLEAR) + .addDefaultImpact(MAINTAINABILITY, MEDIUM) + .addDefaultImpact(RELIABILITY, LOW) + .save(); + } + } + + private static void createIssues(InputFile file, SensorContext context) { + for (int line = 1; line <= file.lines(); line++) { + NewExternalIssue newIssue = context.newExternalIssue(); + newIssue + .engineId(ENGINE_ID) + .ruleId(RULE_ID) + .at(newIssue.newLocation() + .on(file) + .at(file.selectLine(line)) + .message("This issue is generated on each line")) + .remediationEffortMinutes(EFFORT) + .save(); + } + } +} diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedRuleExternalIssueCctPerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedRuleExternalIssueCctPerLineSensor.java new file mode 100644 index 00000000000..772c272cf58 --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OnePredefinedRuleExternalIssueCctPerLineSensor.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.rule; + +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.InputFile.Type; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.xoo.Xoo; + +public class OnePredefinedRuleExternalIssueCctPerLineSensor implements Sensor { + public static final String RULE_ID = "OnePredefinedRuleExternalIssueCctPerLine"; + public static final String ENGINE_ID = "XooEngine"; + public static final Long EFFORT = 10L; + public static final String ACTIVATE = "sonar.onePredefinedRuleExternalIssueCctPerLine.activate"; + private static final String NAME = "One External Issue Per Line With A Predefined Rule in Clean Code Taxonomy format"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name(NAME) + .onlyOnLanguages(Xoo.KEY) + .onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE).orElse(false)); + } + + @Override + public void execute(SensorContext context) { + FileSystem fs = context.fileSystem(); + FilePredicates p = fs.predicates(); + for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(Type.MAIN)))) { + createIssues(file, context); + } + } + + private static void createIssues(InputFile file, SensorContext context) { + for (int line = 1; line <= file.lines(); line++) { + NewExternalIssue newIssue = context.newExternalIssue(); + newIssue + .engineId(ENGINE_ID) + .ruleId(RULE_ID) + .at(newIssue.newLocation() + .on(file) + .at(file.selectLine(line)) + .message("This issue is generated on each line and the rule is predefined")) + .remediationEffortMinutes(EFFORT) + .save(); + } + } +} diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java index c7315d4d991..6f5b2549ee0 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooRulesDefinition.java @@ -76,6 +76,7 @@ public class XooRulesDefinition implements RulesDefinition { defineRulesXoo(context); defineRulesXoo2(context); defineRulesXooExternal(context); + defineRulesXooExternalWithCct(context); } private static void defineRulesXoo2(Context context) { @@ -319,6 +320,18 @@ public class XooRulesDefinition implements RulesDefinition { repo.done(); } + private static void defineRulesXooExternalWithCct(Context context) { + NewRepository repo = context.createExternalRepository(OneExternalIssueCctPerLineSensor.ENGINE_ID, Xoo.KEY).setName(OneExternalIssueCctPerLineSensor.ENGINE_ID); + + repo.createRule(OnePredefinedRuleExternalIssueCctPerLineSensor.RULE_ID) + .setScope(RuleScope.ALL) + .setHtmlDescription("Generates one external issue in each line") + .addDescriptionSection(descriptionSection(INTRODUCTION_SECTION_KEY, "Generates one external issue in each line")) + .setName("One external issue per line"); + + repo.done(); + } + private static void defineRulesXooExternal(Context context) { NewRepository repo = context.createExternalRepository(OneExternalIssuePerLineSensor.ENGINE_ID, Xoo.KEY).setName(OneExternalIssuePerLineSensor.ENGINE_ID); diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java index fa287bcd82c..679292b1fbe 100644 --- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/rule/XooRulesDefinitionTest.java @@ -102,7 +102,7 @@ public class XooRulesDefinitionTest { assertThat(repo).isNotNull(); assertThat(repo.name()).isEqualTo("XooEngine"); assertThat(repo.language()).isEqualTo("xoo"); - assertThat(repo.rules()).hasSize(1); + assertThat(repo.rules()).hasSize(2); } @Test diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/rule/internal/DefaultAdHocRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/rule/internal/DefaultAdHocRule.java index 389a81361f1..a71e1eab76d 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/rule/internal/DefaultAdHocRule.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/rule/internal/DefaultAdHocRule.java @@ -109,13 +109,13 @@ public class DefaultAdHocRule extends DefaultStorable implements AdHocRule, NewA @Override public Map defaultImpacts() { - return impacts; + return impacts.isEmpty() ? Map.of(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM) : impacts; } @CheckForNull @Override public CleanCodeAttribute cleanCodeAttribute() { - return cleanCodeAttribute; + return cleanCodeAttribute == null ? CleanCodeAttribute.defaultCleanCodeAttribute() : cleanCodeAttribute; } @Override 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 a8acb3840ef..37af9929445 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 @@ -27,6 +27,8 @@ import org.sonar.core.config.CorePropertyDefinitions; import org.sonar.core.sarif.SarifSerializerImpl; import org.sonar.scanner.cpd.JavaCpdBlockIndexerSensor; import org.sonar.scanner.deprecated.test.TestPlanBuilder; +import org.sonar.scanner.externalissue.ExternalIssueReportParser; +import org.sonar.scanner.externalissue.ExternalIssueReportValidator; import org.sonar.scanner.externalissue.ExternalIssuesImportSensor; import org.sonar.scanner.externalissue.sarif.DefaultSarif210Importer; import org.sonar.scanner.externalissue.sarif.LocationMapper; @@ -59,6 +61,8 @@ public class BatchComponents { components.add(TestPlanBuilder.class); // External issues + components.add(ExternalIssueReportValidator.class); + components.add(ExternalIssueReportParser.class); components.add(ExternalIssuesImportSensor.class); components.add(ExternalIssuesImportSensor.properties()); components.add(SarifSerializerImpl.class); 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 index 00eeb9a3a8a..c629025b1de 100644 --- 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 @@ -20,60 +20,100 @@ package org.sonar.scanner.externalissue; import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.batch.sensor.rule.NewAdHocRule; +import org.sonar.api.issue.impact.SoftwareQuality; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.scanner.externalissue.ReportParser.Issue; -import org.sonar.scanner.externalissue.ReportParser.Location; -import org.sonar.scanner.externalissue.ReportParser.Report; +import org.sonar.api.server.rule.internal.ImpactMapper; +import org.sonar.scanner.externalissue.ExternalIssueReport.Issue; +import org.sonar.scanner.externalissue.ExternalIssueReport.Location; +import org.sonar.scanner.externalissue.ExternalIssueReport.Rule; public class ExternalIssueImporter { private static final Logger LOG = LoggerFactory.getLogger(ExternalIssueImporter.class); private static final int MAX_UNKNOWN_FILE_PATHS_TO_PRINT = 5; private final SensorContext context; - private final Report report; + private final ExternalIssueReport report; private final Set unknownFiles = new LinkedHashSet<>(); private final Set knownFiles = new LinkedHashSet<>(); - public ExternalIssueImporter(SensorContext context, Report report) { + public ExternalIssueImporter(SensorContext context, ExternalIssueReport report) { this.context = context; this.report = report; } public void execute() { - int issueCount = 0; + if (report.rules != null) { + importRules(); + } else { + importDeprecatedFormat(); + } + } + private void importDeprecatedFormat() { + int issueCount = 0; for (Issue issue : report.issues) { - if (importIssue(issue)) { + if (importDeprecatedIssue(issue)) { issueCount++; } } + logStatistics(issueCount, StringUtils.EMPTY); + } - 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 void importRules() { + for (Rule rule : report.rules) { + NewAdHocRule adHocRule = createAdHocRule(rule); + int issueCount = 0; + for (Issue issue : rule.issues) { + if (importIssue(issue, rule)) { + issueCount++; + } + } + logStatistics(issueCount, String.format(" for ruleId '%s'", rule.ruleId)); + adHocRule.save(); } } - private boolean importIssue(Issue issue) { - NewExternalIssue externalIssue = context.newExternalIssue() - .engineId(issue.engineId) - .ruleId(issue.ruleId) - .severity(Severity.valueOf(issue.severity)) - .type(RuleType.valueOf(issue.type)); + private NewAdHocRule createAdHocRule(Rule rule) { + NewAdHocRule adHocRule = context.newAdHocRule(); + adHocRule.ruleId(rule.ruleId); + adHocRule.name(rule.name); + adHocRule.description(rule.description); + adHocRule.engineId(rule.engineId); + adHocRule.cleanCodeAttribute(CleanCodeAttribute.valueOf(rule.cleanCodeAttribute)); + adHocRule.severity(backmapSeverityFromImpact(rule)); + adHocRule.type(backmapTypeFromImpact(rule)); + for (ExternalIssueReport.Impact impact : rule.impacts) { + adHocRule.addDefaultImpact(SoftwareQuality.valueOf(impact.softwareQuality), + org.sonar.api.issue.impact.Severity.valueOf(impact.severity)); + } + return adHocRule; + } + private static RuleType backmapTypeFromImpact(Rule rule) { + return ImpactMapper.convertToRuleType(SoftwareQuality.valueOf(rule.impacts[0].softwareQuality)); + } + + private static Severity backmapSeverityFromImpact(Rule rule) { + org.sonar.api.issue.impact.Severity impactSeverity = org.sonar.api.issue.impact.Severity.valueOf(rule.impacts[0].severity); + return Severity.valueOf(ImpactMapper.convertToDeprecatedSeverity(impactSeverity)); + } + + private boolean populateCommonValues(Issue issue, NewExternalIssue externalIssue) { if (issue.effortMinutes != null) { externalIssue.remediationEffortMinutes(Long.valueOf(issue.effortMinutes)); } @@ -98,6 +138,37 @@ public class ExternalIssueImporter { } } + private boolean importDeprecatedIssue(Issue issue) { + NewExternalIssue externalIssue = context.newExternalIssue() + .engineId(issue.engineId) + .ruleId(issue.ruleId) + .severity(Severity.valueOf(issue.severity)) + .type(RuleType.valueOf(issue.type)); + + return populateCommonValues(issue, externalIssue); + } + + private boolean importIssue(Issue issue, ExternalIssueReport.Rule rule) { + NewExternalIssue externalIssue = context.newExternalIssue() + .engineId(rule.engineId) + .ruleId(rule.ruleId) + .severity(backmapSeverityFromImpact(rule)) + .type(backmapTypeFromImpact(rule)); + + return populateCommonValues(issue, externalIssue); + } + + private void logStatistics(int issueCount, String additionalInformation) { + String pluralizedIssues = pluralize("issue", issueCount); + String pluralizedFiles = pluralize("file", knownFiles.size()); + LOG.info("Imported {} {} in {} {}{}", issueCount, pluralizedIssues, knownFiles.size(), pluralizedFiles, additionalInformation); + int numberOfUnknownFiles = unknownFiles.size(); + if (numberOfUnknownFiles > 0) { + String limitedUnknownFiles = this.unknownFiles.stream().limit(MAX_UNKNOWN_FILE_PATHS_TO_PRINT).collect(Collectors.joining(", ")); + LOG.info("External issues{} ignored for {} unknown files, including: {}", additionalInformation, numberOfUnknownFiles, limitedUnknownFiles); + } + } + private static String pluralize(String msg, int count) { if (count == 1) { return msg; @@ -123,12 +194,8 @@ public class ExternalIssueImporter { int endLine = (location.textRange.endLine != null) ? location.textRange.endLine : location.textRange.startLine; int endColumn; - if (location.textRange.endColumn == null) { - // assume it's until the last character of the end line - endColumn = file.selectLine(endLine).end().lineOffset(); - } else { - endColumn = location.textRange.endColumn; - } + // assume it's until the last character of the end line + endColumn = Objects.requireNonNullElseGet(location.textRange.endColumn, () -> file.selectLine(endLine).end().lineOffset()); TextPointer end = file.newPointer(endLine, endColumn); newLocation.at(file.newRange(start, end)); } else { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReport.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReport.java new file mode 100644 index 00000000000..fcf19637e37 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReport.java @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 javax.annotation.Nullable; + +public class ExternalIssueReport { + Issue[] issues; + Rule[] rules; + + static class Issue { + @Nullable + String engineId; + @Nullable + String ruleId; + @Nullable + String severity; + @Nullable + String type; + @Nullable + Integer effortMinutes; + Location primaryLocation; + @Nullable + Location[] secondaryLocations; + } + + static class Rule { + String ruleId; + String engineId; + String name; + @Nullable + String description; + String cleanCodeAttribute; + Impact[] impacts; + Issue[] issues; + } + + static class Impact { + String severity; + String softwareQuality; + } + + 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/externalissue/ExternalIssueReportParser.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportParser.java new file mode 100644 index 00000000000..063f086b633 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportParser.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 org.sonar.api.scanner.ScannerSide; + +@ScannerSide +public class ExternalIssueReportParser { + private final Gson gson = new Gson(); + private final ExternalIssueReportValidator externalIssueReportValidator; + + public ExternalIssueReportParser(ExternalIssueReportValidator externalIssueReportValidator) { + this.externalIssueReportValidator = externalIssueReportValidator; + } + + public ExternalIssueReport parse(Path reportPath) { + try (Reader reader = Files.newBufferedReader(reportPath, StandardCharsets.UTF_8)) { + ExternalIssueReport report = gson.fromJson(reader, ExternalIssueReport.class); + externalIssueReportValidator.validate(report, reportPath); + return report; + } catch (JsonIOException | IOException e) { + throw new IllegalStateException("Failed to read external issues report '" + reportPath + "'", e); + } catch (JsonSyntaxException e) { + throw new IllegalStateException("Failed to read external issues report '" + reportPath + "': invalid JSON syntax", e); + } + } + + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportValidator.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportValidator.java new file mode 100644 index 00000000000..d3b81b93218 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueReportValidator.java @@ -0,0 +1,144 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.core.documentation.DocumentationLinkGenerator; + +@ScannerSide +public class ExternalIssueReportValidator { + private static final Logger LOGGER = LoggerFactory.getLogger(ExternalIssueReportValidator.class); + private static final String RULE_ID = "ruleId"; + private static final String SEVERITY = "severity"; + private static final String TYPE = "type"; + private static final String DOCUMENTATION_SUFFIX = "/analyzing-source-code/importing-external-issues/generic-issue-import-format/"; + private final DocumentationLinkGenerator documentationLinkGenerator; + + ExternalIssueReportValidator(DocumentationLinkGenerator documentationLinkGenerator) { + this.documentationLinkGenerator = documentationLinkGenerator; + } + + public void validate(ExternalIssueReport report, Path reportPath) { + if (report.rules != null) { + checkNoField(report.issues, "issues", reportPath); + validateRules(report.rules, reportPath); + } else if (report.issues != null) { + String documentationLink = documentationLinkGenerator.getDocumentationLink(DOCUMENTATION_SUFFIX); + LOGGER.warn("External issues were imported with a deprecated format which will be removed soon. " + + "Please switch to the newest format to fully benefit from Clean Code: {}", documentationLink); + validateIssues(report.issues, reportPath); + } else { + throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field 'rules'", reportPath)); + } + } + + private static void validateIssues(ExternalIssueReport.Issue[] issues, Path reportPath) { + for (ExternalIssueReport.Issue issue : issues) { + mandatoryField(issue.ruleId, RULE_ID, reportPath); + mandatoryField(issue.severity, SEVERITY, reportPath); + mandatoryField(issue.type, TYPE, reportPath); + mandatoryField(issue.engineId, "engineId", reportPath); + validateAlwaysRequiredIssueFields(issue, reportPath); + } + } + + private static void validateRules(ExternalIssueReport.Rule[] rules, Path reportPath) { + for (ExternalIssueReport.Rule rule : rules) { + mandatoryField(rule.ruleId, RULE_ID, reportPath); + mandatoryField(rule.name, "name", reportPath); + mandatoryField(rule.engineId, "engineId", reportPath); + mandatoryField(rule.cleanCodeAttribute, "cleanCodeAttribute", reportPath); + mandatoryArray(rule.impacts, "impacts", reportPath); + validateIssuesAsPartOfARule(rule.issues, reportPath); + } + } + + private static void validateIssuesAsPartOfARule(ExternalIssueReport.Issue[] issues, Path reportPath) { + for (ExternalIssueReport.Issue issue : issues) { + validateAlwaysRequiredIssueFields(issue, reportPath); + checkNoField(issue.severity, SEVERITY, reportPath); + checkNoField(issue.type, TYPE, reportPath); + checkNoField(issue.ruleId, RULE_ID, reportPath); + } + } + + private static void checkNoField(@Nullable Object value, String fieldName, Path reportPath) { + if (value != null) { + throw new IllegalStateException(String.format("Deprecated '%s' field found in the following report: '%s'.", fieldName, reportPath)); + } + } + + private static void validateAlwaysRequiredIssueFields(ExternalIssueReport.Issue issue, Path reportPath) { + mandatoryField(issue.primaryLocation, "primaryLocation", reportPath); + mandatoryFieldPrimaryLocation(issue.primaryLocation.filePath, "filePath", reportPath); + mandatoryFieldPrimaryLocation(issue.primaryLocation.message, "message", reportPath); + + if (issue.primaryLocation.textRange != null) { + mandatoryFieldPrimaryLocation(issue.primaryLocation.textRange.startLine, "startLine of the text range", reportPath); + } + + if (issue.secondaryLocations != null) { + for (ExternalIssueReport.Location l : issue.secondaryLocations) { + mandatoryFieldSecondaryLocation(l.filePath, "filePath", reportPath); + mandatoryFieldSecondaryLocation(l.textRange, "textRange", reportPath); + mandatoryFieldSecondaryLocation(l.textRange.startLine, "startLine of the text range", reportPath); + } + } + } + + private static void mandatoryFieldPrimaryLocation(@Nullable Object value, String fieldName, Path reportPath) { + if (value == null) { + throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in the primary location of" + + " the issue.", reportPath, fieldName)); + } + } + + private static void mandatoryFieldSecondaryLocation(@Nullable Object value, String fieldName, Path reportPath) { + if (value == null) { + throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in a secondary location of" + + " the issue.", reportPath, fieldName)); + } + } + + private static void mandatoryField(@Nullable Object value, String fieldName, Path reportPath) { + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", reportPath, fieldName)); + } + } + + private static void mandatoryArray(@Nullable Object[] value, String fieldName, Path reportPath) { + mandatoryField(value, fieldName, reportPath); + if (value.length == 0) { + throw new IllegalStateException(String.format("Failed to parse report '%s': mandatory array '%s' not populated.", reportPath, + fieldName)); + } + } + + private static void mandatoryField(@Nullable String value, String fieldName, Path reportPath) { + if (StringUtils.isBlank(value)) { + throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", reportPath, fieldName)); + } + } +} 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 index 508523a4527..b2f19d85610 100644 --- 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 @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.api.CoreProperties; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; @@ -32,18 +34,18 @@ 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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.scanner.externalissue.ReportParser.Report; +import org.sonar.api.scanner.ScannerSide; +@ScannerSide public class ExternalIssuesImportSensor implements Sensor { private static final Logger LOG = LoggerFactory.getLogger(ExternalIssuesImportSensor.class); - static final String REPORT_PATHS_PROPERTY_KEY = "sonar.externalIssuesReportPaths"; - + private static final String REPORT_PATHS_PROPERTY_KEY = "sonar.externalIssuesReportPaths"; private final Configuration config; + private final ExternalIssueReportParser externalIssueReportParser; - public ExternalIssuesImportSensor(Configuration config) { + public ExternalIssuesImportSensor(Configuration config, ExternalIssueReportParser externalIssueReportParser) { this.config = config; + this.externalIssueReportParser = externalIssueReportParser; } public static List properties() { @@ -68,8 +70,7 @@ public class ExternalIssuesImportSensor implements Sensor { 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(); + ExternalIssueReport report = externalIssueReportParser.parse(reportFilePath); ExternalIssueImporter issueImporter = new ExternalIssueImporter(context, report); issueImporter.execute(); } 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 deleted file mode 100644 index 485f1ff193e..00000000000 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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; - - public Report() { - // http://stackoverflow.com/a/18645370/229031 - } - } - - static class Issue { - String engineId; - String ruleId; - String severity; - String type; - @Nullable - Integer effortMinutes; - Location primaryLocation; - @Nullable - Location[] secondaryLocations; - - public Issue() { - // http://stackoverflow.com/a/18645370/229031 - } - } - - static class Location { - @Nullable - String message; - String filePath; - @Nullable - TextRange textRange; - - public Location() { - // http://stackoverflow.com/a/18645370/229031 - } - } - - static class TextRange { - Integer startLine; - @Nullable - Integer startColumn; - @Nullable - Integer endLine; - @Nullable - Integer endColumn; - - public TextRange() { - // http://stackoverflow.com/a/18645370/229031 - } - } -} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueImporterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueImporterTest.java index 315991b93fb..7b29c33aa0b 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueImporterTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueImporterTest.java @@ -20,6 +20,7 @@ package org.sonar.scanner.externalissue; import java.io.File; +import java.util.Map; import javax.annotation.Nullable; import org.apache.commons.lang.math.RandomUtils; import org.junit.Before; @@ -33,14 +34,29 @@ import org.sonar.api.batch.fs.internal.TestInputFileBuilder; import org.sonar.api.batch.rule.Severity; import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.batch.sensor.issue.ExternalIssue; +import org.sonar.api.batch.sensor.rule.AdHocRule; +import org.sonar.api.issue.impact.SoftwareQuality; +import org.sonar.api.rules.CleanCodeAttribute; +import org.sonar.api.rules.RuleType; import org.sonar.api.testfixtures.log.LogTester; import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.commons.lang.ObjectUtils.defaultIfNull; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.api.issue.impact.Severity.HIGH; +import static org.sonar.api.issue.impact.Severity.LOW; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY; public class ExternalIssueImporterTest { + + public static final String RULE_ENGINE_ID = "some_rule_engine_id"; + public static final String RULE_ID = "some_rule_id"; + public static final String RULE_NAME = "some_rule_name"; + public static final CleanCodeAttribute RULE_ATTRIBUTE = CleanCodeAttribute.FORMATTED; + @Rule public TemporaryFolder temp = new TemporaryFolder(); @Rule @@ -63,30 +79,167 @@ public class ExternalIssueImporterTest { } @Test - public void import_zero_issues() { - ReportParser.Report report = new ReportParser.Report(); - report.issues = new ReportParser.Issue[0]; + public void execute_whenNewFormatWithZeroIssues() { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Rule rule = createRule(); + rule.issues = new ExternalIssueReport.Issue[0]; + report.rules = new ExternalIssueReport.Rule[]{rule}; ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); underTest.execute(); + assertThat(context.allExternalIssues()).isEmpty(); + assertThat(context.allIssues()).isEmpty(); + assertThat(logs.logs(Level.INFO)).contains("Imported 0 issues in 0 files for ruleId 'some_rule_id'"); + } + + @Test + public void execute_whenNewFormatWithMinimalInfo() { + ExternalIssueReport.Issue input = new ExternalIssueReport.Issue(); + input.primaryLocation = new ExternalIssueReport.Location(); + input.primaryLocation.filePath = sourceFile.getProjectRelativePath(); + input.primaryLocation.message = randomAlphabetic(5); + + runOn(input); + + assertThat(context.allExternalIssues()).hasSize(1); + ExternalIssue output = context.allExternalIssues().iterator().next(); + assertThat(output.engineId()).isEqualTo(RULE_ENGINE_ID); + assertThat(output.ruleId()).isEqualTo(RULE_ID); + assertThat(output.severity()).isEqualTo(Severity.CRITICAL); //backmapped + assertThat(output.type()).isEqualTo(RuleType.VULNERABILITY); //backmapped + assertThat(output.remediationEffort()).isNull(); + assertThat(logs.logs(Level.INFO)).contains("Imported 1 issue in 1 file for ruleId 'some_rule_id'"); + assertThat(context.allAdHocRules()).hasSize(1); + + AdHocRule output1 = context.allAdHocRules().iterator().next(); + assertThat(output1.ruleId()).isEqualTo(RULE_ID); + assertThat(output1.name()).isEqualTo(RULE_NAME); + assertThat(output1.engineId()).isEqualTo(RULE_ENGINE_ID); + assertThat(output1.severity()).isEqualTo(Severity.CRITICAL); //backmapped + assertThat(output1.type()).isEqualTo(RuleType.VULNERABILITY); //backmapped + assertThat(output1.cleanCodeAttribute()).isEqualTo(RULE_ATTRIBUTE); + assertThat(output1.defaultImpacts()).containsExactlyInAnyOrderEntriesOf(Map.of(SECURITY, HIGH, MAINTAINABILITY, LOW)); + } + + @Test + public void execute_whenNewFormatWithCompletePrimaryLocation() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); + input.startLine = 1; + input.startColumn = 4; + input.endLine = 2; + input.endColumn = 3; + + runOn(newIssue(input)); + + assertThat(context.allExternalIssues()).hasSize(1); + ExternalIssue output = context.allExternalIssues().iterator().next(); + assertSameRange(input, output.primaryLocation().textRange()); + } + + @Test + public void execute_whenNewFormatWithNoColumns() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); + input.startLine = 1; + input.startColumn = null; + input.endLine = 2; + input.endColumn = null; + + runOn(newIssue(input)); + + assertThat(context.allExternalIssues()).hasSize(1); + TextRange got = context.allExternalIssues().iterator().next().primaryLocation().textRange(); + assertThat(got.start().line()).isEqualTo(input.startLine); + assertThat(got.start().lineOffset()).isZero(); + assertThat(got.end().line()).isEqualTo(input.startLine); + assertThat(got.end().lineOffset()).isEqualTo(sourceFile.selectLine(input.startLine).end().lineOffset()); + } + + @Test + public void execute_whenNewFormatWithStartButNotEndColumn() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); + input.startLine = 1; + input.startColumn = 3; + input.endLine = 2; + input.endColumn = null; + + runOn(newIssue(input)); + + assertThat(context.allExternalIssues()).hasSize(1); + TextRange got = context.allExternalIssues().iterator().next().primaryLocation().textRange(); + assertThat(got.start().line()).isEqualTo(input.startLine); + assertThat(got.start().lineOffset()).isEqualTo(3); + assertThat(got.end().line()).isEqualTo(input.endLine); + assertThat(got.end().lineOffset()).isEqualTo(sourceFile.selectLine(input.endLine).end().lineOffset()); + } + + @Test + public void execute_whenNewFormatContainsNonExistentCleanCodeAttribute_shouldThrowException() { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Rule rule = createRule("not_existent_attribute", MAINTAINABILITY.name(), HIGH.name()); + rule.issues = new ExternalIssueReport.Issue[]{}; + report.rules = new ExternalIssueReport.Rule[]{rule}; + + ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); + + assertThatThrownBy(underTest::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No enum constant org.sonar.api.rules.CleanCodeAttribute.not_existent_attribute"); + } + + @Test + public void execute_whenNewFormatContainsNonExistentSoftwareQuality_shouldThrowException() { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Rule rule = createRule(CleanCodeAttribute.CONVENTIONAL.name(), "not_existent_software_quality", HIGH.name()); + rule.issues = new ExternalIssueReport.Issue[]{}; + report.rules = new ExternalIssueReport.Rule[]{rule}; + + ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); + + assertThatThrownBy(underTest::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No enum constant org.sonar.api.issue.impact.SoftwareQuality.not_existent_software_quality"); + } + + @Test + public void execute_whenNewFormatContainsNonExistentImpactSeverity_shouldThrowException() { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Rule rule = createRule(CleanCodeAttribute.CONVENTIONAL.name(), SoftwareQuality.RELIABILITY.name(), + "not_existent_impact_severity"); + rule.issues = new ExternalIssueReport.Issue[]{}; + report.rules = new ExternalIssueReport.Rule[]{rule}; + + ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); + + assertThatThrownBy(underTest::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No enum constant org.sonar.api.issue.impact.Severity.not_existent_impact_severity"); + } + + @Test + public void execute_whenDeprecatedFormatWithZeroIssues() { + ExternalIssueReport report = new ExternalIssueReport(); + report.issues = new ExternalIssueReport.Issue[0]; + + ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); + underTest.execute(); assertThat(context.allExternalIssues()).isEmpty(); assertThat(context.allIssues()).isEmpty(); assertThat(logs.logs(Level.INFO)).contains("Imported 0 issues in 0 files"); } @Test - public void import_issue_with_minimal_info() { - ReportParser.Report report = new ReportParser.Report(); - ReportParser.Issue input = new ReportParser.Issue(); + public void execute_whenDeprecatedFormatWithMinimalInfo() { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Issue input = new ExternalIssueReport.Issue(); input.engineId = "findbugs"; input.ruleId = "123"; input.severity = "CRITICAL"; input.type = "BUG"; - input.primaryLocation = new ReportParser.Location(); + input.primaryLocation = new ExternalIssueReport.Location(); input.primaryLocation.filePath = sourceFile.getProjectRelativePath(); input.primaryLocation.message = randomAlphabetic(5); - report.issues = new ReportParser.Issue[] {input}; + report.issues = new ExternalIssueReport.Issue[]{input}; ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); underTest.execute(); @@ -101,33 +254,29 @@ public class ExternalIssueImporterTest { } @Test - public void import_issue_with_complete_primary_location() { - ReportParser.TextRange input = new ReportParser.TextRange(); + public void execute_whenDeprecatedFormatWithCompletePrimaryLocation() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); input.startLine = 1; input.startColumn = 4; input.endLine = 2; input.endColumn = 3; - runOn(newIssue(input)); + runOnDeprecatedFormat(newIssue(input)); assertThat(context.allExternalIssues()).hasSize(1); ExternalIssue output = context.allExternalIssues().iterator().next(); assertSameRange(input, output.primaryLocation().textRange()); } - /** - * If columns are not defined, then issue is assumed to be on the full first line. - * The end line is ignored. - */ @Test - public void import_issue_with_no_columns() { - ReportParser.TextRange input = new ReportParser.TextRange(); + public void execute_whenDeprecatedFormatWithNoColumns() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); input.startLine = 1; input.startColumn = null; input.endLine = 2; input.endColumn = null; - runOn(newIssue(input)); + runOnDeprecatedFormat(newIssue(input)); assertThat(context.allExternalIssues()).hasSize(1); TextRange got = context.allExternalIssues().iterator().next().primaryLocation().textRange(); @@ -137,18 +286,15 @@ public class ExternalIssueImporterTest { assertThat(got.end().lineOffset()).isEqualTo(sourceFile.selectLine(input.startLine).end().lineOffset()); } - /** - * If end column is not defined, then issue is assumed to be until the last character of the end line. - */ @Test - public void import_issue_with_start_but_not_end_column() { - ReportParser.TextRange input = new ReportParser.TextRange(); + public void execute_whenDeprecatedFormatWithStartButNotEndColumn() { + ExternalIssueReport.TextRange input = new ExternalIssueReport.TextRange(); input.startLine = 1; input.startColumn = 3; input.endLine = 2; input.endColumn = null; - runOn(newIssue(input)); + runOnDeprecatedFormat(newIssue(input)); assertThat(context.allExternalIssues()).hasSize(1); TextRange got = context.allExternalIssues().iterator().next().primaryLocation().textRange(); @@ -158,29 +304,59 @@ public class ExternalIssueImporterTest { assertThat(got.end().lineOffset()).isEqualTo(sourceFile.selectLine(input.endLine).end().lineOffset()); } - private void assertSameRange(ReportParser.TextRange expected, TextRange got) { + private static ExternalIssueReport.Rule createRule() { + return createRule(RULE_ATTRIBUTE.name(), SECURITY.name(), HIGH.name()); + } + + private static ExternalIssueReport.Rule createRule(String cleanCodeAttribute, String softwareQuality, String impactSeverity) { + ExternalIssueReport.Rule rule = new ExternalIssueReport.Rule(); + rule.ruleId = RULE_ID; + rule.name = RULE_NAME; + rule.engineId = RULE_ENGINE_ID; + rule.cleanCodeAttribute = cleanCodeAttribute; + ExternalIssueReport.Impact impact1 = new ExternalIssueReport.Impact(); + impact1.severity = impactSeverity; + impact1.softwareQuality = softwareQuality; + ExternalIssueReport.Impact impact2 = new ExternalIssueReport.Impact(); + impact2.severity = LOW.name(); + impact2.softwareQuality = MAINTAINABILITY.name(); + rule.impacts = new ExternalIssueReport.Impact[]{impact1, impact2}; + return rule; + } + + private void assertSameRange(ExternalIssueReport.TextRange expected, TextRange got) { assertThat(got.start().line()).isEqualTo(expected.startLine); assertThat(got.start().lineOffset()).isEqualTo(defaultIfNull(expected.startColumn, 0)); assertThat(got.end().line()).isEqualTo(expected.endLine); assertThat(got.end().lineOffset()).isEqualTo(defaultIfNull(expected.endColumn, 0)); } - private void runOn(ReportParser.Issue input) { - ReportParser.Report report = new ReportParser.Report(); - report.issues = new ReportParser.Issue[] {input}; + private void runOnDeprecatedFormat(ExternalIssueReport.Issue input) { + ExternalIssueReport report = new ExternalIssueReport(); + report.issues = new ExternalIssueReport.Issue[]{input}; + + ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); + underTest.execute(); + } + + private void runOn(ExternalIssueReport.Issue input) { + ExternalIssueReport report = new ExternalIssueReport(); + ExternalIssueReport.Rule rule = createRule(); + rule.issues = new ExternalIssueReport.Issue[]{input}; + report.rules = new ExternalIssueReport.Rule[]{rule}; ExternalIssueImporter underTest = new ExternalIssueImporter(this.context, report); underTest.execute(); } - private ReportParser.Issue newIssue(@Nullable ReportParser.TextRange textRange) { - ReportParser.Issue input = new ReportParser.Issue(); + private ExternalIssueReport.Issue newIssue(@Nullable ExternalIssueReport.TextRange textRange) { + ExternalIssueReport.Issue input = new ExternalIssueReport.Issue(); input.engineId = randomAlphabetic(5); input.ruleId = randomAlphabetic(5); input.severity = "CRITICAL"; input.type = "BUG"; input.effortMinutes = RandomUtils.nextInt(); - input.primaryLocation = new ReportParser.Location(); + input.primaryLocation = new ExternalIssueReport.Location(); input.primaryLocation.filePath = sourceFile.getProjectRelativePath(); input.primaryLocation.message = randomAlphabetic(5); input.primaryLocation.textRange = textRange; diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportParserTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportParserTest.java new file mode 100644 index 00000000000..645ecf5d5b3 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportParserTest.java @@ -0,0 +1,170 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ExternalIssueReportParserTest { + private static final String DEPRECATED_REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/"; + private static final String REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/cct/"; + private final ExternalIssueReportValidator validator = mock(ExternalIssueReportValidator.class); + private final ExternalIssueReportParser externalIssueReportParser = new ExternalIssueReportParser(validator); + private Path reportPath; + + @Test + public void parse_whenCorrectCctFormat_shouldParseCorrectly() { + reportPath = Paths.get(REPORTS_LOCATION + "report.json"); + + ExternalIssueReport report = externalIssueReportParser.parse(reportPath); + + verify(validator).validate(report, reportPath); + assertCctReport(report); + } + + @Test + public void parse_whenCorrectDeprecatedFormat_shouldParseCorrectly() { + reportPath = Paths.get(DEPRECATED_REPORTS_LOCATION + "report.json"); + + ExternalIssueReport report = externalIssueReportParser.parse(reportPath); + + verify(validator).validate(report, reportPath); + assertDeprecatedReport(report); + } + + @Test + public void parse_whenDoesntExist_shouldFail() { + reportPath = Paths.get("unknown.json"); + + assertThatThrownBy(() -> externalIssueReportParser.parse(reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to read external issues report 'unknown.json'"); + } + + @Test + public void parse_whenInvalidDeprecatedFormat_shouldFail() { + reportPath = Paths.get(DEPRECATED_REPORTS_LOCATION + "report_invalid_json.json"); + + assertThatThrownBy(() -> externalIssueReportParser.parse(reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to read external issues report 'src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json': " + + "invalid JSON syntax"); + } + + @Test + public void parse_whenValidatorThrowsException_shouldPropagate() { + reportPath = Paths.get(DEPRECATED_REPORTS_LOCATION + "report.json"); + + doThrow(new IllegalStateException("Just a dummy exception")).when(validator).validate(any(), eq(reportPath)); + + assertThatThrownBy(() -> externalIssueReportParser.parse(reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Just a dummy exception"); + } + + private static void assertDeprecatedReport(ExternalIssueReport report) { + assertThat(report.issues).hasSize(4); + + ExternalIssueReport.Issue issue1 = report.issues[0]; + assertThat(issue1.engineId).isEqualTo("eslint"); + assertThat(issue1.ruleId).isEqualTo("rule1"); + assertThat(issue1.severity).isEqualTo("MAJOR"); + assertThat(issue1.effortMinutes).isEqualTo(40); + assertThat(issue1.type).isEqualTo("CODE_SMELL"); + assertThat(issue1.primaryLocation.filePath).isEqualTo("file1.js"); + assertThat(issue1.primaryLocation.message).isEqualTo("fix the issue here"); + assertThat(issue1.primaryLocation.textRange.startColumn).isEqualTo(2); + assertThat(issue1.primaryLocation.textRange.startLine).isEqualTo(1); + assertThat(issue1.primaryLocation.textRange.endColumn).isEqualTo(4); + assertThat(issue1.primaryLocation.textRange.endLine).isEqualTo(3); + assertThat(issue1.secondaryLocations).isNull(); + + ExternalIssueReport.Issue issue2 = report.issues[3]; + assertThat(issue2.engineId).isEqualTo("eslint"); + assertThat(issue2.ruleId).isEqualTo("rule3"); + assertThat(issue2.severity).isEqualTo("MAJOR"); + assertThat(issue2.effortMinutes).isNull(); + assertThat(issue2.type).isEqualTo("BUG"); + assertThat(issue2.secondaryLocations).hasSize(2); + assertThat(issue2.secondaryLocations[0].filePath).isEqualTo("file1.js"); + assertThat(issue2.secondaryLocations[0].message).isEqualTo("fix the bug here"); + assertThat(issue2.secondaryLocations[0].textRange.startLine).isEqualTo(1); + assertThat(issue2.secondaryLocations[1].filePath).isEqualTo("file2.js"); + assertThat(issue2.secondaryLocations[1].message).isNull(); + assertThat(issue2.secondaryLocations[1].textRange.startLine).isEqualTo(2); + } + + private static void assertCctReport(ExternalIssueReport report) { + assertThat(report.issues).isNull(); + assertThat(report.rules).hasSize(2); + + ExternalIssueReport.Rule rule = report.rules[0]; + assertThat(rule.ruleId).isEqualTo("rule1"); + assertThat(rule.engineId).isEqualTo("test"); + assertThat(rule.name).isEqualTo("just_some_rule_name"); + assertThat(rule.description).isEqualTo("just_some_description"); + assertThat(rule.cleanCodeAttribute).isEqualTo("FORMATTED"); + assertThat(rule.impacts).hasSize(2); + assertThat(rule.impacts[0].severity).isEqualTo("HIGH"); + assertThat(rule.impacts[0].softwareQuality).isEqualTo("MAINTAINABILITY"); + assertThat(rule.impacts[1].severity).isEqualTo("LOW"); + assertThat(rule.impacts[1].softwareQuality).isEqualTo("SECURITY"); + + assertThat(rule.issues).hasSize(4); + + ExternalIssueReport.Issue issue1 = rule.issues[0]; + assertThat(issue1.engineId).isNull(); + assertThat(issue1.ruleId).isNull(); + assertThat(issue1.severity).isNull(); + assertThat(issue1.effortMinutes).isEqualTo(40); + assertThat(issue1.type).isNull(); + assertThat(issue1.primaryLocation.filePath).isEqualTo("file1.js"); + assertThat(issue1.primaryLocation.message).isEqualTo("fix the issue here"); + assertThat(issue1.primaryLocation.textRange.startColumn).isEqualTo(2); + assertThat(issue1.primaryLocation.textRange.startLine).isEqualTo(1); + assertThat(issue1.primaryLocation.textRange.endColumn).isEqualTo(4); + assertThat(issue1.primaryLocation.textRange.endLine).isEqualTo(3); + assertThat(issue1.secondaryLocations).isNull(); + + ExternalIssueReport.Issue issue2 = rule.issues[3]; + assertThat(issue2.engineId).isNull(); + assertThat(issue2.ruleId).isNull(); + assertThat(issue2.severity).isNull(); + assertThat(issue2.effortMinutes).isNull(); + assertThat(issue2.type).isNull(); + assertThat(issue2.secondaryLocations).hasSize(2); + assertThat(issue2.secondaryLocations[0].filePath).isEqualTo("file1.js"); + assertThat(issue2.secondaryLocations[0].message).isEqualTo("fix the bug here"); + assertThat(issue2.secondaryLocations[0].textRange.startLine).isEqualTo(1); + assertThat(issue2.secondaryLocations[1].filePath).isEqualTo("file2.js"); + assertThat(issue2.secondaryLocations[1].message).isNull(); + assertThat(issue2.secondaryLocations[1].textRange.startLine).isEqualTo(2); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportValidatorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportValidatorTest.java new file mode 100644 index 00000000000..8f7bd5ac173 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ExternalIssueReportValidatorTest.java @@ -0,0 +1,379 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogAndArguments; +import org.sonar.api.testfixtures.log.LogTester; +import org.sonar.core.documentation.DocumentationLinkGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ExternalIssueReportValidatorTest { + + private static final String DEPRECATED_REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/"; + private static final String REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/cct/"; + private static final String URL = "/analyzing-source-code/importing-external-issues/generic-issue-import-format/"; + private static final String TEST_URL = "test-url"; + + private final Gson gson = new Gson(); + private final Path reportPath = Paths.get("some-folder"); + private final DocumentationLinkGenerator documentationLinkGenerator = mock(DocumentationLinkGenerator.class); + private final ExternalIssueReportValidator validator = new ExternalIssueReportValidator(documentationLinkGenerator); + + @Rule + public LogTester logTester = new LogTester(); + + @Before + public void setUp() { + when(documentationLinkGenerator.getDocumentationLink(URL)).thenReturn(TEST_URL); + } + + @Test + public void validate_whenCorrect_shouldNotThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + assertThatCode(() -> validator.validate(report, reportPath)).doesNotThrowAnyException(); + } + + @Test + public void validate_whenMissingMandatoryCleanCodeAttributeField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].cleanCodeAttribute = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'cleanCodeAttribute'."); + } + + @Test + public void validate_whenMissingEngineIdField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].engineId = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'engineId'."); + } + + @Test + public void validate_whenMissingFilepathFieldForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].primaryLocation.filePath = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in the primary location of the issue."); + } + + @Test + public void validate_whenMissingImpactsField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].impacts = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'impacts'."); + } + + @Test + public void validate_whenMissingMessageFieldForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].primaryLocation.message = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'message' in the primary location of the issue."); + } + + @Test + public void validate_whenMissingStartLineFieldForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].primaryLocation.textRange.startLine = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in the primary location of the issue."); + } + + @Test + public void validate_whenReportMissingFilePathForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[3].secondaryLocations[0].filePath = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in a secondary location of the issue."); + } + + @Test + public void validate_whenReportMissingTextRangeForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[3].secondaryLocations[0].textRange = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'textRange' in a secondary location of the issue."); + } + + @Test + public void validate_whenReportMissingTextRangeStartLineForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[3].secondaryLocations[0].textRange.startLine = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in a secondary location of the issue."); + } + + @Test + public void validate_whenMissingNameField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].name = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'name'."); + } + + @Test + public void validate_whenMissingPrimaryLocationField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].primaryLocation = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'primaryLocation'."); + } + + @Test + public void validate_whenMissingRuleIdField_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].ruleId = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'."); + } + + @Test + public void validate_whenContainsDeprecatedIssuesEntry_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.issues = new ExternalIssueReport.Issue[] { new ExternalIssueReport.Issue() }; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Deprecated 'issues' field found in the following report: 'some-folder'."); + } + + @Test + public void validate_whenContainsDeprecatedSeverityEntry_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].severity = "MAJOR"; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Deprecated 'severity' field found in the following report: 'some-folder'."); + } + + @Test + public void validate_whenContainsDeprecatedTypeEntry_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].issues[0].type = "BUG"; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Deprecated 'type' field found in the following report: 'some-folder'."); + } + + @Test + public void validate_whenContainsEmptyImpacts_shouldThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].impacts = new ExternalIssueReport.Impact[0]; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': mandatory array 'impacts' not populated."); + } + + @Test + public void validate_whenDeprecatedReportFormat_shouldValidateWithWarningLog() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + assertThatCode(() -> validator.validate(report, reportPath)).doesNotThrowAnyException(); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingEngineId_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].engineId = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'engineId'."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingFilepathForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].primaryLocation.filePath = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in the primary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingMessageForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].primaryLocation.message = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'message' in the primary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].primaryLocation = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'primaryLocation'."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingStartLineForPrimaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].primaryLocation.textRange.startLine = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in the primary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingFilePathForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[3].secondaryLocations[0].filePath = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'filePath' in a secondary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingTextRangeForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[3].secondaryLocations[0].textRange = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'textRange' in a secondary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingTextRangeStartLineForSecondaryLocation_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[3].secondaryLocations[0].textRange.startLine = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'startLine of the text range' in a secondary location of the issue."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingRuleId_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].ruleId = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingSeverity_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].severity = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'severity'."); + assertWarningLog(); + } + + @Test + public void validate_whenDeprecatedReportMissingType_shouldThrowException() throws IOException { + ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION); + report.issues[0].type = null; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'type'."); + assertWarningLog(); + } + + @Test + public void validate_whenEmptyStringForMandatoryField_shouldStillThrowException() throws IOException { + ExternalIssueReport report = read(REPORTS_LOCATION); + report.rules[0].ruleId = ""; + + assertThatThrownBy(() -> validator.validate(report, reportPath)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to parse report 'some-folder': missing mandatory field 'ruleId'."); + } + + private void assertWarningLog() { + assertThat(logTester.getLogs(Level.WARN)) + .extracting(LogAndArguments::getFormattedMsg) + .contains("External issues were imported with a deprecated format which will be removed soon. " + + "Please switch to the newest format to fully benefit from Clean Code: " + TEST_URL); + } + + private ExternalIssueReport read(String location) throws IOException { + Reader reader = Files.newBufferedReader(Paths.get(location + "report.json"), StandardCharsets.UTF_8); + return gson.fromJson(reader, ExternalIssueReport.class); + } + +} 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 deleted file mode 100644 index fdf41c801f7..00000000000 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 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.Test; -import org.sonar.scanner.externalissue.ReportParser.Report; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class ReportParserTest { - - @Test - public void parse_sample() { - ReportParser parser = new ReportParser(Paths.get("src/test/resources/org/sonar/scanner/externalissue/report.json")); - - Report report = parser.parse(); - - assertThat(report.issues).hasSize(4); - 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(40); - 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).isEqualTo(2); - assertThat(report.issues[0].primaryLocation.textRange.startLine).isOne(); - assertThat(report.issues[0].primaryLocation.textRange.endColumn).isEqualTo(4); - assertThat(report.issues[0].primaryLocation.textRange.endLine).isEqualTo(3); - assertThat(report.issues[0].secondaryLocations).isNull(); - - assertThat(report.issues[3].engineId).isEqualTo("eslint"); - assertThat(report.issues[3].ruleId).isEqualTo("rule3"); - assertThat(report.issues[3].severity).isEqualTo("MAJOR"); - assertThat(report.issues[3].effortMinutes).isNull(); - assertThat(report.issues[3].type).isEqualTo("BUG"); - assertThat(report.issues[3].secondaryLocations).hasSize(2); - assertThat(report.issues[3].secondaryLocations[0].filePath).isEqualTo("file1.js"); - assertThat(report.issues[3].secondaryLocations[0].message).isEqualTo("fix the bug here"); - assertThat(report.issues[3].secondaryLocations[0].textRange.startLine).isOne(); - assertThat(report.issues[3].secondaryLocations[1].filePath).isEqualTo("file2.js"); - assertThat(report.issues[3].secondaryLocations[1].message).isNull(); - assertThat(report.issues[3].secondaryLocations[1].textRange.startLine).isEqualTo(2); - } - - 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")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Failed to read external issues report 'unknown.json'"); - } - - @Test - public void fail_if_report_is_not_valid_json() { - ReportParser parser = new ReportParser(path("report_invalid_json.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("invalid JSON syntax"); - } - - @Test - public void fail_if_primaryLocation_not_set() { - ReportParser parser = new ReportParser(path("report_missing_primaryLocation.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'primaryLocation'"); - } - - @Test - public void fail_if_engineId_not_set() { - ReportParser parser = new ReportParser(path("report_missing_engineId.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'engineId'"); - } - - @Test - public void fail_if_ruleId_not_set() { - ReportParser parser = new ReportParser(path("report_missing_ruleId.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'ruleId'"); - } - - @Test - public void fail_if_severity_not_set() { - ReportParser parser = new ReportParser(path("report_missing_severity.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'severity'"); - } - - @Test - public void fail_if_type_not_set() { - ReportParser parser = new ReportParser(path("report_missing_type.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'type'"); - } - - @Test - public void fail_if_filePath_not_set_in_primaryLocation() { - ReportParser parser = new ReportParser(path("report_missing_filePath.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'filePath'"); - } - - @Test - public void fail_if_message_not_set_in_primaryLocation() { - ReportParser parser = new ReportParser(path("report_missing_message.json")); - - assertThatThrownBy(() -> parser.parse()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing mandatory field 'message'"); - } -} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java index 46cc94f03cf..2ecaaf94948 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java @@ -356,7 +356,7 @@ public class DefaultSensorStorageTest { } @Test - public void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() throws IOException { + public void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() { underTest.store(new DefaultAdHocRule().ruleId("ruleId").engineId("engineId") .name("name") @@ -369,9 +369,11 @@ public class DefaultSensorStorageTest { try (CloseableIterator adhocRuleIt = reportReader.readAdHocRules()) { ScannerReport.AdHocRule adhocRule = adhocRuleIt.next(); - assertThat(adhocRule).extracting(r -> r.getRuleId(), r -> r.getName(), r -> r.getSeverity(), r -> r.getType(), r -> r.getDescription()) + assertThat(adhocRule) + .extracting(ScannerReport.AdHocRule::getRuleId, ScannerReport.AdHocRule::getName, ScannerReport.AdHocRule::getSeverity, + ScannerReport.AdHocRule::getType, ScannerReport.AdHocRule::getDescription) .containsExactlyInAnyOrder("ruleId", "name", Constants.Severity.MAJOR, ScannerReport.IssueType.CODE_SMELL, "description"); - assertThat(adhocRule.getDefaultImpactsList()).hasSize(2).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + assertThat(adhocRule.getDefaultImpactsList()).hasSize(2).extracting(ScannerReport.Impact::getSoftwareQuality, ScannerReport.Impact::getSeverity) .containsExactlyInAnyOrder( Tuple.tuple(SoftwareQuality.MAINTAINABILITY.name(), Severity.HIGH.name()), Tuple.tuple(SoftwareQuality.RELIABILITY.name(), Severity.MEDIUM.name())); @@ -381,16 +383,17 @@ public class DefaultSensorStorageTest { } @Test - public void store_whenAdhocRuleIsSpecifiedWithOptionalFieldEmpty_shouldWriteAdhocRuleToReport() throws IOException { + public void store_whenAdhocRuleIsSpecifiedWithOptionalFieldEmpty_shouldWriteAdhocRuleWithDefaultImpactsToReport() { underTest.store(new DefaultAdHocRule().ruleId("ruleId").engineId("engineId") .name("name") .description("description")); try (CloseableIterator adhocRuleIt = reportReader.readAdHocRules()) { ScannerReport.AdHocRule adhocRule = adhocRuleIt.next(); - assertThat(adhocRule).extracting(r -> r.getSeverity(), r -> r.getType()) + assertThat(adhocRule).extracting(ScannerReport.AdHocRule::getSeverity, ScannerReport.AdHocRule::getType) .containsExactlyInAnyOrder(Constants.Severity.UNSET_SEVERITY, ScannerReport.IssueType.UNSET); - assertThat(adhocRule.getDefaultImpactsList()).isEmpty(); - assertThat(adhocRule.getCleanCodeAttribute()).isNullOrEmpty(); + assertThat(adhocRule.getDefaultImpactsList()).extracting(ScannerReport.Impact::getSoftwareQuality, ScannerReport.Impact::getSeverity) + .containsExactlyInAnyOrder(Tuple.tuple(SoftwareQuality.MAINTAINABILITY.name(), Severity.MEDIUM.name())); + assertThat(adhocRule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.CONVENTIONAL.name()); } } } diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json new file mode 100644 index 00000000000..6e858fdf87b --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/invalid_report.json @@ -0,0 +1,3 @@ +{ + "some_other_field": "with_some_values" +} diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/report.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/report.json new file mode 100644 index 00000000000..7f797d663b6 --- /dev/null +++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/cct/report.json @@ -0,0 +1,136 @@ +{ + "rules": [ + { + "ruleId": "rule1", + "name": "just_some_rule_name", + "description": "just_some_description", + "engineId": "test", + "cleanCodeAttribute": "FORMATTED", + "impacts": [ + { + "softwareQuality": "MAINTAINABILITY", + "severity": "HIGH" + }, + { + "softwareQuality": "SECURITY", + "severity": "LOW" + } + ], + "issues": [ + { + "effortMinutes": 40, + "primaryLocation": { + "message": "fix the issue here", + "filePath": "file1.js", + "textRange": { + "startLine": 1, + "startColumn": 2, + "endLine": 3, + "endColumn": 4 + } + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file2.js", + "textRange": { + "startLine": 3 + } + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file3.js" + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file3.js" + }, + "secondaryLocations": [ + { + "message": "fix the bug here", + "filePath": "file1.js", + "textRange": { + "startLine": 1 + } + }, + { + "filePath": "file2.js", + "textRange": { + "startLine": 2 + } + } + ] + } + ] + }, + { + "ruleId": "rule2", + "name": "just_some_other_rule_name", + "description": "just_some_description", + "engineId": "test2", + "cleanCodeAttribute": "IDENTIFIABLE", + "impacts": [ + { + "softwareQuality": "RELIABILITY", + "severity": "LOW" + } + ], + "issues": [ + { + "effortMinutes": 40, + "primaryLocation": { + "message": "fix the issue here", + "filePath": "file1.js", + "textRange": { + "startLine": 1, + "startColumn": 2, + "endLine": 3, + "endColumn": 4 + } + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file2.js", + "textRange": { + "startLine": 3 + } + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file3.js" + } + }, + { + "primaryLocation": { + "message": "fix the bug here", + "filePath": "file3.js" + }, + "secondaryLocations": [ + { + "message": "fix the bug here", + "filePath": "file1.js", + "textRange": { + "startLine": 1 + } + }, + { + "filePath": "file2.js", + "textRange": { + "startLine": 2 + } + } + ] + } + ] + } + ] +} 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 deleted file mode 100644 index 990fa88ce16..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json +++ /dev/null @@ -1,31 +0,0 @@ -{ -"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 deleted file mode 100644 index aa2edd26357..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json +++ /dev/null @@ -1,28 +0,0 @@ -{ -"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_message.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json deleted file mode 100644 index c191559faf7..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json +++ /dev/null @@ -1,28 +0,0 @@ -{ -"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": { - "filePath": "file1.js" - } - } -] -} - 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 deleted file mode 100644 index e86cd2b60fd..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json +++ /dev/null @@ -1,25 +0,0 @@ -{ -"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 deleted file mode 100644 index bbd5b85fbf3..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json +++ /dev/null @@ -1,32 +0,0 @@ -{ -"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 deleted file mode 100644 index b69d9fb4bc6..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json +++ /dev/null @@ -1,32 +0,0 @@ -{ -"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 deleted file mode 100644 index 7321073a6bb..00000000000 --- a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json +++ /dev/null @@ -1,32 +0,0 @@ -{ -"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 - } - } - } -] -} - -- 2.39.5