From 30abc015459aecb07ea2e3fba2b4bba52a652f74 Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Wed, 19 Jul 2023 15:32:14 +0200 Subject: [PATCH] SONAR-19372 Add a new issue visitor in order to apply anticipated transitions to issues --- ...ProjectAnalysisTaskContainerPopulator.java | 8 +- .../AnticipatedTransitionRepository.java | 28 ++++ .../AnticipatedTransitionRepositoryImpl.java | 70 ++++++++++ .../projectanalysis/issue/IssueLifecycle.java | 17 +++ ...itionIssuesToAnticipatedStatesVisitor.java | 79 +++++++++++ .../issue/IssueLifecycleTest.java | 29 ++++ ...nIssuesToAnticipatedStatesVisitorTest.java | 132 ++++++++++++++++++ .../org/sonar/server/issue/IssueStorage.java | 2 +- .../org/sonar/core/issue/DefaultIssue.java | 11 ++ .../AnticipatedTransitionTracker.java | 51 +++++++ .../sonar/core/issue/DefaultIssueTest.java | 13 ++ .../AnticipatedTransitionTrackerTest.java | 107 ++++++++++++++ 12 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepository.java create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepositoryImpl.java create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitor.java create mode 100644 server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitorTest.java create mode 100644 sonar-core/src/main/java/org/sonar/core/issue/tracking/AnticipatedTransitionTracker.java create mode 100644 sonar-core/src/test/java/org/sonar/core/issue/tracking/AnticipatedTransitionTrackerTest.java diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index fdaf5a6ab8e..c39754d47f8 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -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 index 00000000000..88a8d2d6e45 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepository.java @@ -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 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 index 00000000000..d1c23d96ea9 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/AnticipatedTransitionRepositoryImpl.java @@ -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 getAnticipatedTransitionByProjectUuid(String componentUuid, String filePath) { + try (DbSession dbSession = dbClient.openSession(false)) { + EntityDto entityDto = dbClient.entityDao().selectByComponentUuid(dbSession, componentUuid).orElseThrow(IllegalStateException::new); + List anticipatedTransitionDtos = dbClient.anticipatedTransitionDao().selectByProjectUuid(dbSession, entityDto.getUuid()); + return getAnticipatedTransitions(anticipatedTransitionDtos); + } + } + + private Collection getAnticipatedTransitions(List 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() + ); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java index 9d4a6721dc5..517dffec253 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java @@ -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 index 00000000000..c7a3e53b717 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitor.java @@ -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 anticipatedTransitions; + private final AnticipatedTransitionTracker 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 tracking = tracker.track(List.of(issue), anticipatedTransitions); + Map 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()); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java index 78a749c6d7c..e995540494e 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java @@ -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 index 00000000000..51164c3080c --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TransitionIssuesToAnticipatedStatesVisitorTest.java @@ -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 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; + } + +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueStorage.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueStorage.java index ee3b8d79d93..22443b3bf7b 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueStorage.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueStorage.java @@ -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()); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java index abfa4154554..32b5154b0b9 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -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 index 00000000000..d46d3921a85 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/AnticipatedTransitionTracker.java @@ -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 extends AbstractTracker { + + public Tracking track(Collection rawInput, Collection baseInput) { + Tracking 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; + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java index 92ae1d4cd7f..2078f8f60e8 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java @@ -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 index 00000000000..2d88c894266 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/AnticipatedTransitionTrackerTest.java @@ -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 underTest = new AnticipatedTransitionTracker<>(); + + @Test + public void givenIssuesAndAnticipatedTransitions_trackerShouldReturnTheExpectedMatching() { + + List issues = getIssues(); + List anticipatedTransitions = getAnticipatedTransitions(); + + Tracking 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 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 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); + } + +} -- 2.39.5