@jonathan0823/pi-sonarqube 0.2.0 → 0.3.2
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 +25 -5
- package/dist/api.d.ts +7 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +153 -54
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +4 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +57 -22
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -28
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +13 -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 +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +133 -93
- package/dist/ui.js.map +1 -1
- package/package.json +7 -1
- package/src/api.ts +171 -59
- package/src/commands.ts +67 -21
- package/src/index.ts +80 -29
- package/src/types.ts +14 -0
- package/src/ui.ts +172 -130
package/src/api.ts
CHANGED
|
@@ -8,12 +8,41 @@ import type {
|
|
|
8
8
|
SonarIssueFetchOptions,
|
|
9
9
|
SonarDuplicationMeasures,
|
|
10
10
|
IssueSeverityCounts,
|
|
11
|
+
IssueQualityCounts,
|
|
11
12
|
FileDuplication,
|
|
12
13
|
DuplicationBlockGroup,
|
|
13
14
|
} from "./types.js";
|
|
14
|
-
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";
|
|
15
16
|
import { parseProperties } from "./config.js";
|
|
16
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
|
+
|
|
17
46
|
// ── Auth / HTTP helpers ─────────────────────────────────────────────────────
|
|
18
47
|
|
|
19
48
|
export function authHeader(token?: string): string | undefined {
|
|
@@ -37,7 +66,8 @@ export async function fetchJson<T>(url: string, token?: string, signal?: AbortSi
|
|
|
37
66
|
return (await response.json()) as T;
|
|
38
67
|
}
|
|
39
68
|
|
|
40
|
-
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;
|
|
41
71
|
const prefix = `${projectKey}:`;
|
|
42
72
|
if (component.startsWith(prefix)) return component.slice(prefix.length);
|
|
43
73
|
const colon = component.indexOf(":");
|
|
@@ -61,8 +91,25 @@ export function normalizeIssueFilters(filters?: SonarIssueFetchOptions): SonarIs
|
|
|
61
91
|
statuses: normalizeIssueList(filters?.statuses, true),
|
|
62
92
|
types: normalizeIssueList(filters?.types, true),
|
|
63
93
|
rules: normalizeIssueList(filters?.rules),
|
|
94
|
+
softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
|
|
95
|
+
impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
|
|
64
96
|
};
|
|
65
|
-
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;
|
|
100
|
+
}
|
|
101
|
+
|
|
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
|
+
);
|
|
112
|
+
}
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
const EXPLICIT_FILTER_RE = /^([^:=]+)[:=](.+)$/;
|
|
@@ -77,17 +124,38 @@ export function parseIssueFilterToken(token: string): Partial<SonarIssueFetchOpt
|
|
|
77
124
|
const values = explicit[2].split(",").map((p) => p.trim()).filter(Boolean);
|
|
78
125
|
if (values.length === 0) return undefined;
|
|
79
126
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
151
|
}
|
|
86
152
|
|
|
87
153
|
const upper = trimmed.toUpperCase();
|
|
88
154
|
if (SONAR_SEVERITIES.includes(upper as (typeof SONAR_SEVERITIES)[number])) return { severities: [upper] };
|
|
89
155
|
if (SONAR_STATUSES.includes(upper as (typeof SONAR_STATUSES)[number])) return { statuses: [upper] };
|
|
90
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] };
|
|
91
159
|
return undefined;
|
|
92
160
|
}
|
|
93
161
|
|
|
@@ -130,18 +198,21 @@ function mergeFilters(
|
|
|
130
198
|
if (filter.statuses?.length) merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
|
|
131
199
|
if (filter.types?.length) merged.types = [...(merged.types ?? []), ...filter.types];
|
|
132
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];
|
|
133
203
|
}
|
|
134
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
|
|
|
@@ -150,10 +221,13 @@ function severitySortRank(severity: string): number {
|
|
|
150
221
|
case "BLOCKER":
|
|
151
222
|
return 0;
|
|
152
223
|
case "CRITICAL":
|
|
224
|
+
case "HIGH":
|
|
153
225
|
return 1;
|
|
154
226
|
case "MAJOR":
|
|
227
|
+
case "MEDIUM":
|
|
155
228
|
return 2;
|
|
156
229
|
case "MINOR":
|
|
230
|
+
case "LOW":
|
|
157
231
|
return 3;
|
|
158
232
|
case "INFO":
|
|
159
233
|
return 4;
|
|
@@ -328,6 +402,8 @@ function buildIssueSearchUrl(
|
|
|
328
402
|
if (filters?.statuses?.length) url.searchParams.set("statuses", filters.statuses.join(","));
|
|
329
403
|
if (filters?.types?.length) url.searchParams.set("types", filters.types.join(","));
|
|
330
404
|
if (filters?.rules?.length) url.searchParams.set("rules", filters.rules.join(","));
|
|
405
|
+
if (filters?.softwareQualities?.length) url.searchParams.set("impactSoftwareQualities", filters.softwareQualities.join(","));
|
|
406
|
+
if (filters?.impactSeverities?.length) url.searchParams.set("impactSeverities", filters.impactSeverities.join(","));
|
|
331
407
|
return url.toString();
|
|
332
408
|
}
|
|
333
409
|
|
|
@@ -387,6 +463,7 @@ export async function fetchIssues(
|
|
|
387
463
|
signal?: AbortSignal,
|
|
388
464
|
filters?: SonarIssueFetchOptions,
|
|
389
465
|
): Promise<SonarIssue[]> {
|
|
466
|
+
assertFiltersNotAmbiguous(filters);
|
|
390
467
|
const normalizedFilters = normalizeIssueFilters(filters);
|
|
391
468
|
const issues: SonarIssue[] = [];
|
|
392
469
|
const ruleNames = new Map<string, string | undefined>();
|
|
@@ -440,7 +517,7 @@ export async function fetchRuleName(
|
|
|
440
517
|
export function createAnalysisState(
|
|
441
518
|
config: Pick<SonarProjectConfig, "baseDir" | "serverUrl" | "projectKey">,
|
|
442
519
|
issues: SonarIssue[],
|
|
443
|
-
extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures">> = {},
|
|
520
|
+
extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures" | "cleanCodeMode">> = {},
|
|
444
521
|
): SonarAnalysisState {
|
|
445
522
|
return {
|
|
446
523
|
version: 1,
|
|
@@ -454,6 +531,7 @@ export function createAnalysisState(
|
|
|
454
531
|
filters: extras.filters,
|
|
455
532
|
totalIssues: issues.length,
|
|
456
533
|
measures: extras.measures,
|
|
534
|
+
cleanCodeMode: extras.cleanCodeMode,
|
|
457
535
|
issues,
|
|
458
536
|
};
|
|
459
537
|
}
|
|
@@ -467,7 +545,7 @@ export async function fetchDuplicationMeasures(
|
|
|
467
545
|
signal?: AbortSignal,
|
|
468
546
|
): Promise<SonarDuplicationMeasures | undefined> {
|
|
469
547
|
try {
|
|
470
|
-
const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(projectKey)}&metricKeys=duplicated_lines_density,duplicated_lines,duplicated_blocks,duplicated_files`;
|
|
548
|
+
const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(projectKey)}&metricKeys=coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_lines,duplicated_blocks,duplicated_files`;
|
|
471
549
|
const result = await fetchJson<{
|
|
472
550
|
component: { measures: Array<{ metric: string; value: string }> };
|
|
473
551
|
}>(url, token, signal);
|
|
@@ -476,11 +554,15 @@ export async function fetchDuplicationMeasures(
|
|
|
476
554
|
const m = measures.find((m) => m.metric === key);
|
|
477
555
|
return m ? Number.parseFloat(m.value) : 0;
|
|
478
556
|
};
|
|
557
|
+
const hasMetric = (key: string): boolean => measures.some((m) => m.metric === key);
|
|
479
558
|
return {
|
|
480
559
|
duplicatedLinesDensity: getValue("duplicated_lines_density"),
|
|
481
560
|
duplicatedLines: getValue("duplicated_lines"),
|
|
482
561
|
duplicatedBlocks: getValue("duplicated_blocks"),
|
|
483
562
|
duplicatedFiles: getValue("duplicated_files"),
|
|
563
|
+
coverage: hasMetric("coverage") ? getValue("coverage") : undefined,
|
|
564
|
+
linesToCover: hasMetric("lines_to_cover") ? getValue("lines_to_cover") : undefined,
|
|
565
|
+
uncoveredLines: hasMetric("uncovered_lines") ? getValue("uncovered_lines") : undefined,
|
|
484
566
|
};
|
|
485
567
|
} catch {
|
|
486
568
|
return undefined;
|
|
@@ -492,24 +574,61 @@ export async function fetchIssueSeverityCounts(
|
|
|
492
574
|
token: string | undefined,
|
|
493
575
|
projectKey: string,
|
|
494
576
|
signal?: AbortSignal,
|
|
577
|
+
mode?: "STANDARD" | "MQR",
|
|
495
578
|
): Promise<IssueSeverityCounts | undefined> {
|
|
496
579
|
try {
|
|
497
|
-
const
|
|
580
|
+
const facetKey = mode === "MQR" ? "impactSeverities" : "severities";
|
|
581
|
+
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=${facetKey}`;
|
|
498
582
|
const result = await fetchJson<{
|
|
499
583
|
facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
|
|
500
584
|
}>(url, token, signal);
|
|
501
|
-
const facet = result.facets?.find((f) => f.property ===
|
|
585
|
+
const facet = result.facets?.find((f) => f.property === facetKey);
|
|
502
586
|
if (!facet) return undefined;
|
|
503
587
|
const getCount = (severity: string): number => {
|
|
504
588
|
const entry = facet.values.find((v) => v.val === severity);
|
|
505
589
|
return entry?.count ?? 0;
|
|
506
590
|
};
|
|
591
|
+
return mode === "MQR"
|
|
592
|
+
? {
|
|
593
|
+
blocker: getCount("BLOCKER"),
|
|
594
|
+
critical: getCount("HIGH"),
|
|
595
|
+
major: getCount("MEDIUM"),
|
|
596
|
+
minor: getCount("LOW"),
|
|
597
|
+
info: getCount("INFO"),
|
|
598
|
+
}
|
|
599
|
+
: {
|
|
600
|
+
blocker: getCount("BLOCKER"),
|
|
601
|
+
critical: getCount("CRITICAL"),
|
|
602
|
+
major: getCount("MAJOR"),
|
|
603
|
+
minor: getCount("MINOR"),
|
|
604
|
+
info: getCount("INFO"),
|
|
605
|
+
};
|
|
606
|
+
} catch {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function fetchIssueQualityCounts(
|
|
612
|
+
serverUrl: string,
|
|
613
|
+
token: string | undefined,
|
|
614
|
+
projectKey: string,
|
|
615
|
+
signal?: AbortSignal,
|
|
616
|
+
): Promise<IssueQualityCounts | undefined> {
|
|
617
|
+
try {
|
|
618
|
+
const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=impactSoftwareQualities`;
|
|
619
|
+
const result = await fetchJson<{
|
|
620
|
+
facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
|
|
621
|
+
}>(url, token, signal);
|
|
622
|
+
const facet = result.facets?.find((f) => f.property === "impactSoftwareQualities");
|
|
623
|
+
if (!facet) return undefined;
|
|
624
|
+
const getCount = (q: string): number => {
|
|
625
|
+
const entry = facet.values.find((v) => v.val === q);
|
|
626
|
+
return entry?.count ?? 0;
|
|
627
|
+
};
|
|
507
628
|
return {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
minor: getCount("MINOR"),
|
|
512
|
-
info: getCount("INFO"),
|
|
629
|
+
maintainability: getCount("MAINTAINABILITY"),
|
|
630
|
+
reliability: getCount("RELIABILITY"),
|
|
631
|
+
security: getCount("SECURITY"),
|
|
513
632
|
};
|
|
514
633
|
} catch {
|
|
515
634
|
return undefined;
|
|
@@ -524,42 +643,35 @@ export async function fetchFileDuplications(
|
|
|
524
643
|
projectKey: string,
|
|
525
644
|
signal?: AbortSignal,
|
|
526
645
|
): Promise<FileDuplication[]> {
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const results
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
} catch {
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
}),
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
return results.filter((f): f is FileDuplication => f !== null && f.duplicatedBlocks > 0);
|
|
646
|
+
const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_blocks&ps=500`;
|
|
647
|
+
const treeResult = await fetchJson<{
|
|
648
|
+
components: Array<{
|
|
649
|
+
key: string;
|
|
650
|
+
path?: string;
|
|
651
|
+
name?: string;
|
|
652
|
+
measures?: Array<{ metric: string; value: string }>;
|
|
653
|
+
}>;
|
|
654
|
+
}>(treeUrl, token, signal);
|
|
655
|
+
|
|
656
|
+
const results = (treeResult.components ?? [])
|
|
657
|
+
.map((component) => {
|
|
658
|
+
const measures = component.measures ?? [];
|
|
659
|
+
const getValue = (metric: string): number => {
|
|
660
|
+
const m = measures.find((item) => item.metric === metric);
|
|
661
|
+
return m ? Number.parseInt(m.value, 10) : 0;
|
|
662
|
+
};
|
|
663
|
+
const duplicatedBlocks = getValue("duplicated_blocks");
|
|
664
|
+
if (duplicatedBlocks <= 0) return null;
|
|
665
|
+
return {
|
|
666
|
+
filePath: component.path ?? toIssuePath(component.key, projectKey) ?? component.name ?? component.key,
|
|
667
|
+
fileKey: component.key,
|
|
668
|
+
duplicatedLines: getValue("duplicated_lines"),
|
|
669
|
+
duplicatedBlocks,
|
|
670
|
+
};
|
|
671
|
+
})
|
|
672
|
+
.filter((f): f is FileDuplication => f !== null);
|
|
673
|
+
|
|
674
|
+
return results;
|
|
563
675
|
}
|
|
564
676
|
|
|
565
677
|
export async function fetchFileDuplicationBlocks(
|
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, SonarDuplicationMeasures, IssueSeverityCounts, FileDuplication, DuplicationBlockGroup } 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
|
|
|
@@ -69,23 +69,34 @@ export function splitSonarArgumentContext(
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function createFilterCompletionList(): AutocompleteItem[] {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
]),
|
|
78
81
|
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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)];
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
export function sonarArgumentCompletions(
|
|
87
97
|
argumentText: string,
|
|
88
98
|
issues?: SonarIssue[],
|
|
99
|
+
mode?: "STANDARD" | "MQR",
|
|
89
100
|
): AutocompleteItem[] | null {
|
|
90
101
|
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
91
102
|
const lowerCommand = command.toLowerCase();
|
|
@@ -102,13 +113,13 @@ export function sonarArgumentCompletions(
|
|
|
102
113
|
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
103
114
|
return { value: String(index + 1), label: String(index + 1), description: `${issue.severity} ${issue.filePath}${lineSuffix}` };
|
|
104
115
|
});
|
|
105
|
-
const suggestions = [...issueItems, ...createFilterCompletionList()];
|
|
116
|
+
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
106
117
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
107
118
|
return filtered.length > 0 ? filtered : null;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
111
|
-
const suggestions = createFilterCompletionList();
|
|
122
|
+
const suggestions = createFilterCompletionList(mode);
|
|
112
123
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
113
124
|
return filtered.length > 0 ? filtered : null;
|
|
114
125
|
}
|
|
@@ -182,10 +193,16 @@ export function helpText(): string {
|
|
|
182
193
|
"Filters:",
|
|
183
194
|
" /sonarqube issues be CRITICAL",
|
|
184
195
|
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
196
|
+
" /sonarqube issues be quality:RELIABILITY",
|
|
197
|
+
" /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
|
|
185
198
|
"",
|
|
186
199
|
"Autocomplete:",
|
|
187
200
|
" type /sonarqube and press Tab to complete the subcommand or filters",
|
|
188
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
|
+
"",
|
|
189
206
|
"Defaults:",
|
|
190
207
|
" config is stored in .pi/sonarqube.json",
|
|
191
208
|
" use project paths directly (no alias needed)",
|
|
@@ -213,10 +230,22 @@ export function formatMetricsOutput(metrics: {
|
|
|
213
230
|
projectKey: string;
|
|
214
231
|
measures?: SonarDuplicationMeasures;
|
|
215
232
|
issueCounts?: IssueSeverityCounts;
|
|
233
|
+
issueQualityCounts?: IssueQualityCounts;
|
|
234
|
+
cleanCodeMode?: "STANDARD" | "MQR";
|
|
216
235
|
}): string {
|
|
217
236
|
const lines: string[] = [`Metrics for ${metrics.projectKey}`];
|
|
218
237
|
|
|
219
238
|
if (metrics.measures) {
|
|
239
|
+
if (metrics.measures.coverage === undefined) {
|
|
240
|
+
lines.push("Coverage: n/a");
|
|
241
|
+
} else {
|
|
242
|
+
const pct = metrics.measures.coverage.toFixed(1);
|
|
243
|
+
const covered = (metrics.measures.linesToCover ?? 0) - (metrics.measures.uncoveredLines ?? 0);
|
|
244
|
+
const uncovered = metrics.measures.uncoveredLines ?? 0;
|
|
245
|
+
const total = metrics.measures.linesToCover ?? 0;
|
|
246
|
+
lines.push(`Coverage: ${pct}% covered=${covered} uncovered=${uncovered} lines=${total}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
220
249
|
const density = metrics.measures.duplicatedLinesDensity.toFixed(1);
|
|
221
250
|
lines.push(
|
|
222
251
|
metrics.measures.duplicatedBlocks > 0
|
|
@@ -226,14 +255,31 @@ export function formatMetricsOutput(metrics: {
|
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
if (metrics.issueCounts) {
|
|
258
|
+
const counts = metrics.cleanCodeMode === "MQR"
|
|
259
|
+
? [
|
|
260
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
261
|
+
`HIGH ${metrics.issueCounts.critical}`,
|
|
262
|
+
`MEDIUM ${metrics.issueCounts.major}`,
|
|
263
|
+
`LOW ${metrics.issueCounts.minor}`,
|
|
264
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
265
|
+
]
|
|
266
|
+
: [
|
|
267
|
+
`BLOCKER ${metrics.issueCounts.blocker}`,
|
|
268
|
+
`CRITICAL ${metrics.issueCounts.critical}`,
|
|
269
|
+
`MAJOR ${metrics.issueCounts.major}`,
|
|
270
|
+
`MINOR ${metrics.issueCounts.minor}`,
|
|
271
|
+
`INFO ${metrics.issueCounts.info}`,
|
|
272
|
+
];
|
|
273
|
+
lines.push(`Issues: ${counts.join(" ")}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (metrics.issueQualityCounts) {
|
|
229
277
|
const counts = [
|
|
230
|
-
`
|
|
231
|
-
`
|
|
232
|
-
`
|
|
233
|
-
`MINOR ${metrics.issueCounts.minor}`,
|
|
234
|
-
`INFO ${metrics.issueCounts.info}`,
|
|
278
|
+
`MAINTAINABILITY ${metrics.issueQualityCounts.maintainability}`,
|
|
279
|
+
`RELIABILITY ${metrics.issueQualityCounts.reliability}`,
|
|
280
|
+
`SECURITY ${metrics.issueQualityCounts.security}`,
|
|
235
281
|
];
|
|
236
|
-
lines.push(`
|
|
282
|
+
lines.push(`Quality: ${counts.join(" ")}`);
|
|
237
283
|
}
|
|
238
284
|
|
|
239
285
|
return lines.join("\n");
|