]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23363 handle manual impact flag during project analysis
authorLéo Geoffroy <leo.geoffroy@sonarsource.com>
Wed, 6 Nov 2024 15:20:51 +0000 (16:20 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 11 Nov 2024 20:02:44 +0000 (20:02 +0000)
12 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCacheTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultImpact.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java

index 00a2a117a4287e2a5ee18fb1ad26089515756161..80937105adb30829dec55a439d50c24dda27bf9a 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.issue.Issue;
 import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.FieldDiffs;
@@ -136,6 +137,10 @@ public class IssueLifecycle {
       to.setManualSeverity(true);
       to.setSeverity(from.severity());
     }
+
+    from.getImpacts()
+      .stream().filter(DefaultImpact::manualSeverity)
+      .forEach(i -> to.addImpact(i.softwareQuality(), i.severity(), true));
     to.setCleanCodeAttribute(from.getCleanCodeAttribute());
     copyChangesOfIssueFromOtherBranch(to, from);
   }
@@ -212,7 +217,7 @@ public class IssueLifecycle {
     updater.setPastGap(raw, base.gap(), changeContext);
     updater.setPastEffort(raw, base.effort(), changeContext);
     updater.setCodeVariants(raw, requireNonNull(base.codeVariants()), changeContext);
-    updater.setImpacts(raw, base.impacts(), changeContext);
+    updater.setImpacts(raw, base.getImpacts(), changeContext);
     updater.setCleanCodeAttribute(raw, base.getCleanCodeAttribute(), changeContext);
     updater.setPrioritizedRule(raw, base.isPrioritizedRule(), changeContext);
   }
index 5b2760f17507798c6846ec2e20cb43cd30fe061b..15cdef2b1146887cc02d60b63e9963e8019659a5 100644 (file)
@@ -39,6 +39,7 @@ import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.System2;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.FieldDiffs;
@@ -146,7 +147,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     }
 
     for (IssueCache.Impact impact : next.getImpactsList()) {
-      defaultIssue.addImpact(SoftwareQuality.valueOf(impact.getSoftwareQuality()), Severity.valueOf(impact.getSeverity()));
+      defaultIssue.addImpact(SoftwareQuality.valueOf(impact.getSoftwareQuality()), Severity.valueOf(impact.getSeverity()), impact.getManualSeverity());
     }
     for (IssueCache.FieldDiffs protoFieldDiffs : next.getChangesList()) {
       defaultIssue.addChange(toDefaultIssueChanges(protoFieldDiffs));
@@ -204,11 +205,11 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     builder.setIsNoLongerNewCodeReferenceIssue(defaultIssue.isNoLongerNewCodeReferenceIssue());
     defaultIssue.getAnticipatedTransitionUuid().ifPresent(builder::setAnticipatedTransitionUuid);
 
-
-    for (Map.Entry<SoftwareQuality, Severity> impact : defaultIssue.impacts().entrySet()) {
+    for (DefaultImpact impact : defaultIssue.getImpacts()) {
       builder.addImpacts(IssueCache.Impact.newBuilder()
-        .setSoftwareQuality(impact.getKey().name())
-        .setSeverity(impact.getValue().name())
+        .setSoftwareQuality(impact.softwareQuality().name())
+        .setSeverity(impact.severity().name())
+        .setManualSeverity(impact.manualSeverity())
         .build());
     }
     for (FieldDiffs fieldDiffs : defaultIssue.changes()) {
index 8ef383a86876789eef0f06fefacf092a8996e1e0..6b1e4f5e7098b1fec2011a08734bb8b8e66aae64 100644 (file)
@@ -115,4 +115,5 @@ message Diff {
 message Impact {
   required string software_quality = 1;
   required string severity = 2;
+  required bool manual_severity = 3;
 }
index dcfa537892f6a506393b8a16dff2a7a51567326f..66a1e930f1cf6c2ca93e63152f6ce992f3cafee1 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
 import org.sonar.ce.task.projectanalysis.analysis.Branch;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.FieldDiffs;
@@ -397,7 +398,8 @@ public class IssueLifecycleTest {
       .setRuleDescriptionContextKey("spring")
       .setCleanCodeAttribute(CleanCodeAttribute.IDENTIFIABLE)
       .setCodeVariants(Set.of("foo", "bar"))
-      .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)
+      .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW)
+      .addImpact(SoftwareQuality.RELIABILITY, Severity.HIGH)
       .setCreationDate(parseDate("2015-10-01"))
       .setUpdateDate(parseDate("2015-10-02"))
       .setCloseDate(parseDate("2015-10-03"));
@@ -438,7 +440,7 @@ public class IssueLifecycleTest {
       .setGap(15d)
       .setRuleDescriptionContextKey("hibernate")
       .setCodeVariants(Set.of("donut"))
-      .addImpact(SoftwareQuality.RELIABILITY, Severity.LOW)
+      .addImpact(SoftwareQuality.RELIABILITY, Severity.LOW, true)
       .setEffort(Duration.create(15L))
       .setManualSeverity(false)
       .setLocations(issueLocations)
@@ -469,8 +471,10 @@ public class IssueLifecycleTest {
       .containsOnly(entry("foo", new FieldDiffs.Diff<>("bar", "donut")));
     assertThat(raw.changes().get(1).diffs())
       .containsOnly(entry("file", new FieldDiffs.Diff<>("A", "B")));
-    assertThat(raw.impacts())
-      .containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.HIGH);
+    verifyUpdater(raw, messageFormattings, issueLocations);
+  }
+
+  private void verifyUpdater(DefaultIssue raw, DbIssues.MessageFormattings messageFormattings, DbIssues.Locations issueLocations) {
     verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext);
     verify(updater).setPastLine(raw, 10);
     verify(updater).setRuleDescriptionContextKey(raw, "hibernate");
@@ -479,6 +483,7 @@ public class IssueLifecycleTest {
     verify(updater).setPastEffort(raw, Duration.create(15L), issueChangeContext);
     verify(updater).setPastLocations(raw, issueLocations);
     verify(updater).setCleanCodeAttribute(raw, CleanCodeAttribute.FOCUSED, issueChangeContext);
+    verify(updater).setImpacts(raw, Set.of(new DefaultImpact(SoftwareQuality.RELIABILITY, Severity.LOW, true)), issueChangeContext);
   }
 
   @Test
index 2b9639ca815cf141f05bf1a6d9f5ff838b6a4607..7e6dab430d1020111b0394168fa1ee6f2afa9b02 100644 (file)
 package org.sonar.ce.task.projectanalysis.util.cache;
 
 import java.util.Date;
-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.rule.RuleKey;
 import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -59,13 +59,16 @@ public class ProtobufIssueDiskCacheTest {
   @Test
   public void toDefaultIssue_whenImpactIsSet_shouldSetItInDefaultIssue() {
     IssueCache.Issue issue = prepareIssueWithCompulsoryFields()
-      .addImpacts(toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH))
-      .addImpacts(toImpact(SoftwareQuality.RELIABILITY, Severity.LOW))
+      .addImpacts(toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true))
+      .addImpacts(toImpact(SoftwareQuality.RELIABILITY, Severity.LOW, false))
       .build();
 
     DefaultIssue defaultIssue = ProtobufIssueDiskCache.toDefaultIssue(issue);
 
-    assertThat(defaultIssue.impacts()).containsExactlyInAnyOrderEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, SoftwareQuality.RELIABILITY, Severity.LOW));
+    assertThat(defaultIssue.getImpacts())
+      .containsExactly(
+        new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true),
+        new DefaultImpact(SoftwareQuality.RELIABILITY, Severity.LOW, false));
   }
 
   @Test
@@ -103,15 +106,14 @@ public class ProtobufIssueDiskCacheTest {
   @Test
   public void toProto_whenRuleDescriptionContextKeyIsSet_shouldCopyToIssueProto() {
     DefaultIssue defaultIssue = createDefaultIssueWithMandatoryFields();
-    defaultIssue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH);
-    defaultIssue.addImpact(SoftwareQuality.RELIABILITY, Severity.LOW);
+    defaultIssue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true);
+    defaultIssue.addImpact(SoftwareQuality.RELIABILITY, Severity.LOW, false);
 
     IssueCache.Issue issue = ProtobufIssueDiskCache.toProto(IssueCache.Issue.newBuilder(), defaultIssue);
 
     assertThat(issue.getImpactsList()).containsExactly(
-      toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH),
-      toImpact(SoftwareQuality.RELIABILITY, Severity.LOW)
-    );
+      toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true),
+      toImpact(SoftwareQuality.RELIABILITY, Severity.LOW, false));
   }
 
   @Test
@@ -124,8 +126,8 @@ public class ProtobufIssueDiskCacheTest {
     assertThat(issue.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.FOCUSED.name());
   }
 
-  private IssueCache.Impact toImpact(SoftwareQuality softwareQuality, Severity severity) {
-    return IssueCache.Impact.newBuilder().setSoftwareQuality(softwareQuality.name()).setSeverity(severity.name()).build();
+  private IssueCache.Impact toImpact(SoftwareQuality softwareQuality, Severity severity, boolean manualSeverity) {
+    return IssueCache.Impact.newBuilder().setSoftwareQuality(softwareQuality.name()).setSeverity(severity.name()).setManualSeverity(manualSeverity).build();
   }
 
   private static DefaultIssue createDefaultIssueWithMandatoryFields() {
index 8f78835585b45b3e76c0ca2f0d68ec69dd422f35..537b28db3a2756e44b44a80f61040d63896021aa 100644 (file)
@@ -28,6 +28,7 @@ import java.io.Serializable;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -36,7 +37,6 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
-import org.jetbrains.annotations.NotNull;
 import org.sonar.api.issue.IssueStatus;
 import org.sonar.api.issue.impact.Severity;
 import org.sonar.api.issue.impact.SoftwareQuality;
@@ -112,14 +112,14 @@ public final class IssueDto implements Serializable {
   // populate only when retrieving closed issue for issue tracking
   private String closedChangeData;
 
-  private Set<ImpactDto> impacts = new HashSet<>();
+  private Set<ImpactDto> impacts = new LinkedHashSet<>();
 
-  //non-persisted fields
-  private Set<ImpactDto> ruleDefaultImpacts = new HashSet<>();
+  // non-persisted fields
+  private Set<ImpactDto> ruleDefaultImpacts = new LinkedHashSet<>();
   private CleanCodeAttribute cleanCodeAttribute;
   private CleanCodeAttribute ruleCleanCodeAttribute;
 
-  //issues dependency fields, one-one relationship
+  // issues dependency fields, one-one relationship
   private String cveId;
 
   public IssueDto() {
@@ -130,7 +130,7 @@ public final class IssueDto implements Serializable {
    * On batch side, component keys and uuid are useless
    */
   public static IssueDto toDtoForComputationInsert(DefaultIssue issue, String ruleUuid, long now) {
-    return new IssueDto()
+    IssueDto issueDto = new IssueDto()
       .setKee(issue.key())
       .setType(issue.type())
       .setLine(issue.line())
@@ -162,20 +162,14 @@ public final class IssueDto implements Serializable {
       .setQuickFixAvailable(issue.isQuickFixAvailable())
       .setIsNewCodeReferenceIssue(issue.isNewCodeReferenceIssue())
       .setCodeVariants(issue.codeVariants())
-      .replaceAllImpacts(mapToImpactDto(issue.impacts()))
       .setCleanCodeAttribute(issue.getCleanCodeAttribute())
       // technical dates
       .setCreatedAt(now)
       .setUpdatedAt(now)
       .setCveId(issue.getCveId());
-  }
 
-  @NotNull
-  private static Set<ImpactDto> mapToImpactDto(Map<SoftwareQuality, Severity> impacts) {
-    return impacts.entrySet().stream().map(e -> new ImpactDto()
-        .setSoftwareQuality(e.getKey())
-        .setSeverity(e.getValue()))
-      .collect(Collectors.toSet());
+    issue.getImpacts().forEach(i -> issueDto.addImpact(new ImpactDto(i.softwareQuality(), i.severity(), i.manualSeverity())));
+    return issueDto;
   }
 
   /**
@@ -189,7 +183,7 @@ public final class IssueDto implements Serializable {
 
   public static IssueDto toDtoForUpdate(DefaultIssue issue, long now) {
     // Invariant fields, like key and rule, can't be updated
-    return new IssueDto()
+    IssueDto issueDto = new IssueDto()
       .setKee(issue.key())
       .setType(issue.type())
       .setLine(issue.line())
@@ -220,11 +214,14 @@ public final class IssueDto implements Serializable {
       .setQuickFixAvailable(issue.isQuickFixAvailable())
       .setIsNewCodeReferenceIssue(issue.isNewCodeReferenceIssue())
       .setCodeVariants(issue.codeVariants())
-      .replaceAllImpacts(mapToImpactDto(issue.impacts()))
       .setCleanCodeAttribute(issue.getCleanCodeAttribute())
       .setPrioritizedRule(issue.isPrioritizedRule())
       // technical date
       .setUpdatedAt(now);
+
+    issue.getImpacts().forEach(i -> issueDto.addImpact(new ImpactDto(i.softwareQuality(), i.severity(), i.manualSeverity())));
+    return issueDto;
+
   }
 
   public String getKey() {
@@ -833,13 +830,11 @@ public final class IssueDto implements Serializable {
     return this;
   }
 
-
   public IssueDto setRuleDefaultImpacts(Set<ImpactDto> ruleDefaultImpacts) {
     this.ruleDefaultImpacts = new HashSet<>(ruleDefaultImpacts);
     return this;
   }
 
-
   public IssueDto replaceAllImpacts(Collection<ImpactDto> newImpacts) {
     Set<SoftwareQuality> newSoftwareQuality = newImpacts.stream().map(ImpactDto::getSoftwareQuality).collect(Collectors.toSet());
     if (newSoftwareQuality.size() != newImpacts.size()) {
@@ -928,7 +923,7 @@ public final class IssueDto implements Serializable {
     issue.setIsNewCodeReferenceIssue(isNewCodeReferenceIssue);
     issue.setCodeVariants(getCodeVariants());
     issue.setCleanCodeAttribute(cleanCodeAttribute);
-    impacts.forEach(i -> issue.addImpact(i.getSoftwareQuality(), i.getSeverity()));
+    impacts.forEach(i -> issue.addImpact(i.getSoftwareQuality(), i.getSeverity(), i.isManualSeverity()));
     issue.setCveId(cveId);
     return issue;
   }
index a320ae28522d043c1e3504ed7b5d033c95f421a2..537a2d5e96397c54f91627fe825ac6ad25569f47 100644 (file)
@@ -91,7 +91,8 @@ class IssueDtoTest {
       .setIssueUpdateDate(updatedAt)
       .setIssueCloseDate(closedAt)
       .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
-      .addImpact(new ImpactDto().setSoftwareQuality(MAINTAINABILITY).setSeverity(HIGH));
+      .addImpact(new ImpactDto().setSoftwareQuality(MAINTAINABILITY).setSeverity(HIGH).setManualSeverity(true))
+      .addImpact(new ImpactDto().setSoftwareQuality(RELIABILITY).setSeverity(LOW).setManualSeverity(false));
 
     DefaultIssue expected = new DefaultIssue()
       .setKey("100")
@@ -122,7 +123,8 @@ class IssueDtoTest {
       .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
       .setCodeVariants(Set.of())
       .setTags(Set.of())
-      .addImpact(MAINTAINABILITY, HIGH);
+      .addImpact(MAINTAINABILITY, HIGH, true)
+      .addImpact(RELIABILITY, LOW, false);
 
     DefaultIssue issue = dto.toDefaultIssue();
 
@@ -291,26 +293,26 @@ class IssueDtoTest {
       RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
 
     assertThat(issueDto).extracting(IssueDto::getIssueCreationDate, IssueDto::getIssueCloseDate,
-        IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt, IssueDto::getCreatedAt)
+      IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt, IssueDto::getCreatedAt)
       .containsExactly(dateNow, dateNow, dateNow, dateNow.getTime(), now, now);
 
     assertThat(issueDto).extracting(IssueDto::getLine, IssueDto::getMessage,
-        IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
+      IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
       .containsExactly(1, "message", 1.0, 1L, Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED, "BLOCKER");
 
     assertThat(issueDto).extracting(IssueDto::getTags, IssueDto::getCodeVariants, IssueDto::getAuthorLogin)
       .containsExactly(Set.of("todo"), Set.of("variant1", "variant2"), "admin");
 
     assertThat(issueDto).extracting(IssueDto::isManualSeverity, IssueDto::getChecksum, IssueDto::getAssigneeUuid,
-        IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
-        IssueDto::getProjectUuid, IssueDto::getProjectKey, IssueDto::getRuleUuid)
+      IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
+      IssueDto::getProjectUuid, IssueDto::getProjectKey, IssueDto::getRuleUuid)
       .containsExactly(true, "123", "123", true, "123", "componentKey", "123", "projectKey", "ruleUuid");
 
     assertThat(issueDto.isQuickFixAvailable()).isTrue();
     assertThat(issueDto.isNewCodeReferenceIssue()).isTrue();
     assertThat(issueDto.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
-    assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)
-      .containsExactlyInAnyOrder(tuple(MAINTAINABILITY, HIGH), tuple(RELIABILITY, LOW));
+    assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity, ImpactDto::isManualSeverity)
+      .containsExactlyInAnyOrder(tuple(MAINTAINABILITY, HIGH, true), tuple(RELIABILITY, LOW, false));
   }
 
   @Test
@@ -325,25 +327,25 @@ class IssueDtoTest {
       RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
 
     assertThat(issueDto).extracting(IssueDto::getIssueCreationDate, IssueDto::getIssueCloseDate,
-        IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt)
+      IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt)
       .containsExactly(dateNow, dateNow, dateNow, dateNow.getTime(), now);
 
     assertThat(issueDto).extracting(IssueDto::getLine, IssueDto::getMessage,
-        IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
+      IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
       .containsExactly(1, "message", 1.0, 1L, Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED, "BLOCKER");
 
     assertThat(issueDto).extracting(IssueDto::getTags, IssueDto::getCodeVariants, IssueDto::getAuthorLogin)
       .containsExactly(Set.of("todo"), Set.of("variant1", "variant2"), "admin");
 
     assertThat(issueDto).extracting(IssueDto::isManualSeverity, IssueDto::getChecksum, IssueDto::getAssigneeUuid,
-        IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey, IssueDto::getProjectUuid, IssueDto::getProjectKey)
+      IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey, IssueDto::getProjectUuid, IssueDto::getProjectKey)
       .containsExactly(true, "123", "123", true, "123", "componentKey", "123", "projectKey");
 
     assertThat(issueDto.isQuickFixAvailable()).isTrue();
     assertThat(issueDto.isNewCodeReferenceIssue()).isTrue();
     assertThat(issueDto.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
-    assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity)
-      .containsExactlyInAnyOrder(tuple(MAINTAINABILITY, HIGH), tuple(RELIABILITY, LOW));
+    assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity, ImpactDto::isManualSeverity)
+      .containsExactlyInAnyOrder(tuple(MAINTAINABILITY, HIGH, true), tuple(RELIABILITY, LOW, false));
   }
 
   @Test
@@ -399,8 +401,8 @@ class IssueDtoTest {
       .setIsNewCodeReferenceIssue(true)
       .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
       .setCodeVariants(List.of("variant1", "variant2"))
-      .addImpact(MAINTAINABILITY, HIGH)
-      .addImpact(RELIABILITY, LOW);
+      .addImpact(MAINTAINABILITY, HIGH, true)
+      .addImpact(RELIABILITY, LOW, false);
     return defaultIssue;
   }
 
index 7e9e63543af9d8773904e48a2abf673b0bfe7dad..482fc0791a0a7d7fb1767460386ea5e6cb1791b4 100644 (file)
@@ -23,22 +23,19 @@ import com.google.common.base.Joiner;
 import java.time.temporal.ChronoUnit;
 import java.util.Collection;
 import java.util.Date;
-import java.util.EnumMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.issue.IssueStatus;
-import org.sonar.api.issue.impact.Severity;
-import org.sonar.api.issue.impact.SoftwareQuality;
 import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.server.rule.RuleTagFormat;
 import org.sonar.api.utils.Duration;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.IssueChangeContext;
@@ -262,7 +259,7 @@ public class IssueFieldsSetter {
 
   public boolean setIssueStatus(DefaultIssue issue, @Nullable IssueStatus previousIssueStatus, @Nullable IssueStatus newIssueStatus, IssueChangeContext context) {
     if (!Objects.equals(newIssueStatus, previousIssueStatus)) {
-      //Currently, issue status is not persisted in database, but is considered as an issue change
+      // Currently, issue status is not persisted in database, but is considered as an issue change
       issue.setFieldChange(context, ISSUE_STATUS, previousIssueStatus, issue.issueStatus());
       return true;
     }
@@ -346,8 +343,8 @@ public class IssueFieldsSetter {
 
     for (int i = 0; i < l1c.getMessageFormattingCount(); i++) {
       if (l1c.getMessageFormatting(i).getStart() != l2.getMessageFormatting(i).getStart()
-          || l1c.getMessageFormatting(i).getEnd() != l2.getMessageFormatting(i).getEnd()
-          || l1c.getMessageFormatting(i).getType() != l2.getMessageFormatting(i).getType()) {
+        || l1c.getMessageFormatting(i).getEnd() != l2.getMessageFormatting(i).getEnd()
+        || l1c.getMessageFormatting(i).getType() != l2.getMessageFormatting(i).getType()) {
         return false;
       }
     }
@@ -372,7 +369,7 @@ public class IssueFieldsSetter {
   public void setPrioritizedRule(DefaultIssue issue, boolean prioritizedRule, IssueChangeContext context) {
     if (!Objects.equals(prioritizedRule, issue.isPrioritizedRule())) {
       issue.setPrioritizedRule(prioritizedRule);
-      if (!issue.isNew()){
+      if (!issue.isNew()) {
         issue.setUpdateDate(context.date());
         issue.setChanged(true);
       }
@@ -463,14 +460,17 @@ public class IssueFieldsSetter {
     return false;
   }
 
-  public boolean setImpacts(DefaultIssue issue, Map<SoftwareQuality, Severity> previousImpacts, IssueChangeContext context) {
-    Map<SoftwareQuality, Severity> currentImpacts = new EnumMap<>(issue.impacts());
-    if (!previousImpacts.equals(currentImpacts)) {
-      issue.replaceImpacts(currentImpacts);
+  public boolean setImpacts(DefaultIssue issue, Set<DefaultImpact> previousImpacts, IssueChangeContext context) {
+    previousImpacts
+      .stream().filter(DefaultImpact::manualSeverity)
+      .forEach(i -> issue.addImpact(i.softwareQuality(), i.severity(), true));
+
+    if (!previousImpacts.equals(issue.getImpacts())) {
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
       return true;
     }
+
     return false;
   }
 
index 85e08425108b2a4a33a16f42c54fa8d08a18b777..6d80dba6d828dff0b4c9348c6817c336dc8b5ebd 100644 (file)
@@ -24,7 +24,6 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import org.apache.commons.lang3.time.DateUtils;
@@ -36,6 +35,7 @@ import org.sonar.api.issue.impact.SoftwareQuality;
 import org.sonar.api.rules.CleanCodeAttribute;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
+import org.sonar.core.issue.DefaultImpact;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.FieldDiffs;
 import org.sonar.core.issue.IssueChangeContext;
@@ -577,13 +577,52 @@ class IssueFieldsSetterTest {
 
   @Test
   void setImpacts_whenImpactAdded_shouldBeUpdated() {
-    Map<SoftwareQuality, Severity> currentImpacts = Map.of(SoftwareQuality.RELIABILITY, Severity.LOW);
-    Map<SoftwareQuality, Severity> newImpacts = Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH);
+    Set<DefaultImpact> currentImpacts = Set.of(new DefaultImpact(SoftwareQuality.RELIABILITY, Severity.LOW, false));
+    Set<DefaultImpact> newImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, false));
 
-    issue.replaceImpacts(newImpacts);
+    newImpacts
+      .forEach(e -> issue.addImpact(e.softwareQuality(), e.severity(), e.manualSeverity()));
     boolean updated = underTest.setImpacts(issue, currentImpacts, context);
     assertThat(updated).isTrue();
-    assertThat(issue.impacts()).isEqualTo(newImpacts);
+    assertThat(issue.getImpacts()).isEqualTo(newImpacts);
+  }
+
+  @Test
+  void setImpacts_whenImpactExists_shouldNotBeUpdated() {
+    Set<DefaultImpact> currentImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, false));
+    Set<DefaultImpact> newImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, false));
+
+    newImpacts
+      .forEach(e -> issue.addImpact(e.softwareQuality(), e.severity(), e.manualSeverity()));
+    boolean updated = underTest.setImpacts(issue, currentImpacts, context);
+    assertThat(updated).isFalse();
+    assertThat(issue.getImpacts()).isEqualTo(newImpacts);
+  }
+
+  @Test
+  void setImpacts_whenImpactExistsWithManualSeverity_shouldNotBeUpdated() {
+    Set<DefaultImpact> currentImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true));
+    Set<DefaultImpact> newImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, false));
+
+    newImpacts
+      .forEach(e -> issue.addImpact(e.softwareQuality(), e.severity(), e.manualSeverity()));
+    boolean updated = underTest.setImpacts(issue, currentImpacts, context);
+    assertThat(updated).isFalse();
+    assertThat(issue.getImpacts()).isEqualTo(currentImpacts);
+  }
+
+  @Test
+  void setImpacts_whenImpactExistsWithManualSeverityAndNewImpact_shouldBeUpdated() {
+    Set<DefaultImpact> currentImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true));
+    Set<DefaultImpact> newImpacts = Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, false),
+      new DefaultImpact(SoftwareQuality.RELIABILITY, Severity.HIGH, false));
+
+    newImpacts
+      .forEach(e -> issue.addImpact(e.softwareQuality(), e.severity(), e.manualSeverity()));
+    boolean updated = underTest.setImpacts(issue, currentImpacts, context);
+    assertThat(updated).isTrue();
+    assertThat(issue.getImpacts()).isEqualTo(Set.of(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, true),
+      new DefaultImpact(SoftwareQuality.RELIABILITY, Severity.HIGH, false)));
   }
 
   @Test
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultImpact.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultImpact.java
new file mode 100644 (file)
index 0000000..36ea21e
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.core.issue;
+
+import org.sonar.api.issue.impact.Severity;
+import org.sonar.api.issue.impact.SoftwareQuality;
+
+public record DefaultImpact(SoftwareQuality softwareQuality, Severity severity, boolean manualSeverity) {
+}
index b8c46a299af41bcddd91be722847ff5fdd220404..dc4fb271e3d9d31989f075dca1304e1c528d93b6 100644 (file)
@@ -28,14 +28,15 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
-import java.util.EnumMap;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
@@ -137,7 +138,7 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
 
   private String anticipatedTransitionUuid = null;
 
-  private Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> impacts = new EnumMap<>(SoftwareQuality.class);
+  private final Map<SoftwareQuality, DefaultImpact> impacts = new LinkedHashMap<>();
   private CleanCodeAttribute cleanCodeAttribute = null;
 
   private String cveId = null;
@@ -159,17 +160,26 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
 
   @Override
   public Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> impacts() {
-    return impacts;
+    return impacts.values().stream().collect(Collectors.toMap(DefaultImpact::softwareQuality, DefaultImpact::severity));
+  }
+
+  public Set<DefaultImpact> getImpacts() {
+    return new LinkedHashSet<>(this.impacts.values());
   }
 
   public DefaultIssue replaceImpacts(Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> impacts) {
     this.impacts.clear();
-    this.impacts.putAll(impacts);
+    this.impacts.putAll(impacts.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new DefaultImpact(e.getKey(), e.getValue(), false))));
     return this;
   }
 
   public DefaultIssue addImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) {
-    impacts.put(softwareQuality, severity);
+    impacts.put(softwareQuality, new DefaultImpact(softwareQuality, severity, false));
+    return this;
+  }
+
+  public DefaultIssue addImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, boolean manualSeverity) {
+    impacts.put(softwareQuality, new DefaultImpact(softwareQuality, severity, manualSeverity));
     return this;
   }
 
index a50efdc2ef40415e7f5824703889acc5bfd72305..ad935a1f7adf64b5343b421ba09ff514d1d76586 100644 (file)
@@ -323,6 +323,14 @@ class DefaultIssueTest {
     assertThat(issue.impacts()).containsExactlyEntriesOf(Map.of(SoftwareQuality.SECURITY, Severity.LOW));
   }
 
+  @Test
+  void addImpact_shouldReplaceExistingSoftwareQuality() {
+    issue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH);
+    issue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, true);
+    assertThat(issue.getImpacts())
+      .containsExactly(new DefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.LOW, true));
+  }
+
   @Test
   void prioritizedRule_shouldHaveCorrectDefaultValue() {
     assertThat(issue.isPrioritizedRule()).isFalse();