@jonathan0823/pi-sonarqube 0.3.2 → 0.5.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/types.ts CHANGED
@@ -41,6 +41,7 @@ export interface FileDuplication {
41
41
  filePath: string;
42
42
  fileKey: string;
43
43
  duplicatedLines: number;
44
+ duplicatedLinesDensity: number;
44
45
  duplicatedBlocks: number;
45
46
  }
46
47
 
@@ -119,8 +120,30 @@ export interface InitCommandOptions {
119
120
 
120
121
  // ── Constants shared by adapter and extension ───────────────────────────────
121
122
 
122
- export const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"] as const;
123
- export const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"] as const;
123
+ export const SONAR_SEVERITIES = [
124
+ "BLOCKER",
125
+ "CRITICAL",
126
+ "MAJOR",
127
+ "MINOR",
128
+ "INFO",
129
+ ] as const;
130
+ export const SONAR_STATUSES = [
131
+ "OPEN",
132
+ "CONFIRMED",
133
+ "REOPENED",
134
+ "RESOLVED",
135
+ "CLOSED",
136
+ ] as const;
124
137
  export const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
125
- export const SONAR_SOFTWARE_QUALITIES = ["MAINTAINABILITY", "RELIABILITY", "SECURITY"] as const;
126
- export const SONAR_IMPACT_SEVERITIES = ["BLOCKER", "HIGH", "MEDIUM", "LOW", "INFO"] as const;
138
+ export const SONAR_SOFTWARE_QUALITIES = [
139
+ "MAINTAINABILITY",
140
+ "RELIABILITY",
141
+ "SECURITY",
142
+ ] as const;
143
+ export const SONAR_IMPACT_SEVERITIES = [
144
+ "BLOCKER",
145
+ "HIGH",
146
+ "MEDIUM",
147
+ "LOW",
148
+ "INFO",
149
+ ] as const;
package/src/ui.ts CHANGED
@@ -1,14 +1,34 @@
1
- import type { Theme, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ Theme,
3
+ ExtensionContext,
4
+ ExtensionCommandContext,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
7
  import { readFile } from "node:fs/promises";
4
8
  import { resolve } from "node:path";
5
- import type { SonarIssue, SonarAnalysisState, SonarIssueFetchOptions, FileDuplication, DuplicationBlockGroup } from "./types.js";
6
- import { issueFilterLabel, fetchIssues, createAnalysisState, normalizeIssueFilters, fetchCleanCodeMode } from "./api.js";
9
+ import type {
10
+ SonarIssue,
11
+ SonarAnalysisState,
12
+ SonarIssueFetchOptions,
13
+ FileDuplication,
14
+ DuplicationBlockGroup,
15
+ } from "./types.js";
16
+ import {
17
+ issueFilterLabel,
18
+ fetchIssues,
19
+ createAnalysisState,
20
+ normalizeIssueFilters,
21
+ fetchCleanCodeMode,
22
+ } from "./api.js";
7
23
  import { resolveConfig, resolveTarget } from "./config.js";
8
24
 
9
25
  // ── Issue preview ───────────────────────────────────────────────────────────
10
26
 
11
- export async function buildIssuePreview(baseDir: string, issue: SonarIssue, radius = 3): Promise<string> {
27
+ export async function buildIssuePreview(
28
+ baseDir: string,
29
+ issue: SonarIssue,
30
+ radius = 3,
31
+ ): Promise<string> {
12
32
  const filePath = resolve(baseDir, issue.filePath);
13
33
  const content = await readFile(filePath, "utf8");
14
34
  const lines = content.split(/\r?\n/);
@@ -40,7 +60,11 @@ export async function buildDuplicationPreview(
40
60
  cache.set(absolutePath, content);
41
61
  return content.split(/\r?\n/);
42
62
  };
43
- const renderRange = (lines: string[], from: number, size: number): string[] => {
63
+ const renderRange = (
64
+ lines: string[],
65
+ from: number,
66
+ size: number,
67
+ ): string[] => {
44
68
  const start = Math.max(1, from);
45
69
  const end = Math.min(lines.length, from + size - 1);
46
70
  const width = String(end).length;
@@ -76,7 +100,18 @@ export async function buildDuplicationPreview(
76
100
 
77
101
  // ── Analysis UI ─────────────────────────────────────────────────────────────
78
102
 
79
- const ANALYSIS_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
103
+ const ANALYSIS_SPINNER = [
104
+ "⠋",
105
+ "⠙",
106
+ "⠹",
107
+ "⠸",
108
+ "⠼",
109
+ "⠴",
110
+ "⠦",
111
+ "⠧",
112
+ "⠇",
113
+ "⠏",
114
+ ] as const;
80
115
  const ANALYSIS_WIDGET_KEY = "sonarqube-analysis";
81
116
 
82
117
  export interface AnalysisUiHandle {
@@ -84,16 +119,25 @@ export interface AnalysisUiHandle {
84
119
  stop(): void;
85
120
  }
86
121
 
87
- export function startAnalysisUi(ctx: ExtensionContext, projectKey: string): AnalysisUiHandle {
122
+ export function startAnalysisUi(
123
+ ctx: ExtensionContext,
124
+ projectKey: string,
125
+ ): AnalysisUiHandle {
88
126
  let frameIndex = 0;
89
127
  let phase = "Starting...";
90
128
  const render = () => {
91
129
  const frame = ANALYSIS_SPINNER[frameIndex];
92
- ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [`${frame} SonarQube ${projectKey}`, phase]);
130
+ ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [
131
+ `${frame} SonarQube ${projectKey}`,
132
+ phase,
133
+ ]);
93
134
  };
94
135
 
95
136
  ctx.ui.setWorkingMessage(`Analyzing ${projectKey}...`);
96
- ctx.ui.setWorkingIndicator({ frames: ANALYSIS_SPINNER as unknown as string[], intervalMs: 80 });
137
+ ctx.ui.setWorkingIndicator({
138
+ frames: ANALYSIS_SPINNER as unknown as string[],
139
+ intervalMs: 80,
140
+ });
97
141
  render();
98
142
 
99
143
  const timer = setInterval(() => {
@@ -129,11 +173,18 @@ export class IssueBrowser {
129
173
  invalidate(): void {} // NOSONAR - required by TUI interface
130
174
 
131
175
  handleInput(data: string): void {
132
- this.selected = handleListBrowserInput(data, this.selected, this.state.issues.length, this.done);
176
+ this.selected = handleListBrowserInput(
177
+ data,
178
+ this.selected,
179
+ this.state.issues.length,
180
+ this.done,
181
+ );
133
182
  }
134
183
 
135
184
  render(width: number): string[] {
136
- const filterSuffix = this.state.filters ? ` • ${issueFilterLabel(this.state.filters)}` : "";
185
+ const filterSuffix = this.state.filters
186
+ ? ` • ${issueFilterLabel(this.state.filters)}`
187
+ : "";
137
188
  return renderListBrowser(this.theme, width, {
138
189
  title: "SonarQube Issues",
139
190
  subtitle: `${this.state.projectKey} • ${this.state.totalIssues} issue(s)${filterSuffix}`,
@@ -143,9 +194,15 @@ export class IssueBrowser {
143
194
  emptyMessage: "No open issues found.",
144
195
  footer: "Up/Down to move, Enter to preview, Esc to close",
145
196
  renderItem: (issue, issueIndex, isSelected) => {
146
- const marker = isSelected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
147
- const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
148
- const rule = issue.ruleName ? `${issue.rule} (${issue.ruleName})` : issue.rule;
197
+ const marker = isSelected
198
+ ? this.theme.fg("accent", ">")
199
+ : this.theme.fg("dim", " ");
200
+ const location = issue.line
201
+ ? `${issue.filePath}:${issue.line}`
202
+ : issue.filePath;
203
+ const rule = issue.ruleName
204
+ ? `${issue.rule} (${issue.ruleName})`
205
+ : issue.rule;
149
206
  const severity = severityColor(this.theme, issue.severity);
150
207
  return [
151
208
  marker,
@@ -199,7 +256,11 @@ function handleListBrowserInput(
199
256
  return selected;
200
257
  }
201
258
 
202
- function getBrowserWindow(selected: number, total: number, pageSize: number): { start: number; end: number } {
259
+ function getBrowserWindow(
260
+ selected: number,
261
+ total: number,
262
+ pageSize: number,
263
+ ): { start: number; end: number } {
203
264
  const halfWindow = Math.floor(pageSize / 2);
204
265
  const maxStart = Math.max(0, total - pageSize);
205
266
  const start = Math.max(0, Math.min(selected - halfWindow, maxStart));
@@ -230,7 +291,11 @@ function renderListBrowser<T>(
230
291
  ];
231
292
 
232
293
  const pageSize = Math.min(config.pageSize, config.items.length);
233
- const { start, end } = getBrowserWindow(config.selected, config.items.length, pageSize);
294
+ const { start, end } = getBrowserWindow(
295
+ config.selected,
296
+ config.items.length,
297
+ pageSize,
298
+ );
234
299
  const visible = config.items.slice(start, end);
235
300
 
236
301
  if (visible.length === 0) {
@@ -243,14 +308,26 @@ function renderListBrowser<T>(
243
308
  }
244
309
 
245
310
  if (start > 0) {
246
- lines.push(truncateToWidth(theme.fg("dim", `... ${start} more above`), width));
311
+ lines.push(
312
+ truncateToWidth(theme.fg("dim", `... ${start} more above`), width),
313
+ );
247
314
  }
248
315
  for (const [offset, item] of visible.entries()) {
249
316
  const index = start + offset;
250
- lines.push(truncateToWidth(config.renderItem(item, index, index === config.selected), width));
317
+ lines.push(
318
+ truncateToWidth(
319
+ config.renderItem(item, index, index === config.selected),
320
+ width,
321
+ ),
322
+ );
251
323
  }
252
324
  if (end < config.items.length) {
253
- lines.push(truncateToWidth(theme.fg("dim", `... ${config.items.length - end} more below`), width));
325
+ lines.push(
326
+ truncateToWidth(
327
+ theme.fg("dim", `... ${config.items.length - end} more below`),
328
+ width,
329
+ ),
330
+ );
254
331
  }
255
332
 
256
333
  lines.push("", truncateToWidth(theme.fg("dim", config.footer), width));
@@ -271,7 +348,12 @@ export class DuplicationBrowser {
271
348
  invalidate(): void {} // NOSONAR - required by TUI interface
272
349
 
273
350
  handleInput(data: string): void {
274
- this.selected = handleListBrowserInput(data, this.selected, this.files.length, this.done);
351
+ this.selected = handleListBrowserInput(
352
+ data,
353
+ this.selected,
354
+ this.files.length,
355
+ this.done,
356
+ );
275
357
  }
276
358
 
277
359
  render(width: number): string[] {
@@ -284,8 +366,10 @@ export class DuplicationBrowser {
284
366
  emptyMessage: "No duplicate code found.",
285
367
  footer: "Up/Down to move, Enter for details, Esc to close",
286
368
  renderItem: (file, index, isSelected) => {
287
- const marker = isSelected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
288
- const detail = `blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
369
+ const marker = isSelected
370
+ ? this.theme.fg("accent", ">")
371
+ : this.theme.fg("dim", " ");
372
+ const detail = `dup%=${file.duplicatedLinesDensity.toFixed(1)} blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
289
373
  return `${marker} ${String(index + 1).padStart(2, " ")}. ${this.theme.fg("accent", file.filePath)} ${this.theme.fg("dim", detail)}`;
290
374
  },
291
375
  });
@@ -297,7 +381,9 @@ export async function showDuplicationBrowser(
297
381
  files: FileDuplication[],
298
382
  ): Promise<number | null> {
299
383
  if (ctx.mode !== "tui" || files.length === 0) return null;
300
- return await ctx.ui.custom<number | null>((_tui, theme, _kb, done) => new DuplicationBrowser(files, theme, done));
384
+ return await ctx.ui.custom<number | null>(
385
+ (_tui, theme, _kb, done) => new DuplicationBrowser(files, theme, done),
386
+ );
301
387
  }
302
388
 
303
389
  // ── Issue browser / preview helpers (used by index.ts) ──────────────────────
@@ -308,7 +394,9 @@ export async function showIssueBrowser(
308
394
  ): Promise<number | null> {
309
395
  if (ctx.mode !== "tui") return null;
310
396
  if (state.issues.length === 0) return null;
311
- return await ctx.ui.custom<number | null>((_tui, theme, _kb, done) => new IssueBrowser(state, theme, done));
397
+ return await ctx.ui.custom<number | null>(
398
+ (_tui, theme, _kb, done) => new IssueBrowser(state, theme, done),
399
+ );
312
400
  }
313
401
 
314
402
  export async function openIssuePreview(
@@ -341,8 +429,15 @@ export async function loadProjectIssuesFromApi(
341
429
  ctx.signal,
342
430
  normalizedFilters,
343
431
  );
344
- const cleanCodeMode = await fetchCleanCodeMode(config.serverUrl, config.token, ctx.signal);
345
- return createAnalysisState(config, issues, { filters: normalizedFilters, cleanCodeMode });
432
+ const cleanCodeMode = await fetchCleanCodeMode(
433
+ config.serverUrl,
434
+ config.token,
435
+ ctx.signal,
436
+ );
437
+ return createAnalysisState(config, issues, {
438
+ filters: normalizedFilters,
439
+ cleanCodeMode,
440
+ });
346
441
  }
347
442
 
348
443
  export async function resolveTargetState(