@jonathan0823/pi-sonarqube 0.8.2 → 0.9.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 +21 -11
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +36 -5
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +3 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +25 -18
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -13
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -33
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/api.ts +39 -4
- package/src/commands.ts +34 -18
- package/src/config.ts +36 -13
- package/src/index.ts +202 -45
- package/src/types.ts +2 -0
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iFAAiF;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iFAAiF;AA8HjF,+EAA+E;AAE/E,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,SAAS;IACT,UAAU;IACV,OAAO;IACP,OAAO;IACP,MAAM;CACE,CAAC;AACX,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM;IACN,WAAW;IACX,UAAU;IACV,UAAU;IACV,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,YAAY,CAAU,CAAC;AAC3E,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,iBAAiB;IACjB,aAAa;IACb,UAAU;CACF,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,SAAS;IACT,MAAM;IACN,QAAQ;IACR,KAAK;IACL,MAAM;CACE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonathan0823/pi-sonarqube",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "SonarQube extension for Pi — run local analysis and browse issues.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"type": "git",
|
|
45
45
|
"url": "git+https://github.com/Jonathan0823/pi-sonarqube.git"
|
|
46
46
|
},
|
|
47
|
-
"
|
|
48
|
-
"@earendil-works/pi-ai": "
|
|
49
|
-
"@earendil-works/pi-coding-agent": "
|
|
50
|
-
"@earendil-works/pi-tui": "
|
|
51
|
-
"typebox": "
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "*",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
50
|
+
"@earendil-works/pi-tui": "*",
|
|
51
|
+
"typebox": "*"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"c8": "^11.0.0",
|
package/src/api.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
SONAR_IMPACT_SEVERITIES,
|
|
21
21
|
CleanCodeMode,
|
|
22
22
|
} from "./types.js";
|
|
23
|
-
import { parseProperties } from "./config.js";
|
|
23
|
+
import { parseProperties, trimTrailing } from "./config.js";
|
|
24
24
|
|
|
25
25
|
// ── Clean code mode detection ────────────────────────────────────────────────
|
|
26
26
|
|
|
@@ -115,6 +115,7 @@ export function normalizeIssueFilters(
|
|
|
115
115
|
rules: normalizeIssueList(filters?.rules),
|
|
116
116
|
softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
|
|
117
117
|
impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
|
|
118
|
+
issueKeys: normalizeIssueList(filters?.issueKeys),
|
|
118
119
|
pathScope: filters?.pathScope,
|
|
119
120
|
componentKeys: filters?.componentKeys,
|
|
120
121
|
};
|
|
@@ -124,6 +125,7 @@ export function normalizeIssueFilters(
|
|
|
124
125
|
normalized.rules ||
|
|
125
126
|
normalized.softwareQualities ||
|
|
126
127
|
normalized.impactSeverities ||
|
|
128
|
+
normalized.issueKeys ||
|
|
127
129
|
normalized.pathScope ||
|
|
128
130
|
normalized.componentKeys?.length
|
|
129
131
|
? normalized
|
|
@@ -213,15 +215,34 @@ export function parseIssueFilterToken(
|
|
|
213
215
|
return undefined;
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
function parseIssueKeyToken(
|
|
219
|
+
token: string,
|
|
220
|
+
allowIssueKeys: boolean,
|
|
221
|
+
): string[] | undefined {
|
|
222
|
+
if (!allowIssueKeys || !token.toLowerCase().startsWith("issue:")) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const keys = token
|
|
226
|
+
.slice(token.indexOf(":") + 1)
|
|
227
|
+
.split(",")
|
|
228
|
+
.map((key) => key.trim())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
if (keys.length === 0) throw new Error("issue: requires an issue key");
|
|
231
|
+
return keys;
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
export function parseSonarIssueArgs(
|
|
217
235
|
tokens: string[],
|
|
218
236
|
allowIssueIndex = false,
|
|
237
|
+
allowIssueKeys = false,
|
|
219
238
|
): {
|
|
220
239
|
targetInput?: string;
|
|
221
240
|
issueIndex?: number;
|
|
241
|
+
issueKeys?: string[];
|
|
222
242
|
filters?: SonarIssueFetchOptions;
|
|
223
243
|
} {
|
|
224
244
|
const filters: Array<Partial<SonarIssueFetchOptions> | undefined> = [];
|
|
245
|
+
const issueKeys: string[] = [];
|
|
225
246
|
let targetInput: string | undefined;
|
|
226
247
|
let issueIndex: number | undefined;
|
|
227
248
|
|
|
@@ -231,6 +252,11 @@ export function parseSonarIssueArgs(
|
|
|
231
252
|
issueIndex = Number(token);
|
|
232
253
|
continue;
|
|
233
254
|
}
|
|
255
|
+
const parsedIssueKeys = parseIssueKeyToken(token, allowIssueKeys);
|
|
256
|
+
if (parsedIssueKeys) {
|
|
257
|
+
issueKeys.push(...parsedIssueKeys);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
234
260
|
|
|
235
261
|
const parsedFilter = parseIssueFilterToken(token);
|
|
236
262
|
if (parsedFilter) {
|
|
@@ -243,7 +269,12 @@ export function parseSonarIssueArgs(
|
|
|
243
269
|
}
|
|
244
270
|
}
|
|
245
271
|
|
|
246
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
targetInput,
|
|
274
|
+
issueIndex,
|
|
275
|
+
issueKeys: normalizeIssueList(issueKeys),
|
|
276
|
+
filters: mergeFilters(...filters),
|
|
277
|
+
};
|
|
247
278
|
}
|
|
248
279
|
|
|
249
280
|
function mergeFilterField<T>(
|
|
@@ -289,6 +320,8 @@ export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
|
|
|
289
320
|
parts.push(`qualities=${filters.softwareQualities.join(",")}`);
|
|
290
321
|
if (filters.impactSeverities?.length)
|
|
291
322
|
parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
|
|
323
|
+
if (filters.issueKeys?.length)
|
|
324
|
+
parts.push(`issues=${filters.issueKeys.join(",")}`);
|
|
292
325
|
if (filters.pathScope) parts.push(`in:${filters.pathScope}`);
|
|
293
326
|
return parts.join(" • ");
|
|
294
327
|
}
|
|
@@ -302,7 +335,7 @@ export function resolvePathScope(
|
|
|
302
335
|
const absPath = resolve(baseDir, raw);
|
|
303
336
|
const relPath = relative(baseDir, absPath);
|
|
304
337
|
if (relPath === "" || relPath === ".") return projectKey;
|
|
305
|
-
const cleanRel = relPath
|
|
338
|
+
const cleanRel = trimTrailing(relPath, "/\\");
|
|
306
339
|
return `${projectKey}:${cleanRel}`;
|
|
307
340
|
}
|
|
308
341
|
|
|
@@ -508,7 +541,7 @@ function buildIssueSearchUrl(
|
|
|
508
541
|
): string {
|
|
509
542
|
const url = new URL("/api/issues/search", serverUrl);
|
|
510
543
|
url.searchParams.set("projects", projectKey);
|
|
511
|
-
url.searchParams.set("resolved", "false");
|
|
544
|
+
if (!filters?.issueKeys?.length) url.searchParams.set("resolved", "false");
|
|
512
545
|
url.searchParams.set("ps", "100");
|
|
513
546
|
url.searchParams.set("p", String(page));
|
|
514
547
|
if (filters?.severities?.length)
|
|
@@ -529,6 +562,8 @@ function buildIssueSearchUrl(
|
|
|
529
562
|
"impactSeverities",
|
|
530
563
|
filters.impactSeverities.join(","),
|
|
531
564
|
);
|
|
565
|
+
if (filters?.issueKeys?.length)
|
|
566
|
+
url.searchParams.set("issues", filters.issueKeys.join(","));
|
|
532
567
|
if (filters?.componentKeys?.length)
|
|
533
568
|
url.searchParams.set("componentKeys", filters.componentKeys.join(","));
|
|
534
569
|
return url.toString();
|
package/src/commands.ts
CHANGED
|
@@ -157,7 +157,8 @@ function createFilterCompletionList(mode?: CleanCodeMode): AutocompleteItem[] {
|
|
|
157
157
|
{
|
|
158
158
|
value: "in:",
|
|
159
159
|
label: "in:",
|
|
160
|
-
description:
|
|
160
|
+
description:
|
|
161
|
+
"file or directory inside the selected project (e.g. in:src/api.ts or in:src/)",
|
|
161
162
|
},
|
|
162
163
|
];
|
|
163
164
|
|
|
@@ -372,19 +373,10 @@ function getInPathCompletions(
|
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
function getOpenCompletions(
|
|
375
|
-
issues: SonarIssue[] | undefined,
|
|
376
376
|
mode: CleanCodeMode | undefined,
|
|
377
377
|
current: string,
|
|
378
378
|
): AutocompleteItem[] | null {
|
|
379
|
-
const
|
|
380
|
-
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
381
|
-
return {
|
|
382
|
-
value: String(index + 1),
|
|
383
|
-
label: String(index + 1),
|
|
384
|
-
description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
385
|
-
};
|
|
386
|
-
});
|
|
387
|
-
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
379
|
+
const suggestions = createFilterCompletionList(mode);
|
|
388
380
|
const filtered = filterAutocompleteItems(suggestions, current);
|
|
389
381
|
return filtered.length > 0 ? filtered : null;
|
|
390
382
|
}
|
|
@@ -494,7 +486,7 @@ export async function sonarArgumentCompletions(
|
|
|
494
486
|
}
|
|
495
487
|
|
|
496
488
|
if (lowerCommand === "open") {
|
|
497
|
-
return getOpenCompletions(
|
|
489
|
+
return getOpenCompletions(mode, current);
|
|
498
490
|
}
|
|
499
491
|
|
|
500
492
|
if (lowerCommand === "analyze") {
|
|
@@ -547,6 +539,7 @@ export type ParsedSonarCommand =
|
|
|
547
539
|
action: "open";
|
|
548
540
|
targetInput?: string;
|
|
549
541
|
issueIndex?: number;
|
|
542
|
+
issueKeys?: string[];
|
|
550
543
|
filters?: SonarIssueFetchOptions;
|
|
551
544
|
}
|
|
552
545
|
| { action: "metrics"; targetInput?: string }
|
|
@@ -575,7 +568,10 @@ export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
|
575
568
|
return { action: "issues", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
576
569
|
}
|
|
577
570
|
if (head === "open") {
|
|
578
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
action: "open",
|
|
573
|
+
...parseSonarIssueArgs(tokens.slice(1), true, true),
|
|
574
|
+
};
|
|
579
575
|
}
|
|
580
576
|
if (head === "analyze" || head === "run") {
|
|
581
577
|
return { action: "analyze", ...parseSonarIssueArgs(tokens.slice(1)) };
|
|
@@ -600,9 +596,10 @@ export function helpText(): string {
|
|
|
600
596
|
"SonarQube commands:",
|
|
601
597
|
"",
|
|
602
598
|
" /sonarqube init [alias] [path] configure a project target",
|
|
603
|
-
" /sonarqube analyze [target] run analysis for a target
|
|
604
|
-
" /sonarqube issues [target] browse issues for a target
|
|
605
|
-
" /sonarqube open [target] <n> preview issue #n
|
|
599
|
+
" /sonarqube analyze [target] run analysis for a project target",
|
|
600
|
+
" /sonarqube issues [target] browse issues for a project target",
|
|
601
|
+
" /sonarqube open [target] <n> preview positional issue #n",
|
|
602
|
+
" /sonarqube open [target] issue:<key>[,<key>] preview stable issue keys",
|
|
606
603
|
" /sonarqube metrics [target] show project metrics (no scanner)",
|
|
607
604
|
" /sonarqube duplications [target] browse duplicated files and blocks",
|
|
608
605
|
" /sonarqube show this help",
|
|
@@ -620,6 +617,8 @@ export function helpText(): string {
|
|
|
620
617
|
" type /sonarqube and press Tab to complete the subcommand or filters",
|
|
621
618
|
"",
|
|
622
619
|
"Notes:",
|
|
620
|
+
" [target] is a configured alias or SonarQube project root.",
|
|
621
|
+
" Use in:<path> for a file or directory relative to that project root.",
|
|
623
622
|
" Legacy filters (severity, type) and MQR filters (quality, impactSeverity)",
|
|
624
623
|
" cannot be combined in the same command. Use one filter family per query.",
|
|
625
624
|
"",
|
|
@@ -638,16 +637,33 @@ export function sonarErrorMessage(error: unknown): string {
|
|
|
638
637
|
return error instanceof Error ? error.message : String(error);
|
|
639
638
|
}
|
|
640
639
|
|
|
640
|
+
export function assertIssueSelection(
|
|
641
|
+
issueIndex?: number,
|
|
642
|
+
issueKeys?: string[],
|
|
643
|
+
): void {
|
|
644
|
+
if (issueIndex !== undefined && issueKeys?.length) {
|
|
645
|
+
throw new Error("Use either issueIndex or issueKeys, not both.");
|
|
646
|
+
}
|
|
647
|
+
if (issueKeys && issueKeys.length > 10) {
|
|
648
|
+
throw new Error("At most 10 issue keys can be opened at once.");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
641
652
|
// ── Issue formatting ────────────────────────────────────────────────────────
|
|
642
653
|
|
|
643
|
-
export function formatIssue(
|
|
654
|
+
export function formatIssue(
|
|
655
|
+
issue: SonarIssue,
|
|
656
|
+
index?: number,
|
|
657
|
+
includeKey = true,
|
|
658
|
+
): string {
|
|
644
659
|
const loc = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
645
660
|
const prefix =
|
|
646
661
|
typeof index === "number" ? `${String(index).padStart(2, "0")}. ` : "";
|
|
647
662
|
const rule = issue.ruleName
|
|
648
663
|
? `${issue.rule} (${issue.ruleName})`
|
|
649
664
|
: issue.rule;
|
|
650
|
-
|
|
665
|
+
const key = includeKey ? `[${issue.key}] ` : "";
|
|
666
|
+
return `${prefix}${key}${issue.severity} ${loc} — ${rule} — ${issue.message}`;
|
|
651
667
|
}
|
|
652
668
|
|
|
653
669
|
export function formatMetricsOutput(metrics: {
|
package/src/config.ts
CHANGED
|
@@ -11,13 +11,28 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
// ── Generic helpers ─────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
+
/** Remove trailing characters in `chars` from `value`. */
|
|
15
|
+
export function trimTrailing(value: string, chars: string): string {
|
|
16
|
+
let end = value.length;
|
|
17
|
+
while (end > 0 && chars.includes(value[end - 1])) end--;
|
|
18
|
+
return value.slice(0, end);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Remove leading and trailing characters in `chars` from `value`. */
|
|
22
|
+
export function trimChars(value: string, chars: string): string {
|
|
23
|
+
let start = 0;
|
|
24
|
+
let end = value.length;
|
|
25
|
+
while (start < end && chars.includes(value[start])) start++;
|
|
26
|
+
while (end > start && chars.includes(value[end - 1])) end--;
|
|
27
|
+
return value.slice(start, end);
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export function slugify(input: string): string {
|
|
15
31
|
return (
|
|
16
|
-
input
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.slice(0, 80) || "sonarqube"
|
|
32
|
+
trimChars(input.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-").slice(
|
|
33
|
+
0,
|
|
34
|
+
80,
|
|
35
|
+
) || "sonarqube"
|
|
21
36
|
);
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -43,7 +58,7 @@ export function parseProperties(text: string): Record<string, string> {
|
|
|
43
58
|
export function normalizeServerUrl(url: string | undefined): string {
|
|
44
59
|
const fallback = "http://localhost:9000";
|
|
45
60
|
if (!url?.trim()) return fallback;
|
|
46
|
-
return url.trim()
|
|
61
|
+
return trimTrailing(url.trim(), "/");
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
export function resolveProjectKey(
|
|
@@ -131,7 +146,17 @@ export function mergeCommaSeparatedValues(
|
|
|
131
146
|
return [...merged].join(",");
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
|
|
149
|
+
function matchSonarProperty(
|
|
150
|
+
line: string,
|
|
151
|
+
): { indent: string; key: string; value: string } | undefined {
|
|
152
|
+
const trimmedStart = line.trimStart();
|
|
153
|
+
const eq = trimmedStart.indexOf("=");
|
|
154
|
+
if (eq < 0) return undefined;
|
|
155
|
+
const key = trimmedStart.slice(0, eq).trimEnd();
|
|
156
|
+
if (key !== "sonar.sources" && key !== "sonar.exclusions") return undefined;
|
|
157
|
+
const indent = line.slice(0, line.length - trimmedStart.length);
|
|
158
|
+
return { indent, key, value: trimmedStart.slice(eq + 1).trim() };
|
|
159
|
+
}
|
|
135
160
|
|
|
136
161
|
export async function ensureDefaultSonarProjectProperties(
|
|
137
162
|
baseDir: string,
|
|
@@ -149,12 +174,10 @@ export async function ensureDefaultSonarProjectProperties(
|
|
|
149
174
|
let changed = false;
|
|
150
175
|
|
|
151
176
|
const updatedLines = lines.map((line) => {
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
177
|
+
const property = matchSonarProperty(line);
|
|
178
|
+
if (!property) return line;
|
|
154
179
|
|
|
155
|
-
const indent
|
|
156
|
-
const key = match[2];
|
|
157
|
-
const value = match[3] ?? "";
|
|
180
|
+
const { indent, key, value } = property;
|
|
158
181
|
|
|
159
182
|
if (key === "sonar.sources") {
|
|
160
183
|
hasSources = true;
|
|
@@ -188,7 +211,7 @@ export async function ensureDefaultSonarProjectProperties(
|
|
|
188
211
|
return "unchanged";
|
|
189
212
|
}
|
|
190
213
|
|
|
191
|
-
const normalized = updatedLines.join("\n")
|
|
214
|
+
const normalized = trimTrailing(updatedLines.join("\n"), "\n") + "\n";
|
|
192
215
|
await writeFile(path, normalized, "utf8");
|
|
193
216
|
return existing.includes("sonar.sources") ||
|
|
194
217
|
existing.includes("sonar.exclusions")
|