From 3c2145492c1c9333de3fd16d7d862fed63370538 Mon Sep 17 00:00:00 2001 From: Zipeng WU Date: Tue, 17 Oct 2023 12:08:44 +0200 Subject: [PATCH] SONAR-20777 enable lifecycle management on external issues --- .../main/java/org/sonar/xoo/XooPlugin.java | 2 + .../rule/OneExternalIssuePerFoobarSensor.java | 88 +++++++++++++++++++ .../server/issue/TransitionServiceIT.java | 16 ++-- .../server/issue/ws/BulkChangeActionIT.java | 4 +- .../server/issue/ws/DoTransitionActionIT.java | 10 +-- .../sonar/server/issue/TransitionService.java | 4 - .../server/issue/ws/SearchResponseLoader.java | 2 +- 7 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerFoobarSensor.java diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index 4964d6dd316..53e0704634a 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -54,6 +54,7 @@ import org.sonar.xoo.rule.OneCodeSmellIssuePerLineSensor; import org.sonar.xoo.rule.OneCodeSmellIssuePerTestLineSensor; import org.sonar.xoo.rule.OneDayDebtPerFileSensor; import org.sonar.xoo.rule.OneExternalIssueOnProjectSensor; +import org.sonar.xoo.rule.OneExternalIssuePerFoobarSensor; import org.sonar.xoo.rule.OneExternalIssuePerLineSensor; import org.sonar.xoo.rule.OneExternalIssuePerLineWithoutMessageSensor; import org.sonar.xoo.rule.OneIssueOnDirPerFileSensor; @@ -157,6 +158,7 @@ public class XooPlugin implements Plugin { OneIssuePerUnknownFileSensor.class, OneQuickFixPerLineSensor.class, + OneExternalIssuePerFoobarSensor.class, OneExternalIssuePerLineSensor.class, OneExternalIssuePerLineWithoutMessageSensor.class, OneExternalIssueOnProjectSensor.class, diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerFoobarSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerFoobarSensor.java new file mode 100644 index 00000000000..30d532f2e9c --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerFoobarSensor.java @@ -0,0 +1,88 @@ +/* + * 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.xoo.rule; + +import java.io.IOException; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.rules.RuleType; +import org.sonar.xoo.Xoo; + +public class OneExternalIssuePerFoobarSensor implements Sensor { + + public static final String RULE_ID = "OneExternalIssuePerFoobar"; + public static final String TAG = "foobar"; + public static final String ENGINE_ID = "XooEngine"; + public static final String SEVERITY = "MAJOR"; + public static final Long EFFORT = 10L; + public static final RuleType TYPE = RuleType.BUG; + public static final String ACTIVATE = "sonar.oneExternalIssuePerFoobar.activate"; + private static final String NAME = "One External Issue Per Foobar Rule"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name(NAME) + .onlyOnLanguages(Xoo.KEY) + .onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE).orElse(false)); + } + + @Override + public void execute(SensorContext context) { + FileSystem fs = context.fileSystem(); + FilePredicates p = fs.predicates(); + for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(InputFile.Type.MAIN)))) { + createIssues(file, context); + } + } + + private static void createIssues(InputFile file, SensorContext context) { + try { + List lines = IOUtils.readLines(file.inputStream(), file.charset()); + for (int i = 0; i < lines.size(); i++) { + var line = lines.get(i); + if (line.contains(TAG) && !line.contains("//NOSONAR")) { + NewExternalIssue newIssue = context.newExternalIssue(); + newIssue + .engineId(ENGINE_ID) + .ruleId(RULE_ID) + .at(newIssue.newLocation() + .on(file) + .at(file.selectLine(i + 1)) + .message("This issue is generated on line containing foobar string")) + .severity(Severity.valueOf(SEVERITY)) + .remediationEffortMinutes(EFFORT) + .type(TYPE) + .save(); + } + } + } catch (IOException e) { + throw new IllegalStateException("Fail to process " + file, e); + } + } +} diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java index 95c04cd67f3..444e934e1d6 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/TransitionServiceIT.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.core.issue.DefaultIssue; -import org.sonar.core.issue.IssueChangeContext; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ProjectData; @@ -37,7 +36,6 @@ import org.sonar.server.issue.workflow.Transition; import org.sonar.server.tester.UserSessionRule; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.rules.RuleType.CODE_SMELL; @@ -77,7 +75,7 @@ public class TransitionServiceIT { } @Test - public void list_transitions_returns_empty_list_on_external_issue() { + public void list_transitions_on_external_issue() { ProjectData project = db.components().insertPrivateProject(); ComponentDto file = db.components().insertComponent(newFileDto(project.getMainBranchComponent())); RuleDto externalRule = db.rules().insert(r -> r.setIsExternal(true)); @@ -87,7 +85,7 @@ public class TransitionServiceIT { List result = underTest.listTransitions(externalIssue.toDefaultIssue()); - assertThat(result).isEmpty(); + assertThat(result).extracting(Transition::key).containsOnly("confirm", "resolve", "falsepositive", "wontfix"); } @Test @@ -130,16 +128,16 @@ public class TransitionServiceIT { } @Test - public void do_transition_fail_on_external_issue() { + public void do_transition_on_external_issue() { ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); ComponentDto file = db.components().insertComponent(newFileDto(project)); RuleDto externalRule = db.rules().insert(r -> r.setIsExternal(true)); IssueDto externalIssue = db.issues().insert(externalRule, project, file, i -> i.setStatus(STATUS_OPEN).setResolution(null).setType(CODE_SMELL)); + DefaultIssue defaultIssue = externalIssue.toDefaultIssue(); + boolean result = underTest.doTransition(defaultIssue, issueChangeContextByUserBuilder(new Date(), "user_uuid").build(), "confirm"); - IssueChangeContext issueChangeContext = issueChangeContextByUserBuilder(new Date(), "user_uuid").build(); - assertThatThrownBy(() -> underTest.doTransition(defaultIssue, issueChangeContext, "confirm")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transition is not allowed on issues imported from external rule engines"); + assertThat(result).isTrue(); + assertThat(defaultIssue.status()).isEqualTo(STATUS_CONFIRMED); } } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java index 35b763ccba2..2890bda1594 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java @@ -595,7 +595,7 @@ public class BulkChangeActionIT { } @Test - public void ignore_external_issue() { + public void bulk_change_includes_external_issue() { UserDto user = db.users().insertUser(); userSession.logIn(user); ProjectData projectData = db.components().insertPrivateProject(); @@ -611,7 +611,7 @@ public class BulkChangeActionIT { .setDoTransition("confirm") .build()); - checkResponse(response, 2, 1, 1, 0); + checkResponse(response, 2, 2, 0, 0); } @Test diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/DoTransitionActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/DoTransitionActionIT.java index eca330c8545..be0fd60ab93 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/DoTransitionActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/DoTransitionActionIT.java @@ -153,16 +153,16 @@ public class DoTransitionActionIT { } @Test - public void fail_if_external_issue() { + public void transition_succeeds_on_external_issue() { ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); ComponentDto file = db.components().insertComponent(newFileDto(project)); RuleDto externalRule = db.rules().insertIssueRule(r -> r.setIsExternal(true)); IssueDto externalIssue = db.issues().insertIssue(externalRule, project, file, i -> i.setStatus(STATUS_OPEN).setResolution(null).setType(CODE_SMELL)); - userSession.logIn().addProjectPermission(USER, project, file); + userSession.logIn(db.users().insertUser()).addProjectPermission(USER, project, file); - assertThatThrownBy(() -> call(externalIssue.getKey(), "confirm")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transition is not allowed on issues imported from external rule engines"); + call(externalIssue.getKey(), "confirm"); + IssueDto issueReloaded = db.getDbClient().issueDao().selectByKey(db.getSession(), externalIssue.getKey()).get(); + assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED); } @Test diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java index 84544bf1930..d65ca2e0fbe 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TransitionService.java @@ -46,9 +46,6 @@ public class TransitionService { } public List listTransitions(DefaultIssue issue) { - if (issue.isFromExternalRuleEngine()){ - return Collections.emptyList(); - } String projectUuid = requireNonNull(issue.projectUuid()); return workflow.outTransitions(issue) .stream() @@ -58,7 +55,6 @@ public class TransitionService { } public boolean doTransition(DefaultIssue defaultIssue, IssueChangeContext issueChangeContext, String transitionKey) { - checkArgument(!defaultIssue.isFromExternalRuleEngine(), "Transition is not allowed on issues imported from external rule engines"); return workflow.doManualTransition(defaultIssue, transitionKey, issueChangeContext); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java index f0b97838fbc..40b1f443cbd 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java @@ -232,7 +232,7 @@ public class SearchResponseLoader { ComponentDto branch = componentsByProjectUuid.get(issueDto.getProjectUuid()); result.addActions(issueDto.getKey(), listAvailableActions(issueDto, branch)); } - if (fields.contains(TRANSITIONS) && !issueDto.isExternal()) { + if (fields.contains(TRANSITIONS)) { // TODO workflow and action engines must not depend on org.sonar.api.issue.Issue but on a generic interface DefaultIssue issue = issueDto.toDefaultIssue(); result.addTransitions(issue.key(), transitionService.listTransitions(issue)); -- 2.39.5