@jonathan0823/pi-sonarqube 0.1.4 → 0.3.1
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 +47 -5
- package/dist/api.d.ts +10 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +199 -40
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +18 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +96 -23
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -3
- 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 +179 -9
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +12 -3
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +64 -22
- package/dist/ui.js.map +1 -1
- package/package.json +5 -1
- package/src/api.ts +247 -41
- package/src/commands.ts +120 -27
- package/src/config.ts +0 -4
- package/src/index.ts +219 -6
- package/src/types.ts +44 -0
- package/src/ui.ts +84 -34
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
SonarIssueFetchOptions,
|
|
11
11
|
SonarInitConfig,
|
|
12
12
|
InitCommandOptions,
|
|
13
|
+
FileDuplication,
|
|
14
|
+
SonarProjectConfig,
|
|
13
15
|
} from "./types.js";
|
|
14
16
|
import {
|
|
15
17
|
slugify,
|
|
@@ -29,8 +31,15 @@ import {
|
|
|
29
31
|
waitForAnalysis,
|
|
30
32
|
normalizeIssueFilters,
|
|
31
33
|
fetchIssues,
|
|
34
|
+
fetchDuplicationMeasures,
|
|
35
|
+
fetchIssueSeverityCounts,
|
|
36
|
+
fetchIssueQualityCounts,
|
|
37
|
+
fetchFileDuplications,
|
|
38
|
+
fetchFileDuplicationBlocks,
|
|
32
39
|
createAnalysisState,
|
|
33
40
|
issueFilterLabel,
|
|
41
|
+
fetchCleanCodeMode,
|
|
42
|
+
assertFiltersNotAmbiguous,
|
|
34
43
|
} from "./api.js";
|
|
35
44
|
import {
|
|
36
45
|
STATE_TYPE,
|
|
@@ -40,6 +49,9 @@ import {
|
|
|
40
49
|
mergeAutocompleteSuggestions,
|
|
41
50
|
formatIssue,
|
|
42
51
|
formatSummary,
|
|
52
|
+
formatMetricsOutput,
|
|
53
|
+
formatDuplicationsList,
|
|
54
|
+
formatDuplicationBlockDetail,
|
|
43
55
|
formatReport,
|
|
44
56
|
helpText,
|
|
45
57
|
targetLabel,
|
|
@@ -52,10 +64,11 @@ import {
|
|
|
52
64
|
buildIssuePreview,
|
|
53
65
|
openIssuePreview,
|
|
54
66
|
resolveTargetState,
|
|
67
|
+
showDuplicationBrowser,
|
|
55
68
|
} from "./ui.js";
|
|
56
69
|
|
|
57
70
|
const SonarToolParams = Type.Object({
|
|
58
|
-
action: StringEnum(["analyze", "issues", "open"] as const),
|
|
71
|
+
action: StringEnum(["analyze", "issues", "open", "metrics", "duplications"] as const),
|
|
59
72
|
path: Type.Optional(Type.String({ description: "Target alias or project directory to analyze or inspect" })),
|
|
60
73
|
issueIndex: Type.Optional(Type.Number({ description: "1-based issue index to open" })),
|
|
61
74
|
severities: Type.Optional(
|
|
@@ -66,6 +79,12 @@ const SonarToolParams = Type.Object({
|
|
|
66
79
|
),
|
|
67
80
|
types: Type.Optional(Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" }))),
|
|
68
81
|
rules: Type.Optional(Type.Array(Type.String({ description: "Rule keys to fetch" }))),
|
|
82
|
+
softwareQualities: Type.Optional(
|
|
83
|
+
Type.Array(Type.String({ description: "Software quality to fetch (e.g. MAINTAINABILITY)" })),
|
|
84
|
+
),
|
|
85
|
+
impactSeverities: Type.Optional(
|
|
86
|
+
Type.Array(Type.String({ description: "Impact severity to fetch (e.g. HIGH)" })),
|
|
87
|
+
),
|
|
69
88
|
});
|
|
70
89
|
|
|
71
90
|
// ── State restoration ───────────────────────────────────────────────────────
|
|
@@ -126,11 +145,24 @@ async function analyzeProject(
|
|
|
126
145
|
normalizedFilters,
|
|
127
146
|
);
|
|
128
147
|
|
|
148
|
+
analysisUi.setPhase("Fetching metrics...");
|
|
149
|
+
const measures = await fetchDuplicationMeasures(
|
|
150
|
+
config.serverUrl,
|
|
151
|
+
config.token,
|
|
152
|
+
config.projectKey,
|
|
153
|
+
ctx.signal,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
analysisUi.setPhase("Detecting mode...");
|
|
157
|
+
const cleanCodeMode = await fetchCleanCodeMode(config.serverUrl, config.token, ctx.signal);
|
|
158
|
+
|
|
129
159
|
const state = createAnalysisState(config, issues, {
|
|
130
160
|
dashboardUrl,
|
|
131
161
|
ceTaskUrl,
|
|
132
162
|
analysisId,
|
|
133
163
|
filters: normalizedFilters,
|
|
164
|
+
measures,
|
|
165
|
+
cleanCodeMode,
|
|
134
166
|
});
|
|
135
167
|
|
|
136
168
|
pi.appendEntry(STATE_TYPE, state);
|
|
@@ -249,12 +281,25 @@ function renderIssueListResult(
|
|
|
249
281
|
theme: Theme,
|
|
250
282
|
): Text {
|
|
251
283
|
const summary = formatSummary(state);
|
|
252
|
-
if (state.issues.length === 0) {
|
|
284
|
+
if (state.issues.length === 0 && !state.measures) {
|
|
253
285
|
return new Text(theme.fg("success", summary), 0, 0);
|
|
254
286
|
}
|
|
255
287
|
|
|
288
|
+
const lines: string[] = [];
|
|
289
|
+
lines.push(theme.fg("accent", summary));
|
|
290
|
+
if (state.measures) {
|
|
291
|
+
const density = state.measures.duplicatedLinesDensity.toFixed(1);
|
|
292
|
+
const detail = state.measures.duplicatedBlocks > 0
|
|
293
|
+
? `Duplication: ${density}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
|
|
294
|
+
: `Duplication: ${density}% (no duplications detected)`;
|
|
295
|
+
lines.push(theme.fg("dim", detail));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (state.issues.length === 0) {
|
|
299
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
256
302
|
const visible = expanded ? state.issues : state.issues.slice(0, 5);
|
|
257
|
-
const lines: string[] = [theme.fg("accent", summary)];
|
|
258
303
|
for (const [i, issue] of visible.entries()) {
|
|
259
304
|
lines.push(theme.fg("muted", formatIssue(issue, i + 1)));
|
|
260
305
|
}
|
|
@@ -292,7 +337,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
292
337
|
|
|
293
338
|
const argumentText = match[1] ?? "";
|
|
294
339
|
const { current: currentToken } = splitSonarArgumentContext(argumentText);
|
|
295
|
-
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues) ?? [];
|
|
340
|
+
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues, latestState?.cleanCodeMode) ?? [];
|
|
296
341
|
const currentSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
297
342
|
const merged = mergeAutocompleteSuggestions(currentToken, currentSuggestions, extraItems);
|
|
298
343
|
return merged ?? currentSuggestions;
|
|
@@ -320,7 +365,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
320
365
|
promptSnippet: "Run local SonarQube analysis or inspect issue results",
|
|
321
366
|
promptGuidelines: [
|
|
322
367
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
323
|
-
"Use the optional severity, status, type, and
|
|
368
|
+
"Use the optional severity, status, type, rule, quality, and impact severity filters to fetch only the most relevant issues.",
|
|
324
369
|
],
|
|
325
370
|
parameters: SonarToolParams,
|
|
326
371
|
|
|
@@ -331,7 +376,10 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
331
376
|
statuses: params.statuses,
|
|
332
377
|
types: params.types,
|
|
333
378
|
rules: params.rules,
|
|
379
|
+
softwareQualities: params.softwareQualities,
|
|
380
|
+
impactSeverities: params.impactSeverities,
|
|
334
381
|
});
|
|
382
|
+
assertFiltersNotAmbiguous(filters);
|
|
335
383
|
|
|
336
384
|
if (params.action === "analyze") {
|
|
337
385
|
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
@@ -339,6 +387,9 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
339
387
|
return { content: [{ type: "text", text: formatReport(state) }], details: state };
|
|
340
388
|
}
|
|
341
389
|
|
|
390
|
+
if (params.action === "duplications") return toolDuplications(ctx, params.path);
|
|
391
|
+
if (params.action === "metrics") return toolMetrics(ctx, params.path);
|
|
392
|
+
|
|
342
393
|
const targetState = await resolveTargetState(ctx, statesByBaseDir, params.path, filters);
|
|
343
394
|
if (!targetState) {
|
|
344
395
|
return {
|
|
@@ -379,6 +430,8 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
379
430
|
statuses: args.statuses,
|
|
380
431
|
types: args.types,
|
|
381
432
|
rules: args.rules,
|
|
433
|
+
softwareQualities: args.softwareQualities,
|
|
434
|
+
impactSeverities: args.impactSeverities,
|
|
382
435
|
});
|
|
383
436
|
const filterText = filters ? ` ${theme.fg("muted", issueFilterLabel(filters))}` : "";
|
|
384
437
|
return new Text(
|
|
@@ -422,7 +475,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
422
475
|
|
|
423
476
|
pi.registerCommand("sonarqube", {
|
|
424
477
|
description: "Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
425
|
-
getArgumentCompletions: (argumentPrefix) => sonarArgumentCompletions(argumentPrefix, latestState?.issues),
|
|
478
|
+
getArgumentCompletions: (argumentPrefix) => sonarArgumentCompletions(argumentPrefix, latestState?.issues, latestState?.cleanCodeMode),
|
|
426
479
|
handler: async (args, ctx) => {
|
|
427
480
|
try {
|
|
428
481
|
const parsed = parseCommandArgs(args);
|
|
@@ -437,6 +490,12 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
437
490
|
await commandAnalyze(pi, ctx, parsed.targetInput, parsed.filters, rememberState);
|
|
438
491
|
return;
|
|
439
492
|
}
|
|
493
|
+
case "metrics":
|
|
494
|
+
await commandMetrics(ctx, parsed.targetInput);
|
|
495
|
+
return;
|
|
496
|
+
case "duplications":
|
|
497
|
+
await commandDuplications(pi, ctx, parsed.targetInput, parsed.issueIndex);
|
|
498
|
+
return;
|
|
440
499
|
default:
|
|
441
500
|
await commandIssuesOrOpen(pi, ctx, statesByBaseDir, parsed, rememberState);
|
|
442
501
|
}
|
|
@@ -469,6 +528,96 @@ async function commandAnalyze(
|
|
|
469
528
|
}
|
|
470
529
|
}
|
|
471
530
|
|
|
531
|
+
async function commandMetrics(
|
|
532
|
+
ctx: ExtensionCommandContext,
|
|
533
|
+
targetInput?: string,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
const config = await resolveConfig(ctx, targetInput);
|
|
536
|
+
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
537
|
+
fetchDuplicationMeasures(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
538
|
+
fetchIssueSeverityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
539
|
+
fetchIssueQualityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
540
|
+
]);
|
|
541
|
+
if (!measures && !issueCounts && !qualityCounts) {
|
|
542
|
+
if (ctx.hasUI) {
|
|
543
|
+
ctx.ui.notify(`Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`, "warning");
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const text = formatMetricsOutput({ projectKey: config.projectKey, measures, issueCounts, issueQualityCounts: qualityCounts });
|
|
548
|
+
if (ctx.hasUI) {
|
|
549
|
+
ctx.ui.notify(text, "info");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function commandDuplications(
|
|
554
|
+
pi: ExtensionAPI,
|
|
555
|
+
ctx: ExtensionCommandContext,
|
|
556
|
+
targetInput?: string,
|
|
557
|
+
fileIndex?: number,
|
|
558
|
+
): Promise<void> {
|
|
559
|
+
const config = await resolveConfig(ctx, targetInput);
|
|
560
|
+
let files: FileDuplication[];
|
|
561
|
+
try {
|
|
562
|
+
files = await fetchFileDuplications(config.serverUrl, config.token, config.projectKey, ctx.signal);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const msg = sonarErrorMessage(error);
|
|
565
|
+
if (msg.includes("403") || msg.includes("Insufficient privileges")) {
|
|
566
|
+
if (ctx.hasUI) {
|
|
567
|
+
ctx.ui.notify("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.", "warning");
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
if (files.length === 0) {
|
|
574
|
+
if (ctx.hasUI) ctx.ui.notify("No duplications found.", "info");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (fileIndex !== undefined) {
|
|
579
|
+
await showDuplicationDrillDown(ctx, config, files, fileIndex);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
await showDuplicationListOrBrowser(ctx, config, files);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function showDuplicationDrillDown(
|
|
586
|
+
ctx: ExtensionCommandContext,
|
|
587
|
+
config: SonarProjectConfig,
|
|
588
|
+
files: FileDuplication[],
|
|
589
|
+
fileIndex: number,
|
|
590
|
+
): Promise<void> {
|
|
591
|
+
const file = files[fileIndex - 1];
|
|
592
|
+
if (!file) {
|
|
593
|
+
if (ctx.hasUI) ctx.ui.notify(`File #${fileIndex} not found.`, "error");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const groups = await fetchFileDuplicationBlocks(config.serverUrl, config.token, file.fileKey, config.projectKey, ctx.signal);
|
|
597
|
+
const text = formatDuplicationBlockDetail(file.filePath, groups);
|
|
598
|
+
if (ctx.mode === "tui") {
|
|
599
|
+
await ctx.ui.editor(`Duplications in ${file.filePath}`, text);
|
|
600
|
+
} else if (ctx.hasUI) {
|
|
601
|
+
ctx.ui.notify(text, "info");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function showDuplicationListOrBrowser(
|
|
606
|
+
ctx: ExtensionCommandContext,
|
|
607
|
+
config: SonarProjectConfig,
|
|
608
|
+
files: FileDuplication[],
|
|
609
|
+
): Promise<void> {
|
|
610
|
+
if (ctx.mode !== "tui") {
|
|
611
|
+
ctx.ui.notify(formatDuplicationsList(files), "info");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const choice = await showDuplicationBrowser(ctx, files);
|
|
615
|
+
if (choice == null) return;
|
|
616
|
+
const file = files[choice];
|
|
617
|
+
const groups = await fetchFileDuplicationBlocks(config.serverUrl, config.token, file.fileKey, config.projectKey, ctx.signal);
|
|
618
|
+
await ctx.ui.editor(`Duplications in ${file.filePath}`, formatDuplicationBlockDetail(file.filePath, groups));
|
|
619
|
+
}
|
|
620
|
+
|
|
472
621
|
async function commandIssuesOrOpen(
|
|
473
622
|
pi: ExtensionAPI,
|
|
474
623
|
ctx: ExtensionCommandContext,
|
|
@@ -518,6 +667,69 @@ async function commandShowIssues(
|
|
|
518
667
|
}
|
|
519
668
|
}
|
|
520
669
|
|
|
670
|
+
// ── Tool helper functions ────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
async function toolDuplications(ctx: any, path: string | undefined): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
673
|
+
let config;
|
|
674
|
+
try {
|
|
675
|
+
config = await resolveConfig(ctx, path);
|
|
676
|
+
} catch {
|
|
677
|
+
return {
|
|
678
|
+
content: [{ type: "text", text: "Project not configured. Run /sonarqube init first." }],
|
|
679
|
+
details: { error: "Project not configured." },
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
let files: FileDuplication[];
|
|
683
|
+
try {
|
|
684
|
+
files = await fetchFileDuplications(config.serverUrl, config.token, config.projectKey, ctx.signal);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const msg = sonarErrorMessage(error);
|
|
687
|
+
if (msg.includes("403") || msg.includes("Insufficient privileges")) {
|
|
688
|
+
return {
|
|
689
|
+
content: [{ type: "text", 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." }],
|
|
690
|
+
details: { error: "Token missing 'See Source Code' permission." },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
content: [{ type: "text", text: msg }],
|
|
695
|
+
details: { error: msg },
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (files.length === 0) {
|
|
699
|
+
return {
|
|
700
|
+
content: [{ type: "text", text: `No duplications found for ${config.projectKey}.` }],
|
|
701
|
+
details: { error: `No duplications found for ${config.projectKey}.` },
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const text = formatDuplicationsList(files);
|
|
705
|
+
return { content: [{ type: "text", text }], details: { projectKey: config.projectKey, files } };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function toolMetrics(ctx: any, path: string | undefined): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
709
|
+
let config;
|
|
710
|
+
try {
|
|
711
|
+
config = await resolveConfig(ctx, path);
|
|
712
|
+
} catch {
|
|
713
|
+
return {
|
|
714
|
+
content: [{ type: "text", text: "Project not configured. Run /sonarqube init first." }],
|
|
715
|
+
details: { error: "Project not configured." },
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
719
|
+
fetchDuplicationMeasures(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
720
|
+
fetchIssueSeverityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
721
|
+
fetchIssueQualityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
722
|
+
]);
|
|
723
|
+
if (!measures && !issueCounts && !qualityCounts) {
|
|
724
|
+
return {
|
|
725
|
+
content: [{ type: "text", text: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.` }],
|
|
726
|
+
details: { error: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.` },
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
const text = formatMetricsOutput({ projectKey: config.projectKey, measures, issueCounts, issueQualityCounts: qualityCounts });
|
|
730
|
+
return { content: [{ type: "text", text }], details: { projectKey: config.projectKey, measures, issueCounts, issueQualityCounts: qualityCounts } };
|
|
731
|
+
}
|
|
732
|
+
|
|
521
733
|
// ── Re-exports for public API ───────────────────────────────────────────────
|
|
522
734
|
|
|
523
735
|
export type {
|
|
@@ -527,6 +739,7 @@ export type {
|
|
|
527
739
|
SonarProjectConfig,
|
|
528
740
|
SonarIssueFetchOptions,
|
|
529
741
|
SonarInitConfig,
|
|
742
|
+
IssueQualityCounts,
|
|
530
743
|
} from "./types.js";
|
|
531
744
|
|
|
532
745
|
export { projectConfigPath, loadProjectConfig, saveProjectConfig } from "./config.js";
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,44 @@ export interface SonarIssue {
|
|
|
13
13
|
status?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface SonarDuplicationMeasures {
|
|
17
|
+
duplicatedLinesDensity: number;
|
|
18
|
+
duplicatedLines: number;
|
|
19
|
+
duplicatedBlocks: number;
|
|
20
|
+
duplicatedFiles: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IssueSeverityCounts {
|
|
24
|
+
blocker: number;
|
|
25
|
+
critical: number;
|
|
26
|
+
major: number;
|
|
27
|
+
minor: number;
|
|
28
|
+
info: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IssueQualityCounts {
|
|
32
|
+
maintainability: number;
|
|
33
|
+
reliability: number;
|
|
34
|
+
security: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FileDuplication {
|
|
38
|
+
filePath: string;
|
|
39
|
+
fileKey: string;
|
|
40
|
+
duplicatedLines: number;
|
|
41
|
+
duplicatedBlocks: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DuplicationBlock {
|
|
45
|
+
from: number;
|
|
46
|
+
size: number;
|
|
47
|
+
filePath: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DuplicationBlockGroup {
|
|
51
|
+
blocks: DuplicationBlock[];
|
|
52
|
+
}
|
|
53
|
+
|
|
16
54
|
export interface SonarAnalysisState {
|
|
17
55
|
version: 1;
|
|
18
56
|
analyzedAt: string;
|
|
@@ -23,8 +61,10 @@ export interface SonarAnalysisState {
|
|
|
23
61
|
ceTaskUrl?: string;
|
|
24
62
|
analysisId?: string;
|
|
25
63
|
filters?: SonarIssueFetchOptions;
|
|
64
|
+
measures?: SonarDuplicationMeasures;
|
|
26
65
|
totalIssues: number;
|
|
27
66
|
issues: SonarIssue[];
|
|
67
|
+
cleanCodeMode?: "STANDARD" | "MQR";
|
|
28
68
|
}
|
|
29
69
|
|
|
30
70
|
export interface SonarProjectConfig {
|
|
@@ -40,6 +80,8 @@ export interface SonarIssueFetchOptions {
|
|
|
40
80
|
statuses?: string[];
|
|
41
81
|
types?: string[];
|
|
42
82
|
rules?: string[];
|
|
83
|
+
softwareQualities?: string[];
|
|
84
|
+
impactSeverities?: string[];
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
export interface SonarInitConfig {
|
|
@@ -77,3 +119,5 @@ export interface InitCommandOptions {
|
|
|
77
119
|
export const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"] as const;
|
|
78
120
|
export const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"] as const;
|
|
79
121
|
export const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
|
|
122
|
+
export const SONAR_SOFTWARE_QUALITIES = ["MAINTAINABILITY", "RELIABILITY", "SECURITY"] as const;
|
|
123
|
+
export const SONAR_IMPACT_SEVERITIES = ["BLOCKER", "HIGH", "MEDIUM", "LOW", "INFO"] as const;
|
package/src/ui.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { Theme, ExtensionContext, ExtensionCommandContext } from "@earendil
|
|
|
2
2
|
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import type { SonarIssue, SonarAnalysisState, SonarIssueFetchOptions } from "./types.js";
|
|
6
|
-
import { issueFilterLabel, fetchIssues, createAnalysisState, normalizeIssueFilters } from "./api.js";
|
|
5
|
+
import type { SonarIssue, SonarAnalysisState, SonarIssueFetchOptions, FileDuplication } from "./types.js";
|
|
6
|
+
import { issueFilterLabel, fetchIssues, createAnalysisState, normalizeIssueFilters, fetchCleanCodeMode } from "./api.js";
|
|
7
7
|
import { resolveConfig, resolveTarget } from "./config.js";
|
|
8
8
|
|
|
9
9
|
// ── Issue preview ───────────────────────────────────────────────────────────
|
|
@@ -71,8 +71,6 @@ export function startAnalysisUi(ctx: ExtensionContext, projectKey: string): Anal
|
|
|
71
71
|
|
|
72
72
|
export class IssueBrowser {
|
|
73
73
|
private selected = 0;
|
|
74
|
-
private cachedWidth: number | undefined;
|
|
75
|
-
private cachedLines: string[] | undefined;
|
|
76
74
|
|
|
77
75
|
constructor(
|
|
78
76
|
private readonly state: SonarAnalysisState,
|
|
@@ -80,10 +78,7 @@ export class IssueBrowser {
|
|
|
80
78
|
private readonly done: (result: number | null) => void,
|
|
81
79
|
) {}
|
|
82
80
|
|
|
83
|
-
invalidate(): void {
|
|
84
|
-
this.cachedWidth = undefined;
|
|
85
|
-
this.cachedLines = undefined;
|
|
86
|
-
}
|
|
81
|
+
invalidate(): void {} // NOSONAR - required by TUI interface
|
|
87
82
|
|
|
88
83
|
handleInput(data: string): void {
|
|
89
84
|
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
@@ -92,12 +87,10 @@ export class IssueBrowser {
|
|
|
92
87
|
}
|
|
93
88
|
if (matchesKey(data, Key.up)) {
|
|
94
89
|
this.selected = Math.max(0, this.selected - 1);
|
|
95
|
-
this.invalidate();
|
|
96
90
|
return;
|
|
97
91
|
}
|
|
98
92
|
if (matchesKey(data, Key.down)) {
|
|
99
93
|
this.selected = Math.min(this.state.issues.length - 1, this.selected + 1);
|
|
100
|
-
this.invalidate();
|
|
101
94
|
return;
|
|
102
95
|
}
|
|
103
96
|
if (matchesKey(data, Key.enter)) {
|
|
@@ -106,10 +99,6 @@ export class IssueBrowser {
|
|
|
106
99
|
}
|
|
107
100
|
|
|
108
101
|
render(width: number): string[] {
|
|
109
|
-
if (this.cachedLines && this.cachedWidth === width) {
|
|
110
|
-
return this.cachedLines;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
102
|
const lines: string[] = [];
|
|
114
103
|
const title = this.theme.fg("accent", this.theme.bold(" SonarQube Issues "));
|
|
115
104
|
const filterSuffix = this.state.filters ? ` • ${issueFilterLabel(this.state.filters)}` : "";
|
|
@@ -131,8 +120,15 @@ export class IssueBrowser {
|
|
|
131
120
|
const visibleIssues = this.state.issues.slice(start, end);
|
|
132
121
|
|
|
133
122
|
if (visibleIssues.length === 0) {
|
|
134
|
-
lines.push(
|
|
135
|
-
|
|
123
|
+
lines.push(
|
|
124
|
+
truncateToWidth(this.theme.fg("success", "No open issues found."), width),
|
|
125
|
+
"",
|
|
126
|
+
truncateToWidth(
|
|
127
|
+
this.theme.fg("dim", "Up/Down to move, Enter to preview, Esc to close"),
|
|
128
|
+
width,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return lines;
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
if (start > 0) {
|
|
@@ -173,27 +169,10 @@ export class IssueBrowser {
|
|
|
173
169
|
),
|
|
174
170
|
);
|
|
175
171
|
|
|
176
|
-
this.cachedWidth = width;
|
|
177
|
-
this.cachedLines = lines;
|
|
178
172
|
return lines;
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
function finalize(
|
|
183
|
-
lines: string[],
|
|
184
|
-
width: number,
|
|
185
|
-
theme: Theme,
|
|
186
|
-
cachedWidth: number | undefined,
|
|
187
|
-
cachedLines: string[] | undefined,
|
|
188
|
-
): string[] {
|
|
189
|
-
lines.push(
|
|
190
|
-
"",
|
|
191
|
-
truncateToWidth(theme.fg("dim", "Up/Down to move, Enter to preview, Esc to close"), width),
|
|
192
|
-
);
|
|
193
|
-
const result = cachedLines ?? lines;
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
176
|
// ── Severity colors ─────────────────────────────────────────────────────────
|
|
198
177
|
|
|
199
178
|
function severityColor(theme: Theme, severity: string): string {
|
|
@@ -210,6 +189,76 @@ function severityColor(theme: Theme, severity: string): string {
|
|
|
210
189
|
}
|
|
211
190
|
}
|
|
212
191
|
|
|
192
|
+
// ── Duplication browser (used by index.ts) ─────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export class DuplicationBrowser {
|
|
195
|
+
private selected = 0;
|
|
196
|
+
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly files: FileDuplication[],
|
|
199
|
+
private readonly theme: Theme,
|
|
200
|
+
private readonly done: (result: number | null) => void,
|
|
201
|
+
) {}
|
|
202
|
+
|
|
203
|
+
invalidate(): void {} // NOSONAR - required by TUI interface
|
|
204
|
+
|
|
205
|
+
handleInput(data: string): void {
|
|
206
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
207
|
+
this.done(null);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (matchesKey(data, Key.up)) {
|
|
211
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (matchesKey(data, Key.down)) {
|
|
215
|
+
this.selected = Math.min(this.files.length - 1, this.selected + 1);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (matchesKey(data, Key.enter)) {
|
|
219
|
+
this.done(this.selected);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
render(width: number): string[] {
|
|
224
|
+
const lines: string[] = [
|
|
225
|
+
this.theme.fg("accent", this.theme.bold(" SonarQube Duplications ")),
|
|
226
|
+
this.theme.fg("dim", `${this.files.length} file(s) with duplicate code`),
|
|
227
|
+
"",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const pageSize = Math.min(15, this.files.length);
|
|
231
|
+
const halfWindow = Math.floor(pageSize / 2);
|
|
232
|
+
const maxStart = Math.max(0, this.files.length - pageSize);
|
|
233
|
+
const start = Math.max(0, Math.min(this.selected - halfWindow, maxStart));
|
|
234
|
+
const end = Math.min(this.files.length, start + pageSize);
|
|
235
|
+
const visible = this.files.slice(start, end);
|
|
236
|
+
|
|
237
|
+
for (const [offset, file] of visible.entries()) {
|
|
238
|
+
const index = start + offset;
|
|
239
|
+
const isSelected = index === this.selected;
|
|
240
|
+
const marker = isSelected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
|
|
241
|
+
const detail = `blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
|
|
242
|
+
const line = `${marker} ${String(index + 1).padStart(2, " ")}. ${this.theme.fg("accent", file.filePath)} ${this.theme.fg("dim", detail)}`;
|
|
243
|
+
lines.push(truncateToWidth(line, width));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push(
|
|
247
|
+
"",
|
|
248
|
+
this.theme.fg("dim", "Up/Down to move, Enter for details, Esc to close"),
|
|
249
|
+
);
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function showDuplicationBrowser(
|
|
255
|
+
ctx: ExtensionCommandContext,
|
|
256
|
+
files: FileDuplication[],
|
|
257
|
+
): Promise<number | null> {
|
|
258
|
+
if (ctx.mode !== "tui" || files.length === 0) return null;
|
|
259
|
+
return await ctx.ui.custom<number | null>((_tui, theme, _kb, done) => new DuplicationBrowser(files, theme, done));
|
|
260
|
+
}
|
|
261
|
+
|
|
213
262
|
// ── Issue browser / preview helpers (used by index.ts) ──────────────────────
|
|
214
263
|
|
|
215
264
|
export async function showIssueBrowser(
|
|
@@ -251,7 +300,8 @@ export async function loadProjectIssuesFromApi(
|
|
|
251
300
|
ctx.signal,
|
|
252
301
|
normalizedFilters,
|
|
253
302
|
);
|
|
254
|
-
|
|
303
|
+
const cleanCodeMode = await fetchCleanCodeMode(config.serverUrl, config.token, ctx.signal);
|
|
304
|
+
return createAnalysisState(config, issues, { filters: normalizedFilters, cleanCodeMode });
|
|
255
305
|
}
|
|
256
306
|
|
|
257
307
|
export async function resolveTargetState(
|