@jonathan0823/pi-sonarqube 0.2.0 → 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 +22 -2
- package/dist/api.d.ts +6 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +128 -45
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +3 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +33 -16
- 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 +24 -20
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +10 -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.map +1 -1
- package/dist/ui.js +3 -2
- package/dist/ui.js.map +1 -1
- package/package.json +5 -1
- package/src/api.ts +146 -51
- package/src/commands.ts +42 -15
- package/src/index.ts +32 -22
- package/src/types.ts +11 -0
- package/src/ui.ts +3 -2
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
|
|
|
@@ -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" | "measures">> = {},
|
|
517
|
+
extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures" | "cleanCodeMode">> = {},
|
|
444
518
|
): SonarAnalysisState {
|
|
445
519
|
return {
|
|
446
520
|
version: 1,
|
|
@@ -454,6 +528,7 @@ export function createAnalysisState(
|
|
|
454
528
|
filters: extras.filters,
|
|
455
529
|
totalIssues: issues.length,
|
|
456
530
|
measures: extras.measures,
|
|
531
|
+
cleanCodeMode: extras.cleanCodeMode,
|
|
457
532
|
issues,
|
|
458
533
|
};
|
|
459
534
|
}
|
|
@@ -516,6 +591,33 @@ export async function fetchIssueSeverityCounts(
|
|
|
516
591
|
}
|
|
517
592
|
}
|
|
518
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
|
+
|
|
519
621
|
// ── File duplications ─────────────────────────────────────────────────────────
|
|
520
622
|
|
|
521
623
|
export async function fetchFileDuplications(
|
|
@@ -524,42 +626,35 @@ export async function fetchFileDuplications(
|
|
|
524
626
|
projectKey: string,
|
|
525
627
|
signal?: AbortSignal,
|
|
526
628
|
): 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);
|
|
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;
|
|
563
658
|
}
|
|
564
659
|
|
|
565
660
|
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,6 +230,7 @@ export function formatMetricsOutput(metrics: {
|
|
|
213
230
|
projectKey: string;
|
|
214
231
|
measures?: SonarDuplicationMeasures;
|
|
215
232
|
issueCounts?: IssueSeverityCounts;
|
|
233
|
+
issueQualityCounts?: IssueQualityCounts;
|
|
216
234
|
}): string {
|
|
217
235
|
const lines: string[] = [`Metrics for ${metrics.projectKey}`];
|
|
218
236
|
|
|
@@ -236,6 +254,15 @@ export function formatMetricsOutput(metrics: {
|
|
|
236
254
|
lines.push(`Issues: ${counts.join(" ")}`);
|
|
237
255
|
}
|
|
238
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
|
+
|
|
239
266
|
return lines.join("\n");
|
|
240
267
|
}
|
|
241
268
|
|
package/src/index.ts
CHANGED
|
@@ -33,10 +33,13 @@ import {
|
|
|
33
33
|
fetchIssues,
|
|
34
34
|
fetchDuplicationMeasures,
|
|
35
35
|
fetchIssueSeverityCounts,
|
|
36
|
+
fetchIssueQualityCounts,
|
|
36
37
|
fetchFileDuplications,
|
|
37
38
|
fetchFileDuplicationBlocks,
|
|
38
39
|
createAnalysisState,
|
|
39
40
|
issueFilterLabel,
|
|
41
|
+
fetchCleanCodeMode,
|
|
42
|
+
assertFiltersNotAmbiguous,
|
|
40
43
|
} from "./api.js";
|
|
41
44
|
import {
|
|
42
45
|
STATE_TYPE,
|
|
@@ -76,6 +79,12 @@ const SonarToolParams = Type.Object({
|
|
|
76
79
|
),
|
|
77
80
|
types: Type.Optional(Type.Array(Type.String({ description: "Issue type to fetch (e.g. BUG)" }))),
|
|
78
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
|
+
),
|
|
79
88
|
});
|
|
80
89
|
|
|
81
90
|
// ── State restoration ───────────────────────────────────────────────────────
|
|
@@ -144,12 +153,16 @@ async function analyzeProject(
|
|
|
144
153
|
ctx.signal,
|
|
145
154
|
);
|
|
146
155
|
|
|
156
|
+
analysisUi.setPhase("Detecting mode...");
|
|
157
|
+
const cleanCodeMode = await fetchCleanCodeMode(config.serverUrl, config.token, ctx.signal);
|
|
158
|
+
|
|
147
159
|
const state = createAnalysisState(config, issues, {
|
|
148
160
|
dashboardUrl,
|
|
149
161
|
ceTaskUrl,
|
|
150
162
|
analysisId,
|
|
151
163
|
filters: normalizedFilters,
|
|
152
164
|
measures,
|
|
165
|
+
cleanCodeMode,
|
|
153
166
|
});
|
|
154
167
|
|
|
155
168
|
pi.appendEntry(STATE_TYPE, state);
|
|
@@ -324,7 +337,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
324
337
|
|
|
325
338
|
const argumentText = match[1] ?? "";
|
|
326
339
|
const { current: currentToken } = splitSonarArgumentContext(argumentText);
|
|
327
|
-
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues) ?? [];
|
|
340
|
+
const extraItems = sonarArgumentCompletions(argumentText, latestState?.issues, latestState?.cleanCodeMode) ?? [];
|
|
328
341
|
const currentSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
329
342
|
const merged = mergeAutocompleteSuggestions(currentToken, currentSuggestions, extraItems);
|
|
330
343
|
return merged ?? currentSuggestions;
|
|
@@ -352,7 +365,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
352
365
|
promptSnippet: "Run local SonarQube analysis or inspect issue results",
|
|
353
366
|
promptGuidelines: [
|
|
354
367
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
355
|
-
"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.",
|
|
356
369
|
],
|
|
357
370
|
parameters: SonarToolParams,
|
|
358
371
|
|
|
@@ -363,7 +376,10 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
363
376
|
statuses: params.statuses,
|
|
364
377
|
types: params.types,
|
|
365
378
|
rules: params.rules,
|
|
379
|
+
softwareQualities: params.softwareQualities,
|
|
380
|
+
impactSeverities: params.impactSeverities,
|
|
366
381
|
});
|
|
382
|
+
assertFiltersNotAmbiguous(filters);
|
|
367
383
|
|
|
368
384
|
if (params.action === "analyze") {
|
|
369
385
|
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
@@ -414,6 +430,8 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
414
430
|
statuses: args.statuses,
|
|
415
431
|
types: args.types,
|
|
416
432
|
rules: args.rules,
|
|
433
|
+
softwareQualities: args.softwareQualities,
|
|
434
|
+
impactSeverities: args.impactSeverities,
|
|
417
435
|
});
|
|
418
436
|
const filterText = filters ? ` ${theme.fg("muted", issueFilterLabel(filters))}` : "";
|
|
419
437
|
return new Text(
|
|
@@ -457,7 +475,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
457
475
|
|
|
458
476
|
pi.registerCommand("sonarqube", {
|
|
459
477
|
description: "Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
460
|
-
getArgumentCompletions: (argumentPrefix) => sonarArgumentCompletions(argumentPrefix, latestState?.issues),
|
|
478
|
+
getArgumentCompletions: (argumentPrefix) => sonarArgumentCompletions(argumentPrefix, latestState?.issues, latestState?.cleanCodeMode),
|
|
461
479
|
handler: async (args, ctx) => {
|
|
462
480
|
try {
|
|
463
481
|
const parsed = parseCommandArgs(args);
|
|
@@ -515,17 +533,18 @@ async function commandMetrics(
|
|
|
515
533
|
targetInput?: string,
|
|
516
534
|
): Promise<void> {
|
|
517
535
|
const config = await resolveConfig(ctx, targetInput);
|
|
518
|
-
const [measures, issueCounts] = await Promise.all([
|
|
536
|
+
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
519
537
|
fetchDuplicationMeasures(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
520
538
|
fetchIssueSeverityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
539
|
+
fetchIssueQualityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
521
540
|
]);
|
|
522
|
-
if (!measures && !issueCounts) {
|
|
541
|
+
if (!measures && !issueCounts && !qualityCounts) {
|
|
523
542
|
if (ctx.hasUI) {
|
|
524
543
|
ctx.ui.notify(`Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`, "warning");
|
|
525
544
|
}
|
|
526
545
|
return;
|
|
527
546
|
}
|
|
528
|
-
const text = formatMetricsOutput({ projectKey: config.projectKey, measures, issueCounts });
|
|
547
|
+
const text = formatMetricsOutput({ projectKey: config.projectKey, measures, issueCounts, issueQualityCounts: qualityCounts });
|
|
529
548
|
if (ctx.hasUI) {
|
|
530
549
|
ctx.ui.notify(text, "info");
|
|
531
550
|
}
|
|
@@ -552,18 +571,7 @@ async function commandDuplications(
|
|
|
552
571
|
throw error;
|
|
553
572
|
}
|
|
554
573
|
if (files.length === 0) {
|
|
555
|
-
|
|
556
|
-
const projectMeasures = await fetchDuplicationMeasures(config.serverUrl, config.token, config.projectKey, ctx.signal).catch(() => undefined);
|
|
557
|
-
if (projectMeasures && projectMeasures.duplicatedFiles > 0) {
|
|
558
|
-
if (ctx.hasUI) {
|
|
559
|
-
ctx.ui.notify(
|
|
560
|
-
`Cannot list individual files — your SonarQube token lacks "See Source Code" permission. Grant it in SonarQube project permissions, then run /sonarqube init to update the token.`,
|
|
561
|
-
"warning",
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
} else if (ctx.hasUI) {
|
|
565
|
-
ctx.ui.notify("No duplications found.", "info");
|
|
566
|
-
}
|
|
574
|
+
if (ctx.hasUI) ctx.ui.notify("No duplications found.", "info");
|
|
567
575
|
return;
|
|
568
576
|
}
|
|
569
577
|
|
|
@@ -707,18 +715,19 @@ async function toolMetrics(ctx: any, path: string | undefined): Promise<{ conten
|
|
|
707
715
|
details: { error: "Project not configured." },
|
|
708
716
|
};
|
|
709
717
|
}
|
|
710
|
-
const [measures, issueCounts] = await Promise.all([
|
|
718
|
+
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
711
719
|
fetchDuplicationMeasures(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
712
720
|
fetchIssueSeverityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
721
|
+
fetchIssueQualityCounts(config.serverUrl, config.token, config.projectKey, ctx.signal),
|
|
713
722
|
]);
|
|
714
|
-
if (!measures && !issueCounts) {
|
|
723
|
+
if (!measures && !issueCounts && !qualityCounts) {
|
|
715
724
|
return {
|
|
716
725
|
content: [{ type: "text", text: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.` }],
|
|
717
726
|
details: { error: `Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.` },
|
|
718
727
|
};
|
|
719
728
|
}
|
|
720
|
-
const text = formatMetricsOutput({ projectKey: config.projectKey, measures, issueCounts });
|
|
721
|
-
return { content: [{ type: "text", text }], details: { projectKey: config.projectKey, measures, issueCounts } };
|
|
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 } };
|
|
722
731
|
}
|
|
723
732
|
|
|
724
733
|
// ── Re-exports for public API ───────────────────────────────────────────────
|
|
@@ -730,6 +739,7 @@ export type {
|
|
|
730
739
|
SonarProjectConfig,
|
|
731
740
|
SonarIssueFetchOptions,
|
|
732
741
|
SonarInitConfig,
|
|
742
|
+
IssueQualityCounts,
|
|
733
743
|
} from "./types.js";
|
|
734
744
|
|
|
735
745
|
export { projectConfigPath, loadProjectConfig, saveProjectConfig } from "./config.js";
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface IssueSeverityCounts {
|
|
|
28
28
|
info: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export interface IssueQualityCounts {
|
|
32
|
+
maintainability: number;
|
|
33
|
+
reliability: number;
|
|
34
|
+
security: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
export interface FileDuplication {
|
|
32
38
|
filePath: string;
|
|
33
39
|
fileKey: string;
|
|
@@ -58,6 +64,7 @@ export interface SonarAnalysisState {
|
|
|
58
64
|
measures?: SonarDuplicationMeasures;
|
|
59
65
|
totalIssues: number;
|
|
60
66
|
issues: SonarIssue[];
|
|
67
|
+
cleanCodeMode?: "STANDARD" | "MQR";
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export interface SonarProjectConfig {
|
|
@@ -73,6 +80,8 @@ export interface SonarIssueFetchOptions {
|
|
|
73
80
|
statuses?: string[];
|
|
74
81
|
types?: string[];
|
|
75
82
|
rules?: string[];
|
|
83
|
+
softwareQualities?: string[];
|
|
84
|
+
impactSeverities?: string[];
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
export interface SonarInitConfig {
|
|
@@ -110,3 +119,5 @@ export interface InitCommandOptions {
|
|
|
110
119
|
export const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"] as const;
|
|
111
120
|
export const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"] as const;
|
|
112
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
|
@@ -3,7 +3,7 @@ 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
5
|
import type { SonarIssue, SonarAnalysisState, SonarIssueFetchOptions, FileDuplication } from "./types.js";
|
|
6
|
-
import { issueFilterLabel, fetchIssues, createAnalysisState, normalizeIssueFilters } from "./api.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 ───────────────────────────────────────────────────────────
|
|
@@ -300,7 +300,8 @@ export async function loadProjectIssuesFromApi(
|
|
|
300
300
|
ctx.signal,
|
|
301
301
|
normalizedFilters,
|
|
302
302
|
);
|
|
303
|
-
|
|
303
|
+
const cleanCodeMode = await fetchCleanCodeMode(config.serverUrl, config.token, ctx.signal);
|
|
304
|
+
return createAnalysisState(config, issues, { filters: normalizedFilters, cleanCodeMode });
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
export async function resolveTargetState(
|