@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/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
- sonarArgumentCompletions(
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 commandMetrics(
697
- ctx: ExtensionCommandContext,
698
- targetInput?: string,
699
- ): Promise<void> {
700
- const config = await resolveConfig(ctx, targetInput);
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
- ctx.signal,
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
- ctx.signal,
782
+ signal,
712
783
  ),
713
784
  fetchIssueSeverityCounts(
714
785
  config.serverUrl,
715
786
  config.token,
716
787
  config.projectKey,
717
- ctx.signal,
788
+ signal,
718
789
  cleanCodeMode,
719
790
  ),
720
791
  fetchIssueQualityCounts(
721
792
  config.serverUrl,
722
793
  config.token,
723
794
  config.projectKey,
724
- ctx.signal,
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 choice = await showDuplicationBrowser(ctx, files);
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 cleanCodeMode = await fetchCleanCodeMode(
1064
- config.serverUrl,
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?: "STANDARD" | "MQR";
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>;