--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
defineRulesXoo(context);
defineRulesXoo2(context);
defineRulesXooExternal(context);
+ defineRulesXooExternalWithCct(context);
}
private static void defineRulesXoo2(Context context) {
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);
assertThat(repo).isNotNull();
assertThat(repo.name()).isEqualTo("XooEngine");
assertThat(repo.language()).isEqualTo("xoo");
- assertThat(repo.rules()).hasSize(1);
+ assertThat(repo.rules()).hasSize(2);
}
@Test
@Override
public Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> 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
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;
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);
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<String> unknownFiles = new LinkedHashSet<>();
private final Set<String> 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));
}
}
}
+ 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;
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 {
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ * 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));
+ }
+ }
+}
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;
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<PropertyDefinition> properties() {
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();
}
+++ /dev/null
-/*
- * 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
- }
- }
-}
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;
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
}
@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();
}
@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();
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();
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;
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
+++ /dev/null
-/*
- * 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'");
- }
-}
}
@Test
- public void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() throws IOException {
+ public void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() {
underTest.store(new DefaultAdHocRule().ruleId("ruleId").engineId("engineId")
.name("name")
try (CloseableIterator<ScannerReport.AdHocRule> 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()));
}
@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<ScannerReport.AdHocRule> 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());
}
}
}
--- /dev/null
+{
+ "some_other_field": "with_some_values"
+}
--- /dev/null
+{
+ "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
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+++ /dev/null
-{
-"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
- }
- }
- }
-]
-}
-
+++ /dev/null
-{
-"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"
- }
- }
-]
-}
-
+++ /dev/null
-{
-"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"
- }
- }
-]
-}
-
+++ /dev/null
-{
-"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"
- }
-]
-}
-
+++ /dev/null
-{
-"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
- }
- }
- }
-]
-}
-
+++ /dev/null
-{
-"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
- }
- }
- }
-]
-}
-
+++ /dev/null
-{
-"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
- }
- }
- }
-]
-}
-