@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/commands.ts
CHANGED
|
@@ -1,20 +1,57 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteItem,
|
|
3
|
+
AutocompleteSuggestions,
|
|
4
|
+
} from "@earendil-works/pi-tui";
|
|
5
|
+
import type {
|
|
6
|
+
SonarIssue,
|
|
7
|
+
SonarIssueFetchOptions,
|
|
8
|
+
SonarDuplicationMeasures,
|
|
9
|
+
IssueSeverityCounts,
|
|
10
|
+
IssueQualityCounts,
|
|
11
|
+
FileDuplication,
|
|
12
|
+
DuplicationBlockGroup,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import {
|
|
15
|
+
SONAR_SEVERITIES,
|
|
16
|
+
SONAR_STATUSES,
|
|
17
|
+
SONAR_TYPES,
|
|
18
|
+
SONAR_SOFTWARE_QUALITIES,
|
|
19
|
+
SONAR_IMPACT_SEVERITIES,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { readdirSync } from "node:fs";
|
|
23
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
4
24
|
import { looksLikePath } from "./config.js";
|
|
5
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
parseSonarIssueArgs,
|
|
27
|
+
issueFilterLabel,
|
|
28
|
+
fetchProjectProfiles,
|
|
29
|
+
fetchRuleSearch,
|
|
30
|
+
} from "./api.js";
|
|
6
31
|
|
|
7
32
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
8
33
|
|
|
9
34
|
export const STATE_TYPE = "sonarqube-analysis-state";
|
|
10
35
|
|
|
11
36
|
const SONAR_COMMANDS = [
|
|
12
|
-
{
|
|
37
|
+
{
|
|
38
|
+
value: "analyze",
|
|
39
|
+
label: "analyze",
|
|
40
|
+
description: "run analysis and fetch issues",
|
|
41
|
+
},
|
|
13
42
|
{ value: "issues", label: "issues", description: "browse the latest issues" },
|
|
14
43
|
{ value: "open", label: "open", description: "preview a specific issue" },
|
|
15
44
|
{ value: "init", label: "init", description: "configure a project target" },
|
|
16
|
-
{
|
|
17
|
-
|
|
45
|
+
{
|
|
46
|
+
value: "metrics",
|
|
47
|
+
label: "metrics",
|
|
48
|
+
description: "show project metrics (duplication, issue counts)",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "duplications",
|
|
52
|
+
label: "duplications",
|
|
53
|
+
description: "browse duplicated files and blocks",
|
|
54
|
+
},
|
|
18
55
|
] as const;
|
|
19
56
|
|
|
20
57
|
// ── Autocomplete helpers ────────────────────────────────────────────────────
|
|
@@ -24,13 +61,23 @@ export function filterAutocompleteItems(
|
|
|
24
61
|
prefix: string,
|
|
25
62
|
): AutocompleteItem[] {
|
|
26
63
|
const query = prefix.trim().toLowerCase();
|
|
27
|
-
if (!query)
|
|
64
|
+
if (!query)
|
|
65
|
+
return items.map((item) => ({
|
|
66
|
+
value: item.value,
|
|
67
|
+
label: item.label,
|
|
68
|
+
description: item.description,
|
|
69
|
+
}));
|
|
28
70
|
return items
|
|
29
71
|
.filter(
|
|
30
72
|
(item) =>
|
|
31
|
-
item.value.toLowerCase().startsWith(query) ||
|
|
73
|
+
item.value.toLowerCase().startsWith(query) ||
|
|
74
|
+
item.label.toLowerCase().startsWith(query),
|
|
32
75
|
)
|
|
33
|
-
.map((item) => ({
|
|
76
|
+
.map((item) => ({
|
|
77
|
+
value: item.value,
|
|
78
|
+
label: item.label,
|
|
79
|
+
description: item.description,
|
|
80
|
+
}));
|
|
34
81
|
}
|
|
35
82
|
|
|
36
83
|
export function mergeAutocompleteSuggestions(
|
|
@@ -45,14 +92,16 @@ export function mergeAutocompleteSuggestions(
|
|
|
45
92
|
return items.length > 0 ? { prefix, items } : current;
|
|
46
93
|
}
|
|
47
94
|
|
|
48
|
-
export function splitSonarArgumentContext(
|
|
49
|
-
|
|
50
|
-
|
|
95
|
+
export function splitSonarArgumentContext(argumentText: string): {
|
|
96
|
+
command: string;
|
|
97
|
+
current: string;
|
|
98
|
+
tokens: string[];
|
|
99
|
+
} {
|
|
51
100
|
const trimmedLeft = argumentText.replace(/^\s+/, "");
|
|
52
101
|
if (!trimmedLeft) return { command: "", current: "", tokens: [] };
|
|
53
102
|
|
|
54
103
|
const tokens = trimmedLeft.split(/\s+/);
|
|
55
|
-
const current = /\s$/.test(argumentText) ? "" : tokens.pop() ?? "";
|
|
104
|
+
const current = /\s$/.test(argumentText) ? "" : (tokens.pop() ?? "");
|
|
56
105
|
const command = tokens[0] ?? current;
|
|
57
106
|
let tokensOut: string[];
|
|
58
107
|
if (tokens.length > 0) {
|
|
@@ -69,13 +118,21 @@ export function splitSonarArgumentContext(
|
|
|
69
118
|
};
|
|
70
119
|
}
|
|
71
120
|
|
|
72
|
-
function createFilterCompletionList(
|
|
121
|
+
function createFilterCompletionList(
|
|
122
|
+
mode?: "STANDARD" | "MQR",
|
|
123
|
+
): AutocompleteItem[] {
|
|
73
124
|
const buildItems = (
|
|
74
|
-
groups: ReadonlyArray<
|
|
125
|
+
groups: ReadonlyArray<
|
|
126
|
+
readonly [string, readonly string[], string, boolean]
|
|
127
|
+
>,
|
|
75
128
|
): AutocompleteItem[] =>
|
|
76
129
|
groups.flatMap(([prefix, values, description, includeBare]) =>
|
|
77
130
|
values.flatMap((value) => [
|
|
78
|
-
{
|
|
131
|
+
{
|
|
132
|
+
value: `${prefix}:${value}`,
|
|
133
|
+
label: `${prefix}:${value}`,
|
|
134
|
+
description,
|
|
135
|
+
},
|
|
79
136
|
...(includeBare ? [{ value, label: value, description }] : []),
|
|
80
137
|
]),
|
|
81
138
|
);
|
|
@@ -89,46 +146,335 @@ function createFilterCompletionList(mode?: "STANDARD" | "MQR"): AutocompleteItem
|
|
|
89
146
|
["quality", SONAR_SOFTWARE_QUALITIES, "software quality (MQR)", true],
|
|
90
147
|
["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
|
|
91
148
|
] as const;
|
|
149
|
+
const ruleItems = [
|
|
150
|
+
{ value: "rule:", label: "rule:", description: "rule key" },
|
|
151
|
+
{ value: "rules:", label: "rules:", description: "rule key" },
|
|
152
|
+
];
|
|
153
|
+
const scopeItems = [
|
|
154
|
+
{
|
|
155
|
+
value: "in:",
|
|
156
|
+
label: "in:",
|
|
157
|
+
description: "path or dir scope (e.g. in:src/api.ts or in:src/)",
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
return mode === "MQR"
|
|
162
|
+
? [
|
|
163
|
+
...scopeItems,
|
|
164
|
+
...ruleItems,
|
|
165
|
+
...buildItems(mqrGroups),
|
|
166
|
+
...buildItems(legacyGroups),
|
|
167
|
+
]
|
|
168
|
+
: [
|
|
169
|
+
...scopeItems,
|
|
170
|
+
...ruleItems,
|
|
171
|
+
...buildItems(legacyGroups),
|
|
172
|
+
...buildItems(mqrGroups),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listDirCompletions(
|
|
177
|
+
dirPath: string,
|
|
178
|
+
baseDir: string,
|
|
179
|
+
): AutocompleteItem[] | null {
|
|
180
|
+
try {
|
|
181
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
182
|
+
const items: AutocompleteItem[] = [];
|
|
183
|
+
const relativePrefix =
|
|
184
|
+
dirPath === baseDir ? "" : relative(baseDir, dirPath) + "/";
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (entry.name.startsWith(".")) continue;
|
|
188
|
+
const isDir = entry.isDirectory();
|
|
189
|
+
const name = isDir ? entry.name + "/" : entry.name;
|
|
190
|
+
const fullPath = relativePrefix + name;
|
|
191
|
+
items.push({
|
|
192
|
+
value: `in:${fullPath}`,
|
|
193
|
+
label: name,
|
|
194
|
+
description: relativePrefix.replace(/\/$/, ""),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
items.sort((a, b) => {
|
|
199
|
+
const aDir = a.description === "" ? 0 : 1;
|
|
200
|
+
const bDir = b.description === "" ? 0 : 1;
|
|
201
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
202
|
+
// Within same depth, dirs first, then by label
|
|
203
|
+
const aD = a.label.endsWith("/") ? 0 : 1;
|
|
204
|
+
const bD = b.label.endsWith("/") ? 0 : 1;
|
|
205
|
+
return aD - bD || a.label.localeCompare(b.label);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return items.length > 0 ? items.slice(0, 50) : null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fuzzyWithCmd(
|
|
215
|
+
cmd: string,
|
|
216
|
+
args: string[],
|
|
217
|
+
prefix: string,
|
|
218
|
+
baseDir: string,
|
|
219
|
+
): AutocompleteItem[] | null {
|
|
220
|
+
const out = execFileSync(cmd, args, {
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
timeout: 3000,
|
|
223
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
224
|
+
windowsHide: true,
|
|
225
|
+
});
|
|
226
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
227
|
+
if (lines.length === 0) return null;
|
|
228
|
+
|
|
229
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
230
|
+
const matched: AutocompleteItem[] = [];
|
|
231
|
+
const seen = new Set<string>();
|
|
232
|
+
|
|
233
|
+
for (const line of lines) {
|
|
234
|
+
if (matched.length >= 50) break;
|
|
235
|
+
// Normalize: fd outputs dirs with trailing /, rg doesn't
|
|
236
|
+
const isDir = line.endsWith("/");
|
|
237
|
+
const entry = isDir ? line.slice(0, -1) : line;
|
|
238
|
+
if (!entry.toLowerCase().includes(lowerPrefix) || seen.has(entry)) continue;
|
|
239
|
+
seen.add(entry);
|
|
240
|
+
|
|
241
|
+
const dir = dirname(entry);
|
|
242
|
+
const name = basename(entry) + (isDir ? "/" : "");
|
|
243
|
+
matched.push({
|
|
244
|
+
value: `in:${line}`,
|
|
245
|
+
label: name,
|
|
246
|
+
description: dir === "." ? "" : dir + "/",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// For rg (no dirs output), extract ancestor dirs that match
|
|
250
|
+
if (!isDir) {
|
|
251
|
+
addAncestorDirMatches(entry, lowerPrefix, matched, seen);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
matched.sort((a, b) => {
|
|
256
|
+
const aDir = a.label.endsWith("/") ? 0 : 1;
|
|
257
|
+
const bDir = b.label.endsWith("/") ? 0 : 1;
|
|
258
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
259
|
+
return a.label.localeCompare(b.label);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return matched;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* For non-directory entries, extract ancestor directories that match the prefix
|
|
267
|
+
* and add them as autocomplete suggestions.
|
|
268
|
+
*/
|
|
269
|
+
function addAncestorDirMatches(
|
|
270
|
+
entry: string,
|
|
271
|
+
lowerPrefix: string,
|
|
272
|
+
matched: AutocompleteItem[],
|
|
273
|
+
seen: Set<string>,
|
|
274
|
+
): void {
|
|
275
|
+
const segs = entry.split("/");
|
|
276
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
277
|
+
if (matched.length >= 50) break;
|
|
278
|
+
const dirPath = segs.slice(0, i + 1).join("/") + "/";
|
|
279
|
+
if (seen.has(dirPath)) continue;
|
|
280
|
+
seen.add(dirPath);
|
|
281
|
+
if (segs[i].toLowerCase().includes(lowerPrefix)) {
|
|
282
|
+
const parentDir = segs.slice(0, i).join("/");
|
|
283
|
+
matched.push({
|
|
284
|
+
value: `in:${dirPath}`,
|
|
285
|
+
label: segs[i] + "/",
|
|
286
|
+
description: parentDir || "",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function fetchRuleAutocompleteCompletions(
|
|
293
|
+
serverUrl: string,
|
|
294
|
+
token: string | undefined,
|
|
295
|
+
projectKey: string,
|
|
296
|
+
query: string,
|
|
297
|
+
): Promise<AutocompleteItem[] | null> {
|
|
298
|
+
try {
|
|
299
|
+
const profiles = await fetchProjectProfiles(serverUrl, token, projectKey);
|
|
300
|
+
const languages = [
|
|
301
|
+
...new Set(profiles.map((p) => p.language).filter(Boolean)),
|
|
302
|
+
];
|
|
303
|
+
const rules = await fetchRuleSearch(serverUrl, token, query, languages);
|
|
304
|
+
if (rules.length === 0) return null;
|
|
305
|
+
return rules.map((r) => ({
|
|
306
|
+
value: `rule:${r.key}`,
|
|
307
|
+
label: r.key,
|
|
308
|
+
description: r.name ?? "",
|
|
309
|
+
}));
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
92
314
|
|
|
93
|
-
|
|
315
|
+
function getInPathCompletions(
|
|
316
|
+
prefix: string,
|
|
317
|
+
baseDir: string,
|
|
318
|
+
): AutocompleteItem[] | null {
|
|
319
|
+
// Directory browsing: show contents when prefix ends with /
|
|
320
|
+
if (prefix.endsWith("/")) {
|
|
321
|
+
return listDirCompletions(resolve(baseDir, prefix), baseDir);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Empty prefix: show root directory contents
|
|
325
|
+
if (!prefix) {
|
|
326
|
+
return listDirCompletions(baseDir, baseDir);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Try fd first (fast, respects .gitignore, matches Pi TUI behavior)
|
|
330
|
+
try {
|
|
331
|
+
return fuzzyWithCmd(
|
|
332
|
+
"fd",
|
|
333
|
+
[
|
|
334
|
+
"--base-directory",
|
|
335
|
+
baseDir,
|
|
336
|
+
"--type",
|
|
337
|
+
"f",
|
|
338
|
+
"--type",
|
|
339
|
+
"d",
|
|
340
|
+
"--hidden",
|
|
341
|
+
"--exclude",
|
|
342
|
+
".git",
|
|
343
|
+
"--max-results",
|
|
344
|
+
"50",
|
|
345
|
+
"--full-path",
|
|
346
|
+
prefix,
|
|
347
|
+
],
|
|
348
|
+
prefix,
|
|
349
|
+
baseDir,
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
// fd not available
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fallback: rg --files (slower but available on most systems)
|
|
356
|
+
try {
|
|
357
|
+
return fuzzyWithCmd(
|
|
358
|
+
"rg",
|
|
359
|
+
["--files", "--color", "never", "--no-ignore-parent"],
|
|
360
|
+
prefix,
|
|
361
|
+
baseDir,
|
|
362
|
+
);
|
|
363
|
+
} catch {
|
|
364
|
+
// rg not available
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Last resort: list current directory
|
|
368
|
+
return listDirCompletions(baseDir, baseDir);
|
|
94
369
|
}
|
|
95
370
|
|
|
96
|
-
|
|
371
|
+
function getOpenCompletions(
|
|
372
|
+
issues: SonarIssue[] | undefined,
|
|
373
|
+
mode: "STANDARD" | "MQR" | undefined,
|
|
374
|
+
current: string,
|
|
375
|
+
): AutocompleteItem[] | null {
|
|
376
|
+
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
|
|
377
|
+
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
378
|
+
return {
|
|
379
|
+
value: String(index + 1),
|
|
380
|
+
label: String(index + 1),
|
|
381
|
+
description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
385
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
386
|
+
return filtered.length > 0 ? filtered : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function getFilterCompletions(
|
|
390
|
+
current: string,
|
|
391
|
+
mode: "STANDARD" | "MQR" | undefined,
|
|
392
|
+
cwd: string | undefined,
|
|
393
|
+
serverUrl: string | undefined,
|
|
394
|
+
token: string | undefined,
|
|
395
|
+
projectKey: string | undefined,
|
|
396
|
+
): Promise<AutocompleteItem[] | null> {
|
|
397
|
+
if (current.startsWith("in:") && cwd) {
|
|
398
|
+
return getInPathCompletions(current.slice(3), cwd);
|
|
399
|
+
}
|
|
400
|
+
if (
|
|
401
|
+
(current.startsWith("rule:") || current.startsWith("rules:")) &&
|
|
402
|
+
serverUrl &&
|
|
403
|
+
projectKey
|
|
404
|
+
) {
|
|
405
|
+
const query = current.split(":").slice(1).join(":");
|
|
406
|
+
const ruleItems = await fetchRuleAutocompleteCompletions(
|
|
407
|
+
serverUrl,
|
|
408
|
+
token,
|
|
409
|
+
projectKey,
|
|
410
|
+
query,
|
|
411
|
+
);
|
|
412
|
+
const staticSuggestions = createFilterCompletionList(mode);
|
|
413
|
+
const filtered = filterAutocompleteItems(staticSuggestions, current);
|
|
414
|
+
const combined = [
|
|
415
|
+
...(filtered.length > 0 ? filtered : []),
|
|
416
|
+
...(ruleItems ?? []),
|
|
417
|
+
];
|
|
418
|
+
return combined.length > 0 ? combined : null;
|
|
419
|
+
}
|
|
420
|
+
const suggestions = createFilterCompletionList(mode);
|
|
421
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
422
|
+
return filtered.length > 0 ? filtered : null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function sonarArgumentCompletions(
|
|
97
426
|
argumentText: string,
|
|
98
427
|
issues?: SonarIssue[],
|
|
99
428
|
mode?: "STANDARD" | "MQR",
|
|
100
|
-
|
|
429
|
+
cwd?: string,
|
|
430
|
+
serverUrl?: string,
|
|
431
|
+
token?: string,
|
|
432
|
+
projectKey?: string,
|
|
433
|
+
): Promise<AutocompleteItem[] | null> {
|
|
101
434
|
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
102
435
|
const lowerCommand = command.toLowerCase();
|
|
103
|
-
const commandMatches = SONAR_COMMANDS.filter((item) =>
|
|
104
|
-
|
|
436
|
+
const commandMatches = SONAR_COMMANDS.filter((item) =>
|
|
437
|
+
item.value.startsWith(lowerCommand),
|
|
438
|
+
);
|
|
439
|
+
const isFullMatch = SONAR_COMMANDS.some(
|
|
440
|
+
(item) => item.value === lowerCommand,
|
|
441
|
+
);
|
|
105
442
|
|
|
106
443
|
if (!command || commandMatches.length > 1 || !isFullMatch) {
|
|
107
444
|
if (commandMatches.length === 0) return null;
|
|
108
|
-
return commandMatches.map((item) => ({
|
|
445
|
+
return commandMatches.map((item) => ({
|
|
446
|
+
value: item.value,
|
|
447
|
+
label: item.value,
|
|
448
|
+
description: item.description,
|
|
449
|
+
}));
|
|
109
450
|
}
|
|
110
451
|
|
|
111
452
|
if (lowerCommand === "open") {
|
|
112
|
-
|
|
113
|
-
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
114
|
-
return { value: String(index + 1), label: String(index + 1), description: `${issue.severity} ${issue.filePath}${lineSuffix}` };
|
|
115
|
-
});
|
|
116
|
-
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
117
|
-
const filtered = filterAutocompleteItems(suggestions, current);
|
|
118
|
-
return filtered.length > 0 ? filtered : null;
|
|
453
|
+
return getOpenCompletions(issues, mode, current);
|
|
119
454
|
}
|
|
120
455
|
|
|
121
456
|
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
457
|
+
return getFilterCompletions(
|
|
458
|
+
current,
|
|
459
|
+
mode,
|
|
460
|
+
cwd,
|
|
461
|
+
serverUrl,
|
|
462
|
+
token,
|
|
463
|
+
projectKey,
|
|
464
|
+
);
|
|
125
465
|
}
|
|
126
466
|
|
|
127
|
-
if (
|
|
467
|
+
if (
|
|
468
|
+
lowerCommand === "init" ||
|
|
469
|
+
lowerCommand === "metrics" ||
|
|
470
|
+
lowerCommand === "duplications"
|
|
471
|
+
) {
|
|
128
472
|
return null;
|
|
129
473
|
}
|
|
130
474
|
|
|
131
|
-
return tokens.length === 0
|
|
475
|
+
return tokens.length === 0
|
|
476
|
+
? filterAutocompleteItems(SONAR_COMMANDS, current)
|
|
477
|
+
: null;
|
|
132
478
|
}
|
|
133
479
|
|
|
134
480
|
// ── Command arg parsing ─────────────────────────────────────────────────────
|
|
@@ -136,12 +482,25 @@ export function sonarArgumentCompletions(
|
|
|
136
482
|
export type ParsedSonarCommand =
|
|
137
483
|
| { action: "help" }
|
|
138
484
|
| { action: "init"; alias?: string; targetInput?: string }
|
|
139
|
-
| {
|
|
485
|
+
| {
|
|
486
|
+
action: "analyze";
|
|
487
|
+
targetInput?: string;
|
|
488
|
+
filters?: SonarIssueFetchOptions;
|
|
489
|
+
}
|
|
140
490
|
| { action: "issues"; targetInput?: string; filters?: SonarIssueFetchOptions }
|
|
141
|
-
| {
|
|
491
|
+
| {
|
|
492
|
+
action: "open";
|
|
493
|
+
targetInput?: string;
|
|
494
|
+
issueIndex?: number;
|
|
495
|
+
filters?: SonarIssueFetchOptions;
|
|
496
|
+
}
|
|
142
497
|
| { action: "metrics"; targetInput?: string }
|
|
143
|
-
| {
|
|
144
|
-
|
|
498
|
+
| {
|
|
499
|
+
action: "duplications";
|
|
500
|
+
targetInput?: string;
|
|
501
|
+
issueIndex?: number;
|
|
502
|
+
filters?: SonarIssueFetchOptions;
|
|
503
|
+
};
|
|
145
504
|
|
|
146
505
|
export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
147
506
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -170,7 +529,10 @@ export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
|
170
529
|
return { action: "metrics", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
171
530
|
}
|
|
172
531
|
if (head === "duplications") {
|
|
173
|
-
return {
|
|
532
|
+
return {
|
|
533
|
+
action: "duplications",
|
|
534
|
+
...parseSonarIssueArgs(tokens.slice(1), true),
|
|
535
|
+
};
|
|
174
536
|
}
|
|
175
537
|
|
|
176
538
|
return { action: "analyze", ...parseSonarIssueArgs(tokens) };
|
|
@@ -193,6 +555,9 @@ export function helpText(): string {
|
|
|
193
555
|
"Filters:",
|
|
194
556
|
" /sonarqube issues be CRITICAL",
|
|
195
557
|
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
558
|
+
" /sonarqube issues be rule:S1192",
|
|
559
|
+
" /sonarqube issues in:src/api.ts",
|
|
560
|
+
" /sonarqube issues in:src/",
|
|
196
561
|
" /sonarqube issues be quality:RELIABILITY",
|
|
197
562
|
" /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
|
|
198
563
|
"",
|
|
@@ -206,6 +571,7 @@ export function helpText(): string {
|
|
|
206
571
|
"Defaults:",
|
|
207
572
|
" config is stored in .pi/sonarqube.json",
|
|
208
573
|
" use project paths directly (no alias needed)",
|
|
574
|
+
" issue and duplication browsers search file, rule, severity, status, message, and stats",
|
|
209
575
|
].join("\n");
|
|
210
576
|
}
|
|
211
577
|
|
|
@@ -221,8 +587,11 @@ export function sonarErrorMessage(error: unknown): string {
|
|
|
221
587
|
|
|
222
588
|
export function formatIssue(issue: SonarIssue, index?: number): string {
|
|
223
589
|
const loc = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
224
|
-
const prefix =
|
|
225
|
-
|
|
590
|
+
const prefix =
|
|
591
|
+
typeof index === "number" ? `${String(index).padStart(2, "0")}. ` : "";
|
|
592
|
+
const rule = issue.ruleName
|
|
593
|
+
? `${issue.rule} (${issue.ruleName})`
|
|
594
|
+
: issue.rule;
|
|
226
595
|
return `${prefix}${issue.severity} ${loc} — ${rule} — ${issue.message}`;
|
|
227
596
|
}
|
|
228
597
|
|
|
@@ -240,10 +609,14 @@ export function formatMetricsOutput(metrics: {
|
|
|
240
609
|
lines.push("Coverage: n/a");
|
|
241
610
|
} else {
|
|
242
611
|
const pct = metrics.measures.coverage.toFixed(1);
|
|
243
|
-
const covered =
|
|
612
|
+
const covered =
|
|
613
|
+
(metrics.measures.linesToCover ?? 0) -
|
|
614
|
+
(metrics.measures.uncoveredLines ?? 0);
|
|
244
615
|
const uncovered = metrics.measures.uncoveredLines ?? 0;
|
|
245
616
|
const total = metrics.measures.linesToCover ?? 0;
|
|
246
|
-
lines.push(
|
|
617
|
+
lines.push(
|
|
618
|
+
`Coverage: ${pct}% covered=${covered} uncovered=${uncovered} lines=${total}`,
|
|
619
|
+
);
|
|
247
620
|
}
|
|
248
621
|
|
|
249
622
|
const density = metrics.measures.duplicatedLinesDensity.toFixed(1);
|
|
@@ -255,21 +628,22 @@ export function formatMetricsOutput(metrics: {
|
|
|
255
628
|
}
|
|
256
629
|
|
|
257
630
|
if (metrics.issueCounts) {
|
|
258
|
-
const counts =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
631
|
+
const counts =
|
|
632
|
+
metrics.cleanCodeMode === "MQR"
|
|
633
|
+
? [
|
|
634
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
635
|
+
`HIGH ${metrics.issueCounts.critical}`,
|
|
636
|
+
`MEDIUM ${metrics.issueCounts.major}`,
|
|
637
|
+
`LOW ${metrics.issueCounts.minor}`,
|
|
638
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
639
|
+
]
|
|
640
|
+
: [
|
|
641
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
642
|
+
`CRITICAL ${metrics.issueCounts.critical}`,
|
|
643
|
+
`MAJOR ${metrics.issueCounts.major}`,
|
|
644
|
+
`MINOR ${metrics.issueCounts.minor}`,
|
|
645
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
646
|
+
];
|
|
273
647
|
lines.push(`Issues: ${counts.join(" ")}`);
|
|
274
648
|
}
|
|
275
649
|
|
|
@@ -289,12 +663,17 @@ export function formatDuplicationsList(files: FileDuplication[]): string {
|
|
|
289
663
|
if (files.length === 0) return "No duplicated files found.";
|
|
290
664
|
const lines: string[] = [`Duplicated files (${files.length})`];
|
|
291
665
|
for (const [i, f] of files.entries()) {
|
|
292
|
-
lines.push(
|
|
666
|
+
lines.push(
|
|
667
|
+
` ${String(i + 1).padStart(2, " ")}. ${f.filePath} dup%=${f.duplicatedLinesDensity.toFixed(1)} blocks=${f.duplicatedBlocks} lines=${f.duplicatedLines}`,
|
|
668
|
+
);
|
|
293
669
|
}
|
|
294
670
|
return lines.join("\n");
|
|
295
671
|
}
|
|
296
672
|
|
|
297
|
-
export function formatDuplicationBlockDetail(
|
|
673
|
+
export function formatDuplicationBlockDetail(
|
|
674
|
+
filePath: string,
|
|
675
|
+
groups: DuplicationBlockGroup[],
|
|
676
|
+
): string {
|
|
298
677
|
if (groups.length === 0) return `No duplications found in ${filePath}.`;
|
|
299
678
|
const lines: string[] = [`Duplications in ${filePath}`, ""];
|
|
300
679
|
for (const [i, group] of groups.entries()) {
|
|
@@ -308,9 +687,15 @@ export function formatDuplicationBlockDetail(filePath: string, groups: Duplicati
|
|
|
308
687
|
return lines.join("\n");
|
|
309
688
|
}
|
|
310
689
|
|
|
311
|
-
export function formatSummary(state: {
|
|
690
|
+
export function formatSummary(state: {
|
|
691
|
+
totalIssues: number;
|
|
692
|
+
projectKey: string;
|
|
693
|
+
filters?: SonarIssueFetchOptions;
|
|
694
|
+
}): string {
|
|
312
695
|
const issueCount = state.totalIssues;
|
|
313
|
-
const filterLabel = state.filters
|
|
696
|
+
const filterLabel = state.filters
|
|
697
|
+
? ` (${issueFilterLabel(state.filters)})`
|
|
698
|
+
: "";
|
|
314
699
|
if (issueCount === 0) {
|
|
315
700
|
return `SonarQube: no issues found for ${state.projectKey}${filterLabel}`;
|
|
316
701
|
}
|
|
@@ -339,9 +724,12 @@ export function formatReport(state: {
|
|
|
339
724
|
}
|
|
340
725
|
|
|
341
726
|
if (state.measures && state.issues.length > 0) {
|
|
342
|
-
lines.push(
|
|
343
|
-
|
|
344
|
-
|
|
727
|
+
lines.push(
|
|
728
|
+
"",
|
|
729
|
+
state.measures.duplicatedBlocks > 0
|
|
730
|
+
? `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
|
|
731
|
+
: `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% (no duplications detected)`,
|
|
732
|
+
);
|
|
345
733
|
}
|
|
346
734
|
|
|
347
735
|
if (state.issues.length === 0) {
|