From efea2e43ec57a4d9ba03f2fe14a2815465a0d799 Mon Sep 17 00:00:00 2001 From: neha naazneen Date: Mon, 3 Nov 2025 10:09:24 +0530 Subject: [PATCH 1/4] CD-7372 API for Code Snippet Context Extraction Based on Rule Complexity --- .../src/main/java/org/sonar/db/MyBatis.java | 4 + .../org/sonar/db/ai/CsAiRuleCatalogDto.java | 60 ++ .../sonar/db/ai/CsAiRulesCatalogMapper.java | 36 ++ .../java/org/sonar/db/ai/package-info.java | 23 + .../sonar/db/ai/CsAiRulesCatalogMapper.xml | 24 + server/sonar-web/src/main/js/api/issues.ts | 14 + .../server/issue/ws/IssueContextActionIT.java | 518 ++++++++++++++++++ .../server/issue/ws/IssueContextAction.java | 318 +++++++++++ .../sonar/server/issue/ws/IssueWsModule.java | 3 +- .../ws/client/issue/IssuesWsParameters.java | 1 + 10 files changed, 1000 insertions(+), 1 deletion(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRuleCatalogDto.java create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRulesCatalogMapper.java create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/ai/package-info.java create mode 100644 server/sonar-db-dao/src/main/resources/org/sonar/db/ai/CsAiRulesCatalogMapper.xml create mode 100644 server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueContextActionIT.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueContextAction.java 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..ae4944181b5b 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 @@ -164,6 +164,8 @@ import org.sonar.db.rule.RuleMapper; import org.sonar.db.rule.RuleParamDto; import org.sonar.db.rule.RuleRepositoryMapper; +import org.sonar.db.ai.CsAiRuleCatalogDto; +import org.sonar.db.ai.CsAiRulesCatalogMapper; import org.sonar.db.scannercache.ScannerAnalysisCacheMapper; import org.sonar.db.schemamigration.SchemaMigrationDto; import org.sonar.db.schemamigration.SchemaMigrationMapper; @@ -282,10 +284,12 @@ public void start() { confBuilder.loadAlias("UserTokenCount", UserTokenCount.class); confBuilder.loadAlias("UuidWithBranchUuid", UuidWithBranchUuidDto.class); confBuilder.loadAlias("ViewsSnapshot", ViewsSnapshotDto.class); + confBuilder.loadAlias("CsAiRuleCatalog", CsAiRuleCatalogDto.class); confExtensions.forEach(ext -> ext.loadAliases(confBuilder::loadAlias)); // keep them sorted alphabetically Class[] mappers = { + CsAiRulesCatalogMapper.class, ActiveRuleMapper.class, AlmPatMapper.class, AlmSettingMapper.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..6181ec338f95 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRuleCatalogDto.java @@ -0,0 +1,60 @@ +/* + * 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 description; + private String contextSeverity; + + public String getRuleKey() { + return ruleKey; + } + + public CsAiRuleCatalogDto setRuleKey(String ruleKey) { + this.ruleKey = ruleKey; + 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..23b8d12f6d80 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/ai/CsAiRulesCatalogMapper.java @@ -0,0 +1,36 @@ +/* + * 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("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..4b6407a0929d --- /dev/null +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/ai/CsAiRulesCatalogMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + 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..36576b585f41 --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueContextActionIT.java @@ -0,0 +1,518 @@ +/* + * 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..f104af54f495 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueContextAction.java @@ -0,0 +1,318 @@ +/* + * 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_ISSUE; + +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; + +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("rule").setDescription("Optional rule key. If not provided, taken from the issue") + .setRequired(false).setExampleValue("java:S100"); + + action.createParam("Component Key").setDescription("Optional component key. If not provided, taken from the issue") + .setRequired(false).setExampleValue("abc/sample.cls"); + + action.createParam("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 file = componentFinder.getByKey(dbSession, issue.getComponentKey()); + userSession.checkComponentPermission(UserRole.CODEVIEWER, file); + + String ruleKey = getRuleKey(request, issue); + RuleCatalogInfo catalogInfo = lookupRuleCatalog(dbSession, ruleKey); + int centerLine = getCenterLine(issue); + SnippetRange snippetRange = determineSnippetRange(dbSession, file, centerLine, catalogInfo.contextSeverity); + String snippet = extractSnippet(dbSession, file, snippetRange); + + writeResponse(json, issueKey, ruleKey, issue.getComponentKey(), centerLine, catalogInfo, snippet); + } + } + + 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 ruleKey) { + if (ruleKey == null) { + return new RuleCatalogInfo(null, null); + } + CsAiRulesCatalogMapper mapper = dbSession.getMapper(CsAiRulesCatalogMapper.class); + CsAiRuleCatalogDto dto = mapper.selectByRuleKey(ruleKey); + if (dto == null) { + return new RuleCatalogInfo(null, null); + } + 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(); + if (optLines.isPresent()) { + boolean first = true; + for (Line l : optLines.get()) { + if (!first) { + snippetBuilder.append('\n'); + } + first = false; + snippetBuilder.append(l.getSource()); + } + } + return snippetBuilder.toString(); + } + + private void writeResponse(JsonWriter json, String issueKey, String ruleKey, String componentKey, + int centerLine, RuleCatalogInfo catalogInfo, String snippet) { + json.beginObject(); + json.prop("issueKey", issueKey); + if (ruleKey != null) { + json.prop("ruleKey", ruleKey); + } + json.prop("componentKey", componentKey); + json.prop("line", centerLine); + if (catalogInfo.description != null) { + json.prop("ruleDescription", catalogInfo.description); + } + if (catalogInfo.contextSeverity != null) { + json.prop("contextSeverity", catalogInfo.contextSeverity); + } + json.prop("snippet", snippet); + json.endObject(); + } + + 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..66a6469650c6 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"; From 0ef487a3bbabc0694eb462d0b856a9fea555b424 Mon Sep 17 00:00:00 2001 From: neha naazneen Date: Mon, 3 Nov 2025 10:24:58 +0530 Subject: [PATCH 2/4] CD-7372 formatting changes --- .../src/main/java/org/sonar/db/MyBatis.java | 8 +- .../org/sonar/db/ai/CsAiRuleCatalogDto.java | 56 +- .../sonar/db/ai/CsAiRulesCatalogMapper.java | 10 +- .../sonar/db/ai/CsAiRulesCatalogMapper.xml | 7 - .../server/issue/ws/IssueContextActionIT.java | 916 +++++++++--------- .../server/issue/ws/IssueContextAction.java | 15 +- 6 files changed, 500 insertions(+), 512 deletions(-) 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 ae4944181b5b..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; @@ -164,8 +166,6 @@ import org.sonar.db.rule.RuleMapper; import org.sonar.db.rule.RuleParamDto; import org.sonar.db.rule.RuleRepositoryMapper; -import org.sonar.db.ai.CsAiRuleCatalogDto; -import org.sonar.db.ai.CsAiRulesCatalogMapper; import org.sonar.db.scannercache.ScannerAnalysisCacheMapper; import org.sonar.db.schemamigration.SchemaMigrationDto; import org.sonar.db.schemamigration.SchemaMigrationMapper; @@ -223,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); @@ -284,12 +285,10 @@ public void start() { confBuilder.loadAlias("UserTokenCount", UserTokenCount.class); confBuilder.loadAlias("UuidWithBranchUuid", UuidWithBranchUuidDto.class); confBuilder.loadAlias("ViewsSnapshot", ViewsSnapshotDto.class); - confBuilder.loadAlias("CsAiRuleCatalog", CsAiRuleCatalogDto.class); confExtensions.forEach(ext -> ext.loadAliases(confBuilder::loadAlias)); // keep them sorted alphabetically Class[] mappers = { - CsAiRulesCatalogMapper.class, ActiveRuleMapper.class, AlmPatMapper.class, AlmSettingMapper.class, @@ -307,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 index 6181ec338f95..37a320f64625 100644 --- 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 @@ -20,41 +20,35 @@ package org.sonar.db.ai; public class CsAiRuleCatalogDto { - private String ruleKey; - private String description; - private String contextSeverity; - - public String getRuleKey() { - return ruleKey; - } - - public CsAiRuleCatalogDto setRuleKey(String ruleKey) { - this.ruleKey = ruleKey; - 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; - } -} + private String ruleKey; + private String description; + private String contextSeverity; + public String getRuleKey() { + return ruleKey; + } + public CsAiRuleCatalogDto setRuleKey(String ruleKey) { + this.ruleKey = ruleKey; + 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 index 23b8d12f6d80..ae79617d853a 100644 --- 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 @@ -25,12 +25,6 @@ import org.apache.ibatis.annotations.Param; public interface CsAiRulesCatalogMapper { - CsAiRuleCatalogDto selectByRuleKey(@Param("ruleKey") String ruleKey); -} - - - - - - + CsAiRuleCatalogDto selectByRuleKey(@Param("ruleKey") String ruleKey); +} 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 index 4b6407a0929d..754060562e1b 100644 --- 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 @@ -15,10 +15,3 @@ - - - - - - - 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 index 36576b585f41..5a23dc6b3754 100644 --- 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 @@ -55,464 +55,466 @@ 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(); + @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()); } - 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); + + @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"); } - } - private void setUserWithValidPermission(ProjectData projectData) { - userSession.logIn("user") - .addProjectPermission(UserRole.USER, projectData.getProjectDto()) - .addProjectPermission(UserRole.CODEVIEWER, projectData.getProjectDto()); - } + @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 index f104af54f495..e669909f6376 100644 --- 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 @@ -72,10 +72,12 @@ public void define(NewController controller) { action.createParam("rule").setDescription("Optional rule key. If not provided, taken from the issue") .setRequired(false).setExampleValue("java:S100"); - action.createParam("Component Key").setDescription("Optional component key. If not provided, taken from the issue") + action.createParam("Component Key") + .setDescription("Optional component key. If not provided, taken from the issue") .setRequired(false).setExampleValue("abc/sample.cls"); - action.createParam("Context Severity").setDescription("Optional severity of context. If not provided, taken from the issue") + action.createParam("Context Severity") + .setDescription("Optional severity of context. If not provided, taken from the issue") .setRequired(false).setExampleValue("MEDIUM"); } @@ -127,12 +129,13 @@ private int getCenterLine(IssueDto issue) { return line != null && line > 0 ? line : 1; } - private SnippetRange determineSnippetRange(DbSession dbSession, ComponentDto file, int centerLine, String contextSeverity) { + 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); + ? 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); @@ -185,9 +188,11 @@ private void writeResponse(JsonWriter json, String issueKey, String ruleKey, Str } private record RuleCatalogInfo(String description, String contextSeverity) { + } private record SnippetRange(int from, int to) { + } private int[] findEnclosingMethodRange(int centerLine, List lines) { From 2d31a0b7e6d2953f7b4b20203bbe2603beefed8f Mon Sep 17 00:00:00 2001 From: neha naazneen Date: Tue, 11 Nov 2025 15:39:48 +0530 Subject: [PATCH 3/4] CD-7372 renamed incorrect param names for api --- .../org/sonar/server/issue/ws/IssueContextAction.java | 9 ++++++--- .../sonarqube/ws/client/issue/IssuesWsParameters.java | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) 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 index e669909f6376..5fd00f930cc5 100644 --- 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 @@ -20,7 +20,10 @@ 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; @@ -69,14 +72,14 @@ public void define(NewController controller) { action.createParam(PARAM_ISSUE).setDescription("Issue key").setRequired(true) .setExampleValue("AU-Tpxb--iU5OvuD2FLy"); - action.createParam("rule").setDescription("Optional rule key. If not provided, taken from the issue") + action.createParam(PARAM_RULE).setDescription("Optional rule key. If not provided, taken from the issue") .setRequired(false).setExampleValue("java:S100"); - action.createParam("Component Key") + action.createParam(PARAM_COMPONENT_KEY) .setDescription("Optional component key. If not provided, taken from the issue") .setRequired(false).setExampleValue("abc/sample.cls"); - action.createParam("Context Severity") + action.createParam(PARAM_CONTEXT_SEVERITY) .setDescription("Optional severity of context. If not provided, taken from the issue") .setRequired(false).setExampleValue("MEDIUM"); } 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 66a6469650c6..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 @@ -65,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"; From 97497647be90159d410010bfd8fb2e72bb67a440 Mon Sep 17 00:00:00 2001 From: neha naazneen Date: Sat, 29 Nov 2025 17:19:43 +0530 Subject: [PATCH 4/4] CD-7372 issue context api --- .../org/sonar/db/ai/CsAiRuleCatalogDto.java | 10 +++ .../sonar/db/ai/CsAiRulesCatalogMapper.java | 2 +- .../sonar/db/ai/CsAiRulesCatalogMapper.xml | 5 +- .../server/issue/ws/IssueContextAction.java | 85 ++++++++++++++----- 4 files changed, 78 insertions(+), 24 deletions(-) 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 index 37a320f64625..89ca4e00b8ba 100644 --- 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 @@ -22,6 +22,7 @@ public class CsAiRuleCatalogDto { private String ruleKey; + private String language; private String description; private String contextSeverity; @@ -34,6 +35,15 @@ public CsAiRuleCatalogDto setRuleKey(String ruleKey) { return this; } + public String getLanguage() { + return language; + } + + public CsAiRuleCatalogDto setLanguage(String language) { + this.language = language; + return this; + } + public String getDescription() { return description; } 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 index ae79617d853a..8faf3cf5ae9e 100644 --- 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 @@ -26,5 +26,5 @@ public interface CsAiRulesCatalogMapper { - CsAiRuleCatalogDto selectByRuleKey(@Param("ruleKey") String ruleKey); + CsAiRuleCatalogDto selectByRuleKey(@Param("language") String language, @Param("ruleKey") String ruleKey); } 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 index 754060562e1b..63723dd00009 100644 --- 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 @@ -4,14 +4,15 @@ + 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 index 5fd00f930cc5..775f3cd76de3 100644 --- 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 @@ -45,6 +45,7 @@ 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 { @@ -91,16 +92,24 @@ public void handle(Request request, Response response) throws Exception { try (DbSession dbSession = dbClient.openSession(false); JsonWriter json = response.newJsonWriter()) { IssueDto issue = issueFinder.getByKey(dbSession, issueKey); - ComponentDto file = componentFinder.getByKey(dbSession, issue.getComponentKey()); - userSession.checkComponentPermission(UserRole.CODEVIEWER, file); + ComponentDto componentDto = componentFinder.getByKey(dbSession, issue.getComponentKey()); + userSession.checkComponentPermission(UserRole.CODEVIEWER, componentDto); - String ruleKey = getRuleKey(request, issue); - RuleCatalogInfo catalogInfo = lookupRuleCatalog(dbSession, ruleKey); + RuleCatalogInfo catalogInfo = lookupRuleCatalog(dbSession, issue.getRuleRepo(), issue.getRule()); int centerLine = getCenterLine(issue); - SnippetRange snippetRange = determineSnippetRange(dbSession, file, centerLine, catalogInfo.contextSeverity); - String snippet = extractSnippet(dbSession, file, snippetRange); + 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, issueKey, ruleKey, issue.getComponentKey(), centerLine, catalogInfo, snippet); + writeResponse(json, issue, ruleKey, catalogInfo, snippet, branchName, snippetRange); } } @@ -115,14 +124,14 @@ private String getRuleKey(Request request, IssueDto issue) { return null; } - private RuleCatalogInfo lookupRuleCatalog(DbSession dbSession, String ruleKey) { + private RuleCatalogInfo lookupRuleCatalog(DbSession dbSession, String language, String ruleKey) { if (ruleKey == null) { - return new RuleCatalogInfo(null, null); + return new RuleCatalogInfo("", ""); } CsAiRulesCatalogMapper mapper = dbSession.getMapper(CsAiRulesCatalogMapper.class); - CsAiRuleCatalogDto dto = mapper.selectByRuleKey(ruleKey); + CsAiRuleCatalogDto dto = mapper.selectByRuleKey(language, ruleKey); if (dto == null) { - return new RuleCatalogInfo(null, null); + return new RuleCatalogInfo("", ""); } return new RuleCatalogInfo(dto.getDescription(), dto.getContextSeverity()); } @@ -158,6 +167,7 @@ private List loadFileContents(DbSession dbSession, ComponentDto file) { 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()) { @@ -165,31 +175,64 @@ private String extractSnippet(DbSession dbSession, ComponentDto file, SnippetRan snippetBuilder.append('\n'); } first = false; - snippetBuilder.append(l.getSource()); + snippetBuilder.append("line ").append(counter++).append(" - ").append(l.getSource()); } } return snippetBuilder.toString(); } - private void writeResponse(JsonWriter json, String issueKey, String ruleKey, String componentKey, - int centerLine, RuleCatalogInfo catalogInfo, String snippet) { + 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("issueKey", issueKey); - if (ruleKey != null) { - json.prop("ruleKey", ruleKey); - } - json.prop("componentKey", componentKey); - json.prop("line", centerLine); + 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("snippet", snippet); + 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) { }