3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.scanner.externalissue;
22 import java.nio.file.Path;
23 import java.util.HashSet;
25 import javax.annotation.Nullable;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28 import org.sonar.api.scanner.ScannerSide;
29 import org.sonar.core.documentation.DocumentationLinkGenerator;
32 public class ExternalIssueReportValidator {
33 private static final Logger LOGGER = LoggerFactory.getLogger(ExternalIssueReportValidator.class);
34 private static final String ISSUE_RULE_ID = "ruleId";
35 private static final String SEVERITY = "severity";
36 private static final String TYPE = "type";
37 private static final String DOCUMENTATION_SUFFIX = "/analyzing-source-code/importing-external-issues/generic-issue-import-format/";
38 private final DocumentationLinkGenerator documentationLinkGenerator;
40 ExternalIssueReportValidator(DocumentationLinkGenerator documentationLinkGenerator) {
41 this.documentationLinkGenerator = documentationLinkGenerator;
45 * <p>Since we are supporting deprecated format, we decide which format it is in order by:
47 * <li>if both 'rules' and 'issues' fields are present, we assume it is CCT format</li>
48 * <li>if only 'issues' field is present, we assume it is deprecated format</li>
49 * <li>otherwise we throw exception as an invalid report was detected</li>
53 public void validate(ExternalIssueReport report, Path reportPath) {
54 if (report.rules != null && report.issues != null) {
55 Set<String> ruleIds = validateRules(report.rules, reportPath);
56 validateIssuesCctFormat(report.issues, ruleIds, reportPath);
57 } else if (report.rules == null && report.issues != null) {
58 String documentationLink = documentationLinkGenerator.getDocumentationLink(DOCUMENTATION_SUFFIX);
59 LOGGER.warn("External issues were imported with a deprecated format which will be removed soon. " +
60 "Please switch to the newest format to fully benefit from Clean Code: {}", documentationLink);
61 validateIssuesDeprecatedFormat(report.issues, reportPath);
63 throw new IllegalStateException(String.format("Failed to parse report '%s': invalid report detected.", reportPath));
67 private static void validateIssuesCctFormat(ExternalIssueReport.Issue[] issues, Set<String> ruleIds, Path reportPath) {
68 for (ExternalIssueReport.Issue issue : issues) {
69 mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath);
70 checkRuleExistsInReport(ruleIds, issue, reportPath);
71 checkNoField(issue.severity, SEVERITY, reportPath);
72 checkNoField(issue.type, TYPE, reportPath);
73 validateAlwaysRequiredIssueFields(issue, reportPath);
77 private static void validateIssuesDeprecatedFormat(ExternalIssueReport.Issue[] issues, Path reportPath) {
78 for (ExternalIssueReport.Issue issue : issues) {
79 mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath);
80 mandatoryField(issue.severity, SEVERITY, reportPath);
81 mandatoryField(issue.type, TYPE, reportPath);
82 mandatoryField(issue.engineId, "engineId", reportPath);
83 validateAlwaysRequiredIssueFields(issue, reportPath);
87 private static Set<String> validateRules(ExternalIssueReport.Rule[] rules, Path reportPath) {
88 Set<String> ruleIds = new HashSet<>();
89 for (ExternalIssueReport.Rule rule : rules) {
90 mandatoryField(rule.id, "id", reportPath);
91 mandatoryField(rule.name, "name", reportPath);
92 mandatoryField(rule.engineId, "engineId", reportPath);
93 mandatoryField(rule.cleanCodeAttribute, "cleanCodeAttribute", reportPath);
94 checkImpactsArray(rule.impacts, reportPath);
96 if (!ruleIds.add(rule.id)) {
97 throw new IllegalStateException(String.format("Failed to parse report '%s': found duplicate rule ID '%s'.", reportPath, rule.id));
104 private static void checkNoField(@Nullable Object value, String fieldName, Path reportPath) {
106 throw new IllegalStateException(String.format("Deprecated '%s' field found in the following report: '%s'.", fieldName, reportPath));
110 private static void validateAlwaysRequiredIssueFields(ExternalIssueReport.Issue issue, Path reportPath) {
111 mandatoryField(issue.primaryLocation, "primaryLocation", reportPath);
112 mandatoryFieldPrimaryLocation(issue.primaryLocation.filePath, "filePath", reportPath);
113 mandatoryFieldPrimaryLocation(issue.primaryLocation.message, "message", reportPath);
115 if (issue.primaryLocation.textRange != null) {
116 mandatoryFieldPrimaryLocation(issue.primaryLocation.textRange.startLine, "startLine of the text range", reportPath);
119 if (issue.secondaryLocations != null) {
120 for (ExternalIssueReport.Location l : issue.secondaryLocations) {
121 mandatoryFieldSecondaryLocation(l.filePath, "filePath", reportPath);
122 mandatoryFieldSecondaryLocation(l.textRange, "textRange", reportPath);
123 mandatoryFieldSecondaryLocation(l.textRange.startLine, "startLine of the text range", reportPath);
128 private static void mandatoryFieldPrimaryLocation(@Nullable Object value, String fieldName, Path reportPath) {
130 throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in the primary location of" +
131 " the issue.", reportPath, fieldName));
135 private static void mandatoryFieldSecondaryLocation(@Nullable Object value, String fieldName, Path reportPath) {
137 throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in a secondary location of" +
138 " the issue.", reportPath, fieldName));
142 private static void mandatoryField(@Nullable Object value, String fieldName, Path reportPath) {
143 if (value == null || (value instanceof String string && string.isEmpty())) {
144 throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", reportPath, fieldName));
148 private static void checkImpactsArray(@Nullable Object[] value, Path reportPath) {
149 mandatoryField(value, "impacts", reportPath);
150 if (value.length == 0) {
151 throw new IllegalStateException(String.format("Failed to parse report '%s': mandatory array '%s' not populated.", reportPath,
156 private static void checkRuleExistsInReport(Set<String> ruleIds, ExternalIssueReport.Issue issue, Path reportPath) {
157 if (!ruleIds.contains(issue.ruleId)) {
158 throw new IllegalStateException(String.format("Failed to parse report '%s': rule with '%s' not present.", reportPath, issue.ruleId));