@jonathan0823/pi-sonarqube 0.3.2 → 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 +148 -31
- 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 +437 -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,18 +827,43 @@ 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(
|
|
619
|
-
|
|
830
|
+
const groups = await fetchFileDuplicationBlocks(
|
|
831
|
+
config.serverUrl,
|
|
832
|
+
config.token,
|
|
833
|
+
file.fileKey,
|
|
834
|
+
config.projectKey,
|
|
835
|
+
ctx.signal,
|
|
836
|
+
);
|
|
837
|
+
const lines = [`Duplications in ${file.filePath}`];
|
|
838
|
+
for (const [i, group] of groups.entries()) {
|
|
839
|
+
lines.push("", `Block ${i + 1}:`);
|
|
840
|
+
for (const block of group.blocks) {
|
|
841
|
+
const end = block.from + block.size - 1;
|
|
842
|
+
lines.push(` ${block.filePath}:${block.from}-${end}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
ctx.ui.setEditorText(lines.join("\n"));
|
|
846
|
+
ctx.ui.notify("Duplication loaded into editor — press Enter to send", "info");
|
|
620
847
|
}
|
|
621
848
|
|
|
622
849
|
async function commandIssuesOrOpen(
|
|
623
850
|
pi: ExtensionAPI,
|
|
624
851
|
ctx: ExtensionCommandContext,
|
|
625
852
|
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
626
|
-
parsed: {
|
|
853
|
+
parsed: {
|
|
854
|
+
targetInput?: string;
|
|
855
|
+
filters?: SonarIssueFetchOptions;
|
|
856
|
+
action: string;
|
|
857
|
+
issueIndex?: number;
|
|
858
|
+
},
|
|
627
859
|
rememberState: (s: SonarAnalysisState) => void,
|
|
628
860
|
): Promise<void> {
|
|
629
|
-
const targetState = await resolveTargetState(
|
|
861
|
+
const targetState = await resolveTargetState(
|
|
862
|
+
ctx,
|
|
863
|
+
statesByBaseDir,
|
|
864
|
+
parsed.targetInput,
|
|
865
|
+
parsed.filters,
|
|
866
|
+
);
|
|
630
867
|
if (!targetState) {
|
|
631
868
|
if (ctx.hasUI) {
|
|
632
869
|
ctx.ui.notify(
|
|
@@ -664,30 +901,60 @@ async function commandShowIssues(
|
|
|
664
901
|
const choice = await showIssueBrowser(ctx, targetState);
|
|
665
902
|
if (choice != null) {
|
|
666
903
|
const issue = targetState.issues[choice];
|
|
667
|
-
if (issue)
|
|
904
|
+
if (issue) {
|
|
905
|
+
const line = issue.line ? `:${issue.line}` : "";
|
|
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
|
+
);
|
|
912
|
+
ctx.ui.notify("Issue loaded into editor — press Enter to send", "info");
|
|
913
|
+
}
|
|
668
914
|
}
|
|
669
915
|
}
|
|
670
916
|
|
|
671
917
|
// ── Tool helper functions ────────────────────────────────────────────────────
|
|
672
918
|
|
|
673
|
-
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
|
+
}> {
|
|
674
926
|
let config;
|
|
675
927
|
try {
|
|
676
928
|
config = await resolveConfig(ctx, path);
|
|
677
929
|
} catch {
|
|
678
930
|
return {
|
|
679
|
-
content: [
|
|
931
|
+
content: [
|
|
932
|
+
{
|
|
933
|
+
type: "text",
|
|
934
|
+
text: "Project not configured. Run /sonarqube init first.",
|
|
935
|
+
},
|
|
936
|
+
],
|
|
680
937
|
details: { error: "Project not configured." },
|
|
681
938
|
};
|
|
682
939
|
}
|
|
683
940
|
let files: FileDuplication[];
|
|
684
941
|
try {
|
|
685
|
-
files = await fetchFileDuplications(
|
|
942
|
+
files = await fetchFileDuplications(
|
|
943
|
+
config.serverUrl,
|
|
944
|
+
config.token,
|
|
945
|
+
config.projectKey,
|
|
946
|
+
ctx.signal,
|
|
947
|
+
);
|
|
686
948
|
} catch (error) {
|
|
687
949
|
const msg = sonarErrorMessage(error);
|
|
688
950
|
if (msg.includes("403") || msg.includes("Insufficient privileges")) {
|
|
689
951
|
return {
|
|
690
|
-
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
|
+
],
|
|
691
958
|
details: { error: "Token missing 'See Source Code' permission." },
|
|
692
959
|
};
|
|
693
960
|
}
|
|
@@ -698,7 +965,12 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
698
965
|
}
|
|
699
966
|
if (files.length === 0) {
|
|
700
967
|
return {
|
|
701
|
-
content: [
|
|
968
|
+
content: [
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: `No duplications found for ${config.projectKey}.`,
|
|
972
|
+
},
|
|
973
|
+
],
|
|
702
974
|
details: { error: `No duplications found for ${config.projectKey}.` },
|
|
703
975
|
};
|
|
704
976
|
}
|
|
@@ -707,12 +979,23 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
707
979
|
const topContexts = await Promise.all(
|
|
708
980
|
topFiles.map(async (file) => {
|
|
709
981
|
try {
|
|
710
|
-
const groups = await fetchFileDuplicationBlocks(
|
|
711
|
-
|
|
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
|
+
);
|
|
712
994
|
return {
|
|
713
995
|
filePath: file.filePath,
|
|
714
996
|
duplicatedBlocks: file.duplicatedBlocks,
|
|
715
997
|
duplicatedLines: file.duplicatedLines,
|
|
998
|
+
duplicatedLinesDensity: file.duplicatedLinesDensity,
|
|
716
999
|
preview,
|
|
717
1000
|
};
|
|
718
1001
|
} catch (error) {
|
|
@@ -720,6 +1003,7 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
720
1003
|
filePath: file.filePath,
|
|
721
1004
|
duplicatedBlocks: file.duplicatedBlocks,
|
|
722
1005
|
duplicatedLines: file.duplicatedLines,
|
|
1006
|
+
duplicatedLinesDensity: file.duplicatedLinesDensity,
|
|
723
1007
|
preview: `Duplications in ${file.filePath}\n\n<failed to load duplicated lines: ${sonarErrorMessage(error)}>`,
|
|
724
1008
|
};
|
|
725
1009
|
}
|
|
@@ -727,12 +1011,22 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
727
1011
|
);
|
|
728
1012
|
const remaining = files.length - topContexts.length;
|
|
729
1013
|
const content: Array<{ type: "text"; text: string }> = [
|
|
730
|
-
{
|
|
1014
|
+
{
|
|
1015
|
+
type: "text",
|
|
1016
|
+
text: `Duplicated files for ${config.projectKey}: ${files.length}. Showing top ${topContexts.length}.`,
|
|
1017
|
+
},
|
|
731
1018
|
...topContexts.map((item, index) => ({
|
|
732
1019
|
type: "text" as const,
|
|
733
|
-
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}`,
|
|
734
1021
|
})),
|
|
735
|
-
...(remaining > 0
|
|
1022
|
+
...(remaining > 0
|
|
1023
|
+
? [
|
|
1024
|
+
{
|
|
1025
|
+
type: "text" as const,
|
|
1026
|
+
text: `+ ${remaining} more file(s) not shown`,
|
|
1027
|
+
},
|
|
1028
|
+
]
|
|
1029
|
+
: []),
|
|
736
1030
|
];
|
|
737
1031
|
return {
|
|
738
1032
|
content,
|
|
@@ -745,30 +1039,83 @@ async function toolDuplications(ctx: any, path: string | undefined): Promise<{ c
|
|
|
745
1039
|
};
|
|
746
1040
|
}
|
|
747
1041
|
|
|
748
|
-
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
|
+
}> {
|
|
749
1049
|
let config;
|
|
750
1050
|
try {
|
|
751
1051
|
config = await resolveConfig(ctx, path);
|
|
752
1052
|
} catch {
|
|
753
1053
|
return {
|
|
754
|
-
content: [
|
|
1054
|
+
content: [
|
|
1055
|
+
{
|
|
1056
|
+
type: "text",
|
|
1057
|
+
text: "Project not configured. Run /sonarqube init first.",
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
755
1060
|
details: { error: "Project not configured." },
|
|
756
1061
|
};
|
|
757
1062
|
}
|
|
758
|
-
const cleanCodeMode = await fetchCleanCodeMode(
|
|
1063
|
+
const cleanCodeMode = await fetchCleanCodeMode(
|
|
1064
|
+
config.serverUrl,
|
|
1065
|
+
config.token,
|
|
1066
|
+
ctx.signal,
|
|
1067
|
+
);
|
|
759
1068
|
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
760
|
-
fetchDuplicationMeasures(
|
|
761
|
-
|
|
762
|
-
|
|
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
|
+
),
|
|
763
1088
|
]);
|
|
764
1089
|
if (!measures && !issueCounts && !qualityCounts) {
|
|
765
1090
|
return {
|
|
766
|
-
content: [
|
|
767
|
-
|
|
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
|
+
},
|
|
768
1100
|
};
|
|
769
1101
|
}
|
|
770
|
-
const text = formatMetricsOutput({
|
|
771
|
-
|
|
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
|
+
};
|
|
772
1119
|
}
|
|
773
1120
|
|
|
774
1121
|
// ── Re-exports for public API ───────────────────────────────────────────────
|
|
@@ -783,4 +1130,8 @@ export type {
|
|
|
783
1130
|
IssueQualityCounts,
|
|
784
1131
|
} from "./types.js";
|
|
785
1132
|
|
|
786
|
-
export {
|
|
1133
|
+
export {
|
|
1134
|
+
projectConfigPath,
|
|
1135
|
+
loadProjectConfig,
|
|
1136
|
+
saveProjectConfig,
|
|
1137
|
+
} from "./config.js";
|