@jonathan0823/pi-sonarqube 0.8.1 → 0.9.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 +21 -11
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +44 -10
- 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 +119 -42
- 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 +5 -5
- package/src/api.ts +54 -10
- package/src/commands.ts +34 -18
- package/src/config.ts +36 -13
- package/src/index.ts +208 -54
- 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,
|
|
@@ -921,59 +1039,72 @@ async function showDuplicationListOrBrowser(
|
|
|
921
1039
|
config.projectKey,
|
|
922
1040
|
ctx.signal,
|
|
923
1041
|
);
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
ctx.ui.setEditorText(lines.join("\n"));
|
|
1042
|
+
const text = await buildDuplicationPreview(
|
|
1043
|
+
config.baseDir,
|
|
1044
|
+
file.filePath,
|
|
1045
|
+
groups,
|
|
1046
|
+
);
|
|
1047
|
+
ctx.ui.setEditorText(text);
|
|
933
1048
|
ctx.ui.notify("Duplication loaded into editor — press Enter to send", "info");
|
|
934
1049
|
}
|
|
935
1050
|
|
|
936
|
-
|
|
937
|
-
|
|
1051
|
+
type IssueCommandArgs = {
|
|
1052
|
+
targetInput?: string;
|
|
1053
|
+
filters?: SonarIssueFetchOptions;
|
|
1054
|
+
action: string;
|
|
1055
|
+
issueIndex?: number;
|
|
1056
|
+
issueKeys?: string[];
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
async function resolveIssueCommandState(
|
|
938
1060
|
ctx: ExtensionCommandContext,
|
|
939
1061
|
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
issueIndex?: number;
|
|
945
|
-
},
|
|
946
|
-
rememberState: (s: SonarAnalysisState) => void,
|
|
947
|
-
): Promise<void> {
|
|
948
|
-
let { targetInput } = parsed;
|
|
1062
|
+
targetInput: string | undefined,
|
|
1063
|
+
filters: SonarIssueFetchOptions | undefined,
|
|
1064
|
+
): Promise<SonarAnalysisState | undefined> {
|
|
1065
|
+
const originalTargetInput = targetInput;
|
|
949
1066
|
if (!targetInput) {
|
|
950
1067
|
const pickResult = await pickOrResolveTarget(ctx);
|
|
951
|
-
if (pickResult === null) return;
|
|
1068
|
+
if (pickResult === null) return undefined;
|
|
952
1069
|
targetInput = pickResult;
|
|
953
1070
|
}
|
|
954
1071
|
const targetState = await resolveTargetState(
|
|
955
1072
|
ctx,
|
|
956
1073
|
statesByBaseDir,
|
|
957
1074
|
targetInput,
|
|
958
|
-
|
|
1075
|
+
filters,
|
|
959
1076
|
);
|
|
960
|
-
if (!targetState) {
|
|
961
|
-
|
|
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) {
|
|
962
1100
|
ctx.ui.notify(
|
|
963
|
-
|
|
964
|
-
"warning",
|
|
1101
|
+
result.text,
|
|
1102
|
+
result.missingIssueKeys.length ? "warning" : "info",
|
|
965
1103
|
);
|
|
966
1104
|
}
|
|
967
1105
|
return;
|
|
968
1106
|
}
|
|
969
|
-
rememberState(targetState);
|
|
970
|
-
|
|
971
|
-
if (parsed.action === "issues") {
|
|
972
|
-
await commandShowIssues(ctx, targetState);
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
1107
|
|
|
976
|
-
// "open"
|
|
977
1108
|
const index = parsed.issueIndex ?? 1;
|
|
978
1109
|
const issue = targetState.issues[index - 1];
|
|
979
1110
|
if (!issue) {
|
|
@@ -983,6 +1114,29 @@ async function commandIssuesOrOpen(
|
|
|
983
1114
|
await openIssuePreview(ctx, targetState, issue);
|
|
984
1115
|
}
|
|
985
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
|
+
|
|
986
1140
|
async function commandShowIssues(
|
|
987
1141
|
ctx: ExtensionCommandContext,
|
|
988
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
|
|