@jonathan0823/pi-sonarqube 0.1.1 → 0.1.3
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/CONTRIBUTING.md +26 -0
- package/README.md +19 -11
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -51
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +411 -52
package/dist/index.js
CHANGED
|
@@ -10,7 +10,17 @@ const SonarToolParams = Type.Object({
|
|
|
10
10
|
action: StringEnum(["analyze", "issues", "open"]),
|
|
11
11
|
path: Type.Optional(Type.String({ description: "Target alias or project directory to analyze or inspect" })),
|
|
12
12
|
issueIndex: Type.Optional(Type.Number({ description: "1-based issue index to open" })),
|
|
13
|
+
severities: Type.Optional(Type.Array(Type.String({ description: "Issue severity to fetch (e.g. CRITICAL)" }))),
|
|
14
|
+
statuses: Type.Optional(Type.Array(Type.String({ description: "Issue status to fetch (e.g. OPEN)" }))),
|
|
15
|
+
types: Type.Optional(Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" }))),
|
|
16
|
+
rules: Type.Optional(Type.Array(Type.String({ description: "Rule keys to fetch" }))),
|
|
13
17
|
});
|
|
18
|
+
const SONAR_COMMANDS = [
|
|
19
|
+
{ value: "analyze", label: "analyze", description: "run analysis and fetch issues" },
|
|
20
|
+
{ value: "issues", label: "issues", description: "browse the latest issues" },
|
|
21
|
+
{ value: "open", label: "open", description: "preview a specific issue" },
|
|
22
|
+
{ value: "init", label: "init", description: "configure a project target" },
|
|
23
|
+
];
|
|
14
24
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
15
25
|
function nowIso() {
|
|
16
26
|
return new Date().toISOString();
|
|
@@ -48,6 +58,76 @@ function normalizeServerUrl(url) {
|
|
|
48
58
|
return fallback;
|
|
49
59
|
return url.trim().replace(/\/+$/, "");
|
|
50
60
|
}
|
|
61
|
+
const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
|
|
62
|
+
const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"];
|
|
63
|
+
const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"];
|
|
64
|
+
function createAutocompleteItem(value, description) {
|
|
65
|
+
return description ? { value, label: value, description } : { value, label: value };
|
|
66
|
+
}
|
|
67
|
+
function filterAutocompleteItems(items, prefix) {
|
|
68
|
+
const query = prefix.trim().toLowerCase();
|
|
69
|
+
const filtered = query
|
|
70
|
+
? items.filter((item) => item.value.toLowerCase().startsWith(query) || item.label.toLowerCase().startsWith(query))
|
|
71
|
+
: [...items];
|
|
72
|
+
return filtered.map((item) => ({ value: item.value, label: item.label, description: item.description }));
|
|
73
|
+
}
|
|
74
|
+
function mergeAutocompleteSuggestions(prefix, current, extraItems) {
|
|
75
|
+
const merged = new Map();
|
|
76
|
+
for (const item of current?.items ?? [])
|
|
77
|
+
merged.set(item.value, item);
|
|
78
|
+
for (const item of extraItems)
|
|
79
|
+
merged.set(item.value, item);
|
|
80
|
+
const items = [...merged.values()];
|
|
81
|
+
return items.length > 0 ? { prefix, items } : current;
|
|
82
|
+
}
|
|
83
|
+
function splitSonarArgumentContext(argumentText) {
|
|
84
|
+
const trimmedLeft = argumentText.replace(/^\s+/, "");
|
|
85
|
+
if (!trimmedLeft)
|
|
86
|
+
return { command: "", current: "", tokens: [] };
|
|
87
|
+
const tokens = trimmedLeft.split(/\s+/);
|
|
88
|
+
const current = /\s$/.test(argumentText) ? "" : tokens.pop() ?? "";
|
|
89
|
+
return { command: tokens[0] ?? current, current, tokens: tokens.length > 0 ? tokens : current ? [current] : [] };
|
|
90
|
+
}
|
|
91
|
+
function createFilterCompletionList() {
|
|
92
|
+
return [
|
|
93
|
+
...SONAR_SEVERITIES.flatMap((value) => [
|
|
94
|
+
createAutocompleteItem(`severity:${value}`, "severity"),
|
|
95
|
+
createAutocompleteItem(value, "severity"),
|
|
96
|
+
]),
|
|
97
|
+
...SONAR_STATUSES.flatMap((value) => [
|
|
98
|
+
createAutocompleteItem(`status:${value}`, "status"),
|
|
99
|
+
createAutocompleteItem(value, "status"),
|
|
100
|
+
]),
|
|
101
|
+
...SONAR_TYPES.flatMap((value) => [
|
|
102
|
+
createAutocompleteItem(`type:${value}`, "type"),
|
|
103
|
+
createAutocompleteItem(value, "type"),
|
|
104
|
+
]),
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
function sonarArgumentCompletions(argumentText, issues) {
|
|
108
|
+
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
109
|
+
const lowerCommand = command.toLowerCase();
|
|
110
|
+
const commandMatches = SONAR_COMMANDS.filter((item) => item.value.startsWith(lowerCommand));
|
|
111
|
+
if (!command || commandMatches.length > 1 || !SONAR_COMMANDS.some((item) => item.value === lowerCommand)) {
|
|
112
|
+
return commandMatches.length > 0 ? commandMatches.map((item) => createAutocompleteItem(item.value, item.description)) : null;
|
|
113
|
+
}
|
|
114
|
+
if (lowerCommand === "open") {
|
|
115
|
+
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => createAutocompleteItem(String(index + 1), `${issue.severity} ${issue.filePath}${issue.line ? `:${issue.line}` : ""}`));
|
|
116
|
+
const suggestions = [...issueItems, ...createFilterCompletionList()];
|
|
117
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
118
|
+
return filtered.length > 0 ? filtered : null;
|
|
119
|
+
}
|
|
120
|
+
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
121
|
+
const suggestions = createFilterCompletionList();
|
|
122
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
123
|
+
return filtered.length > 0 ? filtered : null;
|
|
124
|
+
}
|
|
125
|
+
if (lowerCommand === "init") {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return tokens.length === 0 ? filterAutocompleteItems(SONAR_COMMANDS, current) : null;
|
|
129
|
+
}
|
|
130
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
51
131
|
function resolveProjectKey(baseDir, props) {
|
|
52
132
|
const fromProps = props["sonar.projectKey"]?.trim();
|
|
53
133
|
if (fromProps)
|
|
@@ -231,6 +311,138 @@ function toIssuePath(component, projectKey) {
|
|
|
231
311
|
const colon = component.indexOf(":");
|
|
232
312
|
return colon >= 0 ? component.slice(colon + 1) : component;
|
|
233
313
|
}
|
|
314
|
+
function normalizeIssueList(values, uppercase = false) {
|
|
315
|
+
const cleaned = values
|
|
316
|
+
?.map((value) => value.trim())
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.map((value) => (uppercase ? value.toUpperCase() : value));
|
|
319
|
+
if (!cleaned?.length)
|
|
320
|
+
return undefined;
|
|
321
|
+
return [...new Set(cleaned)];
|
|
322
|
+
}
|
|
323
|
+
function normalizeIssueFilters(filters) {
|
|
324
|
+
const normalized = {
|
|
325
|
+
severities: normalizeIssueList(filters?.severities, true),
|
|
326
|
+
statuses: normalizeIssueList(filters?.statuses, true),
|
|
327
|
+
types: normalizeIssueList(filters?.types, true),
|
|
328
|
+
rules: normalizeIssueList(filters?.rules),
|
|
329
|
+
};
|
|
330
|
+
return normalized.severities || normalized.statuses || normalized.types || normalized.rules ? normalized : undefined;
|
|
331
|
+
}
|
|
332
|
+
function mergeIssueFilters(...filters) {
|
|
333
|
+
const merged = {};
|
|
334
|
+
for (const filter of filters) {
|
|
335
|
+
if (!filter)
|
|
336
|
+
continue;
|
|
337
|
+
if (filter.severities?.length)
|
|
338
|
+
merged.severities = [...(merged.severities ?? []), ...filter.severities];
|
|
339
|
+
if (filter.statuses?.length)
|
|
340
|
+
merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
|
|
341
|
+
if (filter.types?.length)
|
|
342
|
+
merged.types = [...(merged.types ?? []), ...filter.types];
|
|
343
|
+
if (filter.rules?.length)
|
|
344
|
+
merged.rules = [...(merged.rules ?? []), ...filter.rules];
|
|
345
|
+
}
|
|
346
|
+
return normalizeIssueFilters(merged);
|
|
347
|
+
}
|
|
348
|
+
function splitFilterValues(value) {
|
|
349
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
350
|
+
}
|
|
351
|
+
function parseIssueFilterToken(token) {
|
|
352
|
+
const trimmed = token.trim();
|
|
353
|
+
if (!trimmed)
|
|
354
|
+
return undefined;
|
|
355
|
+
const explicit = trimmed.match(/^([^:=]+)[:=](.+)$/);
|
|
356
|
+
if (explicit) {
|
|
357
|
+
const key = explicit[1].trim().toLowerCase();
|
|
358
|
+
const values = splitFilterValues(explicit[2]);
|
|
359
|
+
if (values.length === 0)
|
|
360
|
+
return undefined;
|
|
361
|
+
if (key === "severity" || key === "severities")
|
|
362
|
+
return { severities: values.map((value) => value.toUpperCase()) };
|
|
363
|
+
if (key === "status" || key === "statuses")
|
|
364
|
+
return { statuses: values.map((value) => value.toUpperCase()) };
|
|
365
|
+
if (key === "type" || key === "types")
|
|
366
|
+
return { types: values.map((value) => value.toUpperCase()) };
|
|
367
|
+
if (key === "rule" || key === "rules")
|
|
368
|
+
return { rules: values };
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
const upper = trimmed.toUpperCase();
|
|
372
|
+
if (SONAR_SEVERITIES.includes(upper))
|
|
373
|
+
return { severities: [upper] };
|
|
374
|
+
if (SONAR_STATUSES.includes(upper))
|
|
375
|
+
return { statuses: [upper] };
|
|
376
|
+
if (SONAR_TYPES.includes(upper))
|
|
377
|
+
return { types: [upper] };
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
function parseSonarIssueArgs(tokens, allowIssueIndex = false) {
|
|
381
|
+
const filters = [];
|
|
382
|
+
let targetInput;
|
|
383
|
+
let issueIndex;
|
|
384
|
+
for (const token of tokens) {
|
|
385
|
+
if (!token)
|
|
386
|
+
continue;
|
|
387
|
+
if (allowIssueIndex && issueIndex === undefined && /^\d+$/.test(token)) {
|
|
388
|
+
issueIndex = Number(token);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const parsedFilter = parseIssueFilterToken(token);
|
|
392
|
+
if (parsedFilter) {
|
|
393
|
+
filters.push(parsedFilter);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (!targetInput) {
|
|
397
|
+
targetInput = token;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
targetInput,
|
|
402
|
+
issueIndex,
|
|
403
|
+
filters: mergeIssueFilters(...filters),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function issueFilterLabel(filters) {
|
|
407
|
+
if (!filters)
|
|
408
|
+
return "";
|
|
409
|
+
const parts = [
|
|
410
|
+
filters.severities?.length ? `severities=${filters.severities.join(",")}` : "",
|
|
411
|
+
filters.statuses?.length ? `statuses=${filters.statuses.join(",")}` : "",
|
|
412
|
+
filters.types?.length ? `types=${filters.types.join(",")}` : "",
|
|
413
|
+
filters.rules?.length ? `rules=${filters.rules.join(",")}` : "",
|
|
414
|
+
].filter(Boolean);
|
|
415
|
+
return parts.join(" • ");
|
|
416
|
+
}
|
|
417
|
+
function severitySortRank(severity) {
|
|
418
|
+
switch (severity.toUpperCase()) {
|
|
419
|
+
case "BLOCKER":
|
|
420
|
+
return 0;
|
|
421
|
+
case "CRITICAL":
|
|
422
|
+
return 1;
|
|
423
|
+
case "MAJOR":
|
|
424
|
+
return 2;
|
|
425
|
+
case "MINOR":
|
|
426
|
+
return 3;
|
|
427
|
+
case "INFO":
|
|
428
|
+
return 4;
|
|
429
|
+
default:
|
|
430
|
+
return 5;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function compareIssuesForContext(left, right) {
|
|
434
|
+
const severityDiff = severitySortRank(left.severity) - severitySortRank(right.severity);
|
|
435
|
+
if (severityDiff !== 0)
|
|
436
|
+
return severityDiff;
|
|
437
|
+
const fileDiff = left.filePath.localeCompare(right.filePath);
|
|
438
|
+
if (fileDiff !== 0)
|
|
439
|
+
return fileDiff;
|
|
440
|
+
const leftLine = left.line ?? Number.POSITIVE_INFINITY;
|
|
441
|
+
const rightLine = right.line ?? Number.POSITIVE_INFINITY;
|
|
442
|
+
if (leftLine !== rightLine)
|
|
443
|
+
return leftLine - rightLine;
|
|
444
|
+
return left.message.localeCompare(right.message);
|
|
445
|
+
}
|
|
234
446
|
// ── Issue helpers ─────────────────────────────────────────────────────────────
|
|
235
447
|
function buildIssuePreview(baseDir, issue, radius = 3) {
|
|
236
448
|
return (async () => {
|
|
@@ -259,9 +471,10 @@ function formatIssue(issue, index) {
|
|
|
259
471
|
function formatSummary(state) {
|
|
260
472
|
const issueCount = state.totalIssues;
|
|
261
473
|
const noIssues = issueCount === 0;
|
|
474
|
+
const filterLabel = state.filters ? ` (${issueFilterLabel(state.filters)})` : "";
|
|
262
475
|
return noIssues
|
|
263
|
-
? `SonarQube: no issues found for ${state.projectKey}`
|
|
264
|
-
: `SonarQube: ${issueCount} issue${issueCount === 1 ? "" : "s"} found for ${state.projectKey}`;
|
|
476
|
+
? `SonarQube: no issues found for ${state.projectKey}${filterLabel}`
|
|
477
|
+
: `SonarQube: ${issueCount} issue${issueCount === 1 ? "" : "s"} found for ${state.projectKey}${filterLabel}`;
|
|
265
478
|
}
|
|
266
479
|
function formatReport(state) {
|
|
267
480
|
const lines = [
|
|
@@ -270,6 +483,9 @@ function formatReport(state) {
|
|
|
270
483
|
`Base dir: ${state.baseDir}`,
|
|
271
484
|
`Issues: ${state.totalIssues}`,
|
|
272
485
|
];
|
|
486
|
+
if (state.filters) {
|
|
487
|
+
lines.push(`Filters: ${issueFilterLabel(state.filters)}`);
|
|
488
|
+
}
|
|
273
489
|
if (state.issues.length === 0) {
|
|
274
490
|
lines.push("No open issues found.");
|
|
275
491
|
return lines.join("\n");
|
|
@@ -296,6 +512,35 @@ function severityColor(theme, severity) {
|
|
|
296
512
|
return theme.fg("muted", severity);
|
|
297
513
|
}
|
|
298
514
|
}
|
|
515
|
+
const ANALYSIS_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
516
|
+
const ANALYSIS_WIDGET_KEY = "sonarqube-analysis";
|
|
517
|
+
function startAnalysisUi(ctx, projectKey) {
|
|
518
|
+
let frameIndex = 0;
|
|
519
|
+
let phase = "Starting...";
|
|
520
|
+
const render = () => {
|
|
521
|
+
const frame = ANALYSIS_SPINNER[frameIndex];
|
|
522
|
+
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [`${frame} SonarQube ${projectKey}`, phase]);
|
|
523
|
+
};
|
|
524
|
+
ctx.ui.setWorkingMessage(`Analyzing ${projectKey}...`);
|
|
525
|
+
ctx.ui.setWorkingIndicator({ frames: ANALYSIS_SPINNER, intervalMs: 80 });
|
|
526
|
+
render();
|
|
527
|
+
const timer = setInterval(() => {
|
|
528
|
+
frameIndex = (frameIndex + 1) % ANALYSIS_SPINNER.length;
|
|
529
|
+
render();
|
|
530
|
+
}, 80);
|
|
531
|
+
return {
|
|
532
|
+
setPhase(nextPhase) {
|
|
533
|
+
phase = nextPhase;
|
|
534
|
+
render();
|
|
535
|
+
},
|
|
536
|
+
stop() {
|
|
537
|
+
clearInterval(timer);
|
|
538
|
+
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, undefined);
|
|
539
|
+
ctx.ui.setWorkingMessage();
|
|
540
|
+
ctx.ui.setWorkingIndicator();
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
299
544
|
// ── Issue Browser UI component ────────────────────────────────────────────────
|
|
300
545
|
class IssueBrowser {
|
|
301
546
|
state;
|
|
@@ -338,7 +583,10 @@ class IssueBrowser {
|
|
|
338
583
|
}
|
|
339
584
|
const lines = [];
|
|
340
585
|
const title = this.theme.fg("accent", this.theme.bold(" SonarQube Issues "));
|
|
341
|
-
const
|
|
586
|
+
const subtitleText = this.state.filters
|
|
587
|
+
? `${this.state.projectKey} • ${this.state.totalIssues} issue(s) • ${issueFilterLabel(this.state.filters)}`
|
|
588
|
+
: `${this.state.projectKey} • ${this.state.totalIssues} issue(s)`;
|
|
589
|
+
const subtitle = this.theme.fg("dim", subtitleText);
|
|
342
590
|
lines.push(truncateToWidth(title, width));
|
|
343
591
|
lines.push(truncateToWidth(subtitle, width));
|
|
344
592
|
lines.push("");
|
|
@@ -386,9 +634,7 @@ async function restoreState(pi, ctx, statesByBaseDir) {
|
|
|
386
634
|
statesByBaseDir.set(latest.baseDir, latest);
|
|
387
635
|
}
|
|
388
636
|
}
|
|
389
|
-
|
|
390
|
-
ctx.ui.setStatus("sonarqube", formatSummary(latest));
|
|
391
|
-
}
|
|
637
|
+
ctx.ui.setStatus("sonarqube", undefined);
|
|
392
638
|
return latest;
|
|
393
639
|
}
|
|
394
640
|
// ── Error hints ───────────────────────────────────────────────────────────────
|
|
@@ -487,14 +733,31 @@ async function fetchRuleName(serverUrl, token, ruleKey, signal) {
|
|
|
487
733
|
return undefined;
|
|
488
734
|
}
|
|
489
735
|
}
|
|
490
|
-
async function fetchIssues(serverUrl, token, projectKey, signal) {
|
|
736
|
+
async function fetchIssues(serverUrl, token, projectKey, signal, filters) {
|
|
737
|
+
const normalizedFilters = normalizeIssueFilters(filters);
|
|
491
738
|
const issues = [];
|
|
492
739
|
const ruleNames = new Map();
|
|
493
740
|
let page = 1;
|
|
494
741
|
let total = 0;
|
|
495
742
|
for (;;) {
|
|
496
|
-
const url =
|
|
497
|
-
|
|
743
|
+
const url = new URL("/api/issues/search", serverUrl);
|
|
744
|
+
url.searchParams.set("projects", projectKey);
|
|
745
|
+
url.searchParams.set("resolved", "false");
|
|
746
|
+
url.searchParams.set("ps", "100");
|
|
747
|
+
url.searchParams.set("p", String(page));
|
|
748
|
+
if (normalizedFilters?.severities?.length) {
|
|
749
|
+
url.searchParams.set("severities", normalizedFilters.severities.join(","));
|
|
750
|
+
}
|
|
751
|
+
if (normalizedFilters?.statuses?.length) {
|
|
752
|
+
url.searchParams.set("statuses", normalizedFilters.statuses.join(","));
|
|
753
|
+
}
|
|
754
|
+
if (normalizedFilters?.types?.length) {
|
|
755
|
+
url.searchParams.set("types", normalizedFilters.types.join(","));
|
|
756
|
+
}
|
|
757
|
+
if (normalizedFilters?.rules?.length) {
|
|
758
|
+
url.searchParams.set("rules", normalizedFilters.rules.join(","));
|
|
759
|
+
}
|
|
760
|
+
const result = await fetchJson(url.toString(), token, signal);
|
|
498
761
|
total = result.total;
|
|
499
762
|
for (const raw of result.issues) {
|
|
500
763
|
if (!ruleNames.has(raw.rule)) {
|
|
@@ -515,6 +778,9 @@ async function fetchIssues(serverUrl, token, projectKey, signal) {
|
|
|
515
778
|
break;
|
|
516
779
|
page += 1;
|
|
517
780
|
}
|
|
781
|
+
if (normalizedFilters && issues.length > 1) {
|
|
782
|
+
issues.sort(compareIssuesForContext);
|
|
783
|
+
}
|
|
518
784
|
return issues;
|
|
519
785
|
}
|
|
520
786
|
function createAnalysisState(config, issues, extras = {}) {
|
|
@@ -527,6 +793,7 @@ function createAnalysisState(config, issues, extras = {}) {
|
|
|
527
793
|
dashboardUrl: extras.dashboardUrl,
|
|
528
794
|
ceTaskUrl: extras.ceTaskUrl,
|
|
529
795
|
analysisId: extras.analysisId,
|
|
796
|
+
filters: extras.filters,
|
|
530
797
|
totalIssues: issues.length,
|
|
531
798
|
issues,
|
|
532
799
|
};
|
|
@@ -540,15 +807,16 @@ async function hasProjectAnalyses(serverUrl, token, projectKey, signal) {
|
|
|
540
807
|
return false;
|
|
541
808
|
}
|
|
542
809
|
}
|
|
543
|
-
async function loadProjectIssuesFromApi(ctx, inputPath) {
|
|
810
|
+
async function loadProjectIssuesFromApi(ctx, inputPath, filters) {
|
|
544
811
|
const config = await resolveConfig(ctx, inputPath);
|
|
545
|
-
const
|
|
546
|
-
|
|
812
|
+
const normalizedFilters = normalizeIssueFilters(filters);
|
|
813
|
+
const issues = await fetchIssues(config.serverUrl, config.token, config.projectKey, ctx.signal, normalizedFilters);
|
|
814
|
+
return createAnalysisState(config, issues, { filters: normalizedFilters });
|
|
547
815
|
}
|
|
548
|
-
async function resolveTargetState(ctx, statesByBaseDir, targetInput) {
|
|
816
|
+
async function resolveTargetState(ctx, statesByBaseDir, targetInput, filters) {
|
|
549
817
|
const resolvedTarget = await resolveTarget(ctx, targetInput);
|
|
550
818
|
try {
|
|
551
|
-
return await loadProjectIssuesFromApi(ctx, targetInput);
|
|
819
|
+
return await loadProjectIssuesFromApi(ctx, targetInput, filters);
|
|
552
820
|
}
|
|
553
821
|
catch (error) {
|
|
554
822
|
const cached = statesByBaseDir.get(resolvedTarget.baseDir);
|
|
@@ -558,43 +826,40 @@ async function resolveTargetState(ctx, statesByBaseDir, targetInput) {
|
|
|
558
826
|
}
|
|
559
827
|
}
|
|
560
828
|
// ── Analysis orchestration ────────────────────────────────────────────────────
|
|
561
|
-
async function analyzeProject(pi, ctx, inputPath) {
|
|
829
|
+
async function analyzeProject(pi, ctx, inputPath, filters) {
|
|
562
830
|
const config = await resolveConfig(ctx, inputPath);
|
|
563
831
|
const baseDirStat = await stat(config.baseDir).catch(() => undefined);
|
|
564
832
|
if (!baseDirStat?.isDirectory()) {
|
|
565
833
|
throw new Error(`Project directory not found: ${config.baseDir}`);
|
|
566
834
|
}
|
|
567
|
-
|
|
568
|
-
ctx.ui.setWorkingIndicator({
|
|
569
|
-
frames: [".", "..", "...", ".."],
|
|
570
|
-
intervalMs: 180,
|
|
571
|
-
});
|
|
572
|
-
ctx.ui.setStatus("sonarqube", `Analyzing ${config.projectKey}`);
|
|
835
|
+
const analysisUi = startAnalysisUi(ctx, config.projectKey);
|
|
573
836
|
try {
|
|
837
|
+
analysisUi.setPhase("Running sonar-scanner...");
|
|
574
838
|
await runScanner(pi, config, ctx.signal);
|
|
839
|
+
analysisUi.setPhase("Reading scanner report...");
|
|
575
840
|
const report = await readReportTask(config.baseDir);
|
|
576
841
|
const ceTaskUrl = report["ceTaskUrl"];
|
|
577
842
|
const dashboardUrl = report["dashboardUrl"];
|
|
578
843
|
const analysisId = ceTaskUrl
|
|
579
|
-
? await waitForAnalysis(config.serverUrl, config.token, ceTaskUrl, ctx.signal)
|
|
844
|
+
? (analysisUi.setPhase("Waiting for SonarQube analysis..."), await waitForAnalysis(config.serverUrl, config.token, ceTaskUrl, ctx.signal))
|
|
580
845
|
: undefined;
|
|
581
|
-
|
|
846
|
+
analysisUi.setPhase("Fetching issues...");
|
|
847
|
+
const normalizedFilters = normalizeIssueFilters(filters);
|
|
848
|
+
const issues = await fetchIssues(config.serverUrl, config.token, config.projectKey, ctx.signal, normalizedFilters);
|
|
582
849
|
const state = createAnalysisState(config, issues, {
|
|
583
850
|
dashboardUrl,
|
|
584
851
|
ceTaskUrl,
|
|
585
852
|
analysisId,
|
|
853
|
+
filters: normalizedFilters,
|
|
586
854
|
});
|
|
587
855
|
pi.appendEntry(STATE_TYPE, state);
|
|
588
|
-
ctx.ui.setStatus("sonarqube", formatSummary(state));
|
|
589
856
|
return state;
|
|
590
857
|
}
|
|
591
858
|
catch (error) {
|
|
592
|
-
ctx.ui.setStatus("sonarqube", undefined);
|
|
593
859
|
throw error instanceof Error ? error : new Error(String(error));
|
|
594
860
|
}
|
|
595
861
|
finally {
|
|
596
|
-
|
|
597
|
-
ctx.ui.setWorkingIndicator();
|
|
862
|
+
analysisUi.stop();
|
|
598
863
|
}
|
|
599
864
|
}
|
|
600
865
|
// ── UI helpers ────────────────────────────────────────────────────────────────
|
|
@@ -608,7 +873,6 @@ async function showIssueBrowser(ctx, state) {
|
|
|
608
873
|
async function openIssuePreview(ctx, state, issue) {
|
|
609
874
|
const preview = await buildIssuePreview(state.baseDir, issue);
|
|
610
875
|
const title = `${issue.filePath}${issue.line ? `:${issue.line}` : ""}`;
|
|
611
|
-
ctx.ui.setStatus("sonarqube", title);
|
|
612
876
|
if (ctx.mode === "tui") {
|
|
613
877
|
await ctx.ui.editor(title, preview);
|
|
614
878
|
return;
|
|
@@ -655,15 +919,22 @@ function helpText() {
|
|
|
655
919
|
return [
|
|
656
920
|
"SonarQube commands:",
|
|
657
921
|
"",
|
|
658
|
-
" /sonarqube init [alias] [path] configure
|
|
659
|
-
" /sonarqube analyze [target] run analysis for
|
|
660
|
-
" /sonarqube issues [target] browse
|
|
661
|
-
" /sonarqube open [target] <n> preview issue #n for target",
|
|
922
|
+
" /sonarqube init [alias] [path] configure a project target",
|
|
923
|
+
" /sonarqube analyze [target] run analysis for a target or path",
|
|
924
|
+
" /sonarqube issues [target] browse issues for a target or path",
|
|
925
|
+
" /sonarqube open [target] <n> preview issue #n for a target or path",
|
|
662
926
|
" /sonarqube show this help",
|
|
663
927
|
"",
|
|
928
|
+
"Filters:",
|
|
929
|
+
" /sonarqube issues be CRITICAL",
|
|
930
|
+
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
931
|
+
"",
|
|
932
|
+
"Autocomplete:",
|
|
933
|
+
" type /sonarqube and press Tab to complete the subcommand or filters",
|
|
934
|
+
"",
|
|
664
935
|
"Defaults:",
|
|
665
936
|
" config is stored in .pi/sonarqube.json",
|
|
666
|
-
"
|
|
937
|
+
" use project paths directly (no alias needed)",
|
|
667
938
|
].join("\n");
|
|
668
939
|
}
|
|
669
940
|
function targetLabel(targetInput) {
|
|
@@ -688,24 +959,15 @@ function parseCommandArgs(args) {
|
|
|
688
959
|
return { action: "init", alias: tokens[1], targetInput: tokens[2] };
|
|
689
960
|
}
|
|
690
961
|
if (head === "issues" || head === "view") {
|
|
691
|
-
return { action: "issues",
|
|
962
|
+
return { action: "issues", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
692
963
|
}
|
|
693
964
|
if (head === "open") {
|
|
694
|
-
|
|
695
|
-
return { action: "open", issueIndex: Number(tokens[1]) };
|
|
696
|
-
}
|
|
697
|
-
const targetInput = tokens[1];
|
|
698
|
-
const maybeIndex = tokens[2];
|
|
699
|
-
return {
|
|
700
|
-
action: "open",
|
|
701
|
-
targetInput,
|
|
702
|
-
issueIndex: maybeIndex && /^\d+$/.test(maybeIndex) ? Number(maybeIndex) : undefined,
|
|
703
|
-
};
|
|
965
|
+
return { action: "open", ...parseSonarIssueArgs(tokens.slice(1), true) };
|
|
704
966
|
}
|
|
705
967
|
if (head === "analyze" || head === "run") {
|
|
706
|
-
return { action: "analyze",
|
|
968
|
+
return { action: "analyze", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
707
969
|
}
|
|
708
|
-
return { action: "analyze",
|
|
970
|
+
return { action: "analyze", ...parseSonarIssueArgs(tokens) };
|
|
709
971
|
}
|
|
710
972
|
// ── Extension entrypoint ──────────────────────────────────────────────────────
|
|
711
973
|
export default function sonarqube(pi) {
|
|
@@ -720,6 +982,30 @@ export default function sonarqube(pi) {
|
|
|
720
982
|
};
|
|
721
983
|
pi.on("session_start", async (_event, ctx) => {
|
|
722
984
|
await restore(ctx);
|
|
985
|
+
if (ctx.hasUI) {
|
|
986
|
+
ctx.ui.addAutocompleteProvider((current) => ({
|
|
987
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
988
|
+
const line = lines[cursorLine] ?? "";
|
|
989
|
+
const beforeCursor = line.slice(0, cursorCol);
|
|
990
|
+
const match = beforeCursor.match(/^\/sonarqube(?:\s+(.*))?$/);
|
|
991
|
+
if (!match) {
|
|
992
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
993
|
+
}
|
|
994
|
+
const argumentText = match[1] ?? "";
|
|
995
|
+
const { current: currentToken } = splitSonarArgumentContext(argumentText);
|
|
996
|
+
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues) ?? [];
|
|
997
|
+
const currentSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
998
|
+
const merged = mergeAutocompleteSuggestions(currentToken, currentSuggestions, extraItems);
|
|
999
|
+
return merged ?? currentSuggestions;
|
|
1000
|
+
},
|
|
1001
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
1002
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
1003
|
+
},
|
|
1004
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
1005
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
1006
|
+
},
|
|
1007
|
+
}));
|
|
1008
|
+
}
|
|
723
1009
|
});
|
|
724
1010
|
pi.on("session_tree", async (_event, ctx) => {
|
|
725
1011
|
await restore(ctx);
|
|
@@ -731,19 +1017,26 @@ export default function sonarqube(pi) {
|
|
|
731
1017
|
promptSnippet: "Run local SonarQube analysis or inspect issue results",
|
|
732
1018
|
promptGuidelines: [
|
|
733
1019
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
1020
|
+
"Use the optional severity, status, type, and rule filters to fetch only the most relevant issues.",
|
|
734
1021
|
],
|
|
735
1022
|
parameters: SonarToolParams,
|
|
736
1023
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
737
1024
|
try {
|
|
1025
|
+
const filters = normalizeIssueFilters({
|
|
1026
|
+
severities: params.severities,
|
|
1027
|
+
statuses: params.statuses,
|
|
1028
|
+
types: params.types,
|
|
1029
|
+
rules: params.rules,
|
|
1030
|
+
});
|
|
738
1031
|
if (params.action === "analyze") {
|
|
739
|
-
const state = await analyzeProject(pi, ctx, params.path);
|
|
1032
|
+
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
740
1033
|
rememberState(state);
|
|
741
1034
|
return {
|
|
742
1035
|
content: [{ type: "text", text: formatReport(state) }],
|
|
743
1036
|
details: state,
|
|
744
1037
|
};
|
|
745
1038
|
}
|
|
746
|
-
const targetState = await resolveTargetState(ctx, statesByBaseDir, params.path);
|
|
1039
|
+
const targetState = await resolveTargetState(ctx, statesByBaseDir, params.path, filters);
|
|
747
1040
|
if (!targetState) {
|
|
748
1041
|
return {
|
|
749
1042
|
content: [{ type: "text", text: "No SonarQube analysis has been run for this target yet." }],
|
|
@@ -777,7 +1070,14 @@ export default function sonarqube(pi) {
|
|
|
777
1070
|
}
|
|
778
1071
|
},
|
|
779
1072
|
renderCall(args, theme, _context) {
|
|
780
|
-
|
|
1073
|
+
const filters = normalizeIssueFilters({
|
|
1074
|
+
severities: args.severities,
|
|
1075
|
+
statuses: args.statuses,
|
|
1076
|
+
types: args.types,
|
|
1077
|
+
rules: args.rules,
|
|
1078
|
+
});
|
|
1079
|
+
const filterText = filters ? ` ${theme.fg("muted", issueFilterLabel(filters))}` : "";
|
|
1080
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("sonarqube"))} ${theme.fg("muted", args.action)}${args.path ? ` ${theme.fg("accent", args.path)}` : ""}${filterText}`, 0, 0);
|
|
781
1081
|
},
|
|
782
1082
|
renderResult(result, { expanded }, theme, _context) {
|
|
783
1083
|
const state = result.details;
|
|
@@ -811,6 +1111,7 @@ export default function sonarqube(pi) {
|
|
|
811
1111
|
});
|
|
812
1112
|
pi.registerCommand("sonarqube", {
|
|
813
1113
|
description: "Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
1114
|
+
getArgumentCompletions: (argumentPrefix) => sonarArgumentCompletions(argumentPrefix, latestState?.issues),
|
|
814
1115
|
handler: async (args, ctx) => {
|
|
815
1116
|
try {
|
|
816
1117
|
const parsed = parseCommandArgs(args);
|
|
@@ -828,7 +1129,7 @@ export default function sonarqube(pi) {
|
|
|
828
1129
|
}
|
|
829
1130
|
// --- Analyze ---
|
|
830
1131
|
if (parsed.action === "analyze") {
|
|
831
|
-
const state = await analyzeProject(pi, ctx, parsed.targetInput);
|
|
1132
|
+
const state = await analyzeProject(pi, ctx, parsed.targetInput, parsed.filters);
|
|
832
1133
|
rememberState(state);
|
|
833
1134
|
if (ctx.hasUI) {
|
|
834
1135
|
const scope = targetLabel(parsed.targetInput);
|
|
@@ -836,7 +1137,7 @@ export default function sonarqube(pi) {
|
|
|
836
1137
|
}
|
|
837
1138
|
return;
|
|
838
1139
|
}
|
|
839
|
-
const targetState = await resolveTargetState(ctx, statesByBaseDir, parsed.targetInput);
|
|
1140
|
+
const targetState = await resolveTargetState(ctx, statesByBaseDir, parsed.targetInput, parsed.filters);
|
|
840
1141
|
if (!targetState) {
|
|
841
1142
|
if (ctx.hasUI) {
|
|
842
1143
|
ctx.ui.notify(`No SonarQube analysis found for this target. Run /sonarqube analyze${targetLabel(parsed.targetInput)} first.`, "warning");
|