@jonathan0823/pi-sonarqube 0.4.0 → 0.5.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/dist/api.d.ts.map +1 -1
- package/dist/api.js +48 -13
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +66 -19
- 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 +133 -29
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -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.map +1 -1
- package/dist/ui.js +41 -11
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +212 -58
- package/src/commands.ts +155 -53
- package/src/config.ts +84 -23
- package/src/index.ts +423 -86
- package/src/types.ts +27 -4
- package/src/ui.ts +121 -26
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
Theme,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
7
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
8
|
import { stat } from "node:fs/promises";
|
|
4
9
|
import { basename } from "node:path";
|
|
@@ -68,22 +73,48 @@ import {
|
|
|
68
73
|
} from "./ui.js";
|
|
69
74
|
|
|
70
75
|
const SonarToolParams = Type.Object({
|
|
71
|
-
action: StringEnum([
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
action: StringEnum([
|
|
77
|
+
"analyze",
|
|
78
|
+
"issues",
|
|
79
|
+
"open",
|
|
80
|
+
"metrics",
|
|
81
|
+
"duplications",
|
|
82
|
+
] as const),
|
|
83
|
+
path: Type.Optional(
|
|
84
|
+
Type.String({
|
|
85
|
+
description: "Target alias or project directory to analyze or inspect",
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
issueIndex: Type.Optional(
|
|
89
|
+
Type.Number({ description: "1-based issue index to open" }),
|
|
90
|
+
),
|
|
74
91
|
severities: Type.Optional(
|
|
75
|
-
Type.Array(
|
|
92
|
+
Type.Array(
|
|
93
|
+
Type.String({ description: "Issue severity to fetch (e.g. CRITICAL)" }),
|
|
94
|
+
),
|
|
76
95
|
),
|
|
77
96
|
statuses: Type.Optional(
|
|
78
|
-
Type.Array(
|
|
97
|
+
Type.Array(
|
|
98
|
+
Type.String({ description: "Issue status to fetch (e.g. OPEN)" }),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
types: Type.Optional(
|
|
102
|
+
Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" })),
|
|
103
|
+
),
|
|
104
|
+
rules: Type.Optional(
|
|
105
|
+
Type.Array(Type.String({ description: "Rule keys to fetch" })),
|
|
79
106
|
),
|
|
80
|
-
types: Type.Optional(Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" }))),
|
|
81
|
-
rules: Type.Optional(Type.Array(Type.String({ description: "Rule keys to fetch" }))),
|
|
82
107
|
softwareQualities: Type.Optional(
|
|
83
|
-
Type.Array(
|
|
108
|
+
Type.Array(
|
|
109
|
+
Type.String({
|
|
110
|
+
description: "Software quality to fetch (e.g. MAINTAINABILITY)",
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
84
113
|
),
|
|
85
114
|
impactSeverities: Type.Optional(
|
|
86
|
-
Type.Array(
|
|
115
|
+
Type.Array(
|
|
116
|
+
Type.String({ description: "Impact severity to fetch (e.g. HIGH)" }),
|
|
117
|
+
),
|
|
87
118
|
),
|
|
88
119
|
});
|
|
89
120
|
|
|
@@ -97,7 +128,11 @@ async function restoreState(
|
|
|
97
128
|
statesByBaseDir.clear();
|
|
98
129
|
let latest: SonarAnalysisState | undefined;
|
|
99
130
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
100
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
entry.type === "custom" &&
|
|
133
|
+
entry.customType === STATE_TYPE &&
|
|
134
|
+
entry.data
|
|
135
|
+
) {
|
|
101
136
|
latest = entry.data as SonarAnalysisState;
|
|
102
137
|
statesByBaseDir.set(latest.baseDir, latest);
|
|
103
138
|
}
|
|
@@ -132,7 +167,12 @@ async function analyzeProject(
|
|
|
132
167
|
const dashboardUrl = report["dashboardUrl"];
|
|
133
168
|
const analysisId = ceTaskUrl
|
|
134
169
|
? (analysisUi.setPhase("Waiting for SonarQube analysis..."),
|
|
135
|
-
await waitForAnalysis(
|
|
170
|
+
await waitForAnalysis(
|
|
171
|
+
config.serverUrl,
|
|
172
|
+
config.token,
|
|
173
|
+
ceTaskUrl,
|
|
174
|
+
ctx.signal,
|
|
175
|
+
))
|
|
136
176
|
: undefined;
|
|
137
177
|
|
|
138
178
|
analysisUi.setPhase("Fetching issues...");
|
|
@@ -154,7 +194,11 @@ async function analyzeProject(
|
|
|
154
194
|
);
|
|
155
195
|
|
|
156
196
|
analysisUi.setPhase("Detecting mode...");
|
|
157
|
-
const cleanCodeMode = await fetchCleanCodeMode(
|
|
197
|
+
const cleanCodeMode = await fetchCleanCodeMode(
|
|
198
|
+
config.serverUrl,
|
|
199
|
+
config.token,
|
|
200
|
+
ctx.signal,
|
|
201
|
+
);
|
|
158
202
|
|
|
159
203
|
const state = createAnalysisState(config, issues, {
|
|
160
204
|
dashboardUrl,
|
|
@@ -200,26 +244,50 @@ async function collectInitInputs(
|
|
|
200
244
|
ctx: ExtensionCommandContext,
|
|
201
245
|
existing: SonarInitConfig | undefined,
|
|
202
246
|
baseDir: string,
|
|
203
|
-
): Promise<
|
|
204
|
-
|
|
247
|
+
): Promise<
|
|
248
|
+
| { serverUrl: string; projectKey: string; token: string | undefined }
|
|
249
|
+
| undefined
|
|
250
|
+
> {
|
|
251
|
+
const serverUrlInput = await ctx.ui.editor(
|
|
252
|
+
"SonarQube server URL",
|
|
253
|
+
existing?.serverUrl ?? "http://localhost:9000",
|
|
254
|
+
);
|
|
205
255
|
if (serverUrlInput === undefined) return undefined;
|
|
206
|
-
const serverUrl = normalizeServerUrl(
|
|
256
|
+
const serverUrl = normalizeServerUrl(
|
|
257
|
+
serverUrlInput || existing?.serverUrl || "http://localhost:9000",
|
|
258
|
+
);
|
|
207
259
|
|
|
208
|
-
const projectKeyInput = await ctx.ui.input(
|
|
260
|
+
const projectKeyInput = await ctx.ui.input(
|
|
261
|
+
"SonarQube project key",
|
|
262
|
+
existing?.projectKey ?? basename(baseDir),
|
|
263
|
+
);
|
|
209
264
|
if (projectKeyInput === undefined) return undefined;
|
|
210
|
-
const projectKey =
|
|
265
|
+
const projectKey =
|
|
266
|
+
projectKeyInput || existing?.projectKey || slugify(basename(baseDir));
|
|
211
267
|
|
|
212
|
-
const tokenInput = await ctx.ui.input(
|
|
268
|
+
const tokenInput = await ctx.ui.input(
|
|
269
|
+
"SonarQube token (optional, press Enter to skip)",
|
|
270
|
+
existing?.token ?? "",
|
|
271
|
+
);
|
|
213
272
|
if (tokenInput === undefined) return undefined;
|
|
214
273
|
|
|
215
274
|
return { serverUrl, projectKey, token: tokenInput.trim() || undefined };
|
|
216
275
|
}
|
|
217
276
|
|
|
218
|
-
async function initCommandHandler(
|
|
219
|
-
|
|
277
|
+
async function initCommandHandler(
|
|
278
|
+
ctx: ExtensionCommandContext,
|
|
279
|
+
options: InitCommandOptions = {},
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
const { baseDir, repoRoot } = await resolveInitTarget(
|
|
282
|
+
ctx,
|
|
283
|
+
options.alias,
|
|
284
|
+
options.targetInput,
|
|
285
|
+
);
|
|
220
286
|
const existing = await loadProjectConfig(baseDir);
|
|
221
287
|
|
|
222
|
-
const confirmed = existing
|
|
288
|
+
const confirmed = existing
|
|
289
|
+
? await confirmOverwrite(ctx, baseDir, existing)
|
|
290
|
+
: true;
|
|
223
291
|
if (!confirmed) return;
|
|
224
292
|
|
|
225
293
|
const inputs = await collectInitInputs(ctx, existing, baseDir);
|
|
@@ -250,7 +318,10 @@ function notifyInitResult(
|
|
|
250
318
|
} else if (propertiesState === "updated") {
|
|
251
319
|
note = ` and merged ${sonarProjectPropertiesPath(baseDir)}`;
|
|
252
320
|
}
|
|
253
|
-
ctx.ui.notify(
|
|
321
|
+
ctx.ui.notify(
|
|
322
|
+
`SonarQube config saved${label} to ${projectConfigPath(baseDir)}${note}`,
|
|
323
|
+
"info",
|
|
324
|
+
);
|
|
254
325
|
}
|
|
255
326
|
|
|
256
327
|
// ── Tool result rendering helpers (reduces renderResult complexity) ─────────
|
|
@@ -289,9 +360,10 @@ function renderIssueListResult(
|
|
|
289
360
|
lines.push(theme.fg("accent", summary));
|
|
290
361
|
if (state.measures) {
|
|
291
362
|
const density = state.measures.duplicatedLinesDensity.toFixed(1);
|
|
292
|
-
const detail =
|
|
293
|
-
|
|
294
|
-
|
|
363
|
+
const detail =
|
|
364
|
+
state.measures.duplicatedBlocks > 0
|
|
365
|
+
? `Duplication: ${density}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
|
|
366
|
+
: `Duplication: ${density}% (no duplications detected)`;
|
|
295
367
|
lines.push(theme.fg("dim", detail));
|
|
296
368
|
}
|
|
297
369
|
|
|
@@ -304,7 +376,9 @@ function renderIssueListResult(
|
|
|
304
376
|
lines.push(theme.fg("muted", formatIssue(issue, i + 1)));
|
|
305
377
|
}
|
|
306
378
|
if (!expanded && state.issues.length > visible.length) {
|
|
307
|
-
lines.push(
|
|
379
|
+
lines.push(
|
|
380
|
+
theme.fg("dim", `... ${state.issues.length - visible.length} more`),
|
|
381
|
+
);
|
|
308
382
|
}
|
|
309
383
|
return new Text(lines.join("\n"), 0, 0);
|
|
310
384
|
}
|
|
@@ -333,22 +407,55 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
333
407
|
const line = lines[cursorLine] ?? "";
|
|
334
408
|
const beforeCursor = line.slice(0, cursorCol);
|
|
335
409
|
const match = /^\/sonarqube(?:\s+(.*))?$/.exec(beforeCursor);
|
|
336
|
-
if (!match)
|
|
410
|
+
if (!match)
|
|
411
|
+
return current.getSuggestions(
|
|
412
|
+
lines,
|
|
413
|
+
cursorLine,
|
|
414
|
+
cursorCol,
|
|
415
|
+
options,
|
|
416
|
+
);
|
|
337
417
|
|
|
338
418
|
const argumentText = match[1] ?? "";
|
|
339
|
-
const { current: currentToken } =
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
419
|
+
const { current: currentToken } =
|
|
420
|
+
splitSonarArgumentContext(argumentText);
|
|
421
|
+
const extraItems =
|
|
422
|
+
sonarArgumentCompletions(
|
|
423
|
+
argumentText,
|
|
424
|
+
latestState?.issues,
|
|
425
|
+
latestState?.cleanCodeMode,
|
|
426
|
+
) ?? [];
|
|
427
|
+
const currentSuggestions = await current.getSuggestions(
|
|
428
|
+
lines,
|
|
429
|
+
cursorLine,
|
|
430
|
+
cursorCol,
|
|
431
|
+
options,
|
|
432
|
+
);
|
|
433
|
+
const merged = mergeAutocompleteSuggestions(
|
|
434
|
+
currentToken,
|
|
435
|
+
currentSuggestions,
|
|
436
|
+
extraItems,
|
|
437
|
+
);
|
|
343
438
|
return merged ?? currentSuggestions;
|
|
344
439
|
},
|
|
345
440
|
|
|
346
441
|
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
347
|
-
return current.applyCompletion(
|
|
442
|
+
return current.applyCompletion(
|
|
443
|
+
lines,
|
|
444
|
+
cursorLine,
|
|
445
|
+
cursorCol,
|
|
446
|
+
item,
|
|
447
|
+
prefix,
|
|
448
|
+
);
|
|
348
449
|
},
|
|
349
450
|
|
|
350
451
|
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
351
|
-
return
|
|
452
|
+
return (
|
|
453
|
+
current.shouldTriggerFileCompletion?.(
|
|
454
|
+
lines,
|
|
455
|
+
cursorLine,
|
|
456
|
+
cursorCol,
|
|
457
|
+
) ?? true
|
|
458
|
+
);
|
|
352
459
|
},
|
|
353
460
|
}));
|
|
354
461
|
}
|
|
@@ -384,24 +491,43 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
384
491
|
if (params.action === "analyze") {
|
|
385
492
|
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
386
493
|
rememberState(state);
|
|
387
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: formatReport(state) }],
|
|
496
|
+
details: state,
|
|
497
|
+
};
|
|
388
498
|
}
|
|
389
499
|
|
|
390
|
-
if (params.action === "duplications")
|
|
500
|
+
if (params.action === "duplications")
|
|
501
|
+
return toolDuplications(ctx, params.path);
|
|
391
502
|
if (params.action === "metrics") return toolMetrics(ctx, params.path);
|
|
392
503
|
|
|
393
|
-
const targetState = await resolveTargetState(
|
|
504
|
+
const targetState = await resolveTargetState(
|
|
505
|
+
ctx,
|
|
506
|
+
statesByBaseDir,
|
|
507
|
+
params.path,
|
|
508
|
+
filters,
|
|
509
|
+
);
|
|
394
510
|
if (!targetState) {
|
|
395
511
|
return {
|
|
396
|
-
content: [
|
|
397
|
-
|
|
512
|
+
content: [
|
|
513
|
+
{
|
|
514
|
+
type: "text",
|
|
515
|
+
text: "No SonarQube analysis has been run for this target yet.",
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
details: {
|
|
519
|
+
error: "No SonarQube analysis has been run for this target yet.",
|
|
520
|
+
},
|
|
398
521
|
};
|
|
399
522
|
}
|
|
400
523
|
|
|
401
524
|
rememberState(targetState);
|
|
402
525
|
|
|
403
526
|
if (params.action === "issues") {
|
|
404
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text", text: formatReport(targetState) }],
|
|
529
|
+
details: targetState,
|
|
530
|
+
};
|
|
405
531
|
}
|
|
406
532
|
|
|
407
533
|
const index = params.issueIndex ?? 1;
|
|
@@ -415,12 +541,20 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
415
541
|
|
|
416
542
|
const preview = await buildIssuePreview(targetState.baseDir, issue);
|
|
417
543
|
return {
|
|
418
|
-
content: [
|
|
544
|
+
content: [
|
|
545
|
+
{
|
|
546
|
+
type: "text",
|
|
547
|
+
text: `${formatIssue(issue, index)}\n\n${preview}`,
|
|
548
|
+
},
|
|
549
|
+
],
|
|
419
550
|
details: { ...targetState, selectedIssue: issue },
|
|
420
551
|
};
|
|
421
552
|
} catch (error) {
|
|
422
553
|
const msg = sonarErrorMessage(error);
|
|
423
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text", text: msg }],
|
|
556
|
+
details: { error: msg },
|
|
557
|
+
};
|
|
424
558
|
}
|
|
425
559
|
},
|
|
426
560
|
|
|
@@ -433,7 +567,9 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
433
567
|
softwareQualities: args.softwareQualities,
|
|
434
568
|
impactSeverities: args.impactSeverities,
|
|
435
569
|
});
|
|
436
|
-
const filterText = filters
|
|
570
|
+
const filterText = filters
|
|
571
|
+
? ` ${theme.fg("muted", issueFilterLabel(filters))}`
|
|
572
|
+
: "";
|
|
437
573
|
return new Text(
|
|
438
574
|
`${theme.fg("toolTitle", theme.bold("sonarqube"))} ${theme.fg("muted", args.action)}${
|
|
439
575
|
args.path ? ` ${theme.fg("accent", args.path)}` : ""
|
|
@@ -460,7 +596,10 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
460
596
|
|
|
461
597
|
if ("selectedIssue" in state && state.selectedIssue) {
|
|
462
598
|
const issue = state.selectedIssue;
|
|
463
|
-
const contentText =
|
|
599
|
+
const contentText =
|
|
600
|
+
result.content[0]?.type === "text"
|
|
601
|
+
? result.content[0].text
|
|
602
|
+
: undefined;
|
|
464
603
|
return renderIssueResult(issue, contentText, expanded, theme);
|
|
465
604
|
}
|
|
466
605
|
|
|
@@ -474,8 +613,14 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
474
613
|
});
|
|
475
614
|
|
|
476
615
|
pi.registerCommand("sonarqube", {
|
|
477
|
-
description:
|
|
478
|
-
|
|
616
|
+
description:
|
|
617
|
+
"Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
618
|
+
getArgumentCompletions: (argumentPrefix) =>
|
|
619
|
+
sonarArgumentCompletions(
|
|
620
|
+
argumentPrefix,
|
|
621
|
+
latestState?.issues,
|
|
622
|
+
latestState?.cleanCodeMode,
|
|
623
|
+
),
|
|
479
624
|
handler: async (args, ctx) => {
|
|
480
625
|
try {
|
|
481
626
|
const parsed = parseCommandArgs(args);
|
|
@@ -484,20 +629,40 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
484
629
|
if (ctx.hasUI) ctx.ui.notify(helpText(), "info");
|
|
485
630
|
return;
|
|
486
631
|
case "init":
|
|
487
|
-
await initCommandHandler(ctx, {
|
|
632
|
+
await initCommandHandler(ctx, {
|
|
633
|
+
alias: parsed.alias,
|
|
634
|
+
targetInput: parsed.targetInput,
|
|
635
|
+
});
|
|
488
636
|
return;
|
|
489
637
|
case "analyze": {
|
|
490
|
-
await commandAnalyze(
|
|
638
|
+
await commandAnalyze(
|
|
639
|
+
pi,
|
|
640
|
+
ctx,
|
|
641
|
+
parsed.targetInput,
|
|
642
|
+
parsed.filters,
|
|
643
|
+
rememberState,
|
|
644
|
+
);
|
|
491
645
|
return;
|
|
492
646
|
}
|
|
493
647
|
case "metrics":
|
|
494
648
|
await commandMetrics(ctx, parsed.targetInput);
|
|
495
649
|
return;
|
|
496
650
|
case "duplications":
|
|
497
|
-
await commandDuplications(
|
|
651
|
+
await commandDuplications(
|
|
652
|
+
pi,
|
|
653
|
+
ctx,
|
|
654
|
+
parsed.targetInput,
|
|
655
|
+
parsed.issueIndex,
|
|
656
|
+
);
|
|
498
657
|
return;
|
|
499
658
|
default:
|
|
500
|
-
await commandIssuesOrOpen(
|
|
659
|
+
await commandIssuesOrOpen(
|
|
660
|
+
pi,
|
|
661
|
+
ctx,
|
|
662
|
+
statesByBaseDir,
|
|
663
|
+
parsed,
|
|
664
|
+
rememberState,
|
|
665
|
+
);
|
|
501
666
|
}
|
|
502
667
|
} catch (error) {
|
|
503
668
|
if (ctx.hasUI) {
|
|
@@ -533,19 +698,48 @@ async function commandMetrics(
|
|
|
533
698
|
targetInput?: string,
|
|
534
699
|
): Promise<void> {
|
|
535
700
|
const config = await resolveConfig(ctx, targetInput);
|
|
536
|
-
const cleanCodeMode = await fetchCleanCodeMode(
|
|
701
|
+
const cleanCodeMode = await fetchCleanCodeMode(
|
|
702
|
+
config.serverUrl,
|
|
703
|
+
config.token,
|
|
704
|
+
ctx.signal,
|
|
705
|
+
);
|
|
537
706
|
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
538
|
-
fetchDuplicationMeasures(
|
|
539
|
-
|
|
540
|
-
|
|
707
|
+
fetchDuplicationMeasures(
|
|
708
|
+
config.serverUrl,
|
|
709
|
+
config.token,
|
|
710
|
+
config.projectKey,
|
|
711
|
+
ctx.signal,
|
|
712
|
+
),
|
|
713
|
+
fetchIssueSeverityCounts(
|
|
714
|
+
config.serverUrl,
|
|
715
|
+
config.token,
|
|
716
|
+
config.projectKey,
|
|
717
|
+
ctx.signal,
|
|
718
|
+
cleanCodeMode,
|
|
719
|
+
),
|
|
720
|
+
fetchIssueQualityCounts(
|
|
721
|
+
config.serverUrl,
|
|
722
|
+
config.token,
|
|
723
|
+
config.projectKey,
|
|
724
|
+
ctx.signal,
|
|
725
|
+
),
|
|
541
726
|
]);
|
|
542
727
|
if (!measures && !issueCounts && !qualityCounts) {
|
|
543
728
|
if (ctx.hasUI) {
|
|
544
|
-
ctx.ui.notify(
|
|
729
|
+
ctx.ui.notify(
|
|
730
|
+
`Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`,
|
|
731
|
+
"warning",
|
|
732
|
+
);
|
|
545
733
|
}
|
|
546
734
|
return;
|
|
547
735
|
}
|
|
548
|
-
const text = formatMetricsOutput({
|
|
736
|
+
const text = formatMetricsOutput({
|
|
737
|
+
projectKey: config.projectKey,
|
|
738
|
+
measures,
|
|
739
|
+
issueCounts,
|
|
740
|
+
issueQualityCounts: qualityCounts,
|
|
741
|
+
cleanCodeMode,
|
|
742
|
+
});
|
|
549
743
|
if (ctx.hasUI) {
|
|
550
744
|
ctx.ui.notify(text, "info");
|
|
551
745
|
}
|
|
@@ -560,12 +754,20 @@ async function commandDuplications(
|
|
|
560
754
|
const config = await resolveConfig(ctx, targetInput);
|
|
561
755
|
let files: FileDuplication[];
|
|
562
756
|
try {
|
|
563
|
-
files = await fetchFileDuplications(
|
|
757
|
+
files = await fetchFileDuplications(
|
|
758
|
+
config.serverUrl,
|
|
759
|
+
config.token,
|
|
760
|
+
config.projectKey,
|
|
761
|
+
ctx.signal,
|
|
762
|
+
);
|
|
564
763
|
} catch (error) {
|
|
565
764
|
const msg = sonarErrorMessage(error);
|
|
566
765
|
if (msg.includes("403") || msg.includes("Insufficient privileges")) {
|
|
567
766
|
if (ctx.hasUI) {
|
|
568
|
-
ctx.ui.notify(
|
|
767
|
+
ctx.ui.notify(
|
|
768
|
+
"SonarQube token needs 'See Source Code' permission to list duplicated files. Update the token permissions in SonarQube and run /sonarqube init to update the token.",
|
|
769
|
+
"warning",
|
|
770
|
+
);
|
|
569
771
|
}
|
|
570
772
|
return;
|
|
571
773
|
}
|
|
@@ -594,8 +796,18 @@ async function showDuplicationDrillDown(
|
|
|
594
796
|
if (ctx.hasUI) ctx.ui.notify(`File #${fileIndex} not found.`, "error");
|
|
595
797
|
return;
|
|
596
798
|
}
|
|
597
|
-
const groups = await fetchFileDuplicationBlocks(
|
|
598
|
-
|
|
799
|
+
const groups = await fetchFileDuplicationBlocks(
|
|
800
|
+
config.serverUrl,
|
|
801
|
+
config.token,
|
|
802
|
+
file.fileKey,
|
|
803
|
+
config.projectKey,
|
|
804
|
+
ctx.signal,
|
|
805
|
+
);
|
|
806
|
+
const text = await buildDuplicationPreview(
|
|
807
|
+
config.baseDir,
|
|
808
|
+
file.filePath,
|
|
809
|
+
groups,
|
|
810
|
+
);
|
|
599
811
|
if (ctx.mode === "tui") {
|
|
600
812
|
await ctx.ui.editor(`Duplications in ${file.filePath}`, text);
|
|
601
813
|
} else if (ctx.hasUI) {
|
|
@@ -615,7 +827,13 @@ async function showDuplicationListOrBrowser(
|
|
|
615
827
|
const choice = await showDuplicationBrowser(ctx, files);
|
|
616
828
|
if (choice == null) return;
|
|
617
829
|
const file = files[choice];
|
|
618
|
-
const groups = await fetchFileDuplicationBlocks(
|
|
830
|
+
const groups = await fetchFileDuplicationBlocks(
|
|
831
|
+
config.serverUrl,
|
|
832
|
+
config.token,
|
|
833
|
+
file.fileKey,
|
|
834
|
+
config.projectKey,
|
|
835
|
+
ctx.signal,
|
|
836
|
+
);
|
|
619
837
|
const lines = [`Duplications in ${file.filePath}`];
|
|
620
838
|
for (const [i, group] of groups.entries()) {
|
|
621
839
|
lines.push("", `Block ${i + 1}:`);
|
|
@@ -632,10 +850,20 @@ async function commandIssuesOrOpen(
|
|
|
632
850
|
pi: ExtensionAPI,
|
|
633
851
|
ctx: ExtensionCommandContext,
|
|
634
852
|
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
635
|
-
parsed: {
|
|
853
|
+
parsed: {
|
|
854
|
+
targetInput?: string;
|
|
855
|
+
filters?: SonarIssueFetchOptions;
|
|
856
|
+
action: string;
|
|
857
|
+
issueIndex?: number;
|
|
858
|
+
},
|
|
636
859
|
rememberState: (s: SonarAnalysisState) => void,
|
|
637
860
|
): Promise<void> {
|
|
638
|
-
const targetState = await resolveTargetState(
|
|
861
|
+
const targetState = await resolveTargetState(
|
|
862
|
+
ctx,
|
|
863
|
+
statesByBaseDir,
|
|
864
|
+
parsed.targetInput,
|
|
865
|
+
parsed.filters,
|
|
866
|
+
);
|
|
639
867
|
if (!targetState) {
|
|
640
868
|
if (ctx.hasUI) {
|
|
641
869
|
ctx.ui.notify(
|
|
@@ -675,8 +903,12 @@ async function commandShowIssues(
|
|
|
675
903
|
const issue = targetState.issues[choice];
|
|
676
904
|
if (issue) {
|
|
677
905
|
const line = issue.line ? `:${issue.line}` : "";
|
|
678
|
-
const rule = issue.ruleName
|
|
679
|
-
|
|
906
|
+
const rule = issue.ruleName
|
|
907
|
+
? `${issue.rule} (${issue.ruleName})`
|
|
908
|
+
: issue.rule;
|
|
909
|
+
ctx.ui.setEditorText(
|
|
910
|
+
`${issue.severity} ${issue.filePath}${line} — ${rule} — ${issue.message}`,
|
|
911
|
+
);
|
|
680
912
|
ctx.ui.notify("Issue loaded into editor — press Enter to send", "info");
|
|
681
913
|
}
|
|
682
914
|
}
|
|
@@ -684,24 +916,45 @@ async function commandShowIssues(
|
|
|
684
916
|
|
|
685
917
|
// ── Tool helper functions ────────────────────────────────────────────────────
|
|
686
918
|
|
|
687
|
-
async function toolDuplications(
|
|
919
|
+
async function toolDuplications(
|
|
920
|
+
ctx: any,
|
|
921
|
+
path: string | undefined,
|
|
922
|
+
): Promise<{
|
|
923
|
+
content: Array<{ type: "text"; text: string }>;
|
|
924
|
+
details: Record<string, unknown>;
|
|
925
|
+
}> {
|
|
688
926
|
let config;
|
|
689
927
|
try {
|
|
690
928
|
config = await resolveConfig(ctx, path);
|
|
691
929
|
} catch {
|
|
692
930
|
return {
|
|
693
|
-
content: [
|
|
931
|
+
content: [
|
|
932
|
+
{
|
|
933
|
+
type: "text",
|
|
934
|
+
text: "Project not configured. Run /sonarqube init first.",
|
|
935
|
+
},
|
|
936
|
+
],
|
|
694
937
|
details: { error: "Project not configured." },
|
|
695
938
|
};
|
|
696
939
|
}
|
|
697
940
|
let files: FileDuplication[];
|
|
698
941
|
try {
|
|
699
|
-
files = await fetchFileDuplications(
|
|
942
|
+
files = await fetchFileDuplications(
|
|
943
|
+
config.serverUrl,
|
|
944
|
+
config.token,
|
|
945
|
+
config.projectKey,
|
|
946
|
+
ctx.signal,
|
|
947
|
+
);
|
|
700
948
|
} catch (error) {
|
|
701
949
|
const msg = sonarErrorMessage(error);
|
|
702
950
|
if (msg.includes("403") || msg.includes("Insufficient privileges")) {
|
|
703
951
|
return {
|
|
704
|
-
content: [
|
|
952
|
+
content: [
|
|
953
|
+
{
|
|
954
|
+
type: "text",
|
|
955
|
+
text: "SonarQube token needs 'See Source Code' permission to list duplicated files. Grant it in SonarQube project permissions, then update the token via /sonarqube init.",
|
|
956
|
+
},
|
|
957
|
+
],
|
|
705
958
|
details: { error: "Token missing 'See Source Code' permission." },
|
|
706
959
|
};
|
|
707
960
|
}
|
|
@@ -712,7 +965,12 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
712
965
|
}
|
|
713
966
|
if (files.length === 0) {
|
|
714
967
|
return {
|
|
715
|
-
content: [
|
|
968
|
+
content: [
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: `No duplications found for ${config.projectKey}.`,
|
|
972
|
+
},
|
|
973
|
+
],
|
|
716
974
|
details: { error: `No duplications found for ${config.projectKey}.` },
|
|
717
975
|
};
|
|
718
976
|
}
|
|
@@ -721,12 +979,23 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
721
979
|
const topContexts = await Promise.all(
|
|
722
980
|
topFiles.map(async (file) => {
|
|
723
981
|
try {
|
|
724
|
-
const groups = await fetchFileDuplicationBlocks(
|
|
725
|
-
|
|
982
|
+
const groups = await fetchFileDuplicationBlocks(
|
|
983
|
+
config.serverUrl,
|
|
984
|
+
config.token,
|
|
985
|
+
file.fileKey,
|
|
986
|
+
config.projectKey,
|
|
987
|
+
ctx.signal,
|
|
988
|
+
);
|
|
989
|
+
const preview = await buildDuplicationPreview(
|
|
990
|
+
config.baseDir,
|
|
991
|
+
file.filePath,
|
|
992
|
+
groups,
|
|
993
|
+
);
|
|
726
994
|
return {
|
|
727
995
|
filePath: file.filePath,
|
|
728
996
|
duplicatedBlocks: file.duplicatedBlocks,
|
|
729
997
|
duplicatedLines: file.duplicatedLines,
|
|
998
|
+
duplicatedLinesDensity: file.duplicatedLinesDensity,
|
|
730
999
|
preview,
|
|
731
1000
|
};
|
|
732
1001
|
} catch (error) {
|
|
@@ -734,6 +1003,7 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
734
1003
|
filePath: file.filePath,
|
|
735
1004
|
duplicatedBlocks: file.duplicatedBlocks,
|
|
736
1005
|
duplicatedLines: file.duplicatedLines,
|
|
1006
|
+
duplicatedLinesDensity: file.duplicatedLinesDensity,
|
|
737
1007
|
preview: `Duplications in ${file.filePath}\n\n<failed to load duplicated lines: ${sonarErrorMessage(error)}>`,
|
|
738
1008
|
};
|
|
739
1009
|
}
|
|
@@ -741,12 +1011,22 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
741
1011
|
);
|
|
742
1012
|
const remaining = files.length - topContexts.length;
|
|
743
1013
|
const content: Array<{ type: "text"; text: string }> = [
|
|
744
|
-
{
|
|
1014
|
+
{
|
|
1015
|
+
type: "text",
|
|
1016
|
+
text: `Duplicated files for ${config.projectKey}: ${files.length}. Showing top ${topContexts.length}.`,
|
|
1017
|
+
},
|
|
745
1018
|
...topContexts.map((item, index) => ({
|
|
746
1019
|
type: "text" as const,
|
|
747
|
-
text: `#${index + 1} ${item.filePath} blocks=${item.duplicatedBlocks} lines=${item.duplicatedLines}\n${item.preview}`,
|
|
1020
|
+
text: `#${index + 1} ${item.filePath} dup%=${item.duplicatedLinesDensity.toFixed(1)} blocks=${item.duplicatedBlocks} lines=${item.duplicatedLines}\n${item.preview}`,
|
|
748
1021
|
})),
|
|
749
|
-
...(remaining > 0
|
|
1022
|
+
...(remaining > 0
|
|
1023
|
+
? [
|
|
1024
|
+
{
|
|
1025
|
+
type: "text" as const,
|
|
1026
|
+
text: `+ ${remaining} more file(s) not shown`,
|
|
1027
|
+
},
|
|
1028
|
+
]
|
|
1029
|
+
: []),
|
|
750
1030
|
];
|
|
751
1031
|
return {
|
|
752
1032
|
content,
|
|
@@ -759,30 +1039,83 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
759
1039
|
};
|
|
760
1040
|
}
|
|
761
1041
|
|
|
762
|
-
async function toolMetrics(
|
|
1042
|
+
async function toolMetrics(
|
|
1043
|
+
ctx: any,
|
|
1044
|
+
path: string | undefined,
|
|
1045
|
+
): Promise<{
|
|
1046
|
+
content: Array<{ type: "text"; text: string }>;
|
|
1047
|
+
details: Record<string, unknown>;
|
|
1048
|
+
}> {
|
|
763
1049
|
let config;
|
|
764
1050
|
try {
|
|
765
1051
|
config = await resolveConfig(ctx, path);
|
|
766
1052
|
} catch {
|
|
767
1053
|
return {
|
|
768
|
-
content: [
|
|
1054
|
+
content: [
|
|
1055
|
+
{
|
|
1056
|
+
type: "text",
|
|
1057
|
+
text: "Project not configured. Run /sonarqube init first.",
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
769
1060
|
details: { error: "Project not configured." },
|
|
770
1061
|
};
|
|
771
1062
|
}
|
|
772
|
-
const cleanCodeMode = await fetchCleanCodeMode(
|
|
1063
|
+
const cleanCodeMode = await fetchCleanCodeMode(
|
|
1064
|
+
config.serverUrl,
|
|
1065
|
+
config.token,
|
|
1066
|
+
ctx.signal,
|
|
1067
|
+
);
|
|
773
1068
|
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
774
|
-
fetchDuplicationMeasures(
|
|
775
|
-
|
|
776
|
-
|
|
1069
|
+
fetchDuplicationMeasures(
|
|
1070
|
+
config.serverUrl,
|
|
1071
|
+
config.token,
|
|
1072
|
+
config.projectKey,
|
|
1073
|
+
ctx.signal,
|
|
1074
|
+
),
|
|
1075
|
+
fetchIssueSeverityCounts(
|
|
1076
|
+
config.serverUrl,
|
|
1077
|
+
config.token,
|
|
1078
|
+
config.projectKey,
|
|
1079
|
+
ctx.signal,
|
|
1080
|
+
cleanCodeMode,
|
|
1081
|
+
),
|
|
1082
|
+
fetchIssueQualityCounts(
|
|
1083
|
+
config.serverUrl,
|
|
1084
|
+
config.token,
|
|
1085
|
+
config.projectKey,
|
|
1086
|
+
ctx.signal,
|
|
1087
|
+
),
|
|
777
1088
|
]);
|
|
778
1089
|
if (!measures && !issueCounts && !qualityCounts) {
|
|
779
1090
|
return {
|
|
780
|
-
content: [
|
|
781
|
-
|
|
1091
|
+
content: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "text",
|
|
1094
|
+
text: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`,
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
details: {
|
|
1098
|
+
error: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`,
|
|
1099
|
+
},
|
|
782
1100
|
};
|
|
783
1101
|
}
|
|
784
|
-
const text = formatMetricsOutput({
|
|
785
|
-
|
|
1102
|
+
const text = formatMetricsOutput({
|
|
1103
|
+
projectKey: config.projectKey,
|
|
1104
|
+
measures,
|
|
1105
|
+
issueCounts,
|
|
1106
|
+
issueQualityCounts: qualityCounts,
|
|
1107
|
+
cleanCodeMode,
|
|
1108
|
+
});
|
|
1109
|
+
return {
|
|
1110
|
+
content: [{ type: "text", text }],
|
|
1111
|
+
details: {
|
|
1112
|
+
projectKey: config.projectKey,
|
|
1113
|
+
measures,
|
|
1114
|
+
issueCounts,
|
|
1115
|
+
issueQualityCounts: qualityCounts,
|
|
1116
|
+
cleanCodeMode,
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
786
1119
|
}
|
|
787
1120
|
|
|
788
1121
|
// ── Re-exports for public API ───────────────────────────────────────────────
|
|
@@ -797,4 +1130,8 @@ export type {
|
|
|
797
1130
|
IssueQualityCounts,
|
|
798
1131
|
} from "./types.js";
|
|
799
1132
|
|
|
800
|
-
export {
|
|
1133
|
+
export {
|
|
1134
|
+
projectConfigPath,
|
|
1135
|
+
loadProjectConfig,
|
|
1136
|
+
saveProjectConfig,
|
|
1137
|
+
} from "./config.js";
|