@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/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
readReportTask,
|
|
36
36
|
waitForAnalysis,
|
|
37
37
|
normalizeIssueFilters,
|
|
38
|
+
normalizeIssueList,
|
|
38
39
|
fetchIssues,
|
|
39
40
|
fetchDuplicationMeasures,
|
|
40
41
|
fetchIssueSeverityCounts,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
helpText,
|
|
61
62
|
targetLabel,
|
|
62
63
|
sonarErrorMessage,
|
|
64
|
+
assertIssueSelection,
|
|
63
65
|
} from "./commands.js";
|
|
64
66
|
|
|
65
67
|
import {
|
|
@@ -83,11 +85,22 @@ const SonarToolParams = Type.Object({
|
|
|
83
85
|
] as const),
|
|
84
86
|
path: Type.Optional(
|
|
85
87
|
Type.String({
|
|
86
|
-
description:
|
|
88
|
+
description:
|
|
89
|
+
"Configured target alias or SonarQube project root directory. Selects the project/configuration; never use it to filter by source file or subdirectory. Use pathScope for that.",
|
|
87
90
|
}),
|
|
88
91
|
),
|
|
89
92
|
issueIndex: Type.Optional(
|
|
90
|
-
Type.Number({
|
|
93
|
+
Type.Number({
|
|
94
|
+
description:
|
|
95
|
+
"Legacy 1-based issue position in this call's query results. Prefer issueKeys for stable opening.",
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
issueKeys: Type.Optional(
|
|
99
|
+
Type.Array(Type.String({ description: "Stable SonarQube issue key" }), {
|
|
100
|
+
description: "One to 10 stable issue keys to open in one batch",
|
|
101
|
+
minItems: 1,
|
|
102
|
+
maxItems: 10,
|
|
103
|
+
}),
|
|
91
104
|
),
|
|
92
105
|
severities: Type.Optional(
|
|
93
106
|
Type.Array(
|
|
@@ -120,7 +133,7 @@ const SonarToolParams = Type.Object({
|
|
|
120
133
|
pathScope: Type.Optional(
|
|
121
134
|
Type.String({
|
|
122
135
|
description:
|
|
123
|
-
"
|
|
136
|
+
"File or directory inside the selected SonarQube project, relative to its project root (e.g. src/api.ts or src/). Filters issues only; use path to select the project.",
|
|
124
137
|
}),
|
|
125
138
|
),
|
|
126
139
|
});
|
|
@@ -356,15 +369,78 @@ function renderEmptyResult(summary: string, theme: Theme): Text {
|
|
|
356
369
|
function renderIssueResult(
|
|
357
370
|
issue: SonarIssue,
|
|
358
371
|
contentText: string | undefined,
|
|
359
|
-
expanded: boolean,
|
|
360
372
|
theme: Theme,
|
|
361
373
|
): Text {
|
|
362
374
|
const text = contentText ?? formatIssue(issue);
|
|
363
|
-
const preview =
|
|
364
|
-
const label = theme.fg("accent", formatIssue(issue));
|
|
375
|
+
const preview = text.split("\n").slice(1).join("\n");
|
|
376
|
+
const label = theme.fg("accent", formatIssue(issue, undefined, false));
|
|
365
377
|
return new Text(preview ? `${label}\n${preview}` : label, 0, 0);
|
|
366
378
|
}
|
|
367
379
|
|
|
380
|
+
async function buildIssueKeyPreviews(
|
|
381
|
+
baseDir: string,
|
|
382
|
+
issueKeys: string[],
|
|
383
|
+
issues: SonarIssue[],
|
|
384
|
+
): Promise<{
|
|
385
|
+
text: string;
|
|
386
|
+
selectedIssues: SonarIssue[];
|
|
387
|
+
missingIssueKeys: string[];
|
|
388
|
+
}> {
|
|
389
|
+
const issuesByKey = new Map(issues.map((issue) => [issue.key, issue]));
|
|
390
|
+
const selectedIssues: SonarIssue[] = [];
|
|
391
|
+
const missingIssueKeys: string[] = [];
|
|
392
|
+
const sections = await Promise.all(
|
|
393
|
+
issueKeys.map(async (key) => {
|
|
394
|
+
const issue = issuesByKey.get(key);
|
|
395
|
+
if (!issue) {
|
|
396
|
+
missingIssueKeys.push(key);
|
|
397
|
+
return `Issue key "${key}" was not found.`;
|
|
398
|
+
}
|
|
399
|
+
selectedIssues.push(issue);
|
|
400
|
+
try {
|
|
401
|
+
const preview = await buildIssuePreview(baseDir, issue);
|
|
402
|
+
return `${formatIssue(issue)}\n\n${preview}`;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return `${formatIssue(issue)}\n\n${sonarErrorMessage(error)}`;
|
|
405
|
+
}
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
return { text: sections.join("\n\n"), selectedIssues, missingIssueKeys };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function renderIssueBatchResult(
|
|
412
|
+
issues: SonarIssue[],
|
|
413
|
+
missingIssueKeys: string[],
|
|
414
|
+
contentText: string | undefined,
|
|
415
|
+
theme: Theme,
|
|
416
|
+
): Text {
|
|
417
|
+
if (contentText) {
|
|
418
|
+
const hiddenKeys = issues.reduce(
|
|
419
|
+
(text, issue) =>
|
|
420
|
+
text.replace(formatIssue(issue), formatIssue(issue, undefined, false)),
|
|
421
|
+
contentText,
|
|
422
|
+
);
|
|
423
|
+
const hiddenMissingKeys = missingIssueKeys.reduce(
|
|
424
|
+
(text, key) =>
|
|
425
|
+
text.replace(
|
|
426
|
+
`Issue key "${key}" was not found.`,
|
|
427
|
+
"Requested issue was not found.",
|
|
428
|
+
),
|
|
429
|
+
hiddenKeys,
|
|
430
|
+
);
|
|
431
|
+
return new Text(hiddenMissingKeys, 0, 0);
|
|
432
|
+
}
|
|
433
|
+
const lines = issues.map((issue) =>
|
|
434
|
+
theme.fg("accent", formatIssue(issue, undefined, false)),
|
|
435
|
+
);
|
|
436
|
+
if (missingIssueKeys.length > 0) {
|
|
437
|
+
lines.push(
|
|
438
|
+
theme.fg("error", "One or more requested issues were not found."),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
442
|
+
}
|
|
443
|
+
|
|
368
444
|
function renderIssueListResult(
|
|
369
445
|
state: SonarAnalysisState,
|
|
370
446
|
expanded: boolean,
|
|
@@ -392,7 +468,7 @@ function renderIssueListResult(
|
|
|
392
468
|
|
|
393
469
|
const visible = expanded ? state.issues : state.issues.slice(0, 5);
|
|
394
470
|
for (const [i, issue] of visible.entries()) {
|
|
395
|
-
lines.push(theme.fg("muted", formatIssue(issue, i + 1)));
|
|
471
|
+
lines.push(theme.fg("muted", formatIssue(issue, i + 1, false)));
|
|
396
472
|
}
|
|
397
473
|
if (!expanded && state.issues.length > visible.length) {
|
|
398
474
|
lines.push(
|
|
@@ -424,6 +500,18 @@ async function cachedServerConfig(cwd: string): Promise<typeof _cachedConfig> {
|
|
|
424
500
|
return _cachedConfig;
|
|
425
501
|
}
|
|
426
502
|
|
|
503
|
+
const SONARQUBE_COMMAND_PREFIX = "/sonarqube";
|
|
504
|
+
|
|
505
|
+
/** Text after "/sonarqube " in an autocomplete prefix, or undefined if it isn't a /sonarqube command. */
|
|
506
|
+
export function sonarqubeArgumentText(prefix: string): string | undefined {
|
|
507
|
+
if (!prefix.startsWith(SONARQUBE_COMMAND_PREFIX)) return undefined;
|
|
508
|
+
const rest = prefix.slice(SONARQUBE_COMMAND_PREFIX.length);
|
|
509
|
+
if (rest === "") return "";
|
|
510
|
+
const trimmed = rest.trimStart();
|
|
511
|
+
if (trimmed === rest) return undefined; // no whitespace after the command
|
|
512
|
+
return trimmed;
|
|
513
|
+
}
|
|
514
|
+
|
|
427
515
|
export default function sonarqube(pi: ExtensionAPI) {
|
|
428
516
|
const statesByBaseDir = new Map<string, SonarAnalysisState>();
|
|
429
517
|
let latestState: SonarAnalysisState | undefined;
|
|
@@ -445,16 +533,14 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
445
533
|
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
446
534
|
const line = lines[cursorLine] ?? "";
|
|
447
535
|
const beforeCursor = line.slice(0, cursorCol);
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
536
|
+
const argumentText = sonarqubeArgumentText(beforeCursor);
|
|
537
|
+
if (argumentText === undefined)
|
|
450
538
|
return current.getSuggestions(
|
|
451
539
|
lines,
|
|
452
540
|
cursorLine,
|
|
453
541
|
cursorCol,
|
|
454
542
|
options,
|
|
455
543
|
);
|
|
456
|
-
|
|
457
|
-
const argumentText = match[1] ?? "";
|
|
458
544
|
const { current: currentToken } =
|
|
459
545
|
splitSonarArgumentContext(argumentText);
|
|
460
546
|
const cfg = await cachedServerConfig(ctx.cwd);
|
|
@@ -516,8 +602,10 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
516
602
|
promptSnippet: "Run local SonarQube analysis or inspect issue results",
|
|
517
603
|
promptGuidelines: [
|
|
518
604
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
605
|
+
"The `path` parameter selects a configured target alias or SonarQube project root. Never pass a source file or subdirectory as `path`.",
|
|
606
|
+
"Use `pathScope` for a file or directory inside that project. Example: path: 'apps/web', pathScope: 'src/components/'.",
|
|
519
607
|
"Use the optional severity, status, type, rule, quality, and impact severity filters to fetch only the most relevant issues.",
|
|
520
|
-
"
|
|
608
|
+
"Prefer stable `issueKeys` over positional `issueIndex` when opening issue source. Multiple keys can be opened in one call.",
|
|
521
609
|
],
|
|
522
610
|
parameters: SonarToolParams,
|
|
523
611
|
|
|
@@ -533,6 +621,11 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
533
621
|
pathScope: params.pathScope,
|
|
534
622
|
});
|
|
535
623
|
assertFiltersNotAmbiguous(filters);
|
|
624
|
+
const issueKeys = normalizeIssueList(params.issueKeys);
|
|
625
|
+
assertIssueSelection(params.issueIndex, issueKeys);
|
|
626
|
+
if (issueKeys?.length && params.action !== "open") {
|
|
627
|
+
throw new Error("issueKeys can only be used with the open action.");
|
|
628
|
+
}
|
|
536
629
|
|
|
537
630
|
if (params.action === "analyze") {
|
|
538
631
|
const state = await analyzeProject(pi, ctx, params.path, filters);
|
|
@@ -551,7 +644,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
551
644
|
ctx,
|
|
552
645
|
statesByBaseDir,
|
|
553
646
|
params.path,
|
|
554
|
-
filters,
|
|
647
|
+
normalizeIssueFilters({ ...filters, issueKeys }),
|
|
555
648
|
);
|
|
556
649
|
if (!targetState) {
|
|
557
650
|
return {
|
|
@@ -576,6 +669,18 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
576
669
|
};
|
|
577
670
|
}
|
|
578
671
|
|
|
672
|
+
if (issueKeys?.length) {
|
|
673
|
+
const result = await buildIssueKeyPreviews(
|
|
674
|
+
targetState.baseDir,
|
|
675
|
+
issueKeys,
|
|
676
|
+
targetState.issues,
|
|
677
|
+
);
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text", text: result.text }],
|
|
680
|
+
details: result,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
579
684
|
const index = params.issueIndex ?? 1;
|
|
580
685
|
const issue = targetState.issues[index - 1];
|
|
581
686
|
if (!issue) {
|
|
@@ -612,6 +717,8 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
612
717
|
rules: args.rules,
|
|
613
718
|
softwareQualities: args.softwareQualities,
|
|
614
719
|
impactSeverities: args.impactSeverities,
|
|
720
|
+
issueKeys: args.issueKeys,
|
|
721
|
+
pathScope: args.pathScope,
|
|
615
722
|
});
|
|
616
723
|
const filterText = filters
|
|
617
724
|
? ` ${theme.fg("muted", issueFilterLabel(filters))}`
|
|
@@ -628,7 +735,12 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
628
735
|
renderResult(result, { expanded }, theme, _context) {
|
|
629
736
|
const state = result.details as
|
|
630
737
|
| SonarAnalysisState
|
|
631
|
-
| {
|
|
738
|
+
| {
|
|
739
|
+
error?: string;
|
|
740
|
+
selectedIssue?: SonarIssue;
|
|
741
|
+
selectedIssues?: SonarIssue[];
|
|
742
|
+
missingIssueKeys?: string[];
|
|
743
|
+
}
|
|
632
744
|
| undefined;
|
|
633
745
|
|
|
634
746
|
if (!state) {
|
|
@@ -640,13 +752,20 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
640
752
|
return renderErrorResult(state.error, theme);
|
|
641
753
|
}
|
|
642
754
|
|
|
755
|
+
const contentText =
|
|
756
|
+
result.content[0]?.type === "text" ? result.content[0].text : undefined;
|
|
757
|
+
|
|
643
758
|
if ("selectedIssue" in state && state.selectedIssue) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
759
|
+
return renderIssueResult(state.selectedIssue, contentText, theme);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if ("selectedIssues" in state && state.selectedIssues) {
|
|
763
|
+
return renderIssueBatchResult(
|
|
764
|
+
state.selectedIssues,
|
|
765
|
+
state.missingIssueKeys ?? [],
|
|
766
|
+
contentText,
|
|
767
|
+
theme,
|
|
768
|
+
);
|
|
650
769
|
}
|
|
651
770
|
|
|
652
771
|
if ("issues" in state) {
|
|
@@ -710,7 +829,6 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
710
829
|
return;
|
|
711
830
|
default:
|
|
712
831
|
await commandIssuesOrOpen(
|
|
713
|
-
pi,
|
|
714
832
|
ctx,
|
|
715
833
|
statesByBaseDir,
|
|
716
834
|
parsed,
|
|
@@ -930,47 +1048,63 @@ async function showDuplicationListOrBrowser(
|
|
|
930
1048
|
ctx.ui.notify("Duplication loaded into editor — press Enter to send", "info");
|
|
931
1049
|
}
|
|
932
1050
|
|
|
933
|
-
|
|
934
|
-
|
|
1051
|
+
type IssueCommandArgs = {
|
|
1052
|
+
targetInput?: string;
|
|
1053
|
+
filters?: SonarIssueFetchOptions;
|
|
1054
|
+
action: string;
|
|
1055
|
+
issueIndex?: number;
|
|
1056
|
+
issueKeys?: string[];
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
async function resolveIssueCommandState(
|
|
935
1060
|
ctx: ExtensionCommandContext,
|
|
936
1061
|
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
issueIndex?: number;
|
|
942
|
-
},
|
|
943
|
-
rememberState: (s: SonarAnalysisState) => void,
|
|
944
|
-
): Promise<void> {
|
|
945
|
-
let { targetInput } = parsed;
|
|
1062
|
+
targetInput: string | undefined,
|
|
1063
|
+
filters: SonarIssueFetchOptions | undefined,
|
|
1064
|
+
): Promise<SonarAnalysisState | undefined> {
|
|
1065
|
+
const originalTargetInput = targetInput;
|
|
946
1066
|
if (!targetInput) {
|
|
947
1067
|
const pickResult = await pickOrResolveTarget(ctx);
|
|
948
|
-
if (pickResult === null) return;
|
|
1068
|
+
if (pickResult === null) return undefined;
|
|
949
1069
|
targetInput = pickResult;
|
|
950
1070
|
}
|
|
951
1071
|
const targetState = await resolveTargetState(
|
|
952
1072
|
ctx,
|
|
953
1073
|
statesByBaseDir,
|
|
954
1074
|
targetInput,
|
|
955
|
-
|
|
1075
|
+
filters,
|
|
956
1076
|
);
|
|
957
|
-
if (!targetState) {
|
|
958
|
-
|
|
1077
|
+
if (!targetState && ctx.hasUI) {
|
|
1078
|
+
ctx.ui.notify(
|
|
1079
|
+
`No SonarQube analysis found for this target. Run /sonarqube analyze${targetLabel(originalTargetInput)} first.`,
|
|
1080
|
+
"warning",
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
return targetState;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function openIssueCommand(
|
|
1087
|
+
ctx: ExtensionCommandContext,
|
|
1088
|
+
targetState: SonarAnalysisState,
|
|
1089
|
+
parsed: IssueCommandArgs,
|
|
1090
|
+
): Promise<void> {
|
|
1091
|
+
if (parsed.issueKeys?.length) {
|
|
1092
|
+
const result = await buildIssueKeyPreviews(
|
|
1093
|
+
targetState.baseDir,
|
|
1094
|
+
parsed.issueKeys,
|
|
1095
|
+
targetState.issues,
|
|
1096
|
+
);
|
|
1097
|
+
if (ctx.mode === "tui") {
|
|
1098
|
+
await ctx.ui.editor("SonarQube issue previews", result.text);
|
|
1099
|
+
} else if (ctx.hasUI) {
|
|
959
1100
|
ctx.ui.notify(
|
|
960
|
-
|
|
961
|
-
"warning",
|
|
1101
|
+
result.text,
|
|
1102
|
+
result.missingIssueKeys.length ? "warning" : "info",
|
|
962
1103
|
);
|
|
963
1104
|
}
|
|
964
1105
|
return;
|
|
965
1106
|
}
|
|
966
|
-
rememberState(targetState);
|
|
967
|
-
|
|
968
|
-
if (parsed.action === "issues") {
|
|
969
|
-
await commandShowIssues(ctx, targetState);
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
1107
|
|
|
973
|
-
// "open"
|
|
974
1108
|
const index = parsed.issueIndex ?? 1;
|
|
975
1109
|
const issue = targetState.issues[index - 1];
|
|
976
1110
|
if (!issue) {
|
|
@@ -980,6 +1114,29 @@ async function commandIssuesOrOpen(
|
|
|
980
1114
|
await openIssuePreview(ctx, targetState, issue);
|
|
981
1115
|
}
|
|
982
1116
|
|
|
1117
|
+
async function commandIssuesOrOpen(
|
|
1118
|
+
ctx: ExtensionCommandContext,
|
|
1119
|
+
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
1120
|
+
parsed: IssueCommandArgs,
|
|
1121
|
+
rememberState: (s: SonarAnalysisState) => void,
|
|
1122
|
+
): Promise<void> {
|
|
1123
|
+
assertIssueSelection(parsed.issueIndex, parsed.issueKeys);
|
|
1124
|
+
const targetState = await resolveIssueCommandState(
|
|
1125
|
+
ctx,
|
|
1126
|
+
statesByBaseDir,
|
|
1127
|
+
parsed.targetInput,
|
|
1128
|
+
normalizeIssueFilters({ ...parsed.filters, issueKeys: parsed.issueKeys }),
|
|
1129
|
+
);
|
|
1130
|
+
if (!targetState) return;
|
|
1131
|
+
|
|
1132
|
+
rememberState(targetState);
|
|
1133
|
+
if (parsed.action === "issues") {
|
|
1134
|
+
await commandShowIssues(ctx, targetState);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
await openIssueCommand(ctx, targetState, parsed);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
983
1140
|
async function commandShowIssues(
|
|
984
1141
|
ctx: ExtensionCommandContext,
|
|
985
1142
|
targetState: SonarAnalysisState,
|
package/src/types.ts
CHANGED
|
@@ -86,6 +86,7 @@ export interface SonarIssueFetchOptions {
|
|
|
86
86
|
rules?: string[];
|
|
87
87
|
softwareQualities?: string[];
|
|
88
88
|
impactSeverities?: string[];
|
|
89
|
+
issueKeys?: string[];
|
|
89
90
|
pathScope?: string;
|
|
90
91
|
componentKeys?: string[];
|
|
91
92
|
}
|
|
@@ -114,6 +115,7 @@ export interface ResolvedTarget {
|
|
|
114
115
|
export interface ParsedSonarIssueArgs {
|
|
115
116
|
targetInput?: string;
|
|
116
117
|
issueIndex?: number;
|
|
118
|
+
issueKeys?: string[];
|
|
117
119
|
filters?: SonarIssueFetchOptions;
|
|
118
120
|
}
|
|
119
121
|
|