]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19372 Add a new issue visitor in order to apply anticipated transitions to...
authorMatteo Mara <matteo.mara@sonarsource.com>
Wed, 19 Jul 2023 13:32:14 +0000 (15:32 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 2 Aug 2023 20:03:03 +0000 (20:03 +0000)
12 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepository.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepositoryImpl.java [new file with mode: 0644]
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/issue/TransitionIssuesToAnticipatedStatesVisitor.java [new file with mode: 0644]
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/issue/TransitionIssuesToAnticipatedStatesVisitorTest.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueStorage.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/main/java/org/sonar/core/issue/tracking/AnticipatedTransitionTracker.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java
sonar-core/src/test/java/org/sonar/core/issue/tracking/AnticipatedTransitionTrackerTest.java [new file with mode: 0644]

index fdaf5a6ab8eb5ab84c43ccf70e6cfe517d5e213e..c39754d47f8c8f07b40309930335aa209ff8fa06 100644 (file)
@@ -51,6 +51,7 @@ import org.sonar.ce.task.projectanalysis.filemove.MutableMovedFilesRepositoryImp
 import org.sonar.ce.task.projectanalysis.filemove.ScoreMatrixDumperImpl;
 import org.sonar.ce.task.projectanalysis.filemove.SourceSimilarityImpl;
 import org.sonar.ce.task.projectanalysis.filesystem.ComputationTempFolderProvider;
+import org.sonar.ce.task.projectanalysis.issue.AnticipatedTransitionRepositoryImpl;
 import org.sonar.ce.task.projectanalysis.issue.BaseIssuesLoader;
 import org.sonar.ce.task.projectanalysis.issue.CloseIssuesOnRemovedComponentsVisitor;
 import org.sonar.ce.task.projectanalysis.issue.ClosedIssuesInputFactory;
@@ -94,6 +95,7 @@ import org.sonar.ce.task.projectanalysis.issue.TrackerRawInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerReferenceBranchInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerSourceBranchInputFactory;
 import org.sonar.ce.task.projectanalysis.issue.TrackerTargetBranchInputFactory;
+import org.sonar.ce.task.projectanalysis.issue.TransitionIssuesToAnticipatedStatesVisitor;
 import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
 import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter;
 import org.sonar.ce.task.projectanalysis.language.LanguageRepositoryImpl;
@@ -256,6 +258,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       // debt)
       RuleTagsCopier.class,
       IssueCreationDateCalculator.class,
+      TransitionIssuesToAnticipatedStatesVisitor.class,
       ComputeLocationHashesVisitor.class,
       DebtCalculator.class,
       EffortAggregator.class,
@@ -326,7 +329,10 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       WebhookPostTask.class,
 
       // notifications
-      NotificationFactory.class);
+      NotificationFactory.class,
+
+      // anticipated transitions
+      AnticipatedTransitionRepositoryImpl.class);
   }
 
 }
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepository.java
new file mode 100644 (file)
index 0000000..88a8d2d
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.issue;
+
+import java.util.Collection;
+import org.sonar.core.issue.AnticipatedTransition;
+
+public interface AnticipatedTransitionRepository {
+  Collection<AnticipatedTransition> getAnticipatedTransitionByProjectUuid(String projectKey, String filePath);
+
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepositoryImpl.java
new file mode 100644 (file)
index 0000000..d1c23d9
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.issue;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.AnticipatedTransition;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.entity.EntityDto;
+import org.sonar.db.issue.AnticipatedTransitionDto;
+
+public class AnticipatedTransitionRepositoryImpl implements AnticipatedTransitionRepository {
+
+  private final DbClient dbClient;
+
+  public AnticipatedTransitionRepositoryImpl(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  public Collection<AnticipatedTransition> getAnticipatedTransitionByProjectUuid(String componentUuid, String filePath) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      EntityDto entityDto = dbClient.entityDao().selectByComponentUuid(dbSession, componentUuid).orElseThrow(IllegalStateException::new);
+      List<AnticipatedTransitionDto> anticipatedTransitionDtos = dbClient.anticipatedTransitionDao().selectByProjectUuid(dbSession, entityDto.getUuid());
+      return getAnticipatedTransitions(anticipatedTransitionDtos);
+    }
+  }
+
+  private Collection<AnticipatedTransition> getAnticipatedTransitions(List<AnticipatedTransitionDto> anticipatedTransitionDtos) {
+    return anticipatedTransitionDtos
+      .stream()
+      .map(this::getAnticipatedTransition)
+      .toList();
+  }
+
+  private AnticipatedTransition getAnticipatedTransition(AnticipatedTransitionDto transitionDto) {
+    return new AnticipatedTransition(
+      transitionDto.getProjectUuid(),
+      "branch",
+      transitionDto.getUserUuid(),
+      RuleKey.parse(transitionDto.getRuleKey()),
+      transitionDto.getMessage(),
+      "filepath",
+      transitionDto.getLine(),
+      transitionDto.getLineHash(),
+      transitionDto.getTransition(),
+      transitionDto.getComment()
+    );
+  }
+
+}
index 9d4a6721dc545afa0b4e2a83d9cebad74ba84491..517dffec2538cf81c8e56165cd8bc538f243338f 100644 (file)
@@ -24,6 +24,7 @@ import com.google.common.base.Preconditions;
 import java.util.Date;
 import java.util.Optional;
 import javax.inject.Inject;
+import org.jetbrains.annotations.NotNull;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rules.RuleType;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
@@ -208,6 +209,22 @@ public class IssueLifecycle {
     workflow.doAutomaticTransition(issue, changeContext);
   }
 
+  public void doManualTransition(DefaultIssue issue, String transitionKey, String userUuid) {
+    workflow.doManualTransition(issue, transitionKey, getIssueChangeContextWithUser(userUuid));
+  }
+
+  public void addComment(DefaultIssue issue, String comment, String userUuid) {
+    updater.addComment(issue, comment, getIssueChangeContextWithUser(userUuid));
+  }
+
+  @NotNull
+  private IssueChangeContext getIssueChangeContextWithUser(String userUuid) {
+    return IssueChangeContext.newBuilder()
+      .setDate(changeContext.date())
+      .setWebhookSource(changeContext.getWebhookSource())
+      .setUserUuid(userUuid).build();
+  }
+
   private void copyFields(DefaultIssue toIssue, DefaultIssue fromIssue) {
     toIssue.setType(fromIssue.type());
     toIssue.setCreationDate(fromIssue.creationDate());
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitor.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitor.java
new file mode 100644 (file)
index 0000000..c7a3e53
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.issue;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.core.issue.AnticipatedTransition;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.tracking.AnticipatedTransitionTracker;
+import org.sonar.core.issue.tracking.Tracking;
+
+import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
+
+/**
+ * Updates issues if an anticipated transition from SonarLint is found
+ */
+public class TransitionIssuesToAnticipatedStatesVisitor extends IssueVisitor {
+
+  private Collection<AnticipatedTransition> anticipatedTransitions;
+  private final AnticipatedTransitionTracker<DefaultIssue, AnticipatedTransition> tracker = new AnticipatedTransitionTracker<>();
+  private final IssueLifecycle issueLifecycle;
+  
+  private final AnticipatedTransitionRepository anticipatedTransitionRepository;
+
+  public TransitionIssuesToAnticipatedStatesVisitor(AnticipatedTransitionRepository anticipatedTransitionRepository, IssueLifecycle issueLifecycle) {
+    this.anticipatedTransitionRepository = anticipatedTransitionRepository;
+    this.issueLifecycle = issueLifecycle;
+  }
+
+  @Override
+  public void beforeComponent(Component component) {
+    if (FILE.equals(component.getType())) {
+      anticipatedTransitions = anticipatedTransitionRepository.getAnticipatedTransitionByProjectUuid(component.getUuid(), component.getName());
+    }
+  }
+
+  @Override
+  public void onIssue(Component component, DefaultIssue issue) {
+    if (issue.isNew()) {
+      Tracking<DefaultIssue, AnticipatedTransition> tracking = tracker.track(List.of(issue), anticipatedTransitions);
+      Map<DefaultIssue, AnticipatedTransition> matchedRaws = tracking.getMatchedRaws();
+      if (matchedRaws.containsKey(issue)) {
+        performAnticipatedTransition(issue, matchedRaws.get(issue));
+      }
+    }
+  }
+
+  @Override
+  public void afterComponent(Component component) {
+    anticipatedTransitions.clear();
+  }
+
+  private void performAnticipatedTransition(DefaultIssue issue, AnticipatedTransition anticipatedTransition) {
+    issue.setBeingClosed(true);
+    issue.setAnticipatedTransitions(true);
+    issueLifecycle.doManualTransition(issue, anticipatedTransition.getTransition(), anticipatedTransition.getUserUuid());
+    issueLifecycle.addComment(issue, anticipatedTransition.getComment(), anticipatedTransition.getUserUuid());
+  }
+
+}
index 78a749c6d7c87fcc07215d9e29d30c82ac612243..e995540494e23a34b4b1a0824360cbc3e8a82b2b 100644 (file)
@@ -349,6 +349,35 @@ public class IssueLifecycleTest {
     verify(workflow).doAutomaticTransition(issue, issueChangeContext);
   }
 
+  @Test
+  public void doManualTransition() {
+    DefaultIssue issue = new DefaultIssue();
+    String transitionKey = "transitionKey";
+    String userUuid = "userUuid";
+
+    underTest.doManualTransition(issue, transitionKey, userUuid);
+
+    verify(workflow).doManualTransition(issue, transitionKey, getIssueChangeContextWithUser(userUuid));
+  }
+
+  @Test
+  public void addComment() {
+    DefaultIssue issue = new DefaultIssue();
+    String comment = "comment";
+    String userUuid = "userUuid";
+
+    underTest.addComment(issue, comment, userUuid);
+
+    verify(updater).addComment(issue, comment, getIssueChangeContextWithUser(userUuid));
+  }
+
+  private IssueChangeContext getIssueChangeContextWithUser(String userUuid) {
+    return IssueChangeContext.newBuilder()
+      .setDate(issueChangeContext.date())
+      .setWebhookSource(issueChangeContext.getWebhookSource())
+      .setUserUuid(userUuid).build();
+  }
+
   @Test
   public void mergeExistingOpenIssue() {
     DefaultIssue raw = new DefaultIssue()
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitorTest.java
new file mode 100644 (file)
index 0000000..51164c3
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.issue;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ComponentImpl;
+import org.sonar.ce.task.projectanalysis.component.ProjectAttributes;
+import org.sonar.ce.task.projectanalysis.component.ReportAttributes;
+import org.sonar.core.issue.AnticipatedTransition;
+import org.sonar.core.issue.DefaultIssue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
+
+public class TransitionIssuesToAnticipatedStatesVisitorTest {
+
+  private final IssueLifecycle issueLifecycle = mock(IssueLifecycle.class);
+
+  private final AnticipatedTransitionRepository anticipatedTransitionRepository = mock(AnticipatedTransitionRepository.class);
+
+  private final TransitionIssuesToAnticipatedStatesVisitor underTest = new TransitionIssuesToAnticipatedStatesVisitor(anticipatedTransitionRepository, issueLifecycle);
+
+  @Test
+  public void givenMatchingAnticipatedTransitions_transitionsShouldBeAppliedToIssues() {
+    Component component = getComponent(Component.Type.FILE);
+    when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
+
+    DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
+
+    underTest.beforeComponent(component);
+    underTest.onIssue(component, issue);
+    underTest.afterComponent(component);
+
+    assertThat(issue.isBeingClosed()).isTrue();
+    assertThat(issue.hasAnticipatedTransitions()).isTrue();
+    verify(issueLifecycle).doManualTransition(issue, "wontfix", "admin");
+    verify(issueLifecycle).addComment(issue, "doing the transition in an anticipated way", "admin");
+  }
+
+  @Test
+  public void givenNonMatchingAnticipatedTransitions_transitionsAreNotAppliedToIssues() {
+    Component component = getComponent(Component.Type.FILE);
+    when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
+
+    DefaultIssue issue = getDefaultIssue(2, "abcdefghf", "another issue message");
+
+    underTest.beforeComponent(component);
+    underTest.onIssue(component, issue);
+    underTest.afterComponent(component);
+
+    assertThat(issue.isBeingClosed()).isFalse();
+    assertThat(issue.hasAnticipatedTransitions()).isFalse();
+    verifyNoInteractions(issueLifecycle);
+  }
+
+  @Test
+  public void givenAFileComponent_theRepositoryIsHitForFetchingAnticipatedTransitions() {
+    Component component = getComponent(Component.Type.FILE);
+    when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(Collections.emptyList());
+
+    underTest.beforeComponent(component);
+
+    verify(anticipatedTransitionRepository).getAnticipatedTransitionByComponent(component);
+  }
+
+  @Test
+  public void givenAProjecComponent_theRepositoryIsNotQueriedForAnticipatedTransitions() {
+    Component component = getComponent(PROJECT);
+    when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(Collections.emptyList());
+
+    underTest.beforeComponent(component);
+
+    verifyNoInteractions(anticipatedTransitionRepository);
+  }
+
+  private Collection<AnticipatedTransition> getAnticipatedTransitions(String projecKey, String fileName) {
+    return Stream.of(new AnticipatedTransition(projecKey, null, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", "wontfix", "doing the transition in an anticipated way")).collect(Collectors.toList());
+  }
+
+  private Component getComponent(Component.Type type) {
+    ComponentImpl.Builder builder = ComponentImpl.builder(type)
+      .setUuid("componentUuid")
+      .setKey("projectKey:filename")
+      .setName("filename")
+      .setStatus(Component.Status.ADDED)
+      .setShortName("filename")
+      .setReportAttributes(mock(ReportAttributes.class));
+
+    if (PROJECT.equals(type)) {
+      builder.setProjectAttributes(mock(ProjectAttributes.class));
+    }
+
+    return builder.build();
+  }
+
+  private DefaultIssue getDefaultIssue(Integer line, String hash, String message) {
+    DefaultIssue defaultIssue = new DefaultIssue();
+    defaultIssue.setLine(line);
+    defaultIssue.setChecksum(hash);
+    defaultIssue.setMessage(message);
+    defaultIssue.setRuleKey(RuleKey.of("repo", "id"));
+    return defaultIssue;
+  }
+
+}
index ee3b8d79d93bf1a963874cf2edcf2a0ee14d350b..22443b3bf7b51ac6515012adec5690e83f1d4ddc 100644 (file)
@@ -44,7 +44,7 @@ public class IssueStorage {
         changeDto.setProjectUuid(issue.projectUuid());
         mapper.insert(changeDto);
       }
-    } else if (!issue.isNew() && diffs != null) {
+    } else if ((!issue.isNew() || issue.hasAnticipatedTransitions()) && diffs != null) {
       IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs, issue.projectUuid());
       changeDto.setUuid(uuidFactory.create());
       changeDto.setProjectUuid(issue.projectUuid());
index abfa41545544f054ad20133a0b0b43624728079c..32b5154b0b9859aba37216b8b9b9dcf9814126bc 100644 (file)
@@ -131,6 +131,8 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
 
   private String ruleDescriptionContextKey = null;
 
+  private boolean anticipatedTransitions = false;
+
   @Override
   public String key() {
     return key;
@@ -689,6 +691,15 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
     return this;
   }
 
+  public boolean hasAnticipatedTransitions() {
+    return anticipatedTransitions;
+  }
+
+  public DefaultIssue setAnticipatedTransitions(boolean anticipatedTransitions) {
+    this.anticipatedTransitions = anticipatedTransitions;
+    return this;
+  }
+
   @Override
   public Integer getLine() {
     return line;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/AnticipatedTransitionTracker.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/AnticipatedTransitionTracker.java
new file mode 100644 (file)
index 0000000..d46d392
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.Collection;
+
+/**
+ * A simplified version of {@link Tracker}, which doesn't use line hash sequences nor block hash sequences and
+ * only has two steps instead of 5 steps.
+ */
+public class AnticipatedTransitionTracker<RAW extends Trackable, BASE extends Trackable> extends AbstractTracker<RAW, BASE> {
+
+  public Tracking<RAW, BASE> track(Collection<RAW> rawInput, Collection<BASE> baseInput) {
+    Tracking<RAW, BASE> tracking = new Tracking<>(rawInput, baseInput);
+
+    // 1. match by rule, line, line hash and message
+    match(tracking, LineAndLineHashAndMessage::new);
+
+    // 2. match issues with same rule, same line and same line hash, but not necessarily with same message
+    match(tracking, LineAndLineHashKey::new);
+
+    // 3. match issues with same rule, same message and same line hash
+    match(tracking, LineHashAndMessageKey::new);
+
+    // 4. match issues with same rule, same line and same message
+    match(tracking, LineAndMessageKey::new);
+
+    // 5. match issues with same rule and same line hash but different line and different message.
+    // See SONAR-2812
+    match(tracking, LineHashKey::new);
+
+    return tracking;
+  }
+}
index 92ae1d4cd7f89114086887c103756cfd95dc6b87..2078f8f60e822d8cf1c0ed874db2ac733deaf48b 100644 (file)
@@ -282,4 +282,17 @@ public class DefaultIssueTest {
   public void codeVariants_whenNull_shouldReturnEmptySet() {
     assertThat(issue.codeVariants()).isEmpty();
   }
+
+  @Test
+  public void issueByDefault_shouldNotHaveAppliedAnticipatedTransitions() {
+    DefaultIssue defaultIssue = new DefaultIssue();
+    assertThat(defaultIssue.hasAnticipatedTransitions()).isFalse();
+  }
+
+  @Test
+  public void anticipatedTransitions_WhenSetTrue_shouldReturnTrue() {
+    DefaultIssue defaultIssue = new DefaultIssue();
+    defaultIssue.setAnticipatedTransitions(true);
+    assertThat(defaultIssue.hasAnticipatedTransitions()).isTrue();
+  }
 }
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/AnticipatedTransitionTrackerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/AnticipatedTransitionTrackerTest.java
new file mode 100644 (file)
index 0000000..2d88c89
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.AnticipatedTransition;
+import org.sonar.core.issue.DefaultIssue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AnticipatedTransitionTrackerTest {
+
+  private final AnticipatedTransitionTracker<DefaultIssue, AnticipatedTransition> underTest = new AnticipatedTransitionTracker<>();
+
+  @Test
+  public void givenIssuesAndAnticipatedTransitions_trackerShouldReturnTheExpectedMatching() {
+
+    List<DefaultIssue> issues = getIssues();
+    List<AnticipatedTransition> anticipatedTransitions = getAnticipatedTransitions();
+
+    Tracking<DefaultIssue, AnticipatedTransition> tracking = underTest.track(issues, anticipatedTransitions);
+
+    var matchedRaws = tracking.getMatchedRaws();
+    var unmatchedRaws = tracking.getUnmatchedRaws().collect(Collectors.toList());
+
+    assertThat(matchedRaws).hasSize(5);
+    assertThat(unmatchedRaws).hasSize(2);
+
+    assertThat(matchedRaws.keySet()).containsExactlyInAnyOrder(issues.get(0), issues.get(1), issues.get(2), issues.get(3), issues.get(4));
+    assertThat(unmatchedRaws).containsExactlyInAnyOrder(issues.get(5), issues.get(6));
+
+    assertThat(matchedRaws).containsEntry(issues.get(0), anticipatedTransitions.get(1))
+      .containsEntry(issues.get(1), anticipatedTransitions.get(0))
+      .containsEntry(issues.get(2), anticipatedTransitions.get(3))
+      .containsEntry(issues.get(3), anticipatedTransitions.get(2))
+      .containsEntry(issues.get(4), anticipatedTransitions.get(6));
+  }
+
+  private List<DefaultIssue> getIssues() {
+    return List.of(
+      getIssue(1, "message1", "hash1", "rule:key1"), //should match transition 2 due to lvl 1 matching
+      getIssue(2, "message2", "hash2", "rule:key2"), //should match transition 1 due to lvl 2 matching
+      getIssue(3, "message3", "hash3", "rule:key3"), //should match transition 4 due to lvl 3 matching
+      getIssue(4, "message4", "hash4", "rule:key4"), //should match transition 3 due to lvl 4 matching
+      getIssue(5, "message5", "hash5", "rule:key5"), //should match transition 7 due to lvl 5 matching
+      getIssue(6, "message6", "hash6", "rule:key6"), //should not match
+      getIssue(7, "message7", "hash7", "rule:key7")  //should not match
+    );
+  }
+
+  private List<AnticipatedTransition> getAnticipatedTransitions() {
+    //Anticipated Transitions with random order
+    return List.of(
+      getAnticipatedTransition(2, "message a bit different 2", "hash2", "rule:key2"),
+      getAnticipatedTransition(1, "message1", "hash1", "rule:key1"),
+      getAnticipatedTransition(4, "message4", "different hash", "rule:key4"),
+      getAnticipatedTransition(13, "message3", "hash3", "rule:key3"),
+      getAnticipatedTransition(16, "different message", "different hash", "rule:key6"),
+      getAnticipatedTransition(7, "different message", "different hash", "rule:key17"),
+      getAnticipatedTransition(15, "different message", "hash5", "rule:key5")
+
+    );
+  }
+
+  private DefaultIssue getIssue(Integer line, String message, String hash, String ruleKey) {
+    return new DefaultIssue()
+      .setKey("key" + line)
+      .setLine(line)
+      .setMessage(message)
+      .setChecksum(hash)
+      .setRuleKey(RuleKey.parse(ruleKey));
+  }
+
+  private AnticipatedTransition getAnticipatedTransition(Integer line, String message, String hash, String ruleKey) {
+    return new AnticipatedTransition("projectKey",
+      null,
+      "userUuid",
+      RuleKey.parse(ruleKey),
+      message,
+      "filePath",
+      line,
+      hash,
+      "transition",
+      null);
+  }
+
+}