diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
index 4c182b744f3f..337bf8af149d 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
@@ -33,6 +33,8 @@
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.session.TransactionIsolationLevel;
+import org.sonar.db.ai.CsAiRuleCatalogDto;
+import org.sonar.db.ai.CsAiRulesCatalogMapper;
import org.sonar.db.alm.pat.AlmPatMapper;
import org.sonar.db.alm.setting.AlmSettingMapper;
import org.sonar.db.alm.setting.ProjectAlmKeyAndProject;
@@ -221,6 +223,7 @@ public void start() {
confBuilder.loadAlias("AnticipatedTransition", AnticipatedTransitionDto.class);
confBuilder.loadAlias("CeTaskCharacteristic", CeTaskCharacteristicDto.class);
confBuilder.loadAlias("Component", ComponentDto.class);
+ confBuilder.loadAlias("CsAiRuleCatalog", CsAiRuleCatalogDto.class);
confBuilder.loadAlias("Cve", CveDto.class);
confBuilder.loadAlias("CveCwe", CveCweDto.class);
confBuilder.loadAlias("DevOpsPermissionsMapping", DevOpsPermissionsMappingDto.class);
@@ -303,6 +306,7 @@ public void start() {
CeTaskMessageMapper.class,
ComponentKeyUpdaterMapper.class,
ComponentMapper.class,
+ CsAiRulesCatalogMapper.class,
CveMapper.class,
CveCweMapper.class,
DefaultQProfileMapper.class,
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRuleCatalogDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRuleCatalogDto.java
new file mode 100644
index 000000000000..89ca4e00b8ba
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRuleCatalogDto.java
@@ -0,0 +1,64 @@
+/*
+ * 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.db.ai;
+
+public class CsAiRuleCatalogDto {
+
+ private String ruleKey;
+ private String language;
+ private String description;
+ private String contextSeverity;
+
+ public String getRuleKey() {
+ return ruleKey;
+ }
+
+ public CsAiRuleCatalogDto setRuleKey(String ruleKey) {
+ this.ruleKey = ruleKey;
+ return this;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public CsAiRuleCatalogDto setLanguage(String language) {
+ this.language = language;
+ return this;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public CsAiRuleCatalogDto setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public String getContextSeverity() {
+ return contextSeverity;
+ }
+
+ public CsAiRuleCatalogDto setContextSeverity(String contextSeverity) {
+ this.contextSeverity = contextSeverity;
+ return this;
+ }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRulesCatalogMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRulesCatalogMapper.java
new file mode 100644
index 000000000000..8faf3cf5ae9e
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRulesCatalogMapper.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+/*
+ * SonarQube
+ */
+package org.sonar.db.ai;
+
+import org.apache.ibatis.annotations.Param;
+
+public interface CsAiRulesCatalogMapper {
+
+ CsAiRuleCatalogDto selectByRuleKey(@Param("language") String language, @Param("ruleKey") String ruleKey);
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/ai/package-info.java b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/package-info.java
new file mode 100644
index 000000000000..4a8efcf5d0db
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.db.ai;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/ai/CsAiRulesCatalogMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/ai/CsAiRulesCatalogMapper.xml
new file mode 100644
index 000000000000..63723dd00009
--- /dev/null
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/ai/CsAiRulesCatalogMapper.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts
index b631960c2f4e..5ea358a17efc 100644
--- a/server/sonar-web/src/main/js/api/issues.ts
+++ b/server/sonar-web/src/main/js/api/issues.ts
@@ -31,6 +31,16 @@ import {
} from '../types/issues';
import { Dict, FacetValue, IssueChangelog, SnippetsByComponent, SourceLine } from '../types/types';
+export type IssueContextResponse = {
+ componentKey?: string;
+ contextSeverity?: 'LOW' | 'MEDIUM' | 'HIGH';
+ issueKey: string;
+ line: number;
+ ruleDescription?: string;
+ ruleKey?: string;
+ snippet: string;
+};
+
export function searchIssues(query: RequestData): Promise {
return getJSON('/api/issues/search', query).catch(error => {
if (error?.status === 403 ) {
@@ -170,3 +180,7 @@ export function getIssueFlowSnippets(issueKey: string): Promise {
+ return getJSON('/api/issues/context', data).catch(throwGlobalError);
+}
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueContextActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueContextActionIT.java
new file mode 100644
index 000000000000..5a23dc6b3754
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueContextActionIT.java
@@ -0,0 +1,520 @@
+/*
+ * 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.server.issue.ws;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ProjectData;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.source.FileSourceDto;
+import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.source.HtmlSourceDecorator;
+import org.sonar.server.source.SourceService;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+class IssueContextActionIT {
+
+ @RegisterExtension
+ private final DbTester db = DbTester.create(System2.INSTANCE);
+ @RegisterExtension
+ private final UserSessionRule userSession = UserSessionRule.standalone();
+
+ private final HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class);
+ private final SourceService sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator);
+ private final IssueFinder issueFinder = new IssueFinder(db.getDbClient(), userSession);
+ private final IssueContextAction underTest = new IssueContextAction(userSession, db.getDbClient(),
+ issueFinder, TestComponentFinder.from(db), sourceService);
+ private final WsActionTester tester = new WsActionTester(underTest);
+
+ @BeforeEach
+ void setUp() {
+ when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString()))
+ .thenAnswer(invocation -> invocation.getArguments()[0].toString());
+ }
+
+ @Test
+ void return_issue_context_with_low_severity_default_range() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(50));
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S100"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(25).setRuleKey(rule.getKey().repository(), rule.getKey().rule()));
+ System.out.println(rule.getKey().repository() + " " + rule.getKey().rule());
+ System.out.println(rule.getRepositoryKey() + " " + rule.getRuleKey());
+ insertRuleCatalog("java:S100", "Test rule", "LOW");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("issueKey").getAsString()).isEqualTo(issue.getKey());
+ assertThat(json.get("ruleKey").getAsString()).isEqualTo("java:S100");
+ assertThat(json.get("line").getAsInt()).isEqualTo(25);
+ assertThat(json.get("ruleDescription").getAsString()).isEqualTo("Test rule");
+ assertThat(json.get("contextSeverity").getAsString()).isEqualTo("LOW");
+ String snippet = json.get("snippet").getAsString();
+ assertThat(snippet.split("\n").length).isEqualTo(11); // 25-5 to 25+5 = 11 lines
+ assertThat(snippet).contains("line 20");
+ assertThat(snippet).contains("line 30");
+ }
+
+ @Test
+ void return_issue_context_with_medium_severity_method_range() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ List sourceLines = List.of(
+ "public class TestClass {",
+ " private String field;",
+ "",
+ " public void testMethod(int param) {",
+ " int x = 10;",
+ " if (x > 5) {",
+ " System.out.println(\"test\");",
+ " }",
+ " }",
+ "",
+ " public void anotherMethod() {",
+ " return;",
+ " }",
+ "}"
+ );
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), sourceLines);
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S200"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(7)
+ .setRuleKey(rule.getKey().repository(), rule.getKey().rule())); // Line inside testMethod
+ insertRuleCatalog("java:S200", "Medium severity rule", "MEDIUM");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("contextSeverity").getAsString()).isEqualTo("MEDIUM");
+ String snippet = json.get("snippet").getAsString();
+ // Should include the entire method from line 4 to 8
+ assertThat(snippet).contains("public void testMethod");
+ assertThat(snippet).contains("System.out.println");
+ assertThat(snippet).doesNotContain("anotherMethod"); // Should not include other methods
+ }
+
+ @Test
+ void return_issue_context_with_high_severity_class_range() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ List sourceLines = List.of(
+ "package com.test;",
+ "",
+ "public class TestClass {",
+ " private String field;",
+ "",
+ " public void testMethod(int param) {",
+ " int x = 10;",
+ " System.out.println(\"test\");",
+ " }",
+ "",
+ " public void anotherMethod() {",
+ " return;",
+ " }",
+ "}"
+ );
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), sourceLines);
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S300"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(7).setRuleKey(rule.getKey().repository(), rule.getKey().rule()));
+ insertRuleCatalog("java:S300", "High severity rule", "HIGH");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("contextSeverity").getAsString()).isEqualTo("HIGH");
+ String snippet = json.get("snippet").getAsString();
+ // Should include the entire class
+ assertThat(snippet).contains("public class TestClass");
+ assertThat(snippet).contains("testMethod");
+ assertThat(snippet).contains("anotherMethod");
+ }
+
+ @Test
+ void use_rule_key_from_request_param_if_provided() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto issueRule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S100"));
+ RuleDto paramRule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S200"));
+ IssueDto issue = db.issues().insertIssue(issueRule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10).setRuleKey(issueRule.getRepositoryKey(), issueRule.getKey().rule()));
+ insertRuleCatalog("java:S200", "Rule from param", "LOW");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .setParam("rule", "java:S200")
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("ruleKey").getAsString()).isEqualTo("java:S200");
+ assertThat(json.get("ruleDescription").getAsString()).isEqualTo("Rule from param");
+ }
+
+ @Test
+ void use_rule_key_from_issue_if_not_provided_in_request() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S100"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10).setRuleKey(rule.getRepositoryKey(), rule.getKey().rule()));
+ insertRuleCatalog("java:S100", "Rule from issue", "LOW");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("ruleKey").getAsString()).isEqualTo("java:S100");
+ assertThat(json.get("ruleDescription").getAsString()).isEqualTo("Rule from issue");
+ }
+
+ @Test
+ void handle_issue_without_rule_key() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10).setRuleKey(null, null));
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.has("ruleKey")).isFalse();
+ assertThat(json.has("ruleDescription")).isFalse();
+ assertThat(json.has("contextSeverity")).isFalse();
+ }
+
+ @Test
+ void handle_issue_with_null_line_number() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(null));
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("line").getAsInt()).isEqualTo(1); // Default to 1
+ String snippet = json.get("snippet").getAsString();
+ assertThat(snippet.split("\n").length).isLessThanOrEqualTo(6); // 1-5 to 1+5, but file might be shorter
+ }
+
+ @Test
+ void handle_issue_with_line_zero() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(0));
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("line").getAsInt()).isEqualTo(1); // Default to 1
+ }
+
+ @Test
+ void handle_rule_catalog_entry_not_found() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S999"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10).setRuleKey(rule.getRepositoryKey(), rule.getRuleKey()));
+ // No catalog entry for java:S999
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("ruleKey").getAsString()).isEqualTo("java:S999");
+ assertThat(json.has("ruleDescription")).isFalse();
+ assertThat(json.has("contextSeverity")).isFalse();
+ // Should use default LOW behavior (±5 lines)
+ String snippet = json.get("snippet").getAsString();
+ assertThat(snippet.split("\n").length).isEqualTo(11);
+ }
+
+ @Test
+ void handle_medium_severity_when_method_signature_not_found() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ List sourceLines = List.of(
+ "line 1",
+ "line 2",
+ "line 3",
+ "if (condition) {", // Control flow, not a method
+ " int x = 10;",
+ " System.out.println(x);",
+ "}"
+ );
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), sourceLines);
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S200"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(5).setRuleKey(rule.getRepositoryKey(), rule.getRuleKey()));
+ insertRuleCatalog("java:S200", "Medium rule", "MEDIUM");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ // Should fall back to ±5 lines when method signature not found
+ String snippet = json.get("snippet").getAsString();
+ assertThat(snippet.split("\n").length).isLessThanOrEqualTo(11);
+ }
+
+ @Test
+ void handle_empty_file() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), List.of());
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(1));
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ String snippet = json.get("snippet").getAsString();
+ assertThat(snippet).isEmpty();
+ }
+
+ @Test
+ void fail_when_user_not_logged_in() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10));
+
+ TestRequest request = tester.newRequest()
+ .setParam("issue", issue.getKey());
+
+ assertThatThrownBy(() -> request.execute())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Authentication is required");
+ }
+
+ @Test
+ void fail_when_user_lacks_codeviewer_permission() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10));
+ userSession.logIn("user").addProjectPermission(UserRole.USER, projectData.getProjectDto());
+
+ TestRequest request = tester.newRequest()
+ .setParam("issue", issue.getKey());
+
+ assertThatThrownBy(() -> request.execute())
+ .isInstanceOf(ForbiddenException.class);
+ }
+
+ @Test
+ void fail_when_issue_not_found() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ setUserWithValidPermission(projectData);
+
+ TestRequest request = tester.newRequest()
+ .setParam("issue", "non-existent-issue-key");
+
+ assertThatThrownBy(() -> request.execute())
+ .isInstanceOf(NotFoundException.class)
+ .hasMessageContaining("Issue with key 'non-existent-issue-key' does not exist");
+ }
+
+ @Test
+ void fail_when_missing_issue_parameter() {
+ userSession.logIn();
+
+ TestRequest request = tester.newRequest();
+
+ assertThatThrownBy(() -> request.execute())
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void handle_case_insensitive_severity() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(20));
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S200"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(10).setRuleKey(rule.getRepositoryKey(), rule.getRuleKey()));
+ insertRuleCatalog("java:S200", "Test rule", "medium"); // lowercase
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ assertThat(json.get("contextSeverity").getAsString()).isEqualTo("medium");
+ // Should use method extraction for MEDIUM
+ }
+
+ @Test
+ void handle_snippet_at_file_boundaries() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), createSourceLines(5));
+ RuleDto rule = db.rules().insertIssueRule();
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(2)); // Near beginning
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ String snippet = json.get("snippet").getAsString();
+ // Should handle gracefully when trying to extract before line 1
+ assertThat(snippet).isNotEmpty();
+ }
+
+ @Test
+ void handle_complex_nested_methods() {
+ ProjectData projectData = db.components().insertPrivateProject();
+ List sourceLines = List.of(
+ "public class Outer {",
+ " public void outerMethod() {",
+ " if (true) {",
+ " innerMethod();",
+ " }",
+ " }",
+ "",
+ " private void innerMethod() {",
+ " int x = 5;",
+ " }",
+ "}"
+ );
+ ComponentDto file = insertFileWithSource(projectData.getMainBranchComponent(), sourceLines);
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRuleKey("java:S200"));
+ IssueDto issue = db.issues().insertIssue(rule, projectData.getMainBranchComponent(), file,
+ i -> i.setLine(9).setRuleKey(rule.getRepositoryKey(), rule.getRuleKey())); // Inside innerMethod
+ insertRuleCatalog("java:S200", "Medium rule", "MEDIUM");
+ setUserWithValidPermission(projectData);
+
+ TestResponse response = tester.newRequest()
+ .setParam("issue", issue.getKey())
+ .execute();
+
+ JsonObject json = JsonParser.parseString(response.getInput()).getAsJsonObject();
+ String snippet = json.get("snippet").getAsString();
+ // Should extract innerMethod, not outerMethod
+ assertThat(snippet).contains("private void innerMethod");
+ assertThat(snippet).contains("int x = 5");
+ }
+
+ private List createSourceLines(int count) {
+ return java.util.stream.IntStream.rangeClosed(1, count)
+ .mapToObj(i -> "line " + i)
+ .toList();
+ }
+
+ private ComponentDto insertFileWithSource(ComponentDto project, List sourceLines) {
+ ComponentDto file = db.components().insertComponent(newFileDto(project, project.uuid()));
+ DbFileSources.Data.Builder sourceDataBuilder = DbFileSources.Data.newBuilder();
+ for (int i = 0; i < sourceLines.size(); i++) {
+ sourceDataBuilder.addLinesBuilder()
+ .setLine(i + 1)
+ .setSource(sourceLines.get(i))
+ .build();
+ }
+ db.getDbClient().fileSourceDao().insert(db.getSession(), new FileSourceDto()
+ .setUuid(java.util.UUID.randomUUID().toString())
+ .setProjectUuid(project.uuid())
+ .setFileUuid(file.uuid())
+ .setSourceData(sourceDataBuilder.build()));
+ db.commit();
+ return file;
+ }
+
+ private void insertRuleCatalog(String ruleKey, String description, String contextSeverity) {
+ try (DbSession session = db.getDbClient().openSession(false)) {
+ java.sql.PreparedStatement stmt = session.getConnection()
+ .prepareStatement(
+ "INSERT INTO cs_ai_rules_catalog (rule_key, description, context_severity) VALUES (?, ?, ?)");
+ stmt.setString(1, ruleKey);
+ stmt.setString(2, description);
+ stmt.setString(3, contextSeverity);
+ stmt.executeUpdate();
+ session.commit();
+ stmt.close();
+ } catch (java.sql.SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setUserWithValidPermission(ProjectData projectData) {
+ userSession.logIn("user")
+ .addProjectPermission(UserRole.USER, projectData.getProjectDto())
+ .addProjectPermission(UserRole.CODEVIEWER, projectData.getProjectDto());
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueContextAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueContextAction.java
new file mode 100644
index 000000000000..775f3cd76de3
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueContextAction.java
@@ -0,0 +1,369 @@
+/*
+ * 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.server.issue.ws;
+
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_CONTEXT;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEY;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CONTEXT_SEVERITY;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULE;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService.NewAction;
+import org.sonar.api.server.ws.WebService.NewController;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.protobuf.DbFileSources.Line;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.source.SourceService;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.ai.CsAiRuleCatalogDto;
+import org.sonar.db.ai.CsAiRulesCatalogMapper;
+import org.sonar.db.component.BranchDto;
+
+public class IssueContextAction implements IssuesWsAction {
+
+ private final UserSession userSession;
+ private final DbClient dbClient;
+ private final IssueFinder issueFinder;
+ private final ComponentFinder componentFinder;
+ private final SourceService sourceService;
+
+ public IssueContextAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder,
+ ComponentFinder componentFinder, SourceService sourceService) {
+ this.userSession = userSession;
+ this.dbClient = dbClient;
+ this.issueFinder = issueFinder;
+ this.componentFinder = componentFinder;
+ this.sourceService = sourceService;
+ }
+
+ @Override
+ public void define(NewController controller) {
+ NewAction action = controller.createAction(ACTION_CONTEXT).setDescription(
+ "Returns violation metadata and a contextual code snippet.")
+ .setSince("10.9").setHandler(this);
+
+ action.createParam(PARAM_ISSUE).setDescription("Issue key").setRequired(true)
+ .setExampleValue("AU-Tpxb--iU5OvuD2FLy");
+
+ action.createParam(PARAM_RULE).setDescription("Optional rule key. If not provided, taken from the issue")
+ .setRequired(false).setExampleValue("java:S100");
+
+ action.createParam(PARAM_COMPONENT_KEY)
+ .setDescription("Optional component key. If not provided, taken from the issue")
+ .setRequired(false).setExampleValue("abc/sample.cls");
+
+ action.createParam(PARAM_CONTEXT_SEVERITY)
+ .setDescription("Optional severity of context. If not provided, taken from the issue")
+ .setRequired(false).setExampleValue("MEDIUM");
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ userSession.checkLoggedIn();
+ String issueKey = request.mandatoryParam(PARAM_ISSUE);
+
+ try (DbSession dbSession = dbClient.openSession(false); JsonWriter json = response.newJsonWriter()) {
+ IssueDto issue = issueFinder.getByKey(dbSession, issueKey);
+ ComponentDto componentDto = componentFinder.getByKey(dbSession, issue.getComponentKey());
+ userSession.checkComponentPermission(UserRole.CODEVIEWER, componentDto);
+
+ RuleCatalogInfo catalogInfo = lookupRuleCatalog(dbSession, issue.getRuleRepo(), issue.getRule());
+ int centerLine = getCenterLine(issue);
+ SnippetRange snippetRange = determineSnippetRange(dbSession, componentDto, centerLine,
+ catalogInfo.contextSeverity);
+ String snippet = extractSnippet(dbSession, componentDto, snippetRange);
+ String ruleKey = getRuleKey(request, issue);
+
+ // Resolve branch name
+ String branchName = null;
+ BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, issue.getProjectUuid()).orElse(null);
+ if (branchDto != null) {
+ branchName = branchDto.isMain() ? "main" : branchDto.getBranchKey();
+ }
+
+ writeResponse(json, issue, ruleKey, catalogInfo, snippet, branchName, snippetRange);
+ }
+ }
+
+ private String getRuleKey(Request request, IssueDto issue) {
+ String ruleParam = request.param("rule");
+ if (ruleParam != null) {
+ return ruleParam;
+ }
+ if (issue.getRuleRepo() != null && issue.getRule() != null) {
+ return issue.getRuleRepo() + ":" + issue.getRule();
+ }
+ return null;
+ }
+
+ private RuleCatalogInfo lookupRuleCatalog(DbSession dbSession, String language, String ruleKey) {
+ if (ruleKey == null) {
+ return new RuleCatalogInfo("", "");
+ }
+ CsAiRulesCatalogMapper mapper = dbSession.getMapper(CsAiRulesCatalogMapper.class);
+ CsAiRuleCatalogDto dto = mapper.selectByRuleKey(language, ruleKey);
+ if (dto == null) {
+ return new RuleCatalogInfo("", "");
+ }
+ return new RuleCatalogInfo(dto.getDescription(), dto.getContextSeverity());
+ }
+
+ private int getCenterLine(IssueDto issue) {
+ Integer line = issue.getLine();
+ return line != null && line > 0 ? line : 1;
+ }
+
+ private SnippetRange determineSnippetRange(DbSession dbSession, ComponentDto file, int centerLine,
+ String contextSeverity) {
+ if ("MEDIUM".equalsIgnoreCase(contextSeverity) || "HIGH".equalsIgnoreCase(contextSeverity)) {
+ List contents = loadFileContents(dbSession, file);
+ int[] range = "HIGH".equalsIgnoreCase(contextSeverity)
+ ? findEnclosingClassRange(centerLine, contents)
+ : findEnclosingMethodRange(centerLine, contents);
+ return new SnippetRange(Math.max(1, range[0]), Math.max(range[0], range[1]));
+ }
+ return new SnippetRange(Math.max(1, centerLine - 5), centerLine + 5);
+ }
+
+ private List loadFileContents(DbSession dbSession, ComponentDto file) {
+ Optional> allLinesOpt = sourceService.getLines(dbSession, file.uuid(), 1, Integer.MAX_VALUE);
+ List contents = new ArrayList<>();
+ if (allLinesOpt.isPresent()) {
+ for (Line l : allLinesOpt.get()) {
+ contents.add(l.getSource());
+ }
+ }
+ return contents;
+ }
+
+ private String extractSnippet(DbSession dbSession, ComponentDto file, SnippetRange range) {
+ Optional> optLines = sourceService.getLines(dbSession, file.uuid(), range.from, range.to);
+ StringBuilder snippetBuilder = new StringBuilder();
+ int counter = 1;
+ if (optLines.isPresent()) {
+ boolean first = true;
+ for (Line l : optLines.get()) {
+ if (!first) {
+ snippetBuilder.append('\n');
+ }
+ first = false;
+ snippetBuilder.append("line ").append(counter++).append(" - ").append(l.getSource());
+ }
+ }
+ return snippetBuilder.toString();
+ }
+
+ private void writeResponse(JsonWriter json, IssueDto issueDto, String ruleKey, RuleCatalogInfo catalogInfo,
+ String snippet, String branchName, SnippetRange snippetRange) {
+ // Derive projectKey and sourceFolder from componentKey
+ DerivedComponentInfo componentInfo = deriveFromComponentKey(issueDto.getComponentKey());
+ int centerLine = getCenterLine(issueDto);
+
+ json.beginObject();
+ json.prop("repo_full_name", issueDto.getComponentKey());
+ json.prop("branch", branchName);
+ json.prop("ruleKey", ruleKey);
+ json.prop("issueKey", issueDto.getKey());
+ json.prop("target_file", componentInfo.sourceFolder);
+ json.prop("violation_lines", centerLine - snippetRange.from + "");
+ json.prop("guard_policy", "loose");
+ json.prop("sourceSnippetStartLine", snippetRange.from);
+ json.prop("sourceSnippetEndLine", snippetRange.to);
+ json.prop("snippetViolationLine", (long) centerLine - snippetRange.from);
+ json.prop("projectKey", componentInfo.projectKey);
+ if (catalogInfo.description != null) {
+ json.prop("ruleDescription", catalogInfo.description);
+ }
+ if (catalogInfo.contextSeverity != null) {
+ json.prop("contextSeverity", catalogInfo.contextSeverity);
+ }
+ json.prop("automation_mode", false);
+ json.prop("codesnippet", snippet);
+ json.endObject();
+ }
+
+ private DerivedComponentInfo deriveFromComponentKey(String componentKey) {
+ if (componentKey == null) {
+ return new DerivedComponentInfo(null, null);
+ }
+ int sep = componentKey.indexOf(':');
+ String proj = sep > 0 ? componentKey.substring(0, sep) : "";
+ String path = sep > 0 && sep + 1 < componentKey.length() ? componentKey.substring(sep + 1) : null;
+ String folder = "";
+ if (path != null) {
+ int slashIdx = path.lastIndexOf('/');
+ if (slashIdx > 0) {
+ folder = path.substring(0, slashIdx);
+ } else {
+ folder = "";
+ }
+ }
+ return new DerivedComponentInfo(proj, folder);
+ }
+
+ private record DerivedComponentInfo(String projectKey, String sourceFolder) {
+
+ }
+
+ private record RuleCatalogInfo(String description, String contextSeverity) {
+
+ }
+
+ private record SnippetRange(int from, int to) {
+
+ }
+
+ private int[] findEnclosingMethodRange(int centerLine, List lines) {
+ int n = lines.size();
+ int i = Math.max(1, centerLine);
+ // 1) find a likely method signature line above the center line
+ int signatureLine = -1;
+ for (int l = i; l >= 1; l--) {
+ String s = lines.get(l - 1).trim();
+ if (isLikelyMethodSignature(s)) {
+ signatureLine = l;
+ break;
+ }
+ }
+ if (signatureLine == -1) {
+ // Fallback to a small context when we cannot be confident
+ int from = Math.max(1, i - 5);
+ int to = Math.min(n, i + 5);
+ return new int[]{from, to};
+ }
+
+ // 2) from the signature line, find the opening brace on this or subsequent lines (to handle signatures broken across lines)
+ int openBraceLine = findOpeningBraceAtOrBelow(signatureLine, lines);
+ if (openBraceLine == -1) {
+ // Likely a language without braces (e.g. Python) or unformatted source, fallback
+ int from = Math.max(1, signatureLine);
+ int to = Math.min(n, signatureLine + 20);
+ return new int[]{from, to};
+ }
+
+ // 3) match braces to find the end of the method block
+ int methodEnd = Math.max(openBraceLine, findMatchingBraceDownwards(openBraceLine, lines));
+ return new int[]{openBraceLine, methodEnd};
+ }
+
+ private int[] findEnclosingClassRange(int centerLine, List lines) {
+ int i = Math.max(1, centerLine);
+ // Search upwards for a line hinting a class declaration
+ int classStartCandidate = i;
+ for (int l = i; l >= 1; l--) {
+ String s = lines.get(l - 1).trim();
+ if (s.matches(".*\\bclass\\b.*\\{.*") || s.startsWith("class ") || s.matches(".*\\bclass\\b.*(:|$)")) {
+ classStartCandidate = l;
+ break;
+ }
+ }
+ int classStart = Math.max(1, findOpeningBraceAtOrAbove(classStartCandidate, lines));
+ int classEnd = Math.max(classStart, findMatchingBraceDownwards(classStart, lines));
+ return new int[]{classStart, classEnd};
+ }
+
+ private int findOpeningBraceAtOrAbove(int fromLine, List lines) {
+ for (int l = fromLine; l >= 1; l--) {
+ String s = lines.get(l - 1);
+ int idx = s.indexOf('{');
+ if (idx >= 0) {
+ return l;
+ }
+ }
+ return Math.max(1, fromLine - 5);
+ }
+
+ private int findOpeningBraceAtOrBelow(int fromLine, List lines) {
+ int n = lines.size();
+ for (int l = fromLine; l <= n; l++) {
+ String s = lines.get(l - 1);
+ if (s.indexOf('{') >= 0) {
+ return l;
+ }
+ // stop early if we hit another declaration to avoid spanning too far
+ String t = s.trim();
+ if (isLikelyMethodSignature(t) || t.contains(" class ")) {
+ break;
+ }
+ }
+ return -1;
+ }
+
+ private int findMatchingBraceDownwards(int startLineWithBrace, List lines) {
+ int depth = 0;
+ boolean started = false;
+ int n = lines.size();
+ for (int l = startLineWithBrace; l <= n; l++) {
+ String s = lines.get(l - 1);
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (ch == '{') {
+ depth++;
+ started = true;
+ } else if (ch == '}') {
+ depth--;
+ if (started && depth == 0) {
+ return l;
+ }
+ }
+ }
+ }
+ return Math.min(n, startLineWithBrace + 50);
+ }
+
+ private boolean isLikelyMethodSignature(String trimmed) {
+ if (trimmed.isEmpty()) {
+ return false;
+ }
+ // Exclude common control statements to avoid capturing blocks like if/for/while/switch/catch
+ String lower = trimmed.toLowerCase();
+ if (lower.startsWith("if ") || lower.startsWith("if(")
+ || lower.startsWith("for ") || lower.startsWith("for(")
+ || lower.startsWith("while ") || lower.startsWith("while(")
+ || lower.startsWith("switch ") || lower.startsWith("switch(")
+ || lower.startsWith("catch ") || lower.startsWith("catch(")
+ || lower.startsWith("else") || lower.startsWith("do ") || lower.equals("do")) {
+ return false;
+ }
+ // Heuristic for method: contains '(' and ')' and not ending with ';' (to exclude declarations) and often followed by '{'
+ if (trimmed.contains("(") && trimmed.contains(")") && !trimmed.endsWith(";")) {
+ // Also ensure there's an identifier before '('
+ int idx = trimmed.indexOf('(');
+ if (idx > 0) {
+ char prev = trimmed.charAt(idx - 1);
+ if (Character.isJavaIdentifierPart(prev)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
index 2bae0e111806..658147f4d051 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
@@ -88,7 +88,8 @@ protected void configureModule() {
AnticipatedTransitionParser.class,
AnticipatedTransitionHandler.class,
AnticipatedTransitionsActionValidator.class,
- AnticipatedTransitionsAction.class
+ AnticipatedTransitionsAction.class,
+ IssueContextAction.class
);
}
}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
index a181cc62b376..cf6cbef3b0cb 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
@@ -42,6 +42,7 @@ public class IssuesWsParameters {
public static final String ACTION_PULL = "pull";
public static final String ACTION_PULL_TAINT = "pull_taint";
public static final String ACTION_ANTICIPATED_TRANSITIONS = "anticipated_transitions";
+ public static final String ACTION_CONTEXT = "context";
public static final String PARAM_ISSUE = "issue";
public static final String PARAM_IMPACT = "impact";
@@ -64,14 +65,17 @@ public class IssuesWsParameters {
public static final String PARAM_RESOLVED = "resolved";
public static final String PARAM_PRIORITIZED_RULE = "prioritizedRule";
public static final String PARAM_COMPONENTS = "components";
+ public static final String PARAM_COMPONENT_KEY = "componentKey";
public static final String PARAM_COMPONENT_KEYS = "componentKeys";
public static final String PARAM_COMPONENT_UUIDS = "componentUuids";
+ public static final String PARAM_CONTEXT_SEVERITY = "contextSeverity";
public static final String PARAM_PROJECTS = "projects";
public static final String PARAM_DIRECTORIES = "directories";
public static final String PARAM_FILES = "files";
public static final String PARAM_ON_COMPONENT_ONLY = "onComponentOnly";
public static final String PARAM_BRANCH = "branch";
public static final String PARAM_PULL_REQUEST = "pullRequest";
+ public static final String PARAM_RULE = "rule";
public static final String PARAM_RULES = "rules";
public static final String PARAM_ASSIGN = "assign";
public static final String PARAM_SET_SEVERITY = "set_severity";