/* * SonarQube * Copyright (C) 2009-2025 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.scanner.externalissue; import java.nio.file.Path; import java.util.HashSet; import java.util.Set; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.rules.RuleType; 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 ISSUE_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; } /** *

Since we are supporting deprecated format, we decide which format it is in order by: *

*

*/ public void validate(ExternalIssueReport report, Path reportPath) { if (report.rules != null && report.issues != null) { Set ruleIds = validateRules(report.rules, reportPath); validateIssuesNewFormat(report.issues, ruleIds, reportPath); } else if (report.rules == null && 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); validateIssuesDeprecatedFormat(report.issues, reportPath); } else { throw new IllegalStateException(String.format("Failed to parse report '%s': invalid report detected.", reportPath)); } } private static void validateIssuesNewFormat(ExternalIssueReport.Issue[] issues, Set ruleIds, Path reportPath) { for (ExternalIssueReport.Issue issue : issues) { mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath); checkRuleExistsInReport(ruleIds, issue, reportPath); checkNoField(issue.severity, SEVERITY, reportPath); checkNoField(issue.type, TYPE, reportPath); validateAlwaysRequiredIssueFields(issue, reportPath); } } private static void validateIssuesDeprecatedFormat(ExternalIssueReport.Issue[] issues, Path reportPath) { for (ExternalIssueReport.Issue issue : issues) { mandatoryField(issue.ruleId, ISSUE_RULE_ID, reportPath); mandatoryField(issue.severity, SEVERITY, reportPath); mandatoryField(issue.type, TYPE, reportPath); mandatoryField(issue.engineId, "engineId", reportPath); validateAlwaysRequiredIssueFields(issue, reportPath); } } private static Set validateRules(ExternalIssueReport.Rule[] rules, Path reportPath) { Set ruleIds = new HashSet<>(); for (ExternalIssueReport.Rule rule : rules) { mandatoryField(rule.id, "id", reportPath); mandatoryField(rule.name, "name", reportPath); mandatoryField(rule.engineId, "engineId", reportPath); validateTypeOrImpacts(reportPath, rule); if (rule.impacts != null) { checkImpactsArray(rule.impacts, reportPath); validateImpactsOnSecurityHostpots(rule.impacts, rule.type, reportPath); } if (rule.type != null) { mandatoryField(rule.severity, SEVERITY, reportPath); } if (rule.severity != null) { mandatoryField(rule.type, TYPE, reportPath); } if (!ruleIds.add(rule.id)) { throw new IllegalStateException(String.format("Failed to parse report '%s': found duplicate rule ID '%s'.", reportPath, rule.id)); } } return ruleIds; } private static void validateImpactsOnSecurityHostpots(ExternalIssueReport.Impact[] impacts, String type, Path reportPath) { if (impacts.length > 0 && RuleType.SECURITY_HOTSPOT.name().equals(type)) { throw new IllegalStateException(String.format("Failed to parse report '%s': impacts should not be provided for rule type 'SECURITY_HOTSPOT'.", reportPath)); } } private static void validateTypeOrImpacts(Path reportPath, ExternalIssueReport.Rule rule) { if (rule.type == null && rule.impacts == null) { throw new IllegalStateException(String.format("Failed to parse report '%s': either type, impacts or both should be provided.", 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 && string.isEmpty())) { throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", reportPath, fieldName)); } } private static void checkImpactsArray(@Nullable Object[] value, Path reportPath) { mandatoryField(value, "impacts", reportPath); if (value.length == 0) { throw new IllegalStateException(String.format("Failed to parse report '%s': mandatory array '%s' not populated.", reportPath, "impacts")); } } private static void checkRuleExistsInReport(Set ruleIds, ExternalIssueReport.Issue issue, Path reportPath) { if (!ruleIds.contains(issue.ruleId)) { throw new IllegalStateException(String.format("Failed to parse report '%s': rule with '%s' not present.", reportPath, issue.ruleId)); } } }