@jonathan0823/pi-sonarqube 0.2.0 → 0.3.2

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/api.ts CHANGED
@@ -8,12 +8,41 @@ import type {
8
8
  SonarIssueFetchOptions,
9
9
  SonarDuplicationMeasures,
10
10
  IssueSeverityCounts,
11
+ IssueQualityCounts,
11
12
  FileDuplication,
12
13
  DuplicationBlockGroup,
13
14
  } from "./types.js";
14
- import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES } from "./types.js";
15
+ import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES, SONAR_SOFTWARE_QUALITIES, SONAR_IMPACT_SEVERITIES } from "./types.js";
15
16
  import { parseProperties } from "./config.js";
16
17
 
18
+ // ── Clean code mode detection ────────────────────────────────────────────────
19
+
20
+ const cleanCodeModeCache = new Map<string, "STANDARD" | "MQR">();
21
+
22
+ export async function fetchCleanCodeMode(
23
+ serverUrl: string,
24
+ token?: string,
25
+ signal?: AbortSignal,
26
+ ): Promise<"STANDARD" | "MQR" | undefined> {
27
+ const cached = cleanCodeModeCache.get(serverUrl);
28
+ if (cached) return cached;
29
+ try {
30
+ const result = await fetchJson<{ mode: string }>(
31
+ `${serverUrl}/api/v2/clean-code-policy/mode`,
32
+ token,
33
+ signal,
34
+ );
35
+ const mode = result.mode?.toUpperCase();
36
+ if (mode === "MQR" || mode === "STANDARD") {
37
+ cleanCodeModeCache.set(serverUrl, mode);
38
+ return mode;
39
+ }
40
+ } catch {
41
+ // ponytail: fall through to legacy behavior when mode lookup is unavailable.
42
+ }
43
+ return undefined;
44
+ }
45
+
17
46
  // ── Auth / HTTP helpers ─────────────────────────────────────────────────────
18
47
 
19
48
  export function authHeader(token?: string): string | undefined {
@@ -37,7 +66,8 @@ export async function fetchJson<T>(url: string, token?: string, signal?: AbortSi
37
66
  return (await response.json()) as T;
38
67
  }
39
68
 
40
- export function toIssuePath(component: string, projectKey: string): string {
69
+ export function toIssuePath(component: string | undefined | null, projectKey: string): string {
70
+ if (typeof component !== "string" || component.length === 0) return projectKey;
41
71
  const prefix = `${projectKey}:`;
42
72
  if (component.startsWith(prefix)) return component.slice(prefix.length);
43
73
  const colon = component.indexOf(":");
@@ -61,8 +91,25 @@ export function normalizeIssueFilters(filters?: SonarIssueFetchOptions): SonarIs
61
91
  statuses: normalizeIssueList(filters?.statuses, true),
62
92
  types: normalizeIssueList(filters?.types, true),
63
93
  rules: normalizeIssueList(filters?.rules),
94
+ softwareQualities: normalizeIssueList(filters?.softwareQualities, true),
95
+ impactSeverities: normalizeIssueList(filters?.impactSeverities, true),
64
96
  };
65
- return normalized.severities || normalized.statuses || normalized.types || normalized.rules ? normalized : undefined;
97
+ return normalized.severities || normalized.statuses || normalized.types || normalized.rules || normalized.softwareQualities || normalized.impactSeverities
98
+ ? normalized
99
+ : undefined;
100
+ }
101
+
102
+ export function assertFiltersNotAmbiguous(filters?: SonarIssueFetchOptions): void {
103
+ if (!filters) return;
104
+ if (
105
+ (filters.severities?.length || filters.types?.length) &&
106
+ (filters.softwareQualities?.length || filters.impactSeverities?.length)
107
+ ) {
108
+ throw new Error(
109
+ "Ambiguous filter combination: mixing legacy filters (severity/type) with MQR filters (quality/impactSeverity) is not allowed. " +
110
+ "Use one filter family per command.",
111
+ );
112
+ }
66
113
  }
67
114
 
68
115
  const EXPLICIT_FILTER_RE = /^([^:=]+)[:=](.+)$/;
@@ -77,17 +124,38 @@ export function parseIssueFilterToken(token: string): Partial<SonarIssueFetchOpt
77
124
  const values = explicit[2].split(",").map((p) => p.trim()).filter(Boolean);
78
125
  if (values.length === 0) return undefined;
79
126
 
80
- if (key === "severity" || key === "severities") return { severities: values.map((v) => v.toUpperCase()) };
81
- if (key === "status" || key === "statuses") return { statuses: values.map((v) => v.toUpperCase()) };
82
- if (key === "type" || key === "types") return { types: values.map((v) => v.toUpperCase()) };
83
- if (key === "rule" || key === "rules") return { rules: values };
84
- return undefined;
127
+ switch (key) {
128
+ case "severity":
129
+ case "severities":
130
+ return { severities: values.map((v) => v.toUpperCase()) };
131
+ case "status":
132
+ case "statuses":
133
+ return { statuses: values.map((v) => v.toUpperCase()) };
134
+ case "type":
135
+ case "types":
136
+ return { types: values.map((v) => v.toUpperCase()) };
137
+ case "rule":
138
+ case "rules":
139
+ return { rules: values };
140
+ case "quality":
141
+ case "qualities":
142
+ case "softwarequality":
143
+ case "softwarequalities":
144
+ return { softwareQualities: values.map((v) => v.toUpperCase()) };
145
+ case "impactseverity":
146
+ case "impactseverities":
147
+ return { impactSeverities: values.map((v) => v.toUpperCase()) };
148
+ default:
149
+ return undefined;
150
+ }
85
151
  }
86
152
 
87
153
  const upper = trimmed.toUpperCase();
88
154
  if (SONAR_SEVERITIES.includes(upper as (typeof SONAR_SEVERITIES)[number])) return { severities: [upper] };
89
155
  if (SONAR_STATUSES.includes(upper as (typeof SONAR_STATUSES)[number])) return { statuses: [upper] };
90
156
  if (SONAR_TYPES.includes(upper as (typeof SONAR_TYPES)[number])) return { types: [upper] };
157
+ if (SONAR_SOFTWARE_QUALITIES.includes(upper as (typeof SONAR_SOFTWARE_QUALITIES)[number])) return { softwareQualities: [upper] };
158
+ if (SONAR_IMPACT_SEVERITIES.includes(upper as (typeof SONAR_IMPACT_SEVERITIES)[number])) return { impactSeverities: [upper] };
91
159
  return undefined;
92
160
  }
93
161
 
@@ -130,18 +198,21 @@ function mergeFilters(
130
198
  if (filter.statuses?.length) merged.statuses = [...(merged.statuses ?? []), ...filter.statuses];
131
199
  if (filter.types?.length) merged.types = [...(merged.types ?? []), ...filter.types];
132
200
  if (filter.rules?.length) merged.rules = [...(merged.rules ?? []), ...filter.rules];
201
+ if (filter.softwareQualities?.length) merged.softwareQualities = [...(merged.softwareQualities ?? []), ...filter.softwareQualities];
202
+ if (filter.impactSeverities?.length) merged.impactSeverities = [...(merged.impactSeverities ?? []), ...filter.impactSeverities];
133
203
  }
134
204
  return normalizeIssueFilters(merged);
135
205
  }
136
206
 
137
207
  export function issueFilterLabel(filters?: SonarIssueFetchOptions): string {
138
208
  if (!filters) return "";
139
- const parts = [
140
- filters.severities?.length ? `severities=${filters.severities.join(",")}` : "",
141
- filters.statuses?.length ? `statuses=${filters.statuses.join(",")}` : "",
142
- filters.types?.length ? `types=${filters.types.join(",")}` : "",
143
- filters.rules?.length ? `rules=${filters.rules.join(",")}` : "",
144
- ].filter(Boolean);
209
+ const parts: string[] = [];
210
+ if (filters.severities?.length) parts.push(`severities=${filters.severities.join(",")}`);
211
+ if (filters.statuses?.length) parts.push(`statuses=${filters.statuses.join(",")}`);
212
+ if (filters.types?.length) parts.push(`types=${filters.types.join(",")}`);
213
+ if (filters.rules?.length) parts.push(`rules=${filters.rules.join(",")}`);
214
+ if (filters.softwareQualities?.length) parts.push(`qualities=${filters.softwareQualities.join(",")}`);
215
+ if (filters.impactSeverities?.length) parts.push(`impactSeverities=${filters.impactSeverities.join(",")}`);
145
216
  return parts.join(" • ");
146
217
  }
147
218
 
@@ -150,10 +221,13 @@ function severitySortRank(severity: string): number {
150
221
  case "BLOCKER":
151
222
  return 0;
152
223
  case "CRITICAL":
224
+ case "HIGH":
153
225
  return 1;
154
226
  case "MAJOR":
227
+ case "MEDIUM":
155
228
  return 2;
156
229
  case "MINOR":
230
+ case "LOW":
157
231
  return 3;
158
232
  case "INFO":
159
233
  return 4;
@@ -328,6 +402,8 @@ function buildIssueSearchUrl(
328
402
  if (filters?.statuses?.length) url.searchParams.set("statuses", filters.statuses.join(","));
329
403
  if (filters?.types?.length) url.searchParams.set("types", filters.types.join(","));
330
404
  if (filters?.rules?.length) url.searchParams.set("rules", filters.rules.join(","));
405
+ if (filters?.softwareQualities?.length) url.searchParams.set("impactSoftwareQualities", filters.softwareQualities.join(","));
406
+ if (filters?.impactSeverities?.length) url.searchParams.set("impactSeverities", filters.impactSeverities.join(","));
331
407
  return url.toString();
332
408
  }
333
409
 
@@ -387,6 +463,7 @@ export async function fetchIssues(
387
463
  signal?: AbortSignal,
388
464
  filters?: SonarIssueFetchOptions,
389
465
  ): Promise<SonarIssue[]> {
466
+ assertFiltersNotAmbiguous(filters);
390
467
  const normalizedFilters = normalizeIssueFilters(filters);
391
468
  const issues: SonarIssue[] = [];
392
469
  const ruleNames = new Map<string, string | undefined>();
@@ -440,7 +517,7 @@ export async function fetchRuleName(
440
517
  export function createAnalysisState(
441
518
  config: Pick<SonarProjectConfig, "baseDir" | "serverUrl" | "projectKey">,
442
519
  issues: SonarIssue[],
443
- extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures">> = {},
520
+ extras: Partial<Pick<SonarAnalysisState, "dashboardUrl" | "ceTaskUrl" | "analysisId" | "filters" | "measures" | "cleanCodeMode">> = {},
444
521
  ): SonarAnalysisState {
445
522
  return {
446
523
  version: 1,
@@ -454,6 +531,7 @@ export function createAnalysisState(
454
531
  filters: extras.filters,
455
532
  totalIssues: issues.length,
456
533
  measures: extras.measures,
534
+ cleanCodeMode: extras.cleanCodeMode,
457
535
  issues,
458
536
  };
459
537
  }
@@ -467,7 +545,7 @@ export async function fetchDuplicationMeasures(
467
545
  signal?: AbortSignal,
468
546
  ): Promise<SonarDuplicationMeasures | undefined> {
469
547
  try {
470
- const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(projectKey)}&metricKeys=duplicated_lines_density,duplicated_lines,duplicated_blocks,duplicated_files`;
548
+ const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(projectKey)}&metricKeys=coverage,lines_to_cover,uncovered_lines,duplicated_lines_density,duplicated_lines,duplicated_blocks,duplicated_files`;
471
549
  const result = await fetchJson<{
472
550
  component: { measures: Array<{ metric: string; value: string }> };
473
551
  }>(url, token, signal);
@@ -476,11 +554,15 @@ export async function fetchDuplicationMeasures(
476
554
  const m = measures.find((m) => m.metric === key);
477
555
  return m ? Number.parseFloat(m.value) : 0;
478
556
  };
557
+ const hasMetric = (key: string): boolean => measures.some((m) => m.metric === key);
479
558
  return {
480
559
  duplicatedLinesDensity: getValue("duplicated_lines_density"),
481
560
  duplicatedLines: getValue("duplicated_lines"),
482
561
  duplicatedBlocks: getValue("duplicated_blocks"),
483
562
  duplicatedFiles: getValue("duplicated_files"),
563
+ coverage: hasMetric("coverage") ? getValue("coverage") : undefined,
564
+ linesToCover: hasMetric("lines_to_cover") ? getValue("lines_to_cover") : undefined,
565
+ uncoveredLines: hasMetric("uncovered_lines") ? getValue("uncovered_lines") : undefined,
484
566
  };
485
567
  } catch {
486
568
  return undefined;
@@ -492,24 +574,61 @@ export async function fetchIssueSeverityCounts(
492
574
  token: string | undefined,
493
575
  projectKey: string,
494
576
  signal?: AbortSignal,
577
+ mode?: "STANDARD" | "MQR",
495
578
  ): Promise<IssueSeverityCounts | undefined> {
496
579
  try {
497
- const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=severities`;
580
+ const facetKey = mode === "MQR" ? "impactSeverities" : "severities";
581
+ const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=${facetKey}`;
498
582
  const result = await fetchJson<{
499
583
  facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
500
584
  }>(url, token, signal);
501
- const facet = result.facets?.find((f) => f.property === "severities");
585
+ const facet = result.facets?.find((f) => f.property === facetKey);
502
586
  if (!facet) return undefined;
503
587
  const getCount = (severity: string): number => {
504
588
  const entry = facet.values.find((v) => v.val === severity);
505
589
  return entry?.count ?? 0;
506
590
  };
591
+ return mode === "MQR"
592
+ ? {
593
+ blocker: getCount("BLOCKER"),
594
+ critical: getCount("HIGH"),
595
+ major: getCount("MEDIUM"),
596
+ minor: getCount("LOW"),
597
+ info: getCount("INFO"),
598
+ }
599
+ : {
600
+ blocker: getCount("BLOCKER"),
601
+ critical: getCount("CRITICAL"),
602
+ major: getCount("MAJOR"),
603
+ minor: getCount("MINOR"),
604
+ info: getCount("INFO"),
605
+ };
606
+ } catch {
607
+ return undefined;
608
+ }
609
+ }
610
+
611
+ export async function fetchIssueQualityCounts(
612
+ serverUrl: string,
613
+ token: string | undefined,
614
+ projectKey: string,
615
+ signal?: AbortSignal,
616
+ ): Promise<IssueQualityCounts | undefined> {
617
+ try {
618
+ const url = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&resolved=false&ps=1&facets=impactSoftwareQualities`;
619
+ const result = await fetchJson<{
620
+ facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
621
+ }>(url, token, signal);
622
+ const facet = result.facets?.find((f) => f.property === "impactSoftwareQualities");
623
+ if (!facet) return undefined;
624
+ const getCount = (q: string): number => {
625
+ const entry = facet.values.find((v) => v.val === q);
626
+ return entry?.count ?? 0;
627
+ };
507
628
  return {
508
- blocker: getCount("BLOCKER"),
509
- critical: getCount("CRITICAL"),
510
- major: getCount("MAJOR"),
511
- minor: getCount("MINOR"),
512
- info: getCount("INFO"),
629
+ maintainability: getCount("MAINTAINABILITY"),
630
+ reliability: getCount("RELIABILITY"),
631
+ security: getCount("SECURITY"),
513
632
  };
514
633
  } catch {
515
634
  return undefined;
@@ -524,42 +643,35 @@ export async function fetchFileDuplications(
524
643
  projectKey: string,
525
644
  signal?: AbortSignal,
526
645
  ): Promise<FileDuplication[]> {
527
- // Get file keys from issues search facets
528
- const facetUrl = `${serverUrl}/api/issues/search?projects=${encodeURIComponent(projectKey)}&ps=1&facets=files`;
529
- const facetResult = await fetchJson<{
530
- facets: Array<{ property: string; values: Array<{ val: string; count: number }> }>;
531
- }>(facetUrl, token, signal);
532
-
533
- const fileFacet = facetResult.facets?.find((f) => f.property === "files");
534
- if (!fileFacet?.values?.length) return [];
535
-
536
- // Query per-file duplication measures. CATCH errors per file (404/403 = inaccessible)
537
- const results: Array<FileDuplication | null> = await Promise.all(
538
- fileFacet.values.map(async (entry) => {
539
- try {
540
- const fileKey = `${projectKey}:${entry.val}`;
541
- const url = `${serverUrl}/api/measures/component?component=${encodeURIComponent(fileKey)}&metricKeys=duplicated_lines,duplicated_blocks`;
542
- const res = await fetchJson<{
543
- component: { measures?: Array<{ metric: string; value: string }> };
544
- }>(url, token, signal);
545
- const measures = res.component.measures ?? [];
546
- const getValue = (metric: string): number => {
547
- const m = measures.find((m) => m.metric === metric);
548
- return m ? Number.parseInt(m.value, 10) : 0;
549
- };
550
- return {
551
- filePath: entry.val,
552
- fileKey: `${projectKey}:${entry.val}`,
553
- duplicatedLines: getValue("duplicated_lines"),
554
- duplicatedBlocks: getValue("duplicated_blocks"),
555
- };
556
- } catch {
557
- return null;
558
- }
559
- }),
560
- );
561
-
562
- return results.filter((f): f is FileDuplication => f !== null && f.duplicatedBlocks > 0);
646
+ const treeUrl = `${serverUrl}/api/measures/component_tree?component=${encodeURIComponent(projectKey)}&qualifiers=FIL&metricKeys=duplicated_lines,duplicated_blocks&ps=500`;
647
+ const treeResult = await fetchJson<{
648
+ components: Array<{
649
+ key: string;
650
+ path?: string;
651
+ name?: string;
652
+ measures?: Array<{ metric: string; value: string }>;
653
+ }>;
654
+ }>(treeUrl, token, signal);
655
+
656
+ const results = (treeResult.components ?? [])
657
+ .map((component) => {
658
+ const measures = component.measures ?? [];
659
+ const getValue = (metric: string): number => {
660
+ const m = measures.find((item) => item.metric === metric);
661
+ return m ? Number.parseInt(m.value, 10) : 0;
662
+ };
663
+ const duplicatedBlocks = getValue("duplicated_blocks");
664
+ if (duplicatedBlocks <= 0) return null;
665
+ return {
666
+ filePath: component.path ?? toIssuePath(component.key, projectKey) ?? component.name ?? component.key,
667
+ fileKey: component.key,
668
+ duplicatedLines: getValue("duplicated_lines"),
669
+ duplicatedBlocks,
670
+ };
671
+ })
672
+ .filter((f): f is FileDuplication => f !== null);
673
+
674
+ return results;
563
675
  }
564
676
 
565
677
  export async function fetchFileDuplicationBlocks(
package/src/commands.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AutocompleteItem, AutocompleteSuggestions } from "@earendil-works/pi-tui";
2
- import type { SonarIssue, SonarIssueFetchOptions, SonarDuplicationMeasures, IssueSeverityCounts, FileDuplication, DuplicationBlockGroup } from "./types.js";
3
- import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES } from "./types.js";
2
+ import type { SonarIssue, SonarIssueFetchOptions, SonarDuplicationMeasures, IssueSeverityCounts, IssueQualityCounts, FileDuplication, DuplicationBlockGroup } from "./types.js";
3
+ import { SONAR_SEVERITIES, SONAR_STATUSES, SONAR_TYPES, SONAR_SOFTWARE_QUALITIES, SONAR_IMPACT_SEVERITIES } from "./types.js";
4
4
  import { looksLikePath } from "./config.js";
5
5
  import { parseSonarIssueArgs, issueFilterLabel } from "./api.js";
6
6
 
@@ -69,23 +69,34 @@ export function splitSonarArgumentContext(
69
69
  };
70
70
  }
71
71
 
72
- function createFilterCompletionList(): AutocompleteItem[] {
73
- const entries: AutocompleteItem[] = [];
74
- const add = (value: string, desc: string) => {
75
- entries.push(
76
- { value: `severity:${value}`, label: `severity:${value}`, description: desc },
77
- { value, label: value, description: desc },
72
+ function createFilterCompletionList(mode?: "STANDARD" | "MQR"): AutocompleteItem[] {
73
+ const buildItems = (
74
+ groups: ReadonlyArray<readonly [string, readonly string[], string, boolean]>,
75
+ ): AutocompleteItem[] =>
76
+ groups.flatMap(([prefix, values, description, includeBare]) =>
77
+ values.flatMap((value) => [
78
+ { value: `${prefix}:${value}`, label: `${prefix}:${value}`, description },
79
+ ...(includeBare ? [{ value, label: value, description }] : []),
80
+ ]),
78
81
  );
79
- };
80
- for (const value of SONAR_SEVERITIES) add(value, "severity");
81
- for (const value of SONAR_STATUSES) add(value, "status");
82
- for (const value of SONAR_TYPES) add(value, "type");
83
- return entries;
82
+
83
+ const legacyGroups = [
84
+ ["severity", SONAR_SEVERITIES, "severity", true],
85
+ ["status", SONAR_STATUSES, "status", true],
86
+ ["type", SONAR_TYPES, "type", true],
87
+ ] as const;
88
+ const mqrGroups = [
89
+ ["quality", SONAR_SOFTWARE_QUALITIES, "software quality (MQR)", true],
90
+ ["impactSeverity", SONAR_IMPACT_SEVERITIES, "impact severity (MQR)", false],
91
+ ] as const;
92
+
93
+ return mode === "MQR" ? [...buildItems(mqrGroups), ...buildItems(legacyGroups)] : [...buildItems(legacyGroups), ...buildItems(mqrGroups)];
84
94
  }
85
95
 
86
96
  export function sonarArgumentCompletions(
87
97
  argumentText: string,
88
98
  issues?: SonarIssue[],
99
+ mode?: "STANDARD" | "MQR",
89
100
  ): AutocompleteItem[] | null {
90
101
  const { command, current, tokens } = splitSonarArgumentContext(argumentText);
91
102
  const lowerCommand = command.toLowerCase();
@@ -102,13 +113,13 @@ export function sonarArgumentCompletions(
102
113
  const lineSuffix = issue.line ? `:${issue.line}` : "";
103
114
  return { value: String(index + 1), label: String(index + 1), description: `${issue.severity} ${issue.filePath}${lineSuffix}` };
104
115
  });
105
- const suggestions = [...issueItems, ...createFilterCompletionList()];
116
+ const suggestions = [...issueItems, ...createFilterCompletionList(mode)];
106
117
  const filtered = filterAutocompleteItems(suggestions, current);
107
118
  return filtered.length > 0 ? filtered : null;
108
119
  }
109
120
 
110
121
  if (lowerCommand === "analyze" || lowerCommand === "issues") {
111
- const suggestions = createFilterCompletionList();
122
+ const suggestions = createFilterCompletionList(mode);
112
123
  const filtered = filterAutocompleteItems(suggestions, current);
113
124
  return filtered.length > 0 ? filtered : null;
114
125
  }
@@ -182,10 +193,16 @@ export function helpText(): string {
182
193
  "Filters:",
183
194
  " /sonarqube issues be CRITICAL",
184
195
  " /sonarqube issues be severity:CRITICAL status:OPEN",
196
+ " /sonarqube issues be quality:RELIABILITY",
197
+ " /sonarqube issues be quality:SECURITY impactSeverity:HIGH",
185
198
  "",
186
199
  "Autocomplete:",
187
200
  " type /sonarqube and press Tab to complete the subcommand or filters",
188
201
  "",
202
+ "Notes:",
203
+ " Legacy filters (severity, type) and MQR filters (quality, impactSeverity)",
204
+ " cannot be combined in the same command. Use one filter family per query.",
205
+ "",
189
206
  "Defaults:",
190
207
  " config is stored in .pi/sonarqube.json",
191
208
  " use project paths directly (no alias needed)",
@@ -213,10 +230,22 @@ export function formatMetricsOutput(metrics: {
213
230
  projectKey: string;
214
231
  measures?: SonarDuplicationMeasures;
215
232
  issueCounts?: IssueSeverityCounts;
233
+ issueQualityCounts?: IssueQualityCounts;
234
+ cleanCodeMode?: "STANDARD" | "MQR";
216
235
  }): string {
217
236
  const lines: string[] = [`Metrics for ${metrics.projectKey}`];
218
237
 
219
238
  if (metrics.measures) {
239
+ if (metrics.measures.coverage === undefined) {
240
+ lines.push("Coverage: n/a");
241
+ } else {
242
+ const pct = metrics.measures.coverage.toFixed(1);
243
+ const covered = (metrics.measures.linesToCover ?? 0) - (metrics.measures.uncoveredLines ?? 0);
244
+ const uncovered = metrics.measures.uncoveredLines ?? 0;
245
+ const total = metrics.measures.linesToCover ?? 0;
246
+ lines.push(`Coverage: ${pct}% covered=${covered} uncovered=${uncovered} lines=${total}`);
247
+ }
248
+
220
249
  const density = metrics.measures.duplicatedLinesDensity.toFixed(1);
221
250
  lines.push(
222
251
  metrics.measures.duplicatedBlocks > 0
@@ -226,14 +255,31 @@ export function formatMetricsOutput(metrics: {
226
255
  }
227
256
 
228
257
  if (metrics.issueCounts) {
258
+ const counts = metrics.cleanCodeMode === "MQR"
259
+ ? [
260
+ `BLOCKER ${metrics.issueCounts.blocker}`,
261
+ `HIGH ${metrics.issueCounts.critical}`,
262
+ `MEDIUM ${metrics.issueCounts.major}`,
263
+ `LOW ${metrics.issueCounts.minor}`,
264
+ `INFO ${metrics.issueCounts.info}`,
265
+ ]
266
+ : [
267
+ `BLOCKER ${metrics.issueCounts.blocker}`,
268
+ `CRITICAL ${metrics.issueCounts.critical}`,
269
+ `MAJOR ${metrics.issueCounts.major}`,
270
+ `MINOR ${metrics.issueCounts.minor}`,
271
+ `INFO ${metrics.issueCounts.info}`,
272
+ ];
273
+ lines.push(`Issues: ${counts.join(" ")}`);
274
+ }
275
+
276
+ if (metrics.issueQualityCounts) {
229
277
  const counts = [
230
- `BLOCKER ${metrics.issueCounts.blocker}`,
231
- `CRITICAL ${metrics.issueCounts.critical}`,
232
- `MAJOR ${metrics.issueCounts.major}`,
233
- `MINOR ${metrics.issueCounts.minor}`,
234
- `INFO ${metrics.issueCounts.info}`,
278
+ `MAINTAINABILITY ${metrics.issueQualityCounts.maintainability}`,
279
+ `RELIABILITY ${metrics.issueQualityCounts.reliability}`,
280
+ `SECURITY ${metrics.issueQualityCounts.security}`,
235
281
  ];
236
- lines.push(`Issues: ${counts.join(" ")}`);
282
+ lines.push(`Quality: ${counts.join(" ")}`);
237
283
  }
238
284
 
239
285
  return lines.join("\n");