@jonathan0823/pi-sonarqube 0.5.0 → 0.7.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 +15 -4
- 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 +4 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +260 -20
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -37
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +42 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +154 -66
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +130 -33
- package/src/commands.ts +368 -27
- package/src/index.ts +117 -56
- package/src/types.ts +5 -1
- package/src/ui.ts +224 -80
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
ensureDefaultSonarProjectProperties,
|
|
30
30
|
saveWorkspaceRegistry,
|
|
31
31
|
resolveConfig,
|
|
32
|
+
loadWorkspaceRegistry,
|
|
32
33
|
} from "./config.js";
|
|
33
34
|
import {
|
|
34
35
|
runScanner,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
openIssuePreview,
|
|
71
72
|
resolveTargetState,
|
|
72
73
|
showDuplicationBrowser,
|
|
74
|
+
showWorkspaceBrowser,
|
|
73
75
|
} from "./ui.js";
|
|
74
76
|
|
|
75
77
|
const SonarToolParams = Type.Object({
|
|
@@ -116,6 +118,12 @@ const SonarToolParams = Type.Object({
|
|
|
116
118
|
Type.String({ description: "Impact severity to fetch (e.g. HIGH)" }),
|
|
117
119
|
),
|
|
118
120
|
),
|
|
121
|
+
pathScope: Type.Optional(
|
|
122
|
+
Type.String({
|
|
123
|
+
description:
|
|
124
|
+
"Scope issues to a specific file or directory path (e.g. src/api.ts or src/). Use this instead of the `path` parameter for file-level filtering.",
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
// ── State restoration ───────────────────────────────────────────────────────
|
|
@@ -150,6 +158,11 @@ async function analyzeProject(
|
|
|
150
158
|
filters?: SonarIssueFetchOptions,
|
|
151
159
|
): Promise<SonarAnalysisState> {
|
|
152
160
|
const config = await resolveConfig(ctx, inputPath);
|
|
161
|
+
_cachedConfig = {
|
|
162
|
+
serverUrl: config.serverUrl,
|
|
163
|
+
projectKey: config.projectKey,
|
|
164
|
+
token: config.token,
|
|
165
|
+
};
|
|
153
166
|
const baseDirStat = await stat(config.baseDir).catch(() => undefined);
|
|
154
167
|
if (!baseDirStat?.isDirectory()) {
|
|
155
168
|
throw new Error(`Project directory not found: ${config.baseDir}`);
|
|
@@ -183,6 +196,7 @@ async function analyzeProject(
|
|
|
183
196
|
config.projectKey,
|
|
184
197
|
ctx.signal,
|
|
185
198
|
normalizedFilters,
|
|
199
|
+
config.baseDir,
|
|
186
200
|
);
|
|
187
201
|
|
|
188
202
|
analysisUi.setPhase("Fetching metrics...");
|
|
@@ -385,6 +399,26 @@ function renderIssueListResult(
|
|
|
385
399
|
|
|
386
400
|
// ── Extension entrypoint ────────────────────────────────────────────────────
|
|
387
401
|
|
|
402
|
+
// ponytail: lazy config cache so rule autocomplete can fetch from the server
|
|
403
|
+
let _cachedConfig:
|
|
404
|
+
{ serverUrl: string; projectKey: string; token?: string } | undefined;
|
|
405
|
+
|
|
406
|
+
async function cachedServerConfig(cwd: string): Promise<typeof _cachedConfig> {
|
|
407
|
+
if (!_cachedConfig) {
|
|
408
|
+
try {
|
|
409
|
+
const cfg = await resolveConfig({ cwd });
|
|
410
|
+
_cachedConfig = {
|
|
411
|
+
serverUrl: cfg.serverUrl,
|
|
412
|
+
projectKey: cfg.projectKey,
|
|
413
|
+
token: cfg.token,
|
|
414
|
+
};
|
|
415
|
+
} catch {
|
|
416
|
+
// fall through
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return _cachedConfig;
|
|
420
|
+
}
|
|
421
|
+
|
|
388
422
|
export default function sonarqube(pi: ExtensionAPI) {
|
|
389
423
|
const statesByBaseDir = new Map<string, SonarAnalysisState>();
|
|
390
424
|
let latestState: SonarAnalysisState | undefined;
|
|
@@ -418,12 +452,17 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
418
452
|
const argumentText = match[1] ?? "";
|
|
419
453
|
const { current: currentToken } =
|
|
420
454
|
splitSonarArgumentContext(argumentText);
|
|
455
|
+
const cfg = await cachedServerConfig(ctx.cwd);
|
|
421
456
|
const extraItems =
|
|
422
|
-
sonarArgumentCompletions(
|
|
457
|
+
(await sonarArgumentCompletions(
|
|
423
458
|
argumentText,
|
|
424
459
|
latestState?.issues,
|
|
425
460
|
latestState?.cleanCodeMode,
|
|
426
|
-
|
|
461
|
+
undefined,
|
|
462
|
+
cfg?.serverUrl,
|
|
463
|
+
cfg?.token,
|
|
464
|
+
cfg?.projectKey,
|
|
465
|
+
)) ?? [];
|
|
427
466
|
const currentSuggestions = await current.getSuggestions(
|
|
428
467
|
lines,
|
|
429
468
|
cursorLine,
|
|
@@ -473,6 +512,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
473
512
|
promptGuidelines: [
|
|
474
513
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
475
514
|
"Use the optional severity, status, type, rule, quality, and impact severity filters to fetch only the most relevant issues.",
|
|
515
|
+
"To scope issues to a specific file or directory, use the `pathScope` parameter (e.g. pathScope: 'src/api.ts') — do not use the `path` parameter for file filtering.",
|
|
476
516
|
],
|
|
477
517
|
parameters: SonarToolParams,
|
|
478
518
|
|
|
@@ -485,6 +525,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
485
525
|
rules: params.rules,
|
|
486
526
|
softwareQualities: params.softwareQualities,
|
|
487
527
|
impactSeverities: params.impactSeverities,
|
|
528
|
+
pathScope: params.pathScope,
|
|
488
529
|
});
|
|
489
530
|
assertFiltersNotAmbiguous(filters);
|
|
490
531
|
|
|
@@ -615,12 +656,18 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
615
656
|
pi.registerCommand("sonarqube", {
|
|
616
657
|
description:
|
|
617
658
|
"Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
618
|
-
getArgumentCompletions: (argumentPrefix) =>
|
|
619
|
-
|
|
659
|
+
getArgumentCompletions: async (argumentPrefix) => {
|
|
660
|
+
const cfg = await cachedServerConfig(process.cwd());
|
|
661
|
+
return sonarArgumentCompletions(
|
|
620
662
|
argumentPrefix,
|
|
621
663
|
latestState?.issues,
|
|
622
664
|
latestState?.cleanCodeMode,
|
|
623
|
-
|
|
665
|
+
process.cwd(),
|
|
666
|
+
cfg?.serverUrl,
|
|
667
|
+
cfg?.token,
|
|
668
|
+
cfg?.projectKey,
|
|
669
|
+
);
|
|
670
|
+
},
|
|
624
671
|
handler: async (args, ctx) => {
|
|
625
672
|
try {
|
|
626
673
|
const parsed = parseCommandArgs(args);
|
|
@@ -653,6 +700,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
653
700
|
ctx,
|
|
654
701
|
parsed.targetInput,
|
|
655
702
|
parsed.issueIndex,
|
|
703
|
+
parsed.filters,
|
|
656
704
|
);
|
|
657
705
|
return;
|
|
658
706
|
default:
|
|
@@ -682,6 +730,11 @@ async function commandAnalyze(
|
|
|
682
730
|
filters: SonarIssueFetchOptions | undefined,
|
|
683
731
|
rememberState: (s: SonarAnalysisState) => void,
|
|
684
732
|
): Promise<void> {
|
|
733
|
+
if (!targetInput) {
|
|
734
|
+
const pickResult = await pickWorkspace(ctx);
|
|
735
|
+
if (pickResult === null) return; // user cancelled
|
|
736
|
+
targetInput = pickResult;
|
|
737
|
+
}
|
|
685
738
|
const state = await analyzeProject(pi, ctx, targetInput, filters);
|
|
686
739
|
rememberState(state);
|
|
687
740
|
if (ctx.hasUI) {
|
|
@@ -693,38 +746,66 @@ async function commandAnalyze(
|
|
|
693
746
|
}
|
|
694
747
|
}
|
|
695
748
|
|
|
696
|
-
async function
|
|
697
|
-
ctx:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
749
|
+
async function pickWorkspace(
|
|
750
|
+
ctx: ExtensionContext,
|
|
751
|
+
): Promise<string | undefined | null> {
|
|
752
|
+
const { registry } = await loadWorkspaceRegistry(ctx.cwd);
|
|
753
|
+
const entries = Object.entries(registry.workspaces);
|
|
754
|
+
if (entries.length === 0) return undefined; // no registry → use cwd
|
|
755
|
+
if (entries.length === 1) return entries[0][0]; // single → use it directly
|
|
756
|
+
if (ctx.mode !== "tui") return undefined; // headless → use cwd
|
|
757
|
+
return await showWorkspaceBrowser(
|
|
758
|
+
ctx,
|
|
759
|
+
entries.map(([alias, path]) => ({ alias, path })),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function loadMetricsData(
|
|
764
|
+
config: SonarProjectConfig,
|
|
765
|
+
signal?: AbortSignal,
|
|
766
|
+
): Promise<{
|
|
767
|
+
cleanCodeMode: Awaited<ReturnType<typeof fetchCleanCodeMode>>;
|
|
768
|
+
measures: Awaited<ReturnType<typeof fetchDuplicationMeasures>>;
|
|
769
|
+
issueCounts: Awaited<ReturnType<typeof fetchIssueSeverityCounts>>;
|
|
770
|
+
qualityCounts: Awaited<ReturnType<typeof fetchIssueQualityCounts>>;
|
|
771
|
+
} | null> {
|
|
701
772
|
const cleanCodeMode = await fetchCleanCodeMode(
|
|
702
773
|
config.serverUrl,
|
|
703
774
|
config.token,
|
|
704
|
-
|
|
775
|
+
signal,
|
|
705
776
|
);
|
|
706
777
|
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
707
778
|
fetchDuplicationMeasures(
|
|
708
779
|
config.serverUrl,
|
|
709
780
|
config.token,
|
|
710
781
|
config.projectKey,
|
|
711
|
-
|
|
782
|
+
signal,
|
|
712
783
|
),
|
|
713
784
|
fetchIssueSeverityCounts(
|
|
714
785
|
config.serverUrl,
|
|
715
786
|
config.token,
|
|
716
787
|
config.projectKey,
|
|
717
|
-
|
|
788
|
+
signal,
|
|
718
789
|
cleanCodeMode,
|
|
719
790
|
),
|
|
720
791
|
fetchIssueQualityCounts(
|
|
721
792
|
config.serverUrl,
|
|
722
793
|
config.token,
|
|
723
794
|
config.projectKey,
|
|
724
|
-
|
|
795
|
+
signal,
|
|
725
796
|
),
|
|
726
797
|
]);
|
|
727
|
-
if (!measures && !issueCounts && !qualityCounts)
|
|
798
|
+
if (!measures && !issueCounts && !qualityCounts) return null;
|
|
799
|
+
return { cleanCodeMode, measures, issueCounts, qualityCounts };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function commandMetrics(
|
|
803
|
+
ctx: ExtensionCommandContext,
|
|
804
|
+
targetInput?: string,
|
|
805
|
+
): Promise<void> {
|
|
806
|
+
const config = await resolveConfig(ctx, targetInput);
|
|
807
|
+
const metrics = await loadMetricsData(config, ctx.signal);
|
|
808
|
+
if (!metrics) {
|
|
728
809
|
if (ctx.hasUI) {
|
|
729
810
|
ctx.ui.notify(
|
|
730
811
|
`Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`,
|
|
@@ -735,10 +816,10 @@ async function commandMetrics(
|
|
|
735
816
|
}
|
|
736
817
|
const text = formatMetricsOutput({
|
|
737
818
|
projectKey: config.projectKey,
|
|
738
|
-
measures,
|
|
739
|
-
issueCounts,
|
|
740
|
-
issueQualityCounts: qualityCounts,
|
|
741
|
-
cleanCodeMode,
|
|
819
|
+
measures: metrics.measures,
|
|
820
|
+
issueCounts: metrics.issueCounts,
|
|
821
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
822
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
742
823
|
});
|
|
743
824
|
if (ctx.hasUI) {
|
|
744
825
|
ctx.ui.notify(text, "info");
|
|
@@ -750,6 +831,7 @@ async function commandDuplications(
|
|
|
750
831
|
ctx: ExtensionCommandContext,
|
|
751
832
|
targetInput?: string,
|
|
752
833
|
fileIndex?: number,
|
|
834
|
+
filters?: SonarIssueFetchOptions,
|
|
753
835
|
): Promise<void> {
|
|
754
836
|
const config = await resolveConfig(ctx, targetInput);
|
|
755
837
|
let files: FileDuplication[];
|
|
@@ -759,6 +841,8 @@ async function commandDuplications(
|
|
|
759
841
|
config.token,
|
|
760
842
|
config.projectKey,
|
|
761
843
|
ctx.signal,
|
|
844
|
+
filters?.pathScope,
|
|
845
|
+
config.baseDir,
|
|
762
846
|
);
|
|
763
847
|
} catch (error) {
|
|
764
848
|
const msg = sonarErrorMessage(error);
|
|
@@ -782,7 +866,7 @@ async function commandDuplications(
|
|
|
782
866
|
await showDuplicationDrillDown(ctx, config, files, fileIndex);
|
|
783
867
|
return;
|
|
784
868
|
}
|
|
785
|
-
await showDuplicationListOrBrowser(ctx, config, files);
|
|
869
|
+
await showDuplicationListOrBrowser(ctx, config, files, filters);
|
|
786
870
|
}
|
|
787
871
|
|
|
788
872
|
async function showDuplicationDrillDown(
|
|
@@ -819,12 +903,14 @@ async function showDuplicationListOrBrowser(
|
|
|
819
903
|
ctx: ExtensionCommandContext,
|
|
820
904
|
config: SonarProjectConfig,
|
|
821
905
|
files: FileDuplication[],
|
|
906
|
+
filters?: SonarIssueFetchOptions,
|
|
822
907
|
): Promise<void> {
|
|
823
908
|
if (ctx.mode !== "tui") {
|
|
824
909
|
ctx.ui.notify(formatDuplicationsList(files), "info");
|
|
825
910
|
return;
|
|
826
911
|
}
|
|
827
|
-
const
|
|
912
|
+
const scopeLabel = filters?.pathScope ? issueFilterLabel(filters) : undefined;
|
|
913
|
+
const choice = await showDuplicationBrowser(ctx, files, scopeLabel);
|
|
828
914
|
if (choice == null) return;
|
|
829
915
|
const file = files[choice];
|
|
830
916
|
const groups = await fetchFileDuplicationBlocks(
|
|
@@ -1060,33 +1146,8 @@ async function toolMetrics(
|
|
|
1060
1146
|
details: { error: "Project not configured." },
|
|
1061
1147
|
};
|
|
1062
1148
|
}
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
config.token,
|
|
1066
|
-
ctx.signal,
|
|
1067
|
-
);
|
|
1068
|
-
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
1069
|
-
fetchDuplicationMeasures(
|
|
1070
|
-
config.serverUrl,
|
|
1071
|
-
config.token,
|
|
1072
|
-
config.projectKey,
|
|
1073
|
-
ctx.signal,
|
|
1074
|
-
),
|
|
1075
|
-
fetchIssueSeverityCounts(
|
|
1076
|
-
config.serverUrl,
|
|
1077
|
-
config.token,
|
|
1078
|
-
config.projectKey,
|
|
1079
|
-
ctx.signal,
|
|
1080
|
-
cleanCodeMode,
|
|
1081
|
-
),
|
|
1082
|
-
fetchIssueQualityCounts(
|
|
1083
|
-
config.serverUrl,
|
|
1084
|
-
config.token,
|
|
1085
|
-
config.projectKey,
|
|
1086
|
-
ctx.signal,
|
|
1087
|
-
),
|
|
1088
|
-
]);
|
|
1089
|
-
if (!measures && !issueCounts && !qualityCounts) {
|
|
1149
|
+
const metrics = await loadMetricsData(config, ctx.signal);
|
|
1150
|
+
if (!metrics) {
|
|
1090
1151
|
return {
|
|
1091
1152
|
content: [
|
|
1092
1153
|
{
|
|
@@ -1101,19 +1162,19 @@ async function toolMetrics(
|
|
|
1101
1162
|
}
|
|
1102
1163
|
const text = formatMetricsOutput({
|
|
1103
1164
|
projectKey: config.projectKey,
|
|
1104
|
-
measures,
|
|
1105
|
-
issueCounts,
|
|
1106
|
-
issueQualityCounts: qualityCounts,
|
|
1107
|
-
cleanCodeMode,
|
|
1165
|
+
measures: metrics.measures,
|
|
1166
|
+
issueCounts: metrics.issueCounts,
|
|
1167
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
1168
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
1108
1169
|
});
|
|
1109
1170
|
return {
|
|
1110
1171
|
content: [{ type: "text", text }],
|
|
1111
1172
|
details: {
|
|
1112
1173
|
projectKey: config.projectKey,
|
|
1113
|
-
measures,
|
|
1114
|
-
issueCounts,
|
|
1115
|
-
issueQualityCounts: qualityCounts,
|
|
1116
|
-
cleanCodeMode,
|
|
1174
|
+
measures: metrics.measures,
|
|
1175
|
+
issueCounts: metrics.issueCounts,
|
|
1176
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
1177
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
1117
1178
|
},
|
|
1118
1179
|
};
|
|
1119
1180
|
}
|
package/src/types.ts
CHANGED
|
@@ -68,7 +68,7 @@ export interface SonarAnalysisState {
|
|
|
68
68
|
measures?: SonarDuplicationMeasures;
|
|
69
69
|
totalIssues: number;
|
|
70
70
|
issues: SonarIssue[];
|
|
71
|
-
cleanCodeMode?:
|
|
71
|
+
cleanCodeMode?: CleanCodeMode;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export interface SonarProjectConfig {
|
|
@@ -86,6 +86,8 @@ export interface SonarIssueFetchOptions {
|
|
|
86
86
|
rules?: string[];
|
|
87
87
|
softwareQualities?: string[];
|
|
88
88
|
impactSeverities?: string[];
|
|
89
|
+
pathScope?: string;
|
|
90
|
+
componentKeys?: string[];
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export interface SonarInitConfig {
|
|
@@ -96,6 +98,8 @@ export interface SonarInitConfig {
|
|
|
96
98
|
|
|
97
99
|
// ── Internal types shared by adapter and extension ──────────────────────────
|
|
98
100
|
|
|
101
|
+
export type CleanCodeMode = "STANDARD" | "MQR";
|
|
102
|
+
|
|
99
103
|
export interface SonarWorkspaceRegistry {
|
|
100
104
|
version: 1;
|
|
101
105
|
workspaces: Record<string, string>;
|