@jonathan0823/pi-sonarqube 0.5.0 → 0.6.0
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 +2 -2
- package/dist/api.d.ts +12 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +80 -25
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +2 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +226 -18
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -35
- 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/dist/ui.d.ts +29 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +130 -70
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +127 -30
- package/src/commands.ts +306 -20
- package/src/index.ts +87 -56
- package/src/types.ts +2 -0
- package/src/ui.ts +175 -84
package/src/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
4
|
import type {
|
|
5
5
|
SonarIssue,
|
|
6
6
|
SonarAnalysisState,
|
|
@@ -114,13 +114,17 @@ export function normalizeIssueFilters(
|
|
|
114
114
|
rules: normalizeIssueList(filters?.rules),
|
|
115
115
|
softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
|
|
116
116
|
impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
|
|
117
|
+
pathScope: filters?.pathScope,
|
|
118
|
+
componentKeys: filters?.componentKeys,
|
|
117
119
|
};
|
|
118
120
|
return normalized.severities ||
|
|
119
121
|
normalized.statuses ||
|
|
120
122
|
normalized.types ||
|
|
121
123
|
normalized.rules ||
|
|
122
124
|
normalized.softwareQualities ||
|
|
123
|
-
normalized.impactSeverities
|
|
125
|
+
normalized.impactSeverities ||
|
|
126
|
+
normalized.pathScope ||
|
|
127
|
+
normalized.componentKeys?.length
|
|
124
128
|
? normalized
|
|
125
129
|
: undefined;
|
|
126
130
|
}
|
|
@@ -178,6 +182,9 @@ export function parseIssueFilterToken(
|
|
|
178
182
|
case "impactseverity":
|
|
179
183
|
case "impactseverities":
|
|
180
184
|
return { impactSeverities: values.map((v) => v.toUpperCase()) };
|
|
185
|
+
case "in":
|
|
186
|
+
case "path":
|
|
187
|
+
return { pathScope: values[0] };
|
|
181
188
|
default:
|
|
182
189
|
return undefined;
|
|
183
190
|
}
|
|
@@ -238,30 +245,26 @@ export function parseSonarIssueArgs(
|
|
|
238
245
|
return { targetInput, issueIndex, filters: mergeFilters(...filters) };
|
|
239
246
|
}
|
|
240
247
|
|
|
248
|
+
function mergeFilterField<T>(
|
|
249
|
+
merged: T[] | undefined,
|
|
250
|
+
incoming?: T[],
|
|
251
|
+
): T[] | undefined {
|
|
252
|
+
return incoming?.length ? [...(merged ?? []), ...incoming] : merged;
|
|
253
|
+
}
|
|
254
|
+
|
|
241
255
|
function mergeFilters(
|
|
242
256
|
...filters: Array<Partial<SonarIssueFetchOptions> | undefined>
|
|
243
257
|
): SonarIssueFetchOptions | undefined {
|
|
244
258
|
const merged: SonarIssueFetchOptions = {};
|
|
245
259
|
for (const filter of filters) {
|
|
246
260
|
if (!filter) continue;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (filter.
|
|
254
|
-
merged.rules = [...(merged.rules ?? []), ...filter.rules];
|
|
255
|
-
if (filter.softwareQualities?.length)
|
|
256
|
-
merged.softwareQualities = [
|
|
257
|
-
...(merged.softwareQualities ?? []),
|
|
258
|
-
...filter.softwareQualities,
|
|
259
|
-
];
|
|
260
|
-
if (filter.impactSeverities?.length)
|
|
261
|
-
merged.impactSeverities = [
|
|
262
|
-
...(merged.impactSeverities ?? []),
|
|
263
|
-
...filter.impactSeverities,
|
|
264
|
-
];
|
|
261
|
+
merged.severities = mergeFilterField(merged.severities, filter.severities);
|
|
262
|
+
merged.statuses = mergeFilterField(merged.statuses, filter.statuses);
|
|
263
|
+
merged.types = mergeFilterField(merged.types, filter.types);
|
|
264
|
+
merged.rules = mergeFilterField(merged.rules, filter.rules);
|
|
265
|
+
merged.softwareQualities = mergeFilterField(merged.softwareQualities, filter.softwareQualities);
|
|
266
|
+
merged.impactSeverities = mergeFilterField(merged.impactSeverities, filter.impactSeverities);
|
|
267
|
+
if (filter.pathScope) merged.pathScope = filter.pathScope;
|
|
265
268
|
}
|
|
266
269
|
return normalizeIssueFilters(merged);
|
|
267
270
|
}
|
|
@@ -279,9 +282,23 @@ export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
|
|
|
279
282
|
parts.push(`qualities=${filters.softwareQualities.join(",")}`);
|
|
280
283
|
if (filters.impactSeverities?.length)
|
|
281
284
|
parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
|
|
285
|
+
if (filters.pathScope) parts.push(`in:${filters.pathScope}`);
|
|
282
286
|
return parts.join(" • ");
|
|
283
287
|
}
|
|
284
288
|
|
|
289
|
+
export function resolvePathScope(
|
|
290
|
+
baseDir: string,
|
|
291
|
+
projectKey: string,
|
|
292
|
+
pathScope: string,
|
|
293
|
+
): string {
|
|
294
|
+
const raw = pathScope.replace(/^@/, "");
|
|
295
|
+
const absPath = resolve(baseDir, raw);
|
|
296
|
+
const relPath = relative(baseDir, absPath);
|
|
297
|
+
if (relPath === "" || relPath === ".") return projectKey;
|
|
298
|
+
const cleanRel = relPath.replace(/[\\/]+$/, "");
|
|
299
|
+
return `${projectKey}:${cleanRel}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
285
302
|
function severitySortRank(severity: string): number {
|
|
286
303
|
switch (severity.toUpperCase()) {
|
|
287
304
|
case "BLOCKER":
|
|
@@ -505,6 +522,11 @@ function buildIssueSearchUrl(
|
|
|
505
522
|
"impactSeverities",
|
|
506
523
|
filters.impactSeverities.join(","),
|
|
507
524
|
);
|
|
525
|
+
if (filters?.componentKeys?.length)
|
|
526
|
+
url.searchParams.set(
|
|
527
|
+
"componentKeys",
|
|
528
|
+
filters.componentKeys.join(","),
|
|
529
|
+
);
|
|
508
530
|
return url.toString();
|
|
509
531
|
}
|
|
510
532
|
|
|
@@ -574,9 +596,19 @@ export async function fetchIssues(
|
|
|
574
596
|
projectKey: string,
|
|
575
597
|
signal?: AbortSignal,
|
|
576
598
|
filters?: SonarIssueFetchOptions,
|
|
599
|
+
baseDir?: string,
|
|
577
600
|
): Promise<SonarIssue[]> {
|
|
578
601
|
assertFiltersNotAmbiguous(filters);
|
|
579
602
|
const normalizedFilters = normalizeIssueFilters(filters);
|
|
603
|
+
|
|
604
|
+
if (normalizedFilters?.pathScope && baseDir) {
|
|
605
|
+
const componentKey = resolvePathScope(
|
|
606
|
+
baseDir,
|
|
607
|
+
projectKey,
|
|
608
|
+
normalizedFilters.pathScope,
|
|
609
|
+
);
|
|
610
|
+
normalizedFilters.componentKeys = [componentKey];
|
|
611
|
+
}
|
|
580
612
|
const issues: SonarIssue[] = [];
|
|
581
613
|
const ruleNames = new Map<string, string | undefined>();
|
|
582
614
|
let page = 1;
|
|
@@ -614,6 +646,62 @@ export async function fetchIssues(
|
|
|
614
646
|
return issues;
|
|
615
647
|
}
|
|
616
648
|
|
|
649
|
+
// ── Rule autocomplete helpers ───────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
export async function fetchProjectProfiles(
|
|
652
|
+
serverUrl: string,
|
|
653
|
+
token: string | undefined,
|
|
654
|
+
projectKey: string,
|
|
655
|
+
signal?: AbortSignal,
|
|
656
|
+
): Promise<Array<{key: string; language: string; name: string}>> {
|
|
657
|
+
try {
|
|
658
|
+
const result = await fetchJson<{
|
|
659
|
+
profiles: Array<{key?: string; language?: string; name?: string}>;
|
|
660
|
+
}>(
|
|
661
|
+
`${serverUrl}/api/qualityprofiles/search?project=${encodeURIComponent(projectKey)}`,
|
|
662
|
+
token,
|
|
663
|
+
signal,
|
|
664
|
+
);
|
|
665
|
+
return (result.profiles ?? []).map(p => ({
|
|
666
|
+
key: p.key ?? '',
|
|
667
|
+
language: p.language ?? '',
|
|
668
|
+
name: p.name ?? '',
|
|
669
|
+
}));
|
|
670
|
+
} catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export async function fetchRuleSearch(
|
|
676
|
+
serverUrl: string,
|
|
677
|
+
token: string | undefined,
|
|
678
|
+
query: string,
|
|
679
|
+
languages?: string[],
|
|
680
|
+
signal?: AbortSignal,
|
|
681
|
+
): Promise<Array<{key: string; name?: string}>> {
|
|
682
|
+
try {
|
|
683
|
+
const params = new URLSearchParams();
|
|
684
|
+
if (query) params.set("q", query);
|
|
685
|
+
params.set("ps", "20");
|
|
686
|
+
params.set("p", "1");
|
|
687
|
+
if (languages?.length) params.set("languages", languages.join(","));
|
|
688
|
+
|
|
689
|
+
const result = await fetchJson<{
|
|
690
|
+
rules: Array<{key?: string; name?: string}>;
|
|
691
|
+
}>(
|
|
692
|
+
`${serverUrl}/api/rules/search?${params.toString()}`,
|
|
693
|
+
token,
|
|
694
|
+
signal,
|
|
695
|
+
);
|
|
696
|
+
return (result.rules ?? []).map(r => ({
|
|
697
|
+
key: r.key ?? '',
|
|
698
|
+
name: r.name,
|
|
699
|
+
}));
|
|
700
|
+
} catch {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
617
705
|
export async function fetchRuleName(
|
|
618
706
|
serverUrl: string,
|
|
619
707
|
token: string | undefined,
|
|
@@ -785,6 +873,8 @@ export async function fetchFileDuplications(
|
|
|
785
873
|
token: string | undefined,
|
|
786
874
|
projectKey: string,
|
|
787
875
|
signal?: AbortSignal,
|
|
876
|
+
pathScope?: string,
|
|
877
|
+
baseDir?: string,
|
|
788
878
|
): Promise<FileDuplication[]> {
|
|
789
879
|
const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_lines_density,duplicated_blocks&ps=500`;
|
|
790
880
|
const treeResult = await fetchJson<{
|
|
@@ -796,7 +886,7 @@ export async function fetchFileDuplications(
|
|
|
796
886
|
}>;
|
|
797
887
|
}>(treeUrl, token, signal);
|
|
798
888
|
|
|
799
|
-
|
|
889
|
+
let results = (treeResult.components ?? [])
|
|
800
890
|
.map((component) => {
|
|
801
891
|
const measures = component.measures ?? [];
|
|
802
892
|
const getValue = (metric: string): number => {
|
|
@@ -817,15 +907,22 @@ export async function fetchFileDuplications(
|
|
|
817
907
|
duplicatedBlocks,
|
|
818
908
|
};
|
|
819
909
|
})
|
|
820
|
-
.filter((f): f is FileDuplication => f !== null)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
910
|
+
.filter((f): f is FileDuplication => f !== null);
|
|
911
|
+
|
|
912
|
+
if (pathScope && baseDir) {
|
|
913
|
+
const absPath = resolve(baseDir, pathScope);
|
|
914
|
+
const relPrefix = relative(baseDir, absPath);
|
|
915
|
+
results = results.filter((f) => f.filePath.startsWith(relPrefix));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
results.sort((left, right) => {
|
|
919
|
+
const densityDiff =
|
|
920
|
+
right.duplicatedLinesDensity - left.duplicatedLinesDensity;
|
|
921
|
+
if (densityDiff !== 0) return densityDiff;
|
|
922
|
+
const linesDiff = right.duplicatedLines - left.duplicatedLines;
|
|
923
|
+
if (linesDiff !== 0) return linesDiff;
|
|
924
|
+
return left.filePath.localeCompare(right.filePath);
|
|
925
|
+
});
|
|
829
926
|
|
|
830
927
|
return results;
|
|
831
928
|
}
|
package/src/commands.ts
CHANGED
|
@@ -18,8 +18,16 @@ import {
|
|
|
18
18
|
SONAR_SOFTWARE_QUALITIES,
|
|
19
19
|
SONAR_IMPACT_SEVERITIES,
|
|
20
20
|
} from "./types.js";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { readdirSync } from "node:fs";
|
|
23
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
21
24
|
import { looksLikePath } from "./config.js";
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
parseSonarIssueArgs,
|
|
27
|
+
issueFilterLabel,
|
|
28
|
+
fetchProjectProfiles,
|
|
29
|
+
fetchRuleSearch,
|
|
30
|
+
} from "./api.js";
|
|
23
31
|
|
|
24
32
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
25
33
|
|
|
@@ -138,17 +146,291 @@ function createFilterCompletionList(
|
|
|
138
146
|
["quality", SONAR_SOFTWARE_QUALITIES, "software quality (MQR)", true],
|
|
139
147
|
["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
|
|
140
148
|
] as const;
|
|
149
|
+
const ruleItems = [
|
|
150
|
+
{ value: "rule:", label: "rule:", description: "rule key" },
|
|
151
|
+
{ value: "rules:", label: "rules:", description: "rule key" },
|
|
152
|
+
];
|
|
153
|
+
const scopeItems = [
|
|
154
|
+
{
|
|
155
|
+
value: "in:",
|
|
156
|
+
label: "in:",
|
|
157
|
+
description: "path or dir scope (e.g. in:src/api.ts or in:src/)",
|
|
158
|
+
},
|
|
159
|
+
];
|
|
141
160
|
|
|
142
161
|
return mode === "MQR"
|
|
143
|
-
? [
|
|
144
|
-
|
|
162
|
+
? [
|
|
163
|
+
...scopeItems,
|
|
164
|
+
...ruleItems,
|
|
165
|
+
...buildItems(mqrGroups),
|
|
166
|
+
...buildItems(legacyGroups),
|
|
167
|
+
]
|
|
168
|
+
: [
|
|
169
|
+
...scopeItems,
|
|
170
|
+
...ruleItems,
|
|
171
|
+
...buildItems(legacyGroups),
|
|
172
|
+
...buildItems(mqrGroups),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listDirCompletions(
|
|
177
|
+
dirPath: string,
|
|
178
|
+
baseDir: string,
|
|
179
|
+
): AutocompleteItem[] | null {
|
|
180
|
+
try {
|
|
181
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
182
|
+
const items: AutocompleteItem[] = [];
|
|
183
|
+
const relativePrefix =
|
|
184
|
+
dirPath === baseDir ? "" : relative(baseDir, dirPath) + "/";
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (entry.name.startsWith(".")) continue;
|
|
188
|
+
const isDir = entry.isDirectory();
|
|
189
|
+
const name = isDir ? entry.name + "/" : entry.name;
|
|
190
|
+
const fullPath = relativePrefix + name;
|
|
191
|
+
items.push({
|
|
192
|
+
value: `in:${fullPath}`,
|
|
193
|
+
label: name,
|
|
194
|
+
description: relativePrefix.replace(/\/$/, ""),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
items.sort((a, b) => {
|
|
199
|
+
const aDir = a.description === "" ? 0 : 1;
|
|
200
|
+
const bDir = b.description === "" ? 0 : 1;
|
|
201
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
202
|
+
// Within same depth, dirs first, then by label
|
|
203
|
+
const aD = a.label.endsWith("/") ? 0 : 1;
|
|
204
|
+
const bD = b.label.endsWith("/") ? 0 : 1;
|
|
205
|
+
return aD - bD || a.label.localeCompare(b.label);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return items.length > 0 ? items.slice(0, 50) : null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fuzzyWithCmd(
|
|
215
|
+
cmd: string,
|
|
216
|
+
args: string[],
|
|
217
|
+
prefix: string,
|
|
218
|
+
baseDir: string,
|
|
219
|
+
): AutocompleteItem[] | null {
|
|
220
|
+
const out = execFileSync(cmd, args, {
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
timeout: 3000,
|
|
223
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
224
|
+
windowsHide: true,
|
|
225
|
+
});
|
|
226
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
227
|
+
if (lines.length === 0) return null;
|
|
228
|
+
|
|
229
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
230
|
+
const matched: AutocompleteItem[] = [];
|
|
231
|
+
const seen = new Set<string>();
|
|
232
|
+
|
|
233
|
+
for (const line of lines) {
|
|
234
|
+
if (matched.length >= 50) break;
|
|
235
|
+
// Normalize: fd outputs dirs with trailing /, rg doesn't
|
|
236
|
+
const isDir = line.endsWith("/");
|
|
237
|
+
const entry = isDir ? line.slice(0, -1) : line;
|
|
238
|
+
if (!entry.toLowerCase().includes(lowerPrefix) || seen.has(entry)) continue;
|
|
239
|
+
seen.add(entry);
|
|
240
|
+
|
|
241
|
+
const dir = dirname(entry);
|
|
242
|
+
const name = basename(entry) + (isDir ? "/" : "");
|
|
243
|
+
matched.push({
|
|
244
|
+
value: `in:${line}`,
|
|
245
|
+
label: name,
|
|
246
|
+
description: dir === "." ? "" : dir + "/",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// For rg (no dirs output), extract ancestor dirs that match
|
|
250
|
+
if (!isDir) {
|
|
251
|
+
addAncestorDirMatches(entry, lowerPrefix, matched, seen);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
matched.sort((a, b) => {
|
|
256
|
+
const aDir = a.label.endsWith("/") ? 0 : 1;
|
|
257
|
+
const bDir = b.label.endsWith("/") ? 0 : 1;
|
|
258
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
259
|
+
return a.label.localeCompare(b.label);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return matched;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* For non-directory entries, extract ancestor directories that match the prefix
|
|
267
|
+
* and add them as autocomplete suggestions.
|
|
268
|
+
*/
|
|
269
|
+
function addAncestorDirMatches(
|
|
270
|
+
entry: string,
|
|
271
|
+
lowerPrefix: string,
|
|
272
|
+
matched: AutocompleteItem[],
|
|
273
|
+
seen: Set<string>,
|
|
274
|
+
): void {
|
|
275
|
+
const segs = entry.split("/");
|
|
276
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
277
|
+
if (matched.length >= 50) break;
|
|
278
|
+
const dirPath = segs.slice(0, i + 1).join("/") + "/";
|
|
279
|
+
if (seen.has(dirPath)) continue;
|
|
280
|
+
seen.add(dirPath);
|
|
281
|
+
if (segs[i].toLowerCase().includes(lowerPrefix)) {
|
|
282
|
+
const parentDir = segs.slice(0, i).join("/");
|
|
283
|
+
matched.push({
|
|
284
|
+
value: `in:${dirPath}`,
|
|
285
|
+
label: segs[i] + "/",
|
|
286
|
+
description: parentDir || "",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
145
290
|
}
|
|
146
291
|
|
|
147
|
-
|
|
292
|
+
async function fetchRuleAutocompleteCompletions(
|
|
293
|
+
serverUrl: string,
|
|
294
|
+
token: string | undefined,
|
|
295
|
+
projectKey: string,
|
|
296
|
+
query: string,
|
|
297
|
+
): Promise<AutocompleteItem[] | null> {
|
|
298
|
+
try {
|
|
299
|
+
const profiles = await fetchProjectProfiles(serverUrl, token, projectKey);
|
|
300
|
+
const languages = [
|
|
301
|
+
...new Set(profiles.map((p) => p.language).filter(Boolean)),
|
|
302
|
+
];
|
|
303
|
+
const rules = await fetchRuleSearch(serverUrl, token, query, languages);
|
|
304
|
+
if (rules.length === 0) return null;
|
|
305
|
+
return rules.map((r) => ({
|
|
306
|
+
value: `rule:${r.key}`,
|
|
307
|
+
label: r.key,
|
|
308
|
+
description: r.name ?? "",
|
|
309
|
+
}));
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getInPathCompletions(
|
|
316
|
+
prefix: string,
|
|
317
|
+
baseDir: string,
|
|
318
|
+
): AutocompleteItem[] | null {
|
|
319
|
+
// Directory browsing: show contents when prefix ends with /
|
|
320
|
+
if (prefix.endsWith("/")) {
|
|
321
|
+
return listDirCompletions(resolve(baseDir, prefix), baseDir);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Empty prefix: show root directory contents
|
|
325
|
+
if (!prefix) {
|
|
326
|
+
return listDirCompletions(baseDir, baseDir);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Try fd first (fast, respects .gitignore, matches Pi TUI behavior)
|
|
330
|
+
try {
|
|
331
|
+
return fuzzyWithCmd(
|
|
332
|
+
"fd",
|
|
333
|
+
[
|
|
334
|
+
"--base-directory",
|
|
335
|
+
baseDir,
|
|
336
|
+
"--type",
|
|
337
|
+
"f",
|
|
338
|
+
"--type",
|
|
339
|
+
"d",
|
|
340
|
+
"--hidden",
|
|
341
|
+
"--exclude",
|
|
342
|
+
".git",
|
|
343
|
+
"--max-results",
|
|
344
|
+
"50",
|
|
345
|
+
"--full-path",
|
|
346
|
+
prefix,
|
|
347
|
+
],
|
|
348
|
+
prefix,
|
|
349
|
+
baseDir,
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
// fd not available
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fallback: rg --files (slower but available on most systems)
|
|
356
|
+
try {
|
|
357
|
+
return fuzzyWithCmd(
|
|
358
|
+
"rg",
|
|
359
|
+
["--files", "--color", "never", "--no-ignore-parent"],
|
|
360
|
+
prefix,
|
|
361
|
+
baseDir,
|
|
362
|
+
);
|
|
363
|
+
} catch {
|
|
364
|
+
// rg not available
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Last resort: list current directory
|
|
368
|
+
return listDirCompletions(baseDir, baseDir);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getOpenCompletions(
|
|
372
|
+
issues: SonarIssue[] | undefined,
|
|
373
|
+
mode: "STANDARD" | "MQR" | undefined,
|
|
374
|
+
current: string,
|
|
375
|
+
): AutocompleteItem[] | null {
|
|
376
|
+
const issueItems = (issues ?? []).slice(0, 10).map((issue, index) => {
|
|
377
|
+
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
378
|
+
return {
|
|
379
|
+
value: String(index + 1),
|
|
380
|
+
label: String(index + 1),
|
|
381
|
+
description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
385
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
386
|
+
return filtered.length > 0 ? filtered : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function getFilterCompletions(
|
|
390
|
+
current: string,
|
|
391
|
+
mode: "STANDARD" | "MQR" | undefined,
|
|
392
|
+
cwd: string | undefined,
|
|
393
|
+
serverUrl: string | undefined,
|
|
394
|
+
token: string | undefined,
|
|
395
|
+
projectKey: string | undefined,
|
|
396
|
+
): Promise<AutocompleteItem[] | null> {
|
|
397
|
+
if (current.startsWith("in:") && cwd) {
|
|
398
|
+
return getInPathCompletions(current.slice(3), cwd);
|
|
399
|
+
}
|
|
400
|
+
if (
|
|
401
|
+
(current.startsWith("rule:") || current.startsWith("rules:")) &&
|
|
402
|
+
serverUrl &&
|
|
403
|
+
projectKey
|
|
404
|
+
) {
|
|
405
|
+
const query = current.split(":").slice(1).join(":");
|
|
406
|
+
const ruleItems = await fetchRuleAutocompleteCompletions(
|
|
407
|
+
serverUrl,
|
|
408
|
+
token,
|
|
409
|
+
projectKey,
|
|
410
|
+
query,
|
|
411
|
+
);
|
|
412
|
+
const staticSuggestions = createFilterCompletionList(mode);
|
|
413
|
+
const filtered = filterAutocompleteItems(staticSuggestions, current);
|
|
414
|
+
const combined = [
|
|
415
|
+
...(filtered.length > 0 ? filtered : []),
|
|
416
|
+
...(ruleItems ?? []),
|
|
417
|
+
];
|
|
418
|
+
return combined.length > 0 ? combined : null;
|
|
419
|
+
}
|
|
420
|
+
const suggestions = createFilterCompletionList(mode);
|
|
421
|
+
const filtered = filterAutocompleteItems(suggestions, current);
|
|
422
|
+
return filtered.length > 0 ? filtered : null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function sonarArgumentCompletions(
|
|
148
426
|
argumentText: string,
|
|
149
427
|
issues?: SonarIssue[],
|
|
150
428
|
mode?: "STANDARD" | "MQR",
|
|
151
|
-
|
|
429
|
+
cwd?: string,
|
|
430
|
+
serverUrl?: string,
|
|
431
|
+
token?: string,
|
|
432
|
+
projectKey?: string,
|
|
433
|
+
): Promise<AutocompleteItem[] | null> {
|
|
152
434
|
const { command, current, tokens } = splitSonarArgumentContext(argumentText);
|
|
153
435
|
const lowerCommand = command.toLowerCase();
|
|
154
436
|
const commandMatches = SONAR_COMMANDS.filter((item) =>
|
|
@@ -168,23 +450,18 @@ export function sonarArgumentCompletions(
|
|
|
168
450
|
}
|
|
169
451
|
|
|
170
452
|
if (lowerCommand === "open") {
|
|
171
|
-
|
|
172
|
-
const lineSuffix = issue.line ? `:${issue.line}` : "";
|
|
173
|
-
return {
|
|
174
|
-
value: String(index + 1),
|
|
175
|
-
label: String(index + 1),
|
|
176
|
-
description: `${issue.severity} ${issue.filePath}${lineSuffix}`,
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
|
|
180
|
-
const filtered = filterAutocompleteItems(suggestions, current);
|
|
181
|
-
return filtered.length > 0 ? filtered : null;
|
|
453
|
+
return getOpenCompletions(issues, mode, current);
|
|
182
454
|
}
|
|
183
455
|
|
|
184
456
|
if (lowerCommand === "analyze" || lowerCommand === "issues") {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
457
|
+
return getFilterCompletions(
|
|
458
|
+
current,
|
|
459
|
+
mode,
|
|
460
|
+
cwd,
|
|
461
|
+
serverUrl,
|
|
462
|
+
token,
|
|
463
|
+
projectKey,
|
|
464
|
+
);
|
|
188
465
|
}
|
|
189
466
|
|
|
190
467
|
if (
|
|
@@ -218,7 +495,12 @@ export type ParsedSonarCommand =
|
|
|
218
495
|
filters?: SonarIssueFetchOptions;
|
|
219
496
|
}
|
|
220
497
|
| { action: "metrics"; targetInput?: string }
|
|
221
|
-
| {
|
|
498
|
+
| {
|
|
499
|
+
action: "duplications";
|
|
500
|
+
targetInput?: string;
|
|
501
|
+
issueIndex?: number;
|
|
502
|
+
filters?: SonarIssueFetchOptions;
|
|
503
|
+
};
|
|
222
504
|
|
|
223
505
|
export function parseCommandArgs(args: string): ParsedSonarCommand {
|
|
224
506
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -273,6 +555,9 @@ export function helpText(): string {
|
|
|
273
555
|
"Filters:",
|
|
274
556
|
" /sonarqube issues be CRITICAL",
|
|
275
557
|
" /sonarqube issues be severity:CRITICAL status:OPEN",
|
|
558
|
+
" /sonarqube issues be rule:S1192",
|
|
559
|
+
" /sonarqube issues in:src/api.ts",
|
|
560
|
+
" /sonarqube issues in:src/",
|
|
276
561
|
" /sonarqube issues be quality:RELIABILITY",
|
|
277
562
|
" /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
|
|
278
563
|
"",
|
|
@@ -286,6 +571,7 @@ export function helpText(): string {
|
|
|
286
571
|
"Defaults:",
|
|
287
572
|
" config is stored in .pi/sonarqube.json",
|
|
288
573
|
" use project paths directly (no alias needed)",
|
|
574
|
+
" issue and duplication browsers search file, rule, severity, status, message, and stats",
|
|
289
575
|
].join("\n");
|
|
290
576
|
}
|
|
291
577
|
|