@jonathan0823/pi-sonarqube 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api.d.ts +12 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +116 -26
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +2 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +285 -30
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +182 -52
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -4
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +29 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +160 -70
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +310 -59
- package/src/commands.ts +454 -66
- package/src/config.ts +84 -23
- package/src/index.ts +462 -94
- package/src/types.ts +29 -4
- package/src/ui.ts +271 -85
package/src/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
4
|
import type {
|
|
5
5
|
SonarIssue,
|
|
6
6
|
SonarAnalysisState,
|
|
@@ -12,7 +12,13 @@ import type {
|
|
|
12
12
|
FileDuplication,
|
|
13
13
|
DuplicationBlockGroup,
|
|
14
14
|
} from "./types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
SONAR_SEVERITIES,
|
|
17
|
+
SONAR_STATUSES,
|
|
18
|
+
SONAR_TYPES,
|
|
19
|
+
SONAR_SOFTWARE_QUALITIES,
|
|
20
|
+
SONAR_IMPACT_SEVERITIES,
|
|
21
|
+
} from "./types.js";
|
|
16
22
|
import { parseProperties } from "./config.js";
|
|
17
23
|
|
|
18
24
|
// ── Clean code mode detection ────────────────────────────────────────────────
|
|
@@ -51,7 +57,11 @@ export function authHeader(token?: string): string | undefined {
|
|
|
51
57
|
return `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
export async function fetchJson<T>(
|
|
60
|
+
export async function fetchJson<T>(
|
|
61
|
+
url: string,
|
|
62
|
+
token?: string,
|
|
63
|
+
signal?: AbortSignal,
|
|
64
|
+
): Promise<T> {
|
|
55
65
|
const headers: Record<string, string> = { Accept: "application/json" };
|
|
56
66
|
const auth = authHeader(token);
|
|
57
67
|
if (auth) headers.Authorization = auth;
|
|
@@ -60,14 +70,20 @@ export async function fetchJson<T>(url: string, token?: string, signal?: AbortSi
|
|
|
60
70
|
if (!response.ok) {
|
|
61
71
|
const body = await response.text().catch(() => "");
|
|
62
72
|
const suffix = body.trim() ? `: ${body.trim().slice(0, 240)}` : "";
|
|
63
|
-
throw new Error(
|
|
73
|
+
throw new Error(
|
|
74
|
+
`SonarQube API request failed (${response.status} ${response.statusText}) for ${url}${suffix}`,
|
|
75
|
+
);
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
return (await response.json()) as T;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
export function toIssuePath(
|
|
70
|
-
|
|
81
|
+
export function toIssuePath(
|
|
82
|
+
component: string | undefined | null,
|
|
83
|
+
projectKey: string,
|
|
84
|
+
): string {
|
|
85
|
+
if (typeof component !== "string" || component.length === 0)
|
|
86
|
+
return projectKey;
|
|
71
87
|
const prefix = `${projectKey}:`;
|
|
72
88
|
if (component.startsWith(prefix)) return component.slice(prefix.length);
|
|
73
89
|
const colon = component.indexOf(":");
|
|
@@ -76,7 +92,10 @@ export function toIssuePath(component: string | undefined | null, projectKey: st
|
|
|
76
92
|
|
|
77
93
|
// ── Issue filter helpers ────────────────────────────────────────────────────
|
|
78
94
|
|
|
79
|
-
export function normalizeIssueList(
|
|
95
|
+
export function normalizeIssueList(
|
|
96
|
+
values?: string[],
|
|
97
|
+
uppercase = false,
|
|
98
|
+
): string[] | undefined {
|
|
80
99
|
const cleaned = values
|
|
81
100
|
?.map((v) => v.trim())
|
|
82
101
|
.filter(Boolean)
|
|
@@ -85,7 +104,9 @@ export function normalizeIssueList(values?: string[], uppercase = false): string
|
|
|
85
104
|
return [...new Set(cleaned)];
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
export function normalizeIssueFilters(
|
|
107
|
+
export function normalizeIssueFilters(
|
|
108
|
+
filters?: SonarIssueFetchOptions,
|
|
109
|
+
): SonarIssueFetchOptions | undefined {
|
|
89
110
|
const normalized = {
|
|
90
111
|
severities: normalizeIssueList(filters?.severities, true),
|
|
91
112
|
statuses: normalizeIssueList(filters?.statuses, true),
|
|
@@ -93,13 +114,24 @@ export function normalizeIssueFilters(filters?: SonarIssueFetchOptions): SonarIs
|
|
|
93
114
|
rules: normalizeIssueList(filters?.rules),
|
|
94
115
|
softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
|
|
95
116
|
impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
|
|
117
|
+
pathScope: filters?.pathScope,
|
|
118
|
+
componentKeys: filters?.componentKeys,
|
|
96
119
|
};
|
|
97
|
-
return normalized.severities ||
|
|
120
|
+
return normalized.severities ||
|
|
121
|
+
normalized.statuses ||
|
|
122
|
+
normalized.types ||
|
|
123
|
+
normalized.rules ||
|
|
124
|
+
normalized.softwareQualities ||
|
|
125
|
+
normalized.impactSeverities ||
|
|
126
|
+
normalized.pathScope ||
|
|
127
|
+
normalized.componentKeys?.length
|
|
98
128
|
? normalized
|
|
99
129
|
: undefined;
|
|
100
130
|
}
|
|
101
131
|
|
|
102
|
-
export function assertFiltersNotAmbiguous(
|
|
132
|
+
export function assertFiltersNotAmbiguous(
|
|
133
|
+
filters?: SonarIssueFetchOptions,
|
|
134
|
+
): void {
|
|
103
135
|
if (!filters) return;
|
|
104
136
|
if (
|
|
105
137
|
(filters.severities?.length || filters.types?.length) &&
|
|
@@ -107,21 +139,26 @@ export function assertFiltersNotAmbiguous(filters?: SonarIssueFetchOptions): voi
|
|
|
107
139
|
) {
|
|
108
140
|
throw new Error(
|
|
109
141
|
"Ambiguous filter combination: mixing legacy filters (severity/type) with MQR filters (quality/impactSeverity) is not allowed. " +
|
|
110
|
-
|
|
142
|
+
"Use one filter family per command.",
|
|
111
143
|
);
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
const EXPLICIT_FILTER_RE = /^([^:=]+)[:=](.+)$/;
|
|
116
148
|
|
|
117
|
-
export function parseIssueFilterToken(
|
|
149
|
+
export function parseIssueFilterToken(
|
|
150
|
+
token: string,
|
|
151
|
+
): Partial<SonarIssueFetchOptions> | undefined {
|
|
118
152
|
const trimmed = token.trim();
|
|
119
153
|
if (!trimmed) return undefined;
|
|
120
154
|
|
|
121
155
|
const explicit = EXPLICIT_FILTER_RE.exec(trimmed);
|
|
122
156
|
if (explicit) {
|
|
123
157
|
const key = explicit[1].trim().toLowerCase();
|
|
124
|
-
const values = explicit[2]
|
|
158
|
+
const values = explicit[2]
|
|
159
|
+
.split(",")
|
|
160
|
+
.map((p) => p.trim())
|
|
161
|
+
.filter(Boolean);
|
|
125
162
|
if (values.length === 0) return undefined;
|
|
126
163
|
|
|
127
164
|
switch (key) {
|
|
@@ -145,24 +182,44 @@ export function parseIssueFilterToken(token: string): Partial<SonarIssueFetchOpt
|
|
|
145
182
|
case "impactseverity":
|
|
146
183
|
case "impactseverities":
|
|
147
184
|
return { impactSeverities: values.map((v) => v.toUpperCase()) };
|
|
185
|
+
case "in":
|
|
186
|
+
case "path":
|
|
187
|
+
return { pathScope: values[0] };
|
|
148
188
|
default:
|
|
149
189
|
return undefined;
|
|
150
190
|
}
|
|
151
191
|
}
|
|
152
192
|
|
|
153
193
|
const upper = trimmed.toUpperCase();
|
|
154
|
-
if (SONAR_SEVERITIES.includes(upper as (typeof SONAR_SEVERITIES)[number]))
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
if (
|
|
194
|
+
if (SONAR_SEVERITIES.includes(upper as (typeof SONAR_SEVERITIES)[number]))
|
|
195
|
+
return { severities: [upper] };
|
|
196
|
+
if (SONAR_STATUSES.includes(upper as (typeof SONAR_STATUSES)[number]))
|
|
197
|
+
return { statuses: [upper] };
|
|
198
|
+
if (SONAR_TYPES.includes(upper as (typeof SONAR_TYPES)[number]))
|
|
199
|
+
return { types: [upper] };
|
|
200
|
+
if (
|
|
201
|
+
SONAR_SOFTWARE_QUALITIES.includes(
|
|
202
|
+
upper as (typeof SONAR_SOFTWARE_QUALITIES)[number],
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return { softwareQualities: [upper] };
|
|
206
|
+
if (
|
|
207
|
+
SONAR_IMPACT_SEVERITIES.includes(
|
|
208
|
+
upper as (typeof SONAR_IMPACT_SEVERITIES)[number],
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
return { impactSeverities: [upper] };
|
|
159
212
|
return undefined;
|
|
160
213
|
}
|
|
161
214
|
|
|
162
215
|
export function parseSonarIssueArgs(
|
|
163
216
|
tokens: string[],
|
|
164
217
|
allowIssueIndex = false,
|
|
165
|
-
): {
|
|
218
|
+
): {
|
|
219
|
+
targetInput?: string;
|
|
220
|
+
issueIndex?: number;
|
|
221
|
+
filters?: SonarIssueFetchOptions;
|
|
222
|
+
} {
|
|
166
223
|
const filters: Array<Partial<SonarIssueFetchOptions> | undefined> = [];
|
|
167
224
|
let targetInput: string | undefined;
|
|
168
225
|
let issueIndex: number | undefined;
|
|
@@ -188,18 +245,26 @@ export function parseSonarIssueArgs(
|
|
|
188
245
|
return { targetInput, issueIndex, filters: mergeFilters(...filters) };
|
|
189
246
|
}
|
|
190
247
|
|
|
248
|
+
function mergeFilterField<T>(
|
|
249
|
+
merged: T[] | undefined,
|
|
250
|
+
incoming?: T[],
|
|
251
|
+
): T[] | undefined {
|
|
252
|
+
return incoming?.length ? [...(merged ?? []), ...incoming] : merged;
|
|
253
|
+
}
|
|
254
|
+
|
|
191
255
|
function mergeFilters(
|
|
192
256
|
...filters: Array<Partial<SonarIssueFetchOptions> | undefined>
|
|
193
257
|
): SonarIssueFetchOptions | undefined {
|
|
194
258
|
const merged: SonarIssueFetchOptions = {};
|
|
195
259
|
for (const filter of filters) {
|
|
196
260
|
if (!filter) continue;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
261
|
+
merged.severities = mergeFilterField(merged.severities, filter.severities);
|
|
262
|
+
merged.statuses = mergeFilterField(merged.statuses, filter.statuses);
|
|
263
|
+
merged.types = mergeFilterField(merged.types, filter.types);
|
|
264
|
+
merged.rules = mergeFilterField(merged.rules, filter.rules);
|
|
265
|
+
merged.softwareQualities = mergeFilterField(merged.softwareQualities, filter.softwareQualities);
|
|
266
|
+
merged.impactSeverities = mergeFilterField(merged.impactSeverities, filter.impactSeverities);
|
|
267
|
+
if (filter.pathScope) merged.pathScope = filter.pathScope;
|
|
203
268
|
}
|
|
204
269
|
return normalizeIssueFilters(merged);
|
|
205
270
|
}
|
|
@@ -207,15 +272,33 @@ function mergeFilters(
|
|
|
207
272
|
export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
|
|
208
273
|
if (!filters) return "";
|
|
209
274
|
const parts: string[] = [];
|
|
210
|
-
if (filters.severities?.length)
|
|
211
|
-
|
|
275
|
+
if (filters.severities?.length)
|
|
276
|
+
parts.push(`severities=${filters.severities.join(",")}`);
|
|
277
|
+
if (filters.statuses?.length)
|
|
278
|
+
parts.push(`statuses=${filters.statuses.join(",")}`);
|
|
212
279
|
if (filters.types?.length) parts.push(`types=${filters.types.join(",")}`);
|
|
213
280
|
if (filters.rules?.length) parts.push(`rules=${filters.rules.join(",")}`);
|
|
214
|
-
if (filters.softwareQualities?.length)
|
|
215
|
-
|
|
281
|
+
if (filters.softwareQualities?.length)
|
|
282
|
+
parts.push(`qualities=${filters.softwareQualities.join(",")}`);
|
|
283
|
+
if (filters.impactSeverities?.length)
|
|
284
|
+
parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
|
|
285
|
+
if (filters.pathScope) parts.push(`in:${filters.pathScope}`);
|
|
216
286
|
return parts.join(" • ");
|
|
217
287
|
}
|
|
218
288
|
|
|
289
|
+
export function resolvePathScope(
|
|
290
|
+
baseDir: string,
|
|
291
|
+
projectKey: string,
|
|
292
|
+
pathScope: string,
|
|
293
|
+
): string {
|
|
294
|
+
const raw = pathScope.replace(/^@/, "");
|
|
295
|
+
const absPath = resolve(baseDir, raw);
|
|
296
|
+
const relPath = relative(baseDir, absPath);
|
|
297
|
+
if (relPath === "" || relPath === ".") return projectKey;
|
|
298
|
+
const cleanRel = relPath.replace(/[\\/]+$/, "");
|
|
299
|
+
return `${projectKey}:${cleanRel}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
219
302
|
function severitySortRank(severity: string): number {
|
|
220
303
|
switch (severity.toUpperCase()) {
|
|
221
304
|
case "BLOCKER":
|
|
@@ -237,7 +320,8 @@ function severitySortRank(severity: string): number {
|
|
|
237
320
|
}
|
|
238
321
|
|
|
239
322
|
function compareIssuesForContext(left: SonarIssue, right: SonarIssue): number {
|
|
240
|
-
const severityDiff =
|
|
323
|
+
const severityDiff =
|
|
324
|
+
severitySortRank(left.severity) - severitySortRank(right.severity);
|
|
241
325
|
if (severityDiff !== 0) return severityDiff;
|
|
242
326
|
const fileDiff = left.filePath.localeCompare(right.filePath);
|
|
243
327
|
if (fileDiff !== 0) return fileDiff;
|
|
@@ -256,7 +340,11 @@ export function sonarScannerInstallHint(): string {
|
|
|
256
340
|
].join(" ");
|
|
257
341
|
}
|
|
258
342
|
|
|
259
|
-
export function extractScannerHint(
|
|
343
|
+
export function extractScannerHint(
|
|
344
|
+
output: string,
|
|
345
|
+
config: SonarProjectConfig,
|
|
346
|
+
exitCode?: number,
|
|
347
|
+
): string {
|
|
260
348
|
const lines = output.toLowerCase();
|
|
261
349
|
|
|
262
350
|
const isNotFoundOnPath =
|
|
@@ -268,7 +356,10 @@ export function extractScannerHint(output: string, config: SonarProjectConfig, e
|
|
|
268
356
|
|
|
269
357
|
const isUnauthorized = /status code 401|status 401|unauthorized/i.test(lines);
|
|
270
358
|
if (isUnauthorized) {
|
|
271
|
-
const defaultHint =
|
|
359
|
+
const defaultHint =
|
|
360
|
+
config.serverUrl === "http://localhost:9000"
|
|
361
|
+
? " (default: http://localhost:9000)"
|
|
362
|
+
: "";
|
|
272
363
|
return [
|
|
273
364
|
`Authentication failed for ${config.serverUrl}${defaultHint}.`,
|
|
274
365
|
"Run `/sonarqube init` to set up your server URL and token, then retry.",
|
|
@@ -285,7 +376,10 @@ export function extractScannerHint(output: string, config: SonarProjectConfig, e
|
|
|
285
376
|
].join(" ");
|
|
286
377
|
}
|
|
287
378
|
|
|
288
|
-
const isConnectionError =
|
|
379
|
+
const isConnectionError =
|
|
380
|
+
/connect econnrefused|connect refused|connect timeout|enotfound|econnreset/i.test(
|
|
381
|
+
lines,
|
|
382
|
+
);
|
|
289
383
|
if (isConnectionError) {
|
|
290
384
|
return [
|
|
291
385
|
`Cannot reach SonarQube server at ${config.serverUrl}.`,
|
|
@@ -294,7 +388,11 @@ export function extractScannerHint(output: string, config: SonarProjectConfig, e
|
|
|
294
388
|
].join(" ");
|
|
295
389
|
}
|
|
296
390
|
|
|
297
|
-
if (
|
|
391
|
+
if (
|
|
392
|
+
/not found|unknown url/i.test(lines) &&
|
|
393
|
+
(config.serverUrl === "http://localhost:9000" ||
|
|
394
|
+
/localhost/i.test(config.serverUrl))
|
|
395
|
+
) {
|
|
298
396
|
return [
|
|
299
397
|
`SonarQube API at ${config.serverUrl} returned "not found".`,
|
|
300
398
|
"The default server URL is http://localhost:9000. Run `/sonarqube init` to configure.",
|
|
@@ -306,7 +404,11 @@ export function extractScannerHint(output: string, config: SonarProjectConfig, e
|
|
|
306
404
|
|
|
307
405
|
// ── Scanner interaction ─────────────────────────────────────────────────────
|
|
308
406
|
|
|
309
|
-
export async function runScanner(
|
|
407
|
+
export async function runScanner(
|
|
408
|
+
pi: ExtensionAPI,
|
|
409
|
+
config: SonarProjectConfig,
|
|
410
|
+
signal?: AbortSignal,
|
|
411
|
+
): Promise<string> {
|
|
310
412
|
const scannerCheck =
|
|
311
413
|
process.platform === "win32"
|
|
312
414
|
? await pi.exec("cmd", ["/d", "/s", "/c", "where", "sonar-scanner"], {
|
|
@@ -356,7 +458,9 @@ export async function runScanner(pi: ExtensionAPI, config: SonarProjectConfig, s
|
|
|
356
458
|
return result.stdout || result.stderr;
|
|
357
459
|
}
|
|
358
460
|
|
|
359
|
-
export async function readReportTask(
|
|
461
|
+
export async function readReportTask(
|
|
462
|
+
baseDir: string,
|
|
463
|
+
): Promise<Record<string, string>> {
|
|
360
464
|
const path = resolve(baseDir, ".scannerwork", "report-task.txt");
|
|
361
465
|
const text = await readFile(path, "utf8");
|
|
362
466
|
return parseProperties(text);
|
|
@@ -375,7 +479,9 @@ export async function waitForAnalysis(
|
|
|
375
479
|
const status = result.task.status.toUpperCase();
|
|
376
480
|
if (status === "SUCCESS") return result.task.analysisId;
|
|
377
481
|
if (status === "FAILED") {
|
|
378
|
-
throw new Error(
|
|
482
|
+
throw new Error(
|
|
483
|
+
`SonarQube analysis failed: ${result.task.errorMessage || "unknown error"}`,
|
|
484
|
+
);
|
|
379
485
|
}
|
|
380
486
|
if (status === "CANCELED") {
|
|
381
487
|
throw new Error("SonarQube analysis was canceled by the server");
|
|
@@ -398,12 +504,29 @@ function buildIssueSearchUrl(
|
|
|
398
504
|
url.searchParams.set("resolved", "false");
|
|
399
505
|
url.searchParams.set("ps", "100");
|
|
400
506
|
url.searchParams.set("p", String(page));
|
|
401
|
-
if (filters?.severities?.length)
|
|
402
|
-
|
|
403
|
-
if (filters?.
|
|
404
|
-
|
|
405
|
-
if (filters?.
|
|
406
|
-
|
|
507
|
+
if (filters?.severities?.length)
|
|
508
|
+
url.searchParams.set("severities", filters.severities.join(","));
|
|
509
|
+
if (filters?.statuses?.length)
|
|
510
|
+
url.searchParams.set("statuses", filters.statuses.join(","));
|
|
511
|
+
if (filters?.types?.length)
|
|
512
|
+
url.searchParams.set("types", filters.types.join(","));
|
|
513
|
+
if (filters?.rules?.length)
|
|
514
|
+
url.searchParams.set("rules", filters.rules.join(","));
|
|
515
|
+
if (filters?.softwareQualities?.length)
|
|
516
|
+
url.searchParams.set(
|
|
517
|
+
"impactSoftwareQualities",
|
|
518
|
+
filters.softwareQualities.join(","),
|
|
519
|
+
);
|
|
520
|
+
if (filters?.impactSeverities?.length)
|
|
521
|
+
url.searchParams.set(
|
|
522
|
+
"impactSeverities",
|
|
523
|
+
filters.impactSeverities.join(","),
|
|
524
|
+
);
|
|
525
|
+
if (filters?.componentKeys?.length)
|
|
526
|
+
url.searchParams.set(
|
|
527
|
+
"componentKeys",
|
|
528
|
+
filters.componentKeys.join(","),
|
|
529
|
+
);
|
|
407
530
|
return url.toString();
|
|
408
531
|
}
|
|
409
532
|
|
|
@@ -439,7 +562,18 @@ export async function fetchIssuePage(
|
|
|
439
562
|
page: number,
|
|
440
563
|
signal?: AbortSignal,
|
|
441
564
|
filters?: SonarIssueFetchOptions,
|
|
442
|
-
): Promise<{
|
|
565
|
+
): Promise<{
|
|
566
|
+
total: number;
|
|
567
|
+
issueRows: Array<{
|
|
568
|
+
key: string;
|
|
569
|
+
rule: string;
|
|
570
|
+
severity: string;
|
|
571
|
+
message: string;
|
|
572
|
+
component: string;
|
|
573
|
+
line?: number;
|
|
574
|
+
status?: string;
|
|
575
|
+
}>;
|
|
576
|
+
}> {
|
|
443
577
|
const url = buildIssueSearchUrl(serverUrl, projectKey, page, filters);
|
|
444
578
|
const result = await fetchJson<{
|
|
445
579
|
total: number;
|
|
@@ -462,9 +596,19 @@ export async function fetchIssues(
|
|
|
462
596
|
projectKey: string,
|
|
463
597
|
signal?: AbortSignal,
|
|
464
598
|
filters?: SonarIssueFetchOptions,
|
|
599
|
+
baseDir?: string,
|
|
465
600
|
): Promise<SonarIssue[]> {
|
|
466
601
|
assertFiltersNotAmbiguous(filters);
|
|
467
602
|
const normalizedFilters = normalizeIssueFilters(filters);
|
|
603
|
+
|
|
604
|
+
if (normalizedFilters?.pathScope && baseDir) {
|
|
605
|
+
const componentKey = resolvePathScope(
|
|
606
|
+
baseDir,
|
|
607
|
+
projectKey,
|
|
608
|
+
normalizedFilters.pathScope,
|
|
609
|
+
);
|
|
610
|
+
normalizedFilters.componentKeys = [componentKey];
|
|
611
|
+
}
|
|
468
612
|
const issues: SonarIssue[] = [];
|
|
469
613
|
const ruleNames = new Map<string, string | undefined>();
|
|
470
614
|
let page = 1;
|
|
@@ -472,13 +616,21 @@ export async function fetchIssues(
|
|
|
472
616
|
|
|
473
617
|
for (;;) {
|
|
474
618
|
const { total: pageTotal, issueRows } = await fetchIssuePage(
|
|
475
|
-
serverUrl,
|
|
619
|
+
serverUrl,
|
|
620
|
+
token,
|
|
621
|
+
projectKey,
|
|
622
|
+
page,
|
|
623
|
+
signal,
|
|
624
|
+
normalizedFilters,
|
|
476
625
|
);
|
|
477
626
|
total = pageTotal;
|
|
478
627
|
|
|
479
628
|
for (const raw of issueRows) {
|
|
480
629
|
if (!ruleNames.has(raw.rule)) {
|
|
481
|
-
ruleNames.set(
|
|
630
|
+
ruleNames.set(
|
|
631
|
+
raw.rule,
|
|
632
|
+
await fetchRuleName(serverUrl, token, raw.rule, signal),
|
|
633
|
+
);
|
|
482
634
|
}
|
|
483
635
|
issues.push(mapRawIssue(raw, projectKey, ruleNames));
|
|
484
636
|
}
|
|
@@ -494,6 +646,62 @@ export async function fetchIssues(
|
|
|
494
646
|
return issues;
|
|
495
647
|
}
|
|
496
648
|
|
|
649
|
+
// ── Rule autocomplete helpers ───────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
export async function fetchProjectProfiles(
|
|
652
|
+
serverUrl: string,
|
|
653
|
+
token: string | undefined,
|
|
654
|
+
projectKey: string,
|
|
655
|
+
signal?: AbortSignal,
|
|
656
|
+
): Promise<Array<{key: string; language: string; name: string}>> {
|
|
657
|
+
try {
|
|
658
|
+
const result = await fetchJson<{
|
|
659
|
+
profiles: Array<{key?: string; language?: string; name?: string}>;
|
|
660
|
+
}>(
|
|
661
|
+
`${serverUrl}/api/qualityprofiles/search?project=${encodeURIComponent(projectKey)}`,
|
|
662
|
+
token,
|
|
663
|
+
signal,
|
|
664
|
+
);
|
|
665
|
+
return (result.profiles ?? []).map(p => ({
|
|
666
|
+
key: p.key ?? '',
|
|
667
|
+
language: p.language ?? '',
|
|
668
|
+
name: p.name ?? '',
|
|
669
|
+
}));
|
|
670
|
+
} catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export async function fetchRuleSearch(
|
|
676
|
+
serverUrl: string,
|
|
677
|
+
token: string | undefined,
|
|
678
|
+
query: string,
|
|
679
|
+
languages?: string[],
|
|
680
|
+
signal?: AbortSignal,
|
|
681
|
+
): Promise<Array<{key: string; name?: string}>> {
|
|
682
|
+
try {
|
|
683
|
+
const params = new URLSearchParams();
|
|
684
|
+
if (query) params.set("q", query);
|
|
685
|
+
params.set("ps", "20");
|
|
686
|
+
params.set("p", "1");
|
|
687
|
+
if (languages?.length) params.set("languages", languages.join(","));
|
|
688
|
+
|
|
689
|
+
const result = await fetchJson<{
|
|
690
|
+
rules: Array<{key?: string; name?: string}>;
|
|
691
|
+
}>(
|
|
692
|
+
`${serverUrl}/api/rules/search?${params.toString()}`,
|
|
693
|
+
token,
|
|
694
|
+
signal,
|
|
695
|
+
);
|
|
696
|
+
return (result.rules ?? []).map(r => ({
|
|
697
|
+
key: r.key ?? '',
|
|
698
|
+
name: r.name,
|
|
699
|
+
}));
|
|
700
|
+
} catch {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
497
705
|
export async function fetchRuleName(
|
|
498
706
|
serverUrl: string,
|
|
499
707
|
token: string | undefined,
|
|
@@ -517,7 +725,17 @@ export async function fetchRuleName(
|
|
|
517
725
|
export function createAnalysisState(
|
|
518
726
|
config: Pick<SonarProjectConfig, "baseDir" | "serverUrl" | "projectKey">,
|
|
519
727
|
issues: SonarIssue[],
|
|
520
|
-
extras: Partial<
|
|
728
|
+
extras: Partial<
|
|
729
|
+
Pick<
|
|
730
|
+
SonarAnalysisState,
|
|
731
|
+
| "dashboardUrl"
|
|
732
|
+
| "ceTaskUrl"
|
|
733
|
+
| "analysisId"
|
|
734
|
+
| "filters"
|
|
735
|
+
| "measures"
|
|
736
|
+
| "cleanCodeMode"
|
|
737
|
+
>
|
|
738
|
+
> = {},
|
|
521
739
|
): SonarAnalysisState {
|
|
522
740
|
return {
|
|
523
741
|
version: 1,
|
|
@@ -554,15 +772,20 @@ export async function fetchDuplicationMeasures(
|
|
|
554
772
|
const m = measures.find((m) => m.metric === key);
|
|
555
773
|
return m ? Number.parseFloat(m.value) : 0;
|
|
556
774
|
};
|
|
557
|
-
const hasMetric = (key: string): boolean =>
|
|
775
|
+
const hasMetric = (key: string): boolean =>
|
|
776
|
+
measures.some((m) => m.metric === key);
|
|
558
777
|
return {
|
|
559
778
|
duplicatedLinesDensity: getValue("duplicated_lines_density"),
|
|
560
779
|
duplicatedLines: getValue("duplicated_lines"),
|
|
561
780
|
duplicatedBlocks: getValue("duplicated_blocks"),
|
|
562
781
|
duplicatedFiles: getValue("duplicated_files"),
|
|
563
782
|
coverage: hasMetric("coverage") ? getValue("coverage") : undefined,
|
|
564
|
-
linesToCover: hasMetric("lines_to_cover")
|
|
565
|
-
|
|
783
|
+
linesToCover: hasMetric("lines_to_cover")
|
|
784
|
+
? getValue("lines_to_cover")
|
|
785
|
+
: undefined,
|
|
786
|
+
uncoveredLines: hasMetric("uncovered_lines")
|
|
787
|
+
? getValue("uncovered_lines")
|
|
788
|
+
: undefined,
|
|
566
789
|
};
|
|
567
790
|
} catch {
|
|
568
791
|
return undefined;
|
|
@@ -580,7 +803,10 @@ export async function fetchIssueSeverityCounts(
|
|
|
580
803
|
const facetKey = mode === "MQR" ? "impactSeverities" : "severities";
|
|
581
804
|
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=${facetKey}`;
|
|
582
805
|
const result = await fetchJson<{
|
|
583
|
-
facets: Array<{
|
|
806
|
+
facets: Array<{
|
|
807
|
+
property: string;
|
|
808
|
+
values: Array<{ val: string; count: number }>;
|
|
809
|
+
}>;
|
|
584
810
|
}>(url, token, signal);
|
|
585
811
|
const facet = result.facets?.find((f) => f.property === facetKey);
|
|
586
812
|
if (!facet) return undefined;
|
|
@@ -617,9 +843,14 @@ export async function fetchIssueQualityCounts(
|
|
|
617
843
|
try {
|
|
618
844
|
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=impactSoftwareQualities`;
|
|
619
845
|
const result = await fetchJson<{
|
|
620
|
-
facets: Array<{
|
|
846
|
+
facets: Array<{
|
|
847
|
+
property: string;
|
|
848
|
+
values: Array<{ val: string; count: number }>;
|
|
849
|
+
}>;
|
|
621
850
|
}>(url, token, signal);
|
|
622
|
-
const facet = result.facets?.find(
|
|
851
|
+
const facet = result.facets?.find(
|
|
852
|
+
(f) => f.property === "impactSoftwareQualities",
|
|
853
|
+
);
|
|
623
854
|
if (!facet) return undefined;
|
|
624
855
|
const getCount = (q: string): number => {
|
|
625
856
|
const entry = facet.values.find((v) => v.val === q);
|
|
@@ -642,8 +873,10 @@ export async function fetchFileDuplications(
|
|
|
642
873
|
token: string | undefined,
|
|
643
874
|
projectKey: string,
|
|
644
875
|
signal?: AbortSignal,
|
|
876
|
+
pathScope?: string,
|
|
877
|
+
baseDir?: string,
|
|
645
878
|
): Promise<FileDuplication[]> {
|
|
646
|
-
const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_blocks&ps=500`;
|
|
879
|
+
const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_lines_density,duplicated_blocks&ps=500`;
|
|
647
880
|
const treeResult = await fetchJson<{
|
|
648
881
|
components: Array<{
|
|
649
882
|
key: string;
|
|
@@ -653,24 +886,44 @@ export async function fetchFileDuplications(
|
|
|
653
886
|
}>;
|
|
654
887
|
}>(treeUrl, token, signal);
|
|
655
888
|
|
|
656
|
-
|
|
889
|
+
let results = (treeResult.components ?? [])
|
|
657
890
|
.map((component) => {
|
|
658
891
|
const measures = component.measures ?? [];
|
|
659
892
|
const getValue = (metric: string): number => {
|
|
660
893
|
const m = measures.find((item) => item.metric === metric);
|
|
661
|
-
return m ? Number.
|
|
894
|
+
return m ? Number.parseFloat(m.value) : 0;
|
|
662
895
|
};
|
|
663
896
|
const duplicatedBlocks = getValue("duplicated_blocks");
|
|
664
897
|
if (duplicatedBlocks <= 0) return null;
|
|
665
898
|
return {
|
|
666
|
-
filePath:
|
|
899
|
+
filePath:
|
|
900
|
+
component.path ??
|
|
901
|
+
toIssuePath(component.key, projectKey) ??
|
|
902
|
+
component.name ??
|
|
903
|
+
component.key,
|
|
667
904
|
fileKey: component.key,
|
|
668
905
|
duplicatedLines: getValue("duplicated_lines"),
|
|
906
|
+
duplicatedLinesDensity: getValue("duplicated_lines_density"),
|
|
669
907
|
duplicatedBlocks,
|
|
670
908
|
};
|
|
671
909
|
})
|
|
672
910
|
.filter((f): f is FileDuplication => f !== null);
|
|
673
911
|
|
|
912
|
+
if (pathScope && baseDir) {
|
|
913
|
+
const absPath = resolve(baseDir, pathScope);
|
|
914
|
+
const relPrefix = relative(baseDir, absPath);
|
|
915
|
+
results = results.filter((f) => f.filePath.startsWith(relPrefix));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
results.sort((left, right) => {
|
|
919
|
+
const densityDiff =
|
|
920
|
+
right.duplicatedLinesDensity - left.duplicatedLinesDensity;
|
|
921
|
+
if (densityDiff !== 0) return densityDiff;
|
|
922
|
+
const linesDiff = right.duplicatedLines - left.duplicatedLines;
|
|
923
|
+
if (linesDiff !== 0) return linesDiff;
|
|
924
|
+
return left.filePath.localeCompare(right.filePath);
|
|
925
|
+
});
|
|
926
|
+
|
|
674
927
|
return results;
|
|
675
928
|
}
|
|
676
929
|
|
|
@@ -695,5 +948,3 @@ export async function fetchFileDuplicationBlocks(
|
|
|
695
948
|
})),
|
|
696
949
|
}));
|
|
697
950
|
}
|
|
698
|
-
|
|
699
|
-
|