From 8e275dad2a5b89ad568f5e050dba731bc8f35d6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:01:00 +0000 Subject: [PATCH 1/2] fix: address formal review findings from all 14 review-sets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Apply Uri.EscapeDataString to projectKey in SonarQubeClient.cs (4 locations) - Fix HttpClient resource leak in Validation.cs (ownsHttpClient false→true) - Add DirectoryNotFoundException to TemporaryDirectory.Dispose() catch - Fix requirements traceability in program.md, sonar-qube-client.md, sonar-quality-result.md Agent-Logs-Url: https://github.com/demaconsulting/SonarMark/sessions/cdc7a37d-c22b-4d98-b5e0-a898c2d9e2d8 Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- docs/design/sonar-mark/program.md | 5 +---- .../sonar-mark/report-generation/sonar-quality-result.md | 3 --- .../sonar-mark/sonar-integration/sonar-qube-client.md | 4 ---- src/DemaConsulting.SonarMark/SelfTest/Validation.cs | 4 ++-- .../SonarIntegration/SonarQubeClient.cs | 8 ++++---- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/design/sonar-mark/program.md b/docs/design/sonar-mark/program.md index 98ea7a9..0e46f61 100644 --- a/docs/design/sonar-mark/program.md +++ b/docs/design/sonar-mark/program.md @@ -42,11 +42,8 @@ before attempting any network call. Missing required parameters are reported via - `SonarMark-Cli-Help` — `Run` prints help and returns when `context.Help` is true - `SonarMark-Server-Connect` — `ProcessSonarAnalysis` validates the `--server` parameter - `SonarMark-Server-Auth` — `SonarQubeClient` is created with the token from context -- `SonarMark-Server-QualityGate` — `GetQualityResultByBranchAsync` fetches quality gate status -- `SonarMark-Server-Issues` — quality result includes the issues collection -- `SonarMark-Server-HotSpots` — quality result includes the hot-spots collection - `SonarMark-Server-ProjectKey` — `ProcessSonarAnalysis` validates the `--project-key` parameter - `SonarMark-Server-Branch` — branch is passed through to `GetQualityResultByBranchAsync` - `SonarMark-Report-Markdown` — `File.WriteAllText` writes the markdown report when `--report` is set -- `SonarMark-Validation-Run` — `Run` delegates to `Validation.Run` when `context.Validate` is true +- `SonarMark-Report-Depth` — `context.ReportDepth` is passed to `ToMarkdown` and shown in help text - `SonarMark-Enforce-ExitCode` — `context.ExitCode` is returned from `Main` diff --git a/docs/design/sonar-mark/report-generation/sonar-quality-result.md b/docs/design/sonar-mark/report-generation/sonar-quality-result.md index 098a7d5..b70ca98 100644 --- a/docs/design/sonar-mark/report-generation/sonar-quality-result.md +++ b/docs/design/sonar-mark/report-generation/sonar-quality-result.md @@ -37,9 +37,6 @@ output and parsed by tools that understand that format. ## Satisfies Requirements -- `SonarMark-Server-QualityGate` — `QualityGateStatus` and `Conditions` hold the quality gate data -- `SonarMark-Server-Issues` — `Issues` holds the list of fetched issues -- `SonarMark-Server-HotSpots` — `HotSpots` holds the list of fetched hot-spots - `SonarMark-Report-Markdown` — `ToMarkdown` generates the markdown report content - `SonarMark-Report-Depth` — the `depth` parameter controls heading levels - `SonarMark-Report-QualityGate` — quality gate status and conditions are included in the report diff --git a/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md b/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md index fd5e56a..58fdbe7 100644 --- a/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md +++ b/docs/design/sonar-mark/sonar-integration/sonar-qube-client.md @@ -42,10 +42,6 @@ errors from network errors and report them appropriately. ## Satisfies Requirements -- `SonarMark-Server-Connect` — establishes HTTP connections to the SonarQube/SonarCloud server -- `SonarMark-Server-Auth` — applies token-based Basic Authentication to all requests - `SonarMark-Server-QualityGate` — fetches quality gate status and conditions from the API - `SonarMark-Server-Issues` — fetches issues with pagination from the API - `SonarMark-Server-HotSpots` — fetches security hot-spots with pagination from the API -- `SonarMark-Server-ProjectKey` — passes the project key as a query parameter on all requests -- `SonarMark-Server-Branch` — passes the branch name as an optional query parameter diff --git a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs index 43ce98e..c3df308 100644 --- a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs +++ b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs @@ -64,7 +64,7 @@ public static void Run(Context context) }; // Create mock HTTP client factory - var mockFactory = (string? _) => new SonarQubeClient(CreateMockHttpClient(), false); + var mockFactory = (string? _) => new SonarQubeClient(CreateMockHttpClient(), true); // Run core functionality tests RunQualityGateRetrievalTest(context, testResults, mockFactory); @@ -620,7 +620,7 @@ public void Dispose() Directory.Delete(DirectoryPath, true); } } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException) { // Ignore cleanup errors during disposal } diff --git a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs index c812aed..fa1d3d1 100644 --- a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs +++ b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs @@ -129,7 +129,7 @@ private async Task GetProjectNameByKeyAsync( CancellationToken cancellationToken) { // Build API URL for component information - var url = $"{serverUrl.TrimEnd('/')}/api/components/show?component={projectKey}"; + var url = $"{serverUrl.TrimEnd('/')}/api/components/show?component={Uri.EscapeDataString(projectKey)}"; // Fetch component data from server using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); @@ -177,7 +177,7 @@ private async Task GetProjectNameByKeyAsync( CancellationToken cancellationToken) { // Build API URL with project key and optional branch parameter - var url = $"{serverUrl.TrimEnd('/')}/api/qualitygates/project_status?projectKey={projectKey}"; + var url = $"{serverUrl.TrimEnd('/')}/api/qualitygates/project_status?projectKey={Uri.EscapeDataString(projectKey)}"; if (!string.IsNullOrWhiteSpace(branch)) { url += $"&branch={Uri.EscapeDataString(branch)}"; @@ -272,7 +272,7 @@ private async Task> GetIssuesAsync( { // Build base API URL with project key and issue statuses filter var baseUrl = - $"{serverUrl.TrimEnd('/')}/api/issues/search?componentKeys={projectKey}&issueStatuses=OPEN,CONFIRMED&ps=100"; + $"{serverUrl.TrimEnd('/')}/api/issues/search?componentKeys={Uri.EscapeDataString(projectKey)}&issueStatuses=OPEN,CONFIRMED&ps=100"; if (!string.IsNullOrWhiteSpace(branch)) { baseUrl += $"&branch={Uri.EscapeDataString(branch)}"; @@ -339,7 +339,7 @@ private async Task> GetHotSpotsAsync( CancellationToken cancellationToken) { // Build base API URL with project key and page size - var baseUrl = $"{serverUrl.TrimEnd('/')}/api/hotspots/search?projectKey={projectKey}&ps=100"; + var baseUrl = $"{serverUrl.TrimEnd('/')}/api/hotspots/search?projectKey={Uri.EscapeDataString(projectKey)}&ps=100"; if (!string.IsNullOrWhiteSpace(branch)) { baseUrl += $"&branch={Uri.EscapeDataString(branch)}"; From 96265d14b29dfe96bae1b008fcc923e8ba2d9fcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:14:26 +0000 Subject: [PATCH 2/2] fix: resolve SonarQube static analysis complaints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add JsonContentType constant in Validation.cs to replace 5 repeated 'application/json' literals - Extract HasMorePages helper from FetchPaginatedAsync to reduce cognitive complexity from 16 to ≤15 Agent-Logs-Url: https://github.com/demaconsulting/SonarMark/sessions/471c3d8f-8f03-4821-a918-d8a520a8fcfc Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- .../SelfTest/Validation.cs | 15 ++++--- .../SonarIntegration/SonarQubeClient.cs | 39 +++++++++++-------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs index c3df308..636f38d 100644 --- a/src/DemaConsulting.SonarMark/SelfTest/Validation.cs +++ b/src/DemaConsulting.SonarMark/SelfTest/Validation.cs @@ -33,6 +33,11 @@ namespace DemaConsulting.SonarMark.SelfTest; /// internal static class Validation { + /// + /// MIME type for JSON content used in mock HTTP responses. + /// + private const string JsonContentType = "application/json"; + /// /// Special project key used for mock validation data. /// @@ -461,7 +466,7 @@ protected override Task SendAsync(HttpRequestMessage reques """; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, JsonContentType) }); } @@ -493,7 +498,7 @@ protected override Task SendAsync(HttpRequestMessage reques """; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, JsonContentType) }); } @@ -526,7 +531,7 @@ protected override Task SendAsync(HttpRequestMessage reques """; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, JsonContentType) }); } @@ -549,7 +554,7 @@ protected override Task SendAsync(HttpRequestMessage reques """; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, JsonContentType) }); } @@ -572,7 +577,7 @@ protected override Task SendAsync(HttpRequestMessage reques """; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(json, Encoding.UTF8, JsonContentType) }); } diff --git a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs index fa1d3d1..17aacd5 100644 --- a/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs +++ b/src/DemaConsulting.SonarMark/SonarIntegration/SonarQubeClient.cs @@ -401,31 +401,38 @@ private async Task> FetchPaginatedAsync( break; } - var pageIndex = pagingElement.TryGetProperty("pageIndex", out var pageIndexElement) - ? pageIndexElement.GetInt32() - : pageNumber; - var pageSize = pagingElement.TryGetProperty("pageSize", out var pageSizeElement) - ? pageSizeElement.GetInt32() - : 100; - var total = pagingElement.TryGetProperty("total", out var totalElement) - ? totalElement.GetInt32() - : 0; - - // Stop when all pages have been retrieved - if (total > pageIndex * pageSize) - { - pageNumber++; - } - else + if (!HasMorePages(pagingElement, pageNumber)) { break; } + + pageNumber++; } while (true); return allItems; } + /// + /// Determines whether additional pages remain based on the paging element of a SonarQube API response. + /// + /// The paging JSON element from the API response. + /// The current page number (used as fallback when pageIndex is absent). + /// True if more pages remain; otherwise false. + private static bool HasMorePages(JsonElement pagingElement, int pageNumber) + { + var pageIndex = pagingElement.TryGetProperty("pageIndex", out var pageIndexElement) + ? pageIndexElement.GetInt32() + : pageNumber; + var pageSize = pagingElement.TryGetProperty("pageSize", out var pageSizeElement) + ? pageSizeElement.GetInt32() + : 100; + var total = pagingElement.TryGetProperty("total", out var totalElement) + ? totalElement.GetInt32() + : 0; + return total > pageIndex * pageSize; + } + /// /// Parses hot-spots from a JSON array element ///