From dddfee0f55914f56d168ae830038bd58fe9e1e8a Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Wed, 9 Aug 2023 16:49:35 +0200 Subject: [PATCH] SONAR-20021 Manage external issues and adhoc rules --- .../issue/AdHocRuleCreatorIT.java | 16 ++++ .../issue/AdHocRuleCreator.java | 34 ++++++- .../projectanalysis/issue/NewAdHocRule.java | 91 ++++++++++++++++++- .../ce/task/projectanalysis/issue/Rule.java | 4 + .../task/projectanalysis/issue/RuleImpl.java | 14 ++- .../issue/RuleRepositoryImpl.java | 10 +- .../issue/TrackerRawInputFactory.java | 46 ++++++++-- .../task/projectanalysis/issue/DumbRule.java | 21 ++++- .../issue/NewAdHocRuleTest.java | 70 ++++++++++++++ .../issue/TrackerRawInputFactoryTest.java | 64 +++++++++++++ .../issue/internal/DefaultExternalIssue.java | 16 ++-- .../internal/DefaultExternalIssueTest.java | 55 ++++++----- .../sonar/scanner/issue/IssuePublisher.java | 29 ++++-- .../scanner/issue/IssuePublisherTest.java | 24 ++++- .../src/main/protobuf/scanner_report.proto | 16 ++-- 15 files changed, 442 insertions(+), 68 deletions(-) diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreatorIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreatorIT.java index df05ba8d268..cc9f807ab70 100644 --- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreatorIT.java +++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreatorIT.java @@ -19,9 +19,12 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import org.assertj.core.groups.Tuple; import org.junit.Test; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; import org.sonar.core.util.SequenceUuidFactory; @@ -35,6 +38,7 @@ import org.sonar.server.rule.index.RuleIndexer; import static org.apache.commons.lang.StringUtils.repeat; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; public class AdHocRuleCreatorIT { @@ -66,6 +70,9 @@ public class AdHocRuleCreatorIT { assertThat(rule.getAdHocDescription()).isNull(); assertThat(rule.getAdHocSeverity()).isNull(); assertThat(rule.getAdHocType()).isNull(); + assertThat(rule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.defaultCleanCodeAttribute()); + assertThat(rule.getDefaultImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactly(Tuple.tuple(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)); } @Test @@ -77,6 +84,9 @@ public class AdHocRuleCreatorIT { .setDescription("A description") .setSeverity(Constants.Severity.BLOCKER) .setType(ScannerReport.IssueType.BUG) + .setCleanCodeAttribute(CleanCodeAttribute.DISTINCT.name()) + .addDefaultImpacts(ScannerReport.Impact.newBuilder().setSoftwareQuality(SoftwareQuality.RELIABILITY.name()) + .setSeverity(org.sonar.api.issue.impact.Severity.LOW.name()).build()) .build()); RuleDto rule = underTest.persistAndIndex(dbSession, addHocRule); @@ -94,6 +104,9 @@ public class AdHocRuleCreatorIT { assertThat(rule.getAdHocDescription()).isEqualTo("A description"); assertThat(rule.getAdHocSeverity()).isEqualTo(Severity.BLOCKER); assertThat(rule.getAdHocType()).isEqualTo(RuleType.BUG.getDbConstant()); + assertThat(rule.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.DISTINCT); + assertThat(rule.getDefaultImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactly(tuple(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.LOW)); } @Test @@ -164,6 +177,9 @@ public class AdHocRuleCreatorIT { .setDescription(rule.getAdHocDescription()) .setSeverity(Constants.Severity.valueOf(rule.getAdHocSeverity())) .setType(ScannerReport.IssueType.forNumber(rule.getAdHocType())) + .addDefaultImpacts(ScannerReport.Impact.newBuilder().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY.name()) + .setSeverity(org.sonar.api.issue.impact.Severity.HIGH.name()).build()) + .setCleanCodeAttribute(CleanCodeAttribute.CLEAR.name()) .build())); assertThat(ruleUpdated).isNotNull(); diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreator.java index 81526cf28c5..773afc8b070 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreator.java @@ -20,13 +20,19 @@ package org.sonar.ce.task.projectanalysis.issue; import com.google.common.base.Preconditions; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.Uuids; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.issue.ImpactDto; import org.sonar.db.rule.RuleDao; import org.sonar.db.rule.RuleDto; import org.sonar.server.rule.index.RuleIndexer; @@ -62,8 +68,8 @@ public class AdHocRuleCreator { RuleDto ruleDtoToUpdate = findOrCreateRuleDto(dbSession, adHoc, dao, now); + boolean changed = false; if (adHoc.hasDetails()) { - boolean changed = false; if (!Objects.equals(ruleDtoToUpdate.getAdHocName(), adHoc.getName())) { ruleDtoToUpdate.setAdHocName(substring(adHoc.getName(), 0, MAX_LENGTH_AD_HOC_NAME)); changed = true; @@ -81,11 +87,25 @@ public class AdHocRuleCreator { ruleDtoToUpdate.setAdHocType(ruleType); changed = true; } - if (changed) { - ruleDtoToUpdate.setUpdatedAt(now); - dao.update(dbSession, ruleDtoToUpdate); - } + } + + if (!Objects.equals(ruleDtoToUpdate.getCleanCodeAttribute(), adHoc.getCleanCodeAttribute())) { + ruleDtoToUpdate.setCleanCodeAttribute(adHoc.getCleanCodeAttribute()); + changed = true; + } + Map currentImpacts = ruleDtoToUpdate.getDefaultImpacts().stream() + .collect(Collectors.toMap(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)); + if (!Objects.equals(currentImpacts, adHoc.getDefaultImpacts())) { + ruleDtoToUpdate.replaceAllDefaultImpacts(adHoc.getDefaultImpacts().entrySet().stream() + .map(i -> createImpactDto(i.getKey(), i.getValue())) + .toList()); + changed = true; + } + + if (changed) { + ruleDtoToUpdate.setUpdatedAt(now); + dao.update(dbSession, ruleDtoToUpdate); } RuleDto ruleDto = dao.selectOrFailByKey(dbSession, adHoc.getKey()); @@ -93,6 +113,10 @@ public class AdHocRuleCreator { return ruleDto; } + private static ImpactDto createImpactDto(SoftwareQuality softwareQuality, Severity severity) { + return new ImpactDto().setUuid(Uuids.create()).setSoftwareQuality(softwareQuality).setSeverity(severity); + } + private RuleDto findOrCreateRuleDto(DbSession dbSession, NewAdHocRule adHoc, RuleDao dao, long now) { Optional existingRuleDtoOpt = dbClient.ruleDao().selectByKey(dbSession, adHoc.getKey()); if (existingRuleDtoOpt.isEmpty()) { diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRule.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRule.java index abada431256..e83059b5acc 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRule.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRule.java @@ -20,11 +20,21 @@ package org.sonar.ce.task.projectanalysis.issue; import com.google.common.base.Preconditions; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.internal.ImpactMapper; import org.sonar.scanner.protocol.Constants; import org.sonar.scanner.protocol.output.ScannerReport; @@ -42,20 +52,83 @@ public class NewAdHocRule { private final RuleType ruleType; private final boolean hasDetails; + private final CleanCodeAttribute cleanCodeAttribute; + + private final Map defaultImpacts = new EnumMap<>(SoftwareQuality.class); + public NewAdHocRule(ScannerReport.AdHocRule ruleFromScannerReport) { Preconditions.checkArgument(isNotBlank(ruleFromScannerReport.getEngineId()), "'engine id' not expected to be null for an ad hoc rule"); Preconditions.checkArgument(isNotBlank(ruleFromScannerReport.getRuleId()), "'rule id' not expected to be null for an ad hoc rule"); Preconditions.checkArgument(isNotBlank(ruleFromScannerReport.getName()), "'name' not expected to be null for an ad hoc rule"); - Preconditions.checkArgument(ruleFromScannerReport.getSeverity() != Constants.Severity.UNSET_SEVERITY , "'severity' not expected to be null for an ad hoc rule"); - Preconditions.checkArgument(ruleFromScannerReport.getType() != ScannerReport.IssueType.UNSET, "'issue type' not expected to be null for an ad hoc rule"); + Preconditions.checkArgument(!ruleFromScannerReport.getDefaultImpactsList().isEmpty() || ruleFromScannerReport.getSeverity() != Constants.Severity.UNSET_SEVERITY, + "'severity' not expected to be null for an ad hoc rule, or impacts should be provided instead"); + Preconditions.checkArgument(!ruleFromScannerReport.getDefaultImpactsList().isEmpty() || ruleFromScannerReport.getType() != ScannerReport.IssueType.UNSET, + "'issue type' not expected to be null for an ad hoc rule, or impacts should be provided instead"); this.key = RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + ruleFromScannerReport.getEngineId(), ruleFromScannerReport.getRuleId()); this.engineId = ruleFromScannerReport.getEngineId(); this.ruleId = ruleFromScannerReport.getRuleId(); this.name = ruleFromScannerReport.getName(); this.description = trimToNull(ruleFromScannerReport.getDescription()); - this.severity = ruleFromScannerReport.getSeverity().name(); - this.ruleType = RuleType.valueOf(ruleFromScannerReport.getType().name()); this.hasDetails = true; + this.cleanCodeAttribute = mapCleanCodeAttribute(trimToNull(ruleFromScannerReport.getCleanCodeAttribute())); + this.ruleType = determineType(ruleFromScannerReport); + this.severity = determineSeverity(ruleFromScannerReport); + this.defaultImpacts.putAll(determineImpacts(ruleFromScannerReport)); + + } + + private Map determineImpacts(ScannerReport.AdHocRule ruleFromScannerReport) { + if (ruleFromScannerReport.getType().equals(ScannerReport.IssueType.SECURITY_HOTSPOT)) { + return Collections.emptyMap(); + } + Map impacts = mapImpacts(ruleFromScannerReport.getDefaultImpactsList()); + if (impacts.isEmpty()) { + return Map.of(ImpactMapper.convertToSoftwareQuality(this.ruleType), + ImpactMapper.convertToImpactSeverity(this.severity)); + } else { + return impacts; + } + } + + private static RuleType determineType(ScannerReport.AdHocRule ruleFromScannerReport) { + if (ruleFromScannerReport.getType() != ScannerReport.IssueType.UNSET) { + return RuleType.valueOf(ruleFromScannerReport.getType().name()); + } + Map impacts = mapImpacts(ruleFromScannerReport.getDefaultImpactsList()); + Map.Entry bestImpactForBackMapping = ImpactMapper.getBestImpactForBackmapping(impacts); + return ImpactMapper.convertToRuleType(bestImpactForBackMapping.getKey()); + } + + private static String determineSeverity(ScannerReport.AdHocRule ruleFromScannerReport) { + if (ruleFromScannerReport.getSeverity() != Constants.Severity.UNSET_SEVERITY) { + return ruleFromScannerReport.getSeverity().name(); + } + Map impacts = mapImpacts(ruleFromScannerReport.getDefaultImpactsList()); + Map.Entry bestImpactForBackMapping = ImpactMapper.getBestImpactForBackmapping(impacts); + return ImpactMapper.convertToDeprecatedSeverity(bestImpactForBackMapping.getValue()); + } + + private static CleanCodeAttribute mapCleanCodeAttribute(@Nullable String cleanCodeAttribute) { + if (cleanCodeAttribute == null) { + return CleanCodeAttribute.defaultCleanCodeAttribute(); + } + return CleanCodeAttribute.valueOf(cleanCodeAttribute); + } + + private static Map mapImpacts(List impacts) { + if (!impacts.isEmpty()) { + return impacts.stream() + .collect(Collectors.toMap(i -> mapSoftwareQuality(i.getSoftwareQuality()), i -> mapImpactSeverity(i.getSeverity()))); + } + return Collections.emptyMap(); + } + + private static Severity mapImpactSeverity(String severity) { + return Severity.valueOf(severity); + } + + private static SoftwareQuality mapSoftwareQuality(String softwareQuality) { + return SoftwareQuality.valueOf(softwareQuality); } public NewAdHocRule(ScannerReport.ExternalIssue fromIssue) { @@ -69,6 +142,8 @@ public class NewAdHocRule { this.severity = null; this.ruleType = null; this.hasDetails = false; + this.cleanCodeAttribute = CleanCodeAttribute.defaultCleanCodeAttribute(); + this.defaultImpacts.put(SoftwareQuality.MAINTAINABILITY, Severity.MEDIUM); } public RuleKey getKey() { @@ -107,6 +182,14 @@ public class NewAdHocRule { return hasDetails; } + public CleanCodeAttribute getCleanCodeAttribute() { + return cleanCodeAttribute; + } + + public Map getDefaultImpacts() { + return defaultImpacts; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java index f59c3a04d56..53e41475825 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java @@ -26,6 +26,7 @@ import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; @@ -69,4 +70,7 @@ public interface Rule { Set getSecurityStandards(); Map getDefaultImpacts(); + + @CheckForNull + CleanCodeAttribute cleanCodeAttribute(); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java index 265e5c51033..42c5d5c26c3 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java @@ -31,9 +31,11 @@ import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction; +import org.sonar.db.issue.ImpactDto; import org.sonar.db.rule.RuleDescriptionSectionDto; import org.sonar.db.rule.RuleDto; @@ -57,6 +59,7 @@ public class RuleImpl implements Rule { private final String severity; private final Set securityStandards; private final Map defaultImpacts; + private final CleanCodeAttribute cleanCodeAttribute; public RuleImpl(RuleDto dto) { this.uuid = dto.getUuid(); @@ -73,8 +76,9 @@ public class RuleImpl implements Rule { this.defaultRuleDescription = getNonNullDefaultRuleDescription(dto); this.severity = Optional.ofNullable(dto.getSeverityString()).orElse(dto.getAdHocSeverity()); this.securityStandards = dto.getSecurityStandards(); - this.defaultImpacts = dto.getDefaultImpacts().stream() - .collect(Collectors.toMap(i -> i.getSoftwareQuality(), i -> i.getSeverity())); + this.defaultImpacts = dto.getDefaultImpacts() + .stream().collect(Collectors.toMap(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)); + this.cleanCodeAttribute = dto.getCleanCodeAttribute(); } private static String getNonNullDefaultRuleDescription(RuleDto dto) { @@ -149,6 +153,12 @@ public class RuleImpl implements Rule { return defaultImpacts; } + @CheckForNull + @Override + public CleanCodeAttribute cleanCodeAttribute() { + return cleanCodeAttribute; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java index aa7ad4e89fc..5d4dd2f96f1 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java @@ -31,6 +31,7 @@ import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.core.util.stream.MoreCollectors; @@ -231,8 +232,13 @@ public class RuleRepositoryImpl implements RuleRepository { @Override public Map getDefaultImpacts() { - //TODO external issues - return Collections.emptyMap(); + return addHocRule.getDefaultImpacts(); + } + + @CheckForNull + @Override + public CleanCodeAttribute cleanCodeAttribute() { + return addHocRule.getCleanCodeAttribute(); } } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java index 17fcfd71b47..5da64cf469c 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.internal.ImpactMapper; import org.sonar.api.utils.Duration; import org.sonar.ce.task.projectanalysis.batch.BatchReportReader; import org.sonar.ce.task.projectanalysis.component.Component; @@ -46,6 +47,7 @@ import org.sonar.core.issue.tracking.LineHashSequence; import org.sonar.core.util.CloseableIterator; import org.sonar.db.protobuf.DbCommons; import org.sonar.db.protobuf.DbIssues; +import org.sonar.scanner.protocol.Constants; import org.sonar.scanner.protocol.Constants.Severity; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.protocol.output.ScannerReport.IssueType; @@ -213,11 +215,17 @@ public class TrackerRawInputFactory { private DefaultIssue toExternalIssue(LineHashSequence lineHashSeq, ScannerReport.ExternalIssue reportExternalIssue, Map adHocRuleMap) { DefaultIssue issue = new DefaultIssue(); - RuleType type = toRuleType(reportExternalIssue.getType()); - init(issue, type == RuleType.SECURITY_HOTSPOT ? STATUS_TO_REVIEW : STATUS_OPEN); - RuleKey ruleKey = RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + reportExternalIssue.getEngineId(), reportExternalIssue.getRuleId()); issue.setRuleKey(ruleKey); + ruleRepository.addOrUpdateAddHocRuleIfNeeded(ruleKey, () -> toAdHocRule(reportExternalIssue, adHocRuleMap.get(issue.ruleKey()))); + + Rule existingRule = ruleRepository.getByKey(ruleKey); + issue.setSeverity(determineDeprecatedSeverity(reportExternalIssue, existingRule)); + issue.setType(determineDeprecatedType(reportExternalIssue, existingRule)); + issue.replaceImpacts(convertImpacts(issue.ruleKey(), reportExternalIssue.getImpactsList())); + + init(issue, issue.type() == RuleType.SECURITY_HOTSPOT ? STATUS_TO_REVIEW : STATUS_OPEN); + if (reportExternalIssue.hasTextRange()) { int startLine = reportExternalIssue.getTextRange().getStartLine(); issue.setLine(startLine); @@ -231,9 +239,6 @@ public class TrackerRawInputFactory { issue.setMessageFormattings(convertMessageFormattings(reportExternalIssue.getMsgFormattingList())); } } - if (reportExternalIssue.getSeverity() != Severity.UNSET_SEVERITY) { - issue.setSeverity(reportExternalIssue.getSeverity().name()); - } issue.setEffort(Duration.create(reportExternalIssue.getEffort() != 0 ? reportExternalIssue.getEffort() : DEFAULT_EXTERNAL_ISSUE_EFFORT)); DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder(); if (reportExternalIssue.hasTextRange()) { @@ -247,9 +252,7 @@ public class TrackerRawInputFactory { } issue.setIsFromExternalRuleEngine(true); issue.setLocations(dbLocationsBuilder.build()); - issue.setType(type); - ruleRepository.addOrUpdateAddHocRuleIfNeeded(ruleKey, () -> toAdHocRule(reportExternalIssue, adHocRuleMap.get(issue.ruleKey()))); return issue; } @@ -330,6 +333,33 @@ public class TrackerRawInputFactory { targetRange.setEndOffset(sourceRange.getEndOffset()); return targetRange; } + + private RuleType determineDeprecatedType(ScannerReport.ExternalIssue reportExternalIssue, Rule rule) { + if (reportExternalIssue.getType() != ScannerReport.IssueType.UNSET) { + return toRuleType(reportExternalIssue.getType()); + } else if (rule.getType() != null) { + return rule.getType(); + } else if (!rule.getDefaultImpacts().isEmpty()) { + SoftwareQuality impactSoftwareQuality = ImpactMapper.getBestImpactForBackmapping(rule.getDefaultImpacts()).getKey(); + return ImpactMapper.convertToRuleType(impactSoftwareQuality); + } else { + throw new IllegalArgumentException("Cannot determine the type for issue of rule %s".formatted(reportExternalIssue.getRuleId())); + } + } + + private static String determineDeprecatedSeverity(ScannerReport.ExternalIssue reportExternalIssue, Rule rule) { + if (reportExternalIssue.getSeverity() != Constants.Severity.UNSET_SEVERITY) { + return reportExternalIssue.getSeverity().name(); + } else if (rule.getSeverity() != null) { + return rule.getSeverity(); + } else if (!rule.getDefaultImpacts().isEmpty()) { + org.sonar.api.issue.impact.Severity impactSeverity = ImpactMapper.getBestImpactForBackmapping(rule.getDefaultImpacts()).getValue(); + return ImpactMapper.convertToDeprecatedSeverity(impactSeverity); + } else { + throw new IllegalArgumentException("Cannot determine the severity for issue of rule %s".formatted(reportExternalIssue.getRuleId())); + } + } + } private Map convertImpacts(RuleKey ruleKey, List overridenImpactsList) { diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java index 1e551f5dbeb..40259efce2d 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java @@ -29,6 +29,7 @@ import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; @@ -48,6 +49,8 @@ public class DumbRule implements Rule { private boolean isAdHoc; private final Set securityStandards = new HashSet<>(); private final Map defaultImpacts = new EnumMap<>(SoftwareQuality.class); + private CleanCodeAttribute cleanCodeAttribute; + private String severity; public DumbRule(RuleKey key) { this.key = key; @@ -108,7 +111,7 @@ public class DumbRule implements Rule { @Override public String getSeverity() { - return null; + return severity; } @Override @@ -121,6 +124,12 @@ public class DumbRule implements Rule { return defaultImpacts; } + @CheckForNull + @Override + public CleanCodeAttribute cleanCodeAttribute() { + return cleanCodeAttribute; + } + @Override public boolean isExternal() { return isExternal; @@ -166,6 +175,11 @@ public class DumbRule implements Rule { return this; } + public DumbRule setSeverity(String severity) { + this.severity = severity; + return this; + } + public DumbRule setPluginKey(String pluginKey) { this.pluginKey = pluginKey; return this; @@ -186,4 +200,9 @@ public class DumbRule implements Rule { return this; } + public DumbRule setCleanCodeAttribute(CleanCodeAttribute cleanCodeAttribute) { + this.cleanCodeAttribute = cleanCodeAttribute; + return this; + } + } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRuleTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRuleTest.java index 3c19c9aeb0f..63e7e4a2c2d 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRuleTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/NewAdHocRuleTest.java @@ -19,7 +19,12 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import java.util.Map; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; +import org.sonar.api.rules.RuleType; +import org.sonar.scanner.protocol.Constants; import org.sonar.scanner.protocol.output.ScannerReport; import static org.assertj.core.api.Assertions.assertThat; @@ -49,4 +54,69 @@ public class NewAdHocRuleTest { .hasSameHashCodeAs(adHocRule2); assertThat(adHocRule1.hashCode()).isNotEqualTo(anotherAdHocRule.hashCode()); } + + @Test + public void constructor_whenAdhocRuleHasProvidedImpact_shouldMapTypeAndSeverityAccordingly() { + NewAdHocRule adHocRule = new NewAdHocRule(ScannerReport.AdHocRule.newBuilder() + .setEngineId("eslint").setRuleId("no-cond-assign").setName("name") + .addDefaultImpacts(ScannerReport.Impact.newBuilder().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY.name()).setSeverity(Severity.LOW.name()).build()) + .build()); + + assertThat(adHocRule.getRuleType()).isEqualTo(RuleType.CODE_SMELL); + assertThat(adHocRule.getSeverity()).isEqualTo(org.sonar.api.batch.rule.Severity.MINOR.name()); + } + + @Test + public void constructor_whenAdhocRuleHasNoProvidedImpact_shouldMapDefaultImpactAccordingly() { + NewAdHocRule adHocRule = new NewAdHocRule(ScannerReport.AdHocRule.newBuilder() + .setEngineId("eslint").setRuleId("no-cond-assign").setName("name") + .setType(ScannerReport.IssueType.CODE_SMELL) + .setSeverity(Constants.Severity.MINOR) + .build()); + + assertThat(adHocRule.getDefaultImpacts()) + .containsExactlyEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, Severity.LOW)); + } + + @Test + public void constructor_whenAdhocRuleHasBothTypeAndSeverityAndProvidedImpact_shouldKeepSeverityAndTypeAndImpacts() { + NewAdHocRule adHocRule = new NewAdHocRule(ScannerReport.AdHocRule.newBuilder() + .setEngineId("eslint").setRuleId("no-cond-assign").setName("name") + .setType(ScannerReport.IssueType.CODE_SMELL) + .setSeverity(Constants.Severity.MINOR) + .addDefaultImpacts(ScannerReport.Impact.newBuilder().setSoftwareQuality(SoftwareQuality.RELIABILITY.name()) + .setSeverity(Severity.HIGH.name()).build()) + .build()); + + assertThat(adHocRule.getDefaultImpacts()) + .containsExactlyEntriesOf(Map.of(SoftwareQuality.RELIABILITY, Severity.HIGH)); + + assertThat(adHocRule.getRuleType()).isEqualTo(RuleType.CODE_SMELL); + assertThat(adHocRule.getSeverity()).isEqualTo(org.sonar.api.batch.rule.Severity.MINOR.name()); + + } + + @Test + public void constructor_whenNoSeverityNorImpactsAreProvided_shouldThrowIllegalArgumentException() { + ScannerReport.AdHocRule scannerReport = ScannerReport.AdHocRule.newBuilder() + .setEngineId("eslint").setRuleId("no-cond-assign").setName("name") + .build(); + + assertThatThrownBy(() -> new NewAdHocRule(scannerReport)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'severity' not expected to be null for an ad hoc rule, or impacts should be provided instead"); + } + + @Test + public void constructor_whenNoTypeNorImpactsAreProvided_shouldThrowIllegalArgumentException() { + ScannerReport.AdHocRule scannerReport = ScannerReport.AdHocRule.newBuilder() + .setSeverity(Constants.Severity.MINOR) + .setEngineId("eslint").setRuleId("no-cond-assign").setName("name") + .build(); + + assertThatThrownBy(() -> new NewAdHocRule(scannerReport)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'issue type' not expected to be null for an ad hoc rule, or impacts should be provided instead"); + } + } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java index a40e033b5f6..431a717a203 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java @@ -28,6 +28,8 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -335,6 +337,9 @@ public class TrackerRawInputFactoryTest { @Test @UseDataProvider("ruleTypeAndStatusByIssueType") public void load_external_issues_from_report(IssueType issueType, RuleType expectedRuleType, String expectedStatus) { + registerRule(RuleKey.of("external_eslint", "S001"), "rule", r -> { + r.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW); + }); ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder() .setTextRange(newTextRange(2)) .setMsg("the message") @@ -345,6 +350,8 @@ public class TrackerRawInputFactoryTest { .setEffort(20L) .setType(issueType) .addFlow(ScannerReport.Flow.newBuilder().setType(FlowType.DATA).addLocation(ScannerReport.IssueLocation.newBuilder().build()).build()) + .addImpacts(ScannerReport.Impact.newBuilder().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY.name()) + .setSeverity(org.sonar.api.issue.impact.Severity.MEDIUM.name()).build()) .build(); reportReader.putExternalIssues(FILE.getReportAttributes().getRef(), asList(reportIssue)); Input input = underTest.create(FILE); @@ -378,6 +385,8 @@ public class TrackerRawInputFactoryTest { assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2)); assertThat(issue.tags()).isEmpty(); assertInitializedExternalIssue(issue, expectedStatus); + + assertThat(issue.impacts()).containsExactlyEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)); } @DataProvider @@ -393,6 +402,8 @@ public class TrackerRawInputFactoryTest { @Test @UseDataProvider("ruleTypeAndStatusByIssueType") public void load_external_issues_from_report_with_default_effort(IssueType issueType, RuleType expectedRuleType, String expectedStatus) { + registerRule(RuleKey.of("external_eslint", "S001"), "rule", r -> { + }); ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder() .setTextRange(newTextRange(2)) .setMsg("the message") @@ -422,6 +433,59 @@ public class TrackerRawInputFactoryTest { assertInitializedExternalIssue(issue, expectedStatus); } + @Test + public void create_whenSeverityAndTypeNotProvided_shouldTakeFromTheRule() { + registerRule(RuleKey.of("external_eslint", "S001"), "rule", r -> { + r.setType(RuleType.BUG); + r.setSeverity(Severity.MAJOR); + }); + ScannerReport.ExternalIssue reportIssue = createIssue(null, null); + reportReader.putExternalIssues(FILE.getReportAttributes().getRef(), asList(reportIssue)); + Input input = underTest.create(FILE); + + Collection issues = input.getIssues(); + assertThat(issues).hasSize(1); + DefaultIssue issue = Iterators.getOnlyElement(issues.iterator()); + + assertThat(issue.type()).isEqualTo(RuleType.BUG); + assertThat(issue.severity()).isEqualTo(Severity.MAJOR); + } + + @Test + public void create_whenSeverityAndTypeNotProvidedByIssueAndRule_shouldTakeFromTheRuleImpact() { + registerRule(RuleKey.of("external_eslint", "S001"), "rule", r -> { + r.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM); + }); + ScannerReport.ExternalIssue reportIssue = createIssue(null, null); + reportReader.putExternalIssues(FILE.getReportAttributes().getRef(), asList(reportIssue)); + Input input = underTest.create(FILE); + + Collection issues = input.getIssues(); + assertThat(issues).hasSize(1); + DefaultIssue issue = Iterators.getOnlyElement(issues.iterator()); + + assertThat(issue.type()).isEqualTo(RuleType.CODE_SMELL); + assertThat(issue.severity()).isEqualTo(Severity.MAJOR); + } + + @NotNull + private ScannerReport.ExternalIssue createIssue(@Nullable RuleType ruleType, @Nullable String severity) { + ScannerReport.ExternalIssue.Builder builder = ScannerReport.ExternalIssue.newBuilder() + .setTextRange(newTextRange(2)) + .setMsg("the message") + .setEngineId("eslint") + .setRuleId("S001"); + if (ruleType != null) { + builder.setType(IssueType.valueOf(ruleType.name())); + } + + if (severity != null) { + builder.setSeverity(Constants.Severity.valueOf(severity)); + } + + return builder.build(); + } + @Test public void excludes_issues_on_inactive_rules() { RuleKey ruleKey = RuleKey.of("java", "S001"); diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java index b8cd113e0b7..43da50257ab 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java @@ -19,7 +19,7 @@ */ package org.sonar.api.batch.sensor.issue.internal; -import java.util.Collections; +import java.util.EnumMap; import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -44,6 +44,8 @@ public class DefaultExternalIssue extends AbstractDefaultIssue impacts = new EnumMap<>(SoftwareQuality.class); + private CleanCodeAttribute cleanCodeAttribute; public DefaultExternalIssue(DefaultInputProject project) { this(project, null); @@ -67,7 +69,8 @@ public class DefaultExternalIssue extends AbstractDefaultIssue impacts() { - return Collections.emptyMap(); + return impacts; } @CheckForNull @Override public CleanCodeAttribute cleanCodeAttribute() { - return null; + return cleanCodeAttribute; } @Override @@ -152,7 +153,8 @@ public class DefaultExternalIssue extends AbstractDefaultIssue issue.save()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Type is mandatory"); + assertThat(issue.type()).isNull(); + assertThat(issue.severity()).isNull(); + assertThat(issue.impacts()).isEmpty(); + + issue.save(); + + verify(storage).store(issue); } @Test @@ -145,21 +171,4 @@ public class DefaultExternalIssueTest { .isInstanceOf(IllegalStateException.class) .hasMessage("External issues must have a message"); } - - @Test - public void fail_to_store_if_no_severity() { - SensorStorage storage = mock(SensorStorage.class); - DefaultExternalIssue issue = new DefaultExternalIssue(project, storage) - .at(new DefaultIssueLocation() - .on(inputFile) - .at(inputFile.selectLine(1)) - .message("Wrong way!")) - .forRule(RuleKey.of("repo", "rule")) - .remediationEffortMinutes(10L) - .type(RuleType.BUG); - - assertThatThrownBy(() -> issue.save()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Severity is mandatory"); - } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java index 3b2f99fecaf..c8f4dce11e0 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java @@ -40,6 +40,8 @@ import org.sonar.api.batch.sensor.issue.MessageFormatting; import org.sonar.api.batch.sensor.issue.NewIssue.FlowType; import org.sonar.api.batch.sensor.issue.internal.DefaultIssueFlow; import org.sonar.api.issue.impact.SoftwareQuality; +import org.sonar.api.rules.CleanCodeAttribute; +import org.sonar.api.rules.RuleType; import org.sonar.scanner.protocol.Constants.Severity; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation; @@ -87,9 +89,9 @@ public class IssuePublisher { private static boolean noSonar(DefaultInputComponent inputComponent, Issue issue) { TextRange textRange = issue.primaryLocation().textRange(); return inputComponent.isFile() - && textRange != null - && ((DefaultInputFile) inputComponent).hasNoSonarAt(textRange.start().line()) - && !StringUtils.containsIgnoreCase(issue.ruleKey().rule(), "nosonar"); + && textRange != null + && ((DefaultInputFile) inputComponent).hasNoSonarAt(textRange.start().line()) + && !StringUtils.containsIgnoreCase(issue.ruleKey().rule(), "nosonar"); } public void initAndAddExternalIssue(ExternalIssue issue) { @@ -161,15 +163,11 @@ public class IssuePublisher { private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) { // primary location of an external issue must have a message String primaryMessage = issue.primaryLocation().message(); - Severity severity = Severity.valueOf(issue.severity().name()); - IssueType issueType = IssueType.valueOf(issue.type().name()); - ScannerReport.ExternalIssue.Builder builder = ScannerReport.ExternalIssue.newBuilder(); ScannerReport.IssueLocation.Builder locationBuilder = IssueLocation.newBuilder(); ScannerReport.TextRange.Builder textRangeBuilder = ScannerReport.TextRange.newBuilder(); + // non-null fields - builder.setSeverity(severity); - builder.setType(issueType); builder.setEngineId(issue.engineId()); builder.setRuleId(issue.ruleId()); builder.setMsg(primaryMessage); @@ -178,6 +176,20 @@ public class IssuePublisher { locationBuilder.addAllMsgFormatting(toProtobufMessageFormattings(issue.primaryLocation().messageFormattings())); locationBuilder.setComponentRef(componentRef); TextRange primaryTextRange = issue.primaryLocation().textRange(); + + //nullable fields + CleanCodeAttribute cleanCodeAttribute = issue.cleanCodeAttribute(); + if (cleanCodeAttribute != null) { + builder.setCleanCodeAttribute(cleanCodeAttribute.name()); + } + org.sonar.api.batch.rule.Severity severity = issue.severity(); + if (severity != null) { + builder.setSeverity(Severity.valueOf(severity.name())); + } + RuleType issueType = issue.type(); + if (issueType != null) { + builder.setType(IssueType.valueOf(issueType.name())); + } if (primaryTextRange != null) { builder.setTextRange(toProtobufTextRange(textRangeBuilder, primaryTextRange)); } @@ -186,6 +198,7 @@ public class IssuePublisher { builder.setEffort(effort); } applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows()); + builder.addAllImpacts(toProtobufImpacts(issue.impacts())); return builder.build(); } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java index c8bc5f78445..c399103480d 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java @@ -42,9 +42,9 @@ import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation; import org.sonar.api.batch.sensor.issue.internal.DefaultMessageFormatting; -import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.protocol.output.ScannerReport.FlowType; @@ -60,7 +60,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.sonar.api.batch.sensor.issue.MessageFormatting.Type.CODE; -import static org.sonar.api.issue.impact.SoftwareQuality.*; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; @RunWith(MockitoJUnitRunner.class) public class IssuePublisherTest { @@ -210,6 +211,25 @@ public class IssuePublisherTest { assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL); } + @Test + public void initAndAddExternalIssue_whenImpactAndCleanCodeAttributeProvided_shouldPopulateReportFields() { + initModuleIssues(); + + DefaultExternalIssue issue = new DefaultExternalIssue(project) + .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo")) + .cleanCodeAttribute(CleanCodeAttribute.CLEAR) + .forRule(JAVA_RULE_KEY) + .addImpact(MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW); + + moduleIssues.initAndAddExternalIssue(issue); + + ArgumentCaptor argument = ArgumentCaptor.forClass(ScannerReport.ExternalIssue.class); + verify(reportPublisher.getWriter()).appendComponentExternalIssue(eq(file.scannerId()), argument.capture()); + assertThat(argument.getValue().getImpactsList()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactly(tuple(MAINTAINABILITY.name(), org.sonar.api.issue.impact.Severity.LOW.name())); + assertThat(argument.getValue().getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.CLEAR.name()); + } + @Test public void use_severity_from_active_rule_if_no_severity_on_issue() { initModuleIssues(); diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index 5de3e742b21..ec1ef8d57b6 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -208,13 +208,15 @@ message ExternalIssue { string engine_id = 1; string rule_id = 2; string msg = 3; - Severity severity = 4; + optional Severity severity = 4; int64 effort = 5; TextRange text_range = 6; repeated Flow flow = 7; - IssueType type = 8; + optional IssueType type = 8; repeated MessageFormatting msgFormatting = 9; - repeated Impact overridenImpacts = 10; + repeated Impact impacts = 10; + optional string cleanCodeAttribute = 11; + } message AdHocRule { @@ -222,8 +224,10 @@ message AdHocRule { string rule_id = 2; string name = 3; string description = 4; - Severity severity = 5; - IssueType type = 6; + optional Severity severity = 5; + optional IssueType type = 6; + optional string cleanCodeAttribute = 7; + repeated Impact defaultImpacts = 8; } enum IssueType { @@ -367,4 +371,4 @@ message AnalysisWarning { message Impact { string software_quality = 1; string severity = 2; -} \ No newline at end of file +} -- 2.39.5