@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/api.ts
CHANGED
|
@@ -6,10 +6,43 @@ import type {
|
|
|
6
6
|
SonarAnalysisState,
|
|
7
7
|
SonarProjectConfig,
|
|
8
8
|
SonarIssueFetchOptions,
|
|
9
|
+
SonarDuplicationMeasures,
|
|
10
|
+
IssueSeverityCounts,
|
|
11
|
+
IssueQualityCounts,
|
|
12
|
+
FileDuplication,
|
|
13
|
+
DuplicationBlockGroup,
|
|
9
14
|
} from "./types.js";
|
|
10
|
-
import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES } from "./types.js";
|
|
15
|
+
import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES, SONAR_SOFTWARE_QUALITIES, SONAR_IMPACT_SEVERITIES } from "./types.js";
|
|
11
16
|
import { parseProperties } from "./config.js";
|
|
12
17
|
|
|
18
|
+
// ── Clean code mode detection ────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const cleanCodeModeCache = new Map<string, "STANDARD" | "MQR">();
|
|
21
|
+
|
|
22
|
+
export async function fetchCleanCodeMode(
|
|
23
|
+
serverUrl: string,
|
|
24
|
+
token?: string,
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
): Promise<"STANDARD" | "MQR" | undefined> {
|
|
27
|
+
const cached = cleanCodeModeCache.get(serverUrl);
|
|
28
|
+
if (cached) return cached;
|
|
29
|
+
try {
|
|
30
|
+
const result = await fetchJson<{ mode: string }>(
|
|
31
|
+
`${serverUrl}/api/v2/clean-code-policy/mode`,
|
|
32
|
+
token,
|
|
33
|
+
signal,
|
|
34
|
+
);
|
|
35
|
+
const mode = result.mode?.toUpperCase();
|
|
36
|
+
if (mode === "MQR" || mode === "STANDARD") {
|
|
37
|
+
cleanCodeModeCache.set(serverUrl, mode);
|
|
38
|
+
return mode;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ponytail: fall through to legacy behavior when mode lookup is unavailable.
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
// ── Auth / HTTP helpers ─────────────────────────────────────────────────────
|
|
14
47
|
|
|
15
48
|
export function authHeader(token?: string): string | undefined {
|
|
@@ -33,7 +66,8 @@ export async function fetchJson<T>(url: string, token?: string, signal?: AbortSi
|
|
|
33
66
|
return (await response.json()) as T;
|
|
34
67
|
}
|
|
35
68
|
|
|
36
|
-
export function toIssuePath(component: string, projectKey: string): string {
|
|
69
|
+
export function toIssuePath(component: string | undefined | null, projectKey: string): string {
|
|
70
|
+
if (typeof component !== "string" || component.length === 0) return projectKey;
|
|
37
71
|
const prefix = `${projectKey}:`;
|
|
38
72
|
if (component.startsWith(prefix)) return component.slice(prefix.length);
|
|
39
73
|
const colon = component.indexOf(":");
|
|
@@ -57,26 +91,25 @@ export function normalizeIssueFilters(filters?: SonarIssueFetchOptions): SonarIs
|
|
|
57
91
|
statuses: normalizeIssueList(filters?.statuses, true),
|
|
58
92
|
types: normalizeIssueList(filters?.types, true),
|
|
59
93
|
rules: normalizeIssueList(filters?.rules),
|
|
94
|
+
softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
|
|
95
|
+
impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
|
|
60
96
|
};
|
|
61
|
-
return normalized.severities || normalized.statuses || normalized.types || normalized.rules
|
|
97
|
+
return normalized.severities || normalized.statuses || normalized.types || normalized.rules || normalized.softwareQualities || normalized.impactSeverities
|
|
98
|
+
? normalized
|
|
99
|
+
: undefined;
|
|
62
100
|
}
|
|
63
101
|
|
|
64
|
-
export function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
export function assertFiltersNotAmbiguous(filters?: SonarIssueFetchOptions): void {
|
|
103
|
+
if (!filters) return;
|
|
104
|
+
if (
|
|
105
|
+
(filters.severities?.length || filters.types?.length) &&
|
|
106
|
+
(filters.softwareQualities?.length || filters.impactSeverities?.length)
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Ambiguous filter combination: mixing legacy filters (severity/type) with MQR filters (quality/impactSeverity) is not allowed. " +
|
|
110
|
+
"Use one filter family per command.",
|
|
111
|
+
);
|
|
74
112
|
}
|
|
75
|
-
return normalizeIssueFilters(merged);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function splitFilterValues(value: string): string[] {
|
|
79
|
-
return value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
const EXPLICIT_FILTER_RE = /^([^:=]+)[:=](.+)$/;
|
|
@@ -88,20 +121,41 @@ export function parseIssueFilterToken(token: string): Partial<SonarIssueFetchOpt
|
|
|
88
121
|
const explicit = EXPLICIT_FILTER_RE.exec(trimmed);
|
|
89
122
|
if (explicit) {
|
|
90
123
|
const key = explicit[1].trim().toLowerCase();
|
|
91
|
-
const values =
|
|
124
|
+
const values = explicit[2].split(",").map((p) => p.trim()).filter(Boolean);
|
|
92
125
|
if (values.length === 0) return undefined;
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
switch (key) {
|
|
128
|
+
case "severity":
|
|
129
|
+
case "severities":
|
|
130
|
+
return { severities: values.map((v) => v.toUpperCase()) };
|
|
131
|
+
case "status":
|
|
132
|
+
case "statuses":
|
|
133
|
+
return { statuses: values.map((v) => v.toUpperCase()) };
|
|
134
|
+
case "type":
|
|
135
|
+
case "types":
|
|
136
|
+
return { types: values.map((v) => v.toUpperCase()) };
|
|
137
|
+
case "rule":
|
|
138
|
+
case "rules":
|
|
139
|
+
return { rules: values };
|
|
140
|
+
case "quality":
|
|
141
|
+
case "qualities":
|
|
142
|
+
case "softwarequality":
|
|
143
|
+
case "softwarequalities":
|
|
144
|
+
return { softwareQualities: values.map((v) => v.toUpperCase()) };
|
|
145
|
+
case "impactseverity":
|
|
146
|
+
case "impactseverities":
|
|
147
|
+
return { impactSeverities: values.map((v) => v.toUpperCase()) };
|
|
148
|
+
default:
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
99
151
|
}
|
|
100
152
|
|
|
101
153
|
const upper = trimmed.toUpperCase();
|
|
102
154
|
if (SONAR_SEVERITIES.includes(upper as (typeof SONAR_SEVERITIES)[number])) return { severities: [upper] };
|
|
103
155
|
if (SONAR_STATUSES.includes(upper as (typeof SONAR_STATUSES)[number])) return { statuses: [upper] };
|
|
104
156
|
if (SONAR_TYPES.includes(upper as (typeof SONAR_TYPES)[number])) return { types: [upper] };
|
|
157
|
+
if (SONAR_SOFTWARE_QUALITIES.includes(upper as (typeof SONAR_SOFTWARE_QUALITIES)[number])) return { softwareQualities: [upper] };
|
|
158
|
+
if (SONAR_IMPACT_SEVERITIES.includes(upper as (typeof SONAR_IMPACT_SEVERITIES)[number])) return { impactSeverities: [upper] };
|
|
105
159
|
return undefined;
|
|
106
160
|
}
|
|
107
161
|
|
|
@@ -131,17 +185,34 @@ export function parseSonarIssueArgs(
|
|
|
131
185
|
}
|
|
132
186
|
}
|
|
133
187
|
|
|
134
|
-
return { targetInput, issueIndex, filters:
|
|
188
|
+
return { targetInput, issueIndex, filters: mergeFilters(...filters) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mergeFilters(
|
|
192
|
+
...filters: Array<Partial<SonarIssueFetchOptions> | undefined>
|
|
193
|
+
): SonarIssueFetchOptions | undefined {
|
|
194
|
+
const merged: SonarIssueFetchOptions = {};
|
|
195
|
+
for (const filter of filters) {
|
|
196
|
+
if (!filter) continue;
|
|
197
|
+
if (filter.severities?.length) merged.severities = [...(merged.severities ?? []), ...filter.severities];
|
|
198
|
+
if (filter.statuses?.length) merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
|
|
199
|
+
if (filter.types?.length) merged.types = [...(merged.types ?? []), ...filter.types];
|
|
200
|
+
if (filter.rules?.length) merged.rules = [...(merged.rules ?? []), ...filter.rules];
|
|
201
|
+
if (filter.softwareQualities?.length) merged.softwareQualities = [...(merged.softwareQualities ?? []), ...filter.softwareQualities];
|
|
202
|
+
if (filter.impactSeverities?.length) merged.impactSeverities = [...(merged.impactSeverities ?? []), ...filter.impactSeverities];
|
|
203
|
+
}
|
|
204
|
+
return normalizeIssueFilters(merged);
|
|
135
205
|
}
|
|
136
206
|
|
|
137
207
|
export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
|
|
138
208
|
if (!filters) return "";
|
|
139
|
-
const parts = [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
const parts: string[] = [];
|
|
210
|
+
if (filters.severities?.length) parts.push(`severities=${filters.severities.join(",")}`);
|
|
211
|
+
if (filters.statuses?.length) parts.push(`statuses=${filters.statuses.join(",")}`);
|
|
212
|
+
if (filters.types?.length) parts.push(`types=${filters.types.join(",")}`);
|
|
213
|
+
if (filters.rules?.length) parts.push(`rules=${filters.rules.join(",")}`);
|
|
214
|
+
if (filters.softwareQualities?.length) parts.push(`qualities=${filters.softwareQualities.join(",")}`);
|
|
215
|
+
if (filters.impactSeverities?.length) parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
|
|
145
216
|
return parts.join(" • ");
|
|
146
217
|
}
|
|
147
218
|
|
|
@@ -328,6 +399,8 @@ function buildIssueSearchUrl(
|
|
|
328
399
|
if (filters?.statuses?.length) url.searchParams.set("statuses", filters.statuses.join(","));
|
|
329
400
|
if (filters?.types?.length) url.searchParams.set("types", filters.types.join(","));
|
|
330
401
|
if (filters?.rules?.length) url.searchParams.set("rules", filters.rules.join(","));
|
|
402
|
+
if (filters?.softwareQualities?.length) url.searchParams.set("impactSoftwareQualities", filters.softwareQualities.join(","));
|
|
403
|
+
if (filters?.impactSeverities?.length) url.searchParams.set("impactSeverities", filters.impactSeverities.join(","));
|
|
331
404
|
return url.toString();
|
|
332
405
|
}
|
|
333
406
|
|
|
@@ -387,6 +460,7 @@ export async function fetchIssues(
|
|
|
387
460
|
signal?: AbortSignal,
|
|
388
461
|
filters?: SonarIssueFetchOptions,
|
|
389
462
|
): Promise<SonarIssue[]> {
|
|
463
|
+
assertFiltersNotAmbiguous(filters);
|
|
390
464
|
const normalizedFilters = normalizeIssueFilters(filters);
|
|
391
465
|
const issues: SonarIssue[] = [];
|
|
392
466
|
const ruleNames = new Map<string, string | undefined>();
|
|
@@ -440,7 +514,7 @@ export async function fetchRuleName(
|
|
|
440
514
|
export function createAnalysisState(
|
|
441
515
|
config: Pick<SonarProjectConfig, "baseDir" | "serverUrl" | "projectKey">,
|
|
442
516
|
issues: SonarIssue[],
|
|
443
|
-
extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters">> = {},
|
|
517
|
+
extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures" | "cleanCodeMode">> = {},
|
|
444
518
|
): SonarAnalysisState {
|
|
445
519
|
return {
|
|
446
520
|
version: 1,
|
|
@@ -453,24 +527,156 @@ export function createAnalysisState(
|
|
|
453
527
|
analysisId: extras.analysisId,
|
|
454
528
|
filters: extras.filters,
|
|
455
529
|
totalIssues: issues.length,
|
|
530
|
+
measures: extras.measures,
|
|
531
|
+
cleanCodeMode: extras.cleanCodeMode,
|
|
456
532
|
issues,
|
|
457
533
|
};
|
|
458
534
|
}
|
|
459
535
|
|
|
460
|
-
|
|
536
|
+
// ── Measures ──────────────────────────────────────────────────────────────────
|
|
537
|
+
|
|
538
|
+
export async function fetchDuplicationMeasures(
|
|
461
539
|
serverUrl: string,
|
|
462
540
|
token: string | undefined,
|
|
463
541
|
projectKey: string,
|
|
464
542
|
signal?: AbortSignal,
|
|
465
|
-
): Promise<
|
|
543
|
+
): Promise<SonarDuplicationMeasures | undefined> {
|
|
466
544
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
545
|
+
const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(projectKey)}&metricKeys=duplicated_lines_density,duplicated_lines,duplicated_blocks,duplicated_files`;
|
|
546
|
+
const result = await fetchJson<{
|
|
547
|
+
component: { measures: Array<{ metric: string; value: string }> };
|
|
548
|
+
}>(url, token, signal);
|
|
549
|
+
const measures = result.component.measures;
|
|
550
|
+
const getValue = (key: string): number => {
|
|
551
|
+
const m = measures.find((m) => m.metric === key);
|
|
552
|
+
return m ? Number.parseFloat(m.value) : 0;
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
duplicatedLinesDensity: getValue("duplicated_lines_density"),
|
|
556
|
+
duplicatedLines: getValue("duplicated_lines"),
|
|
557
|
+
duplicatedBlocks: getValue("duplicated_blocks"),
|
|
558
|
+
duplicatedFiles: getValue("duplicated_files"),
|
|
559
|
+
};
|
|
473
560
|
} catch {
|
|
474
|
-
return
|
|
561
|
+
return undefined;
|
|
475
562
|
}
|
|
476
563
|
}
|
|
564
|
+
|
|
565
|
+
export async function fetchIssueSeverityCounts(
|
|
566
|
+
serverUrl: string,
|
|
567
|
+
token: string | undefined,
|
|
568
|
+
projectKey: string,
|
|
569
|
+
signal?: AbortSignal,
|
|
570
|
+
): Promise<IssueSeverityCounts | undefined> {
|
|
571
|
+
try {
|
|
572
|
+
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=severities`;
|
|
573
|
+
const result = await fetchJson<{
|
|
574
|
+
facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
|
|
575
|
+
}>(url, token, signal);
|
|
576
|
+
const facet = result.facets?.find((f) => f.property === "severities");
|
|
577
|
+
if (!facet) return undefined;
|
|
578
|
+
const getCount = (severity: string): number => {
|
|
579
|
+
const entry = facet.values.find((v) => v.val === severity);
|
|
580
|
+
return entry?.count ?? 0;
|
|
581
|
+
};
|
|
582
|
+
return {
|
|
583
|
+
blocker: getCount("BLOCKER"),
|
|
584
|
+
critical: getCount("CRITICAL"),
|
|
585
|
+
major: getCount("MAJOR"),
|
|
586
|
+
minor: getCount("MINOR"),
|
|
587
|
+
info: getCount("INFO"),
|
|
588
|
+
};
|
|
589
|
+
} catch {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function fetchIssueQualityCounts(
|
|
595
|
+
serverUrl: string,
|
|
596
|
+
token: string | undefined,
|
|
597
|
+
projectKey: string,
|
|
598
|
+
signal?: AbortSignal,
|
|
599
|
+
): Promise<IssueQualityCounts | undefined> {
|
|
600
|
+
try {
|
|
601
|
+
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=impactSoftwareQualities`;
|
|
602
|
+
const result = await fetchJson<{
|
|
603
|
+
facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
|
|
604
|
+
}>(url, token, signal);
|
|
605
|
+
const facet = result.facets?.find((f) => f.property === "impactSoftwareQualities");
|
|
606
|
+
if (!facet) return undefined;
|
|
607
|
+
const getCount = (q: string): number => {
|
|
608
|
+
const entry = facet.values.find((v) => v.val === q);
|
|
609
|
+
return entry?.count ?? 0;
|
|
610
|
+
};
|
|
611
|
+
return {
|
|
612
|
+
maintainability: getCount("MAINTAINABILITY"),
|
|
613
|
+
reliability: getCount("RELIABILITY"),
|
|
614
|
+
security: getCount("SECURITY"),
|
|
615
|
+
};
|
|
616
|
+
} catch {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── File duplications ─────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
export async function fetchFileDuplications(
|
|
624
|
+
serverUrl: string,
|
|
625
|
+
token: string | undefined,
|
|
626
|
+
projectKey: string,
|
|
627
|
+
signal?: AbortSignal,
|
|
628
|
+
): Promise<FileDuplication[]> {
|
|
629
|
+
const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_blocks&ps=500`;
|
|
630
|
+
const treeResult = await fetchJson<{
|
|
631
|
+
components: Array<{
|
|
632
|
+
key: string;
|
|
633
|
+
path?: string;
|
|
634
|
+
name?: string;
|
|
635
|
+
measures?: Array<{ metric: string; value: string }>;
|
|
636
|
+
}>;
|
|
637
|
+
}>(treeUrl, token, signal);
|
|
638
|
+
|
|
639
|
+
const results = (treeResult.components ?? [])
|
|
640
|
+
.map((component) => {
|
|
641
|
+
const measures = component.measures ?? [];
|
|
642
|
+
const getValue = (metric: string): number => {
|
|
643
|
+
const m = measures.find((item) => item.metric === metric);
|
|
644
|
+
return m ? Number.parseInt(m.value, 10) : 0;
|
|
645
|
+
};
|
|
646
|
+
const duplicatedBlocks = getValue("duplicated_blocks");
|
|
647
|
+
if (duplicatedBlocks <= 0) return null;
|
|
648
|
+
return {
|
|
649
|
+
filePath: component.path ?? toIssuePath(component.key, projectKey) ?? component.name ?? component.key,
|
|
650
|
+
fileKey: component.key,
|
|
651
|
+
duplicatedLines: getValue("duplicated_lines"),
|
|
652
|
+
duplicatedBlocks,
|
|
653
|
+
};
|
|
654
|
+
})
|
|
655
|
+
.filter((f): f is FileDuplication => f !== null);
|
|
656
|
+
|
|
657
|
+
return results;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export async function fetchFileDuplicationBlocks(
|
|
661
|
+
serverUrl: string,
|
|
662
|
+
token: string | undefined,
|
|
663
|
+
fileKey: string,
|
|
664
|
+
projectKey: string,
|
|
665
|
+
signal?: AbortSignal,
|
|
666
|
+
): Promise<DuplicationBlockGroup[]> {
|
|
667
|
+
const url = `${serverUrl}/api/duplications/show?key=${encodeURIComponent(fileKey)}`;
|
|
668
|
+
const result = await fetchJson<{
|
|
669
|
+
duplications: Array<{
|
|
670
|
+
blocks: Array<{ from: number; size: number; component: string }>;
|
|
671
|
+
}>;
|
|
672
|
+
}>(url, token, signal);
|
|
673
|
+
return result.duplications.map((d) => ({
|
|
674
|
+
blocks: d.blocks.map((b) => ({
|
|
675
|
+
from: b.from,
|
|
676
|
+
size: b.size,
|
|
677
|
+
filePath: toIssuePath(b.component, projectKey),
|
|
678
|
+
})),
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
package/src/commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AutocompleteItem, AutocompleteSuggestions } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { SonarIssue, SonarIssueFetchOptions } from "./types.js";
|
|
3
|
-
import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES } from "./types.js";
|
|
2
|
+
import type { SonarIssue, SonarIssueFetchOptions, SonarDuplicationMeasures, IssueSeverityCounts, IssueQualityCounts, FileDuplication, DuplicationBlockGroup } from "./types.js";
|
|
3
|
+
import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES, SONAR_SOFTWARE_QUALITIES, SONAR_IMPACT_SEVERITIES } from "./types.js";
|
|
4
4
|
import { looksLikePath } from "./config.js";
|
|
5
5
|
import { parseSonarIssueArgs, issueFilterLabel } from "./api.js";
|
|
6
6
|
|
|
@@ -13,14 +13,12 @@ const SONAR_COMMANDS = [
|
|
|
13
13
|
{ value: "issues", label: "issues", description: "browse the latest issues" },
|
|
14
14
|
{ value: "open", label: "open", description: "preview a specific issue" },
|
|
15
15
|
{ value: "init", label: "init", description: "configure a project target" },
|
|
16
|
+
{ value: "metrics", label: "metrics", description: "show project metrics (duplication, issue counts)" },
|
|
17
|
+
{ value: "duplications", label: "duplications", description: "browse duplicated files and blocks" },
|
|
16
18
|
] as const;
|
|
17
19
|
|
|
18
20
|
// ── Autocomplete helpers ────────────────────────────────────────────────────
|
|
19
21
|
|
|
20
|
-
function createAutocompleteItem(value: string, description?: string): AutocompleteItem {
|
|
21
|
-
return description ? { value, label: value, description } : { value, label: value };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
22
|
export function filterAutocompleteItems(
|
|
25
23
|
items: readonly { value: string; label: string; description?: string }[],
|
|
26
24
|
prefix: string,
|
|
@@ -71,23 +69,34 @@ export function splitSonarArgumentContext(
|
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
function createFilterCompletionList(): AutocompleteItem[] {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
function createFilterCompletionList(mode?: "STANDARD" | "MQR"): AutocompleteItem[] {
|
|
73
|
+
const buildItems = (
|
|
74
|
+
groups: ReadonlyArray<readonly [string, readonly string[], string, boolean]>,
|
|
75
|
+
): AutocompleteItem[] =>
|
|
76
|
+
groups.flatMap(([prefix, values, description, includeBare]) =>
|
|
77
|
+
values.flatMap((value) => [
|
|
78
|
+
{ value: `${prefix}:${value}`, label: `${prefix}:${value}`, description },
|
|
79
|
+
...(includeBare ? [{ value, label: value, description }] : []),
|
|
80
|
+
]),
|
|
80
81
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
|
|
83
|
+
const legacyGroups = [
|
|
84
|
+
["severity", SONAR_SEVERITIES, "severity", true],
|
|
85
|
+
["status", SONAR_STATUSES, "status", true],
|
|
86
|
+
["type", SONAR_TYPES, "type", true],
|
|
87
|
+
] as const;
|
|
88
|
+
const mqrGroups = [
|
|
89
|
+
["quality", SONAR_SOFTWARE_QUALITIES, "software quality (MQR)", true],
|
|
90
|
+
["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
return mode === "MQR" ? [...buildItems(mqrGroups), ...buildItems(legacyGroups)] : [...buildItems(legacyGroups), ...buildItems(mqrGroups)];
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
export function sonarArgumentCompletions(
|
|
89
97
|
argumentText: string,
|
|
90
98
|
issues?: SonarIssue[],
|
|
99
|
+
mode?: "STANDARD" | "MQR",
|
|
91
100
|
): AutocompleteItem[] | null {
|
|
92
101
|
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
93
102
|
const lowerCommand = command.toLowerCase();
|
|
@@ -96,29 +105,26 @@ export function sonarArgumentCompletions(
|
|
|
96
105
|
|
|
97
106
|
if (!command || commandMatches.length > 1 || !isFullMatch) {
|
|
98
107
|
if (commandMatches.length === 0) return null;
|
|
99
|
-
return commandMatches.map((item) =>
|
|
108
|
+
return commandMatches.map((item) => ({ value: item.value, label: item.value, description: item.description }));
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
if (lowerCommand === "open") {
|
|
103
112
|
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
|
|
104
113
|
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
105
|
-
return
|
|
106
|
-
String(index + 1),
|
|
107
|
-
`${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
108
|
-
);
|
|
114
|
+
return { value: String(index + 1), label: String(index + 1), description: `${issue.severity} ${issue.filePath}${lineSuffix}` };
|
|
109
115
|
});
|
|
110
|
-
const suggestions = [...issueItems, ...createFilterCompletionList()];
|
|
116
|
+
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
111
117
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
112
118
|
return filtered.length > 0 ? filtered : null;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
116
|
-
const suggestions = createFilterCompletionList();
|
|
122
|
+
const suggestions = createFilterCompletionList(mode);
|
|
117
123
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
118
124
|
return filtered.length > 0 ? filtered : null;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
if (lowerCommand === "init") {
|
|
127
|
+
if (lowerCommand === "init" || lowerCommand === "metrics" || lowerCommand === "duplications") {
|
|
122
128
|
return null;
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -132,7 +138,10 @@ export type ParsedSonarCommand =
|
|
|
132
138
|
| { action: "init"; alias?: string; targetInput?: string }
|
|
133
139
|
| { action: "analyze"; targetInput?: string; filters?: SonarIssueFetchOptions }
|
|
134
140
|
| { action: "issues"; targetInput?: string; filters?: SonarIssueFetchOptions }
|
|
135
|
-
| { action: "open"; targetInput?: string; issueIndex?: number; filters?: SonarIssueFetchOptions }
|
|
141
|
+
| { action: "open"; targetInput?: string; issueIndex?: number; filters?: SonarIssueFetchOptions }
|
|
142
|
+
| { action: "metrics"; targetInput?: string }
|
|
143
|
+
| { action: "duplications"; targetInput?: string; issueIndex?: number };
|
|
144
|
+
|
|
136
145
|
|
|
137
146
|
export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
138
147
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -157,6 +166,12 @@ export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
|
157
166
|
if (head === "analyze" || head === "run") {
|
|
158
167
|
return { action: "analyze", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
159
168
|
}
|
|
169
|
+
if (head === "metrics") {
|
|
170
|
+
return { action: "metrics", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
171
|
+
}
|
|
172
|
+
if (head === "duplications") {
|
|
173
|
+
return { action: "duplications", ...parseSonarIssueArgs(tokens.slice(1), true) };
|
|
174
|
+
}
|
|
160
175
|
|
|
161
176
|
return { action: "analyze", ...parseSonarIssueArgs(tokens) };
|
|
162
177
|
}
|
|
@@ -171,15 +186,23 @@ export function helpText(): string {
|
|
|
171
186
|
" /sonarqube analyze [target] run analysis for a target or path",
|
|
172
187
|
" /sonarqube issues [target] browse issues for a target or path",
|
|
173
188
|
" /sonarqube open [target] <n> preview issue #n for a target or path",
|
|
189
|
+
" /sonarqube metrics [target] show project metrics (no scanner)",
|
|
190
|
+
" /sonarqube duplications [target] browse duplicated files and blocks",
|
|
174
191
|
" /sonarqube show this help",
|
|
175
192
|
"",
|
|
176
193
|
"Filters:",
|
|
177
194
|
" /sonarqube issues be CRITICAL",
|
|
178
195
|
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
196
|
+
" /sonarqube issues be quality:RELIABILITY",
|
|
197
|
+
" /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
|
|
179
198
|
"",
|
|
180
199
|
"Autocomplete:",
|
|
181
200
|
" type /sonarqube and press Tab to complete the subcommand or filters",
|
|
182
201
|
"",
|
|
202
|
+
"Notes:",
|
|
203
|
+
" Legacy filters (severity, type) and MQR filters (quality, impactSeverity)",
|
|
204
|
+
" cannot be combined in the same command. Use one filter family per query.",
|
|
205
|
+
"",
|
|
183
206
|
"Defaults:",
|
|
184
207
|
" config is stored in .pi/sonarqube.json",
|
|
185
208
|
" use project paths directly (no alias needed)",
|
|
@@ -203,6 +226,69 @@ export function formatIssue(issue: SonarIssue, index?: number): string {
|
|
|
203
226
|
return `${prefix}${issue.severity} ${loc} — ${rule} — ${issue.message}`;
|
|
204
227
|
}
|
|
205
228
|
|
|
229
|
+
export function formatMetricsOutput(metrics: {
|
|
230
|
+
projectKey: string;
|
|
231
|
+
measures?: SonarDuplicationMeasures;
|
|
232
|
+
issueCounts?: IssueSeverityCounts;
|
|
233
|
+
issueQualityCounts?: IssueQualityCounts;
|
|
234
|
+
}): string {
|
|
235
|
+
const lines: string[] = [`Metrics for ${metrics.projectKey}`];
|
|
236
|
+
|
|
237
|
+
if (metrics.measures) {
|
|
238
|
+
const density = metrics.measures.duplicatedLinesDensity.toFixed(1);
|
|
239
|
+
lines.push(
|
|
240
|
+
metrics.measures.duplicatedBlocks > 0
|
|
241
|
+
? `Duplication: ${density}% lines=${metrics.measures.duplicatedLines} blocks=${metrics.measures.duplicatedBlocks} files=${metrics.measures.duplicatedFiles}`
|
|
242
|
+
: `Duplication: ${density}% (no duplications detected)`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (metrics.issueCounts) {
|
|
247
|
+
const counts = [
|
|
248
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
249
|
+
`CRITICAL ${metrics.issueCounts.critical}`,
|
|
250
|
+
`MAJOR ${metrics.issueCounts.major}`,
|
|
251
|
+
`MINOR ${metrics.issueCounts.minor}`,
|
|
252
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
253
|
+
];
|
|
254
|
+
lines.push(`Issues: ${counts.join(" ")}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (metrics.issueQualityCounts) {
|
|
258
|
+
const counts = [
|
|
259
|
+
`MAINTAINABILITY ${metrics.issueQualityCounts.maintainability}`,
|
|
260
|
+
`RELIABILITY ${metrics.issueQualityCounts.reliability}`,
|
|
261
|
+
`SECURITY ${metrics.issueQualityCounts.security}`,
|
|
262
|
+
];
|
|
263
|
+
lines.push(`Quality: ${counts.join(" ")}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function formatDuplicationsList(files: FileDuplication[]): string {
|
|
270
|
+
if (files.length === 0) return "No duplicated files found.";
|
|
271
|
+
const lines: string[] = [`Duplicated files (${files.length})`];
|
|
272
|
+
for (const [i, f] of files.entries()) {
|
|
273
|
+
lines.push(` ${String(i + 1).padStart(2, " ")}. ${f.filePath} blocks=${f.duplicatedBlocks} lines=${f.duplicatedLines}`);
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatDuplicationBlockDetail(filePath: string, groups: DuplicationBlockGroup[]): string {
|
|
279
|
+
if (groups.length === 0) return `No duplications found in ${filePath}.`;
|
|
280
|
+
const lines: string[] = [`Duplications in ${filePath}`, ""];
|
|
281
|
+
for (const [i, group] of groups.entries()) {
|
|
282
|
+
lines.push(`Block ${i + 1}:`);
|
|
283
|
+
for (const block of group.blocks) {
|
|
284
|
+
const end = block.from + block.size - 1;
|
|
285
|
+
lines.push(` ${block.filePath}:${block.from}-${end}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push("");
|
|
288
|
+
}
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
206
292
|
export function formatSummary(state: { totalIssues: number; projectKey: string; filters?: SonarIssueFetchOptions }): string {
|
|
207
293
|
const issueCount = state.totalIssues;
|
|
208
294
|
const filterLabel = state.filters ? ` (${issueFilterLabel(state.filters)})` : "";
|
|
@@ -219,6 +305,7 @@ export function formatReport(state: {
|
|
|
219
305
|
baseDir: string;
|
|
220
306
|
totalIssues: number;
|
|
221
307
|
filters?: SonarIssueFetchOptions;
|
|
308
|
+
measures?: SonarDuplicationMeasures;
|
|
222
309
|
issues: SonarIssue[];
|
|
223
310
|
}): string {
|
|
224
311
|
const lines = [
|
|
@@ -232,8 +319,14 @@ export function formatReport(state: {
|
|
|
232
319
|
lines.push(`Filters: ${issueFilterLabel(state.filters)}`);
|
|
233
320
|
}
|
|
234
321
|
|
|
322
|
+
if (state.measures && state.issues.length > 0) {
|
|
323
|
+
lines.push("", state.measures.duplicatedBlocks > 0
|
|
324
|
+
? `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% lines=${state.measures.duplicatedLines} blocks=${state.measures.duplicatedBlocks} files=${state.measures.duplicatedFiles}`
|
|
325
|
+
: `Duplication: ${state.measures.duplicatedLinesDensity.toFixed(1)}% (no duplications detected)`);
|
|
326
|
+
}
|
|
327
|
+
|
|
235
328
|
if (state.issues.length === 0) {
|
|
236
|
-
lines.push("No open issues found.");
|
|
329
|
+
if (!state.measures) lines.push("No open issues found.");
|
|
237
330
|
return lines.join("\n");
|
|
238
331
|
}
|
|
239
332
|
|