opencode-sonarqube 1.3.0 → 1.4.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.
Files changed (2) hide show
  1. package/dist/index.js +356 -247
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3999,47 +3999,32 @@ function numberToRating(num) {
3999
3999
  const ratings = ["A", "B", "C", "D", "E"];
4000
4000
  return ratings[num - 1] ?? "?";
4001
4001
  }
4002
- function SonarQubeError(message, code, statusCode) {
4002
+ function createSonarQubeError(name, message, code, statusCode) {
4003
4003
  const error45 = new Error(message);
4004
- error45.name = "SonarQubeError";
4004
+ error45.name = name;
4005
4005
  error45.code = code;
4006
4006
  error45.statusCode = statusCode;
4007
4007
  return error45;
4008
4008
  }
4009
+ function SonarQubeError(message, code, statusCode) {
4010
+ return createSonarQubeError("SonarQubeError", message, code, statusCode);
4011
+ }
4009
4012
  function ConnectionError(message) {
4010
- const error45 = new Error(message);
4011
- error45.name = "ConnectionError";
4012
- error45.code = "CONNECTION_ERROR";
4013
- return error45;
4013
+ return createSonarQubeError("ConnectionError", message, "CONNECTION_ERROR");
4014
4014
  }
4015
4015
  function AuthenticationError(message) {
4016
- const error45 = new Error(message);
4017
- error45.name = "AuthenticationError";
4018
- error45.code = "AUTH_ERROR";
4019
- error45.statusCode = 401;
4020
- return error45;
4016
+ return createSonarQubeError("AuthenticationError", message, "AUTH_ERROR", 401);
4021
4017
  }
4022
4018
  function ProjectNotFoundError(projectKey) {
4023
- const error45 = new Error(`Project '${projectKey}' not found`);
4024
- error45.name = "ProjectNotFoundError";
4025
- error45.code = "PROJECT_NOT_FOUND";
4026
- error45.statusCode = 404;
4027
- return error45;
4019
+ return createSonarQubeError("ProjectNotFoundError", `Project '${projectKey}' not found`, "PROJECT_NOT_FOUND", 404);
4028
4020
  }
4029
4021
  function RateLimitError(retryAfter) {
4030
4022
  const hasRetryAfter = retryAfter !== undefined && retryAfter > 0;
4031
4023
  const retryMessage = hasRetryAfter ? `. Retry after ${retryAfter}s` : "";
4032
- const error45 = new Error(`Rate limit exceeded${retryMessage}`);
4033
- error45.name = "RateLimitError";
4034
- error45.code = "RATE_LIMIT";
4035
- error45.statusCode = 429;
4036
- return error45;
4024
+ return createSonarQubeError("RateLimitError", `Rate limit exceeded${retryMessage}`, "RATE_LIMIT", 429);
4037
4025
  }
4038
4026
  function SetupError(message) {
4039
- const error45 = new Error(message);
4040
- error45.name = "SetupError";
4041
- error45.code = "SETUP_ERROR";
4042
- return error45;
4027
+ return createSonarQubeError("SetupError", message, "SETUP_ERROR");
4043
4028
  }
4044
4029
  var SonarQubeConfigSchema, ProjectStateSchema, ENTERPRISE_THRESHOLDS, STANDARD_THRESHOLDS, RELAXED_THRESHOLDS;
4045
4030
  var init_types2 = __esm(() => {
@@ -4409,31 +4394,14 @@ async function parsePackageJson(directory) {
4409
4394
  }
4410
4395
  function detectLanguages(checks3) {
4411
4396
  const languages = [];
4412
- if (checks3.hasPackageJson) {
4413
- if (checks3.hasTsConfig) {
4414
- languages.push("typescript", "javascript");
4415
- } else {
4416
- languages.push("javascript");
4397
+ for (const detector of LANGUAGE_DETECTORS) {
4398
+ if (detector.check(checks3)) {
4399
+ languages.push(detector.name);
4400
+ if (detector.includes) {
4401
+ languages.push(...detector.includes);
4402
+ }
4417
4403
  }
4418
4404
  }
4419
- if (checks3.hasRequirements || checks3.hasPyproject) {
4420
- languages.push("python");
4421
- }
4422
- if (checks3.hasPom || checks3.hasGradle) {
4423
- languages.push("java");
4424
- }
4425
- if (checks3.hasGoMod) {
4426
- languages.push("go");
4427
- }
4428
- if (checks3.hasCargo) {
4429
- languages.push("rust");
4430
- }
4431
- if (checks3.hasComposer) {
4432
- languages.push("php");
4433
- }
4434
- if (checks3.hasGemfile) {
4435
- languages.push("ruby");
4436
- }
4437
4405
  return languages.length > 0 ? languages : ["generic"];
4438
4406
  }
4439
4407
  function detectSourceDirectories(directory) {
@@ -4470,11 +4438,25 @@ async function detectProjectType(directory) {
4470
4438
  });
4471
4439
  return result;
4472
4440
  }
4473
- var logger2;
4441
+ var logger2, LANGUAGE_DETECTORS;
4474
4442
  var init_detection = __esm(() => {
4475
4443
  init_logger();
4476
4444
  init_patterns();
4477
4445
  logger2 = new Logger("scanner-config");
4446
+ LANGUAGE_DETECTORS = [
4447
+ {
4448
+ name: "typescript",
4449
+ check: (c) => c.hasPackageJson && c.hasTsConfig,
4450
+ includes: ["javascript"]
4451
+ },
4452
+ { name: "javascript", check: (c) => c.hasPackageJson && !c.hasTsConfig },
4453
+ { name: "python", check: (c) => c.hasRequirements || c.hasPyproject },
4454
+ { name: "java", check: (c) => c.hasPom || c.hasGradle },
4455
+ { name: "go", check: (c) => c.hasGoMod },
4456
+ { name: "rust", check: (c) => c.hasCargo },
4457
+ { name: "php", check: (c) => c.hasComposer },
4458
+ { name: "ruby", check: (c) => c.hasGemfile }
4459
+ ];
4478
4460
  });
4479
4461
 
4480
4462
  // src/scanner/config/properties.ts
@@ -4573,6 +4555,21 @@ class BaseAPI {
4573
4555
  this.client = client;
4574
4556
  this.logger = logger4 ?? new Logger(serviceName);
4575
4557
  }
4558
+ validateProjectKey(projectKey, methodName) {
4559
+ if (!projectKey) {
4560
+ this.logger.error(`${methodName}: projectKey is empty/undefined!`);
4561
+ return false;
4562
+ }
4563
+ return true;
4564
+ }
4565
+ async safeRequest(request, fallback, context) {
4566
+ try {
4567
+ return await request();
4568
+ } catch (error45) {
4569
+ this.logger.warn(`${context}: ${error45 instanceof Error ? error45.message : String(error45)}`);
4570
+ return fallback;
4571
+ }
4572
+ }
4576
4573
  }
4577
4574
  var init_base_api = __esm(() => {
4578
4575
  init_logger();
@@ -17553,9 +17550,39 @@ init_base_api();
17553
17550
  init_client();
17554
17551
  init_projects();
17555
17552
 
17553
+ // src/utils/paths.ts
17554
+ function extractFilePathFromComponentKey(componentKey) {
17555
+ const colonIndex = componentKey.indexOf(":");
17556
+ if (colonIndex >= 0) {
17557
+ return componentKey.substring(colonIndex + 1);
17558
+ }
17559
+ return componentKey;
17560
+ }
17561
+ function shortenPath(path, maxLength = 50) {
17562
+ if (path.length <= maxLength)
17563
+ return path;
17564
+ return "..." + path.slice(-(maxLength - 3));
17565
+ }
17566
+
17556
17567
  // src/api/issues.ts
17557
17568
  init_base_api();
17558
17569
 
17570
+ // src/constants.ts
17571
+ var API = {
17572
+ DEFAULT_PAGE_SIZE: 500,
17573
+ DEFAULT_LIMIT: 20,
17574
+ MAX_PAGES: 10,
17575
+ POLL_INTERVAL_MS: 2000,
17576
+ ANALYSIS_TIMEOUT_MS: 300000,
17577
+ SOURCE_CONTEXT_LINES: 5
17578
+ };
17579
+ var RATINGS = ["A", "B", "C", "D", "E"];
17580
+ function numericToRating(value) {
17581
+ const index = Math.min(Math.max(Math.round(value) - 1, 0), 4);
17582
+ return RATINGS[index];
17583
+ }
17584
+
17585
+ // src/api/issues.ts
17559
17586
  class IssuesAPI extends BaseAPI {
17560
17587
  constructor(client, logger4) {
17561
17588
  super(client, "sonarqube-issues", logger4);
@@ -17570,7 +17597,7 @@ class IssuesAPI extends BaseAPI {
17570
17597
  resolved: options.resolved === undefined ? undefined : String(options.resolved),
17571
17598
  branch: options.onBranch,
17572
17599
  inNewCodePeriod: options.inNewCode === undefined ? undefined : String(options.inNewCode),
17573
- ps: options.pageSize ?? 500,
17600
+ ps: options.pageSize ?? API.DEFAULT_PAGE_SIZE,
17574
17601
  p: options.page ?? 1
17575
17602
  });
17576
17603
  }
@@ -17585,7 +17612,7 @@ class IssuesAPI extends BaseAPI {
17585
17612
  types: options.types,
17586
17613
  resolved: false,
17587
17614
  page,
17588
- pageSize: 500
17615
+ pageSize: API.DEFAULT_PAGE_SIZE
17589
17616
  });
17590
17617
  allIssues.push(...response.issues);
17591
17618
  const totalPages = Math.ceil(response.paging.total / response.paging.pageSize);
@@ -17621,11 +17648,7 @@ class IssuesAPI extends BaseAPI {
17621
17648
  }
17622
17649
  return issues.map((issue2) => {
17623
17650
  const componentPath = componentPaths.get(issue2.component);
17624
- let filePath = componentPath ?? issue2.component;
17625
- const colonIndex = filePath.indexOf(":");
17626
- if (colonIndex !== -1) {
17627
- filePath = filePath.substring(colonIndex + 1);
17628
- }
17651
+ const filePath = extractFilePathFromComponentKey(componentPath ?? issue2.component);
17629
17652
  return {
17630
17653
  key: issue2.key,
17631
17654
  severity: issue2.severity,
@@ -17685,7 +17708,7 @@ class IssuesAPI extends BaseAPI {
17685
17708
  try {
17686
17709
  const response = await this.client.get("/api/hotspots/search", {
17687
17710
  projectKey,
17688
- ps: 500,
17711
+ ps: API.DEFAULT_PAGE_SIZE,
17689
17712
  p: 1
17690
17713
  });
17691
17714
  this.logger.info(`Found ${response.hotspots.length} security hotspots for project ${projectKey}`);
@@ -18045,7 +18068,6 @@ class QualityGateAPI extends BaseAPI {
18045
18068
  }
18046
18069
  // src/api/rules.ts
18047
18070
  init_base_api();
18048
-
18049
18071
  class RulesAPI extends BaseAPI {
18050
18072
  cache = new Map;
18051
18073
  constructor(client, logger4) {
@@ -18107,7 +18129,7 @@ class RulesAPI extends BaseAPI {
18107
18129
  try {
18108
18130
  const response = await this.client.get("/api/rules/search", {
18109
18131
  rule_key: uncached.join(","),
18110
- ps: 500
18132
+ ps: API.DEFAULT_PAGE_SIZE
18111
18133
  });
18112
18134
  for (const rule of response.rules) {
18113
18135
  const details = this.parseRuleResponse(rule);
@@ -18148,6 +18170,32 @@ class RulesAPI extends BaseAPI {
18148
18170
  // src/api/sources.ts
18149
18171
  init_base_api();
18150
18172
 
18173
+ // src/utils/group-by.ts
18174
+ function groupBy(items, keyFn) {
18175
+ const groups = {};
18176
+ for (const item of items) {
18177
+ const key = keyFn(item);
18178
+ if (!groups[key]) {
18179
+ groups[key] = [];
18180
+ }
18181
+ groups[key].push(item);
18182
+ }
18183
+ return groups;
18184
+ }
18185
+
18186
+ // src/api/sources.ts
18187
+ function createIssueWithContext(issue2, sourceContext) {
18188
+ return {
18189
+ key: issue2.key,
18190
+ rule: issue2.rule,
18191
+ severity: issue2.severity,
18192
+ message: issue2.message,
18193
+ component: issue2.component,
18194
+ line: issue2.line,
18195
+ sourceContext
18196
+ };
18197
+ }
18198
+
18151
18199
  class SourcesAPI extends BaseAPI {
18152
18200
  constructor(client, logger4) {
18153
18201
  super(client, "sonarqube-sources", logger4);
@@ -18181,20 +18229,12 @@ class SourcesAPI extends BaseAPI {
18181
18229
  };
18182
18230
  }
18183
18231
  groupIssuesByComponent(issues) {
18184
- const grouped = new Map;
18185
- for (const issue2 of issues) {
18186
- const existing = grouped.get(issue2.component) ?? [];
18187
- existing.push(issue2);
18188
- grouped.set(issue2.component, existing);
18189
- }
18190
- return grouped;
18232
+ const grouped = groupBy(issues, (issue2) => issue2.component);
18233
+ return new Map(Object.entries(grouped));
18191
18234
  }
18192
- createIssueWithContext(issue2, component, lineMap, contextLines) {
18193
- if (issue2.line === undefined) {
18194
- return { ...issue2, sourceContext: undefined };
18195
- }
18196
- const contextStart = Math.max(1, issue2.line - contextLines);
18197
- const contextEnd = issue2.line + contextLines;
18235
+ buildSourceContext(component, issueLine, lineMap, contextLines) {
18236
+ const contextStart = Math.max(1, issueLine - contextLines);
18237
+ const contextEnd = issueLine + contextLines;
18198
18238
  const contextSourceLines = [];
18199
18239
  for (let l = contextStart;l <= contextEnd; l++) {
18200
18240
  const sourceLine = lineMap.get(l);
@@ -18203,20 +18243,21 @@ class SourcesAPI extends BaseAPI {
18203
18243
  }
18204
18244
  }
18205
18245
  return {
18206
- ...issue2,
18207
- sourceContext: {
18208
- component,
18209
- lines: contextSourceLines,
18210
- startLine: contextStart,
18211
- endLine: contextEnd
18212
- }
18246
+ component,
18247
+ lines: contextSourceLines,
18248
+ startLine: contextStart,
18249
+ endLine: contextEnd
18213
18250
  };
18214
18251
  }
18252
+ createIssueWithSourceContext(issue2, lineMap, contextLines) {
18253
+ if (issue2.line === undefined) {
18254
+ return createIssueWithContext(issue2);
18255
+ }
18256
+ const sourceContext = this.buildSourceContext(issue2.component, issue2.line, lineMap, contextLines);
18257
+ return createIssueWithContext(issue2, sourceContext);
18258
+ }
18215
18259
  addIssuesWithoutContext(issues) {
18216
- return issues.map((issue2) => ({
18217
- ...issue2,
18218
- sourceContext: undefined
18219
- }));
18260
+ return issues.map((issue2) => createIssueWithContext(issue2));
18220
18261
  }
18221
18262
  async getIssuesWithContext(issues, contextLines = 3) {
18222
18263
  const results = [];
@@ -18233,7 +18274,7 @@ class SourcesAPI extends BaseAPI {
18233
18274
  const sourceLines = await this.getSourceLines(component, minLine, maxLine);
18234
18275
  const lineMap = new Map(sourceLines.map((l) => [l.line, l]));
18235
18276
  for (const issue2 of componentIssues) {
18236
- results.push(this.createIssueWithContext(issue2, component, lineMap, contextLines));
18277
+ results.push(this.createIssueWithSourceContext(issue2, lineMap, contextLines));
18237
18278
  }
18238
18279
  } catch {
18239
18280
  results.push(...this.addIssuesWithoutContext(componentIssues));
@@ -18254,7 +18295,7 @@ class SourcesAPI extends BaseAPI {
18254
18295
  return ["", "```", ...codeLines, "```"];
18255
18296
  }
18256
18297
  formatIssueWithContext(issue2) {
18257
- const filePath = issue2.component.includes(":") ? issue2.component.split(":").slice(1).join(":") : issue2.component;
18298
+ const filePath = extractFilePathFromComponentKey(issue2.component);
18258
18299
  const lines = [
18259
18300
  `### ${issue2.severity}: ${issue2.message}`,
18260
18301
  this.formatFileLocation(filePath, issue2.line),
@@ -18339,12 +18380,12 @@ class DuplicationsAPI extends BaseAPI {
18339
18380
  if (!originalFile || !duplicateFile)
18340
18381
  continue;
18341
18382
  result.push({
18342
- originalFile: this.extractFilePath(originalFile.key),
18383
+ originalFile: extractFilePathFromComponentKey(originalFile.key),
18343
18384
  originalLines: {
18344
18385
  from: originalBlock.from,
18345
18386
  to: originalBlock.from + originalBlock.size - 1
18346
18387
  },
18347
- duplicateFile: this.extractFilePath(duplicateFile.key),
18388
+ duplicateFile: extractFilePathFromComponentKey(duplicateFile.key),
18348
18389
  duplicateLines: {
18349
18390
  from: dupBlock.from,
18350
18391
  to: dupBlock.from + dupBlock.size - 1
@@ -18385,9 +18426,6 @@ class DuplicationsAPI extends BaseAPI {
18385
18426
  return lines.join(`
18386
18427
  `);
18387
18428
  }
18388
- extractFilePath(componentKey) {
18389
- return componentKey.includes(":") ? componentKey.split(":").slice(1).join(":") : componentKey;
18390
- }
18391
18429
  }
18392
18430
  // src/api/ce.ts
18393
18431
  init_base_api();
@@ -18433,7 +18471,7 @@ class ComputeEngineAPI extends BaseAPI {
18433
18471
  onlyCurrents: options.onlyCurrents?.toString(),
18434
18472
  maxExecutedAt: options.maxExecutedAt,
18435
18473
  minSubmittedAt: options.minSubmittedAt,
18436
- ps: options.pageSize ?? 20
18474
+ ps: options.pageSize ?? API.DEFAULT_LIMIT
18437
18475
  });
18438
18476
  return response.tasks;
18439
18477
  } catch (error45) {
@@ -18452,29 +18490,42 @@ class ComputeEngineAPI extends BaseAPI {
18452
18490
  const { current, queue } = await this.getComponentTasks(componentKey);
18453
18491
  return current !== undefined || queue.length > 0;
18454
18492
  }
18455
- async waitForTask(taskId, pollIntervalMs = 2000, maxWaitMs = 300000) {
18493
+ shouldContinuePolling(startTime, maxWaitMs) {
18494
+ return Date.now() - startTime < maxWaitMs;
18495
+ }
18496
+ isTaskComplete(status) {
18497
+ return ["SUCCESS", "FAILED", "CANCELED"].includes(status);
18498
+ }
18499
+ handleTaskNotFound(taskId, retries, maxRetries) {
18500
+ if (retries >= maxRetries) {
18501
+ this.logger.warn(`Failed to get task ${taskId}: Not found after ${maxRetries} retries`);
18502
+ return { shouldContinue: false, newRetries: retries };
18503
+ }
18504
+ return { shouldContinue: true, newRetries: retries + 1 };
18505
+ }
18506
+ async waitForTask(taskId, pollIntervalMs = API.POLL_INTERVAL_MS, maxWaitMs = API.ANALYSIS_TIMEOUT_MS) {
18456
18507
  this.logger.info(`Waiting for task to complete: ${taskId}`);
18457
18508
  const startTime = Date.now();
18458
18509
  let taskNotFoundRetries = 0;
18459
18510
  const maxTaskNotFoundRetries = 10;
18460
18511
  debugLog(`waitForTask: starting for ${taskId}`);
18461
- while (Date.now() - startTime < maxWaitMs) {
18512
+ while (this.shouldContinuePolling(startTime, maxWaitMs)) {
18462
18513
  const task = await this.getTask(taskId);
18463
18514
  if (!task) {
18464
- taskNotFoundRetries++;
18465
- debugLog(`waitForTask: task not found, retry ${taskNotFoundRetries}/${maxTaskNotFoundRetries}`);
18466
- if (taskNotFoundRetries >= maxTaskNotFoundRetries) {
18467
- this.logger.warn(`Task ${taskId} not found after ${maxTaskNotFoundRetries} retries`);
18515
+ const result = this.handleTaskNotFound(taskId, taskNotFoundRetries, maxTaskNotFoundRetries);
18516
+ taskNotFoundRetries = result.newRetries;
18517
+ if (!result.shouldContinue) {
18468
18518
  debugLog(`waitForTask: giving up after ${maxTaskNotFoundRetries} retries`);
18469
18519
  return;
18470
18520
  }
18521
+ debugLog(`waitForTask: task not found, retry ${taskNotFoundRetries}/${maxTaskNotFoundRetries}`);
18471
18522
  this.logger.debug(`Task ${taskId} not found yet, retrying (${taskNotFoundRetries}/${maxTaskNotFoundRetries})...`);
18472
18523
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
18473
18524
  continue;
18474
18525
  }
18475
18526
  taskNotFoundRetries = 0;
18476
18527
  debugLog(`waitForTask: task found, status=${task.status}`);
18477
- if (task.status === "SUCCESS" || task.status === "FAILED" || task.status === "CANCELED") {
18528
+ if (this.isTaskComplete(task.status)) {
18478
18529
  this.logger.info(`Task completed with status: ${task.status}`);
18479
18530
  debugLog(`waitForTask: task completed with ${task.status}`);
18480
18531
  return task;
@@ -18486,7 +18537,7 @@ class ComputeEngineAPI extends BaseAPI {
18486
18537
  debugLog(`waitForTask: timeout after ${maxWaitMs}ms`);
18487
18538
  return;
18488
18539
  }
18489
- async waitForAnalysis(componentKey, timeoutMs = 300000, pollIntervalMs = 2000) {
18540
+ async waitForAnalysis(componentKey, timeoutMs = API.ANALYSIS_TIMEOUT_MS, pollIntervalMs = API.POLL_INTERVAL_MS) {
18490
18541
  this.logger.info(`Waiting for analysis to complete: ${componentKey}`);
18491
18542
  const startTime = Date.now();
18492
18543
  let lastTaskId;
@@ -18550,8 +18601,7 @@ class ProjectAnalysesAPI extends BaseAPI {
18550
18601
  projectKey: options.projectKey,
18551
18602
  projectKeyLength: options.projectKey?.length
18552
18603
  });
18553
- if (!options.projectKey) {
18554
- this.logger.error(`getAnalyses: projectKey is empty/undefined!`);
18604
+ if (!this.validateProjectKey(options.projectKey, "getAnalyses")) {
18555
18605
  return [];
18556
18606
  }
18557
18607
  try {
@@ -18614,18 +18664,14 @@ class QualityProfilesAPI extends BaseAPI {
18614
18664
  }
18615
18665
  async getProjectProfiles(projectKey) {
18616
18666
  this.logger.info(`>>> getProjectProfiles called`, { projectKey, projectKeyLength: projectKey?.length });
18617
- if (!projectKey) {
18618
- this.logger.error(`getProjectProfiles: projectKey is empty/undefined!`);
18667
+ if (!this.validateProjectKey(projectKey, "getProjectProfiles")) {
18619
18668
  return [];
18620
18669
  }
18621
- try {
18670
+ return this.safeRequest(async () => {
18622
18671
  const response = await this.client.get("/api/qualityprofiles/search", { project: projectKey });
18623
18672
  this.logger.info(`<<< getProjectProfiles success`, { profileCount: response.profiles.length });
18624
18673
  return response.profiles;
18625
- } catch (error45) {
18626
- this.logger.error(`getProjectProfiles failed`, { error: String(error45), projectKey });
18627
- return [];
18628
- }
18674
+ }, [], `getProjectProfiles failed for ${projectKey}`);
18629
18675
  }
18630
18676
  async getAllProfiles(language) {
18631
18677
  const languageInfo = language ? ` for ${language}` : "";
@@ -18640,12 +18686,9 @@ class QualityProfilesAPI extends BaseAPI {
18640
18686
  }
18641
18687
  async getInheritance(profileKey) {
18642
18688
  this.logger.debug(`Getting inheritance for profile ${profileKey}`);
18643
- try {
18689
+ return this.safeRequest(async () => {
18644
18690
  return await this.client.get("/api/qualityprofiles/inheritance", { qualityProfile: profileKey });
18645
- } catch (error45) {
18646
- this.logger.warn(`Failed to get inheritance: ${error45}`);
18647
- return;
18648
- }
18691
+ }, undefined, `Failed to get inheritance for ${profileKey}`);
18649
18692
  }
18650
18693
  formatProfilesForAgent(profiles, projectKey) {
18651
18694
  if (profiles.length === 0) {
@@ -18705,18 +18748,14 @@ class BranchesAPI extends BaseAPI {
18705
18748
  }
18706
18749
  async getBranches(projectKey) {
18707
18750
  this.logger.info(`>>> getBranches called`, { projectKey, projectKeyLength: projectKey?.length });
18708
- if (!projectKey) {
18709
- this.logger.error(`getBranches: projectKey is empty/undefined!`);
18751
+ if (!this.validateProjectKey(projectKey, "getBranches")) {
18710
18752
  return [];
18711
18753
  }
18712
- try {
18754
+ return this.safeRequest(async () => {
18713
18755
  const response = await this.client.get("/api/project_branches/list", { project: projectKey });
18714
18756
  this.logger.info(`<<< getBranches success`, { branchCount: response.branches.length });
18715
18757
  return response.branches;
18716
- } catch (error45) {
18717
- this.logger.error(`getBranches failed`, { error: String(error45), projectKey });
18718
- return [];
18719
- }
18758
+ }, [], `getBranches failed for ${projectKey}`);
18720
18759
  }
18721
18760
  async getMainBranch(projectKey) {
18722
18761
  const branches = await this.getBranches(projectKey);
@@ -18728,16 +18767,13 @@ class BranchesAPI extends BaseAPI {
18728
18767
  }
18729
18768
  async deleteBranch(projectKey, branchName) {
18730
18769
  this.logger.debug(`Deleting branch ${branchName} from ${projectKey}`);
18731
- try {
18770
+ return this.safeRequest(async () => {
18732
18771
  await this.client.post("/api/project_branches/delete", {
18733
18772
  project: projectKey,
18734
18773
  branch: branchName
18735
18774
  });
18736
18775
  return true;
18737
- } catch (error45) {
18738
- this.logger.warn(`Failed to delete branch: ${error45}`);
18739
- return false;
18740
- }
18776
+ }, false, `Failed to delete branch ${branchName}`);
18741
18777
  }
18742
18778
  async renameMainBranch(projectKey, newName) {
18743
18779
  this.logger.debug(`Renaming main branch to ${newName} in ${projectKey}`);
@@ -18819,6 +18855,36 @@ class BranchesAPI extends BaseAPI {
18819
18855
  }
18820
18856
  // src/api/metrics.ts
18821
18857
  init_base_api();
18858
+
18859
+ // src/utils/metric-formatter.ts
18860
+ var MetricFormatter = {
18861
+ rating(value) {
18862
+ const num = typeof value === "string" ? Number.parseFloat(value) : value;
18863
+ return numericToRating(num);
18864
+ },
18865
+ percentage(value, decimals = 1) {
18866
+ const num = typeof value === "string" ? Number.parseFloat(value) : value;
18867
+ return `${num.toFixed(decimals)}%`;
18868
+ },
18869
+ duration(minutes) {
18870
+ if (minutes < 60)
18871
+ return `${minutes}min`;
18872
+ const hours = Math.floor(minutes / 60);
18873
+ const mins = minutes % 60;
18874
+ return `${hours}h ${mins}min`;
18875
+ },
18876
+ formatByType(metricKey, value) {
18877
+ if (metricKey.includes("rating")) {
18878
+ return this.rating(value);
18879
+ }
18880
+ if (metricKey.includes("coverage") || metricKey.includes("duplicat")) {
18881
+ return this.percentage(value);
18882
+ }
18883
+ return value;
18884
+ }
18885
+ };
18886
+
18887
+ // src/api/metrics.ts
18822
18888
  var COMMON_METRICS = [
18823
18889
  "ncloc",
18824
18890
  "lines",
@@ -18854,28 +18920,22 @@ class MetricsAPI extends BaseAPI {
18854
18920
  }
18855
18921
  async getMetricDefinitions() {
18856
18922
  this.logger.debug("Getting metric definitions");
18857
- try {
18858
- const response = await this.client.get("/api/metrics/search", { ps: 500 });
18923
+ return this.safeRequest(async () => {
18924
+ const response = await this.client.get("/api/metrics/search", { ps: API.DEFAULT_PAGE_SIZE });
18859
18925
  return response.metrics;
18860
- } catch (error45) {
18861
- this.logger.warn(`Failed to get metrics: ${error45}`);
18862
- return [];
18863
- }
18926
+ }, [], "Failed to get metric definitions");
18864
18927
  }
18865
18928
  async getMeasures(options) {
18866
18929
  const metrics = options.metricKeys ?? [...COMMON_METRICS];
18867
18930
  this.logger.debug(`Getting measures for ${options.componentKey}`);
18868
- try {
18931
+ return this.safeRequest(async () => {
18869
18932
  const response = await this.client.get("/api/measures/component", {
18870
18933
  component: options.componentKey,
18871
18934
  metricKeys: metrics.join(","),
18872
18935
  branch: options.branch
18873
18936
  });
18874
18937
  return response.component.measures;
18875
- } catch (error45) {
18876
- this.logger.warn(`Failed to get measures: ${error45}`);
18877
- return [];
18878
- }
18938
+ }, [], `Failed to get measures for ${options.componentKey}`);
18879
18939
  }
18880
18940
  async getMeasuresWithPeriod(options) {
18881
18941
  const metrics = options.metricKeys ?? [...COMMON_METRICS];
@@ -18956,19 +19016,21 @@ class MetricsAPI extends BaseAPI {
18956
19016
  }
18957
19017
  formatMetricValue(metricKey, value) {
18958
19018
  if (metricKey.includes("rating")) {
18959
- const ratings = ["A", "B", "C", "D", "E"];
18960
- const index = Number.parseInt(value, 10) - 1;
18961
- return ratings[index] ?? value;
19019
+ const numericValue = Number.parseInt(value, 10);
19020
+ if (numericValue >= 1 && numericValue <= 5) {
19021
+ return MetricFormatter.rating(numericValue);
19022
+ }
19023
+ return value;
18962
19024
  }
18963
19025
  if (metricKey.includes("coverage") || metricKey.includes("density") || metricKey.includes("ratio")) {
18964
- return `${value}%`;
19026
+ return MetricFormatter.percentage(value, 1);
18965
19027
  }
18966
19028
  if (metricKey === "sqale_index") {
18967
19029
  const minutes = Number.parseInt(value, 10);
18968
19030
  if (minutes < 60)
18969
19031
  return `${minutes}min`;
18970
19032
  if (minutes < 1440)
18971
- return `${Math.floor(minutes / 60)}h ${minutes % 60}min`;
19033
+ return MetricFormatter.duration(minutes);
18972
19034
  return `${Math.floor(minutes / 1440)}d`;
18973
19035
  }
18974
19036
  return value;
@@ -19058,8 +19120,7 @@ class ComponentsAPI extends BaseAPI {
19058
19120
  "|------|------|-------|--------|-------|"
19059
19121
  ];
19060
19122
  for (const file2 of files.slice(0, 20)) {
19061
- const shortPath = this.shortenPath(file2.path);
19062
- lines.push(`| ${shortPath} | ${file2.bugs} | ${file2.vulnerabilities} | ${file2.codeSmells} | **${file2.total}** |`);
19123
+ lines.push(`| ${shortenPath(file2.path)} | ${file2.bugs} | ${file2.vulnerabilities} | ${file2.codeSmells} | **${file2.total}** |`);
19063
19124
  }
19064
19125
  if (files.length > 20) {
19065
19126
  lines.push(`
@@ -19077,14 +19138,6 @@ class ComponentsAPI extends BaseAPI {
19077
19138
  return lines.join(`
19078
19139
  `);
19079
19140
  }
19080
- shortenPath(path) {
19081
- if (path.length <= 50)
19082
- return path;
19083
- const parts = path.split("/");
19084
- if (parts.length <= 2)
19085
- return path;
19086
- return `.../${parts.slice(-2).join("/")}`;
19087
- }
19088
19141
  }
19089
19142
 
19090
19143
  // src/api/index.ts
@@ -19156,6 +19209,18 @@ function createSonarQubeAPI(config3, state, logger4) {
19156
19209
  init_config3();
19157
19210
 
19158
19211
  // src/utils/severity.ts
19212
+ var SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
19213
+ var SEVERITY_LEVELS = {
19214
+ blocker: ["BLOCKER"],
19215
+ critical: ["BLOCKER", "CRITICAL"],
19216
+ major: ["BLOCKER", "CRITICAL", "MAJOR"],
19217
+ minor: ["BLOCKER", "CRITICAL", "MAJOR", "MINOR"],
19218
+ info: ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"],
19219
+ all: ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"]
19220
+ };
19221
+ function getSeveritiesFromLevel(level) {
19222
+ return SEVERITY_LEVELS[level.toLowerCase()] ?? [...SEVERITIES];
19223
+ }
19159
19224
  var SEVERITY_PRIORITY = {
19160
19225
  BLOCKER: 5,
19161
19226
  CRITICAL: 4,
@@ -19652,6 +19717,128 @@ function formatEmptyState(entity, projectKey, positiveMessage) {
19652
19717
  return lines.join(`
19653
19718
  `);
19654
19719
  }
19720
+ // src/tools/formatters/suggestions.ts
19721
+ function formatFixSuggestions(issues) {
19722
+ const blockers = issues.filter((i) => i.severity === "BLOCKER");
19723
+ const critical = issues.filter((i) => i.severity === "CRITICAL");
19724
+ if (blockers.length === 0 && critical.length === 0) {
19725
+ return "";
19726
+ }
19727
+ let output = `
19728
+
19729
+ ---
19730
+
19731
+ ## Fix Suggestions
19732
+
19733
+ `;
19734
+ output += `Based on the issues found, here are recommended actions:
19735
+
19736
+ `;
19737
+ if (blockers.length > 0) {
19738
+ output += `### Immediate Fixes Required (Blockers)
19739
+ `;
19740
+ for (const issue2 of blockers.slice(0, 5)) {
19741
+ output += `1. **${issue2.file}:${issue2.line ?? "?"}** - ${issue2.message}
19742
+ `;
19743
+ output += ` - Rule: \`${issue2.rule}\` - Review and fix this security/reliability issue
19744
+
19745
+ `;
19746
+ }
19747
+ }
19748
+ if (critical.length > 0) {
19749
+ output += `### High Priority Fixes (Critical)
19750
+ `;
19751
+ for (const issue2 of critical.slice(0, 5)) {
19752
+ output += `1. **${issue2.file}:${issue2.line ?? "?"}** - ${issue2.message}
19753
+ `;
19754
+ output += ` - Rule: \`${issue2.rule}\`
19755
+
19756
+ `;
19757
+ }
19758
+ }
19759
+ return output;
19760
+ }
19761
+ // src/utils/error-messages.ts
19762
+ var ErrorMessages = {
19763
+ missingParameter(param, example) {
19764
+ let msg = `Missing required parameter: ${param}`;
19765
+ if (example) {
19766
+ msg += `
19767
+
19768
+ Example: ${example}`;
19769
+ }
19770
+ return msg;
19771
+ },
19772
+ notFound(entity, key, suggestions) {
19773
+ let msg = `${entity} not found: ${key}`;
19774
+ if (suggestions?.length) {
19775
+ const suggestionList = suggestions.map((s) => "- " + s).join(`
19776
+ `);
19777
+ msg += `
19778
+
19779
+ Did you mean:
19780
+ ` + suggestionList;
19781
+ }
19782
+ return msg;
19783
+ },
19784
+ configurationMissing(message, requiredSteps) {
19785
+ let msg = message;
19786
+ if (requiredSteps?.length) {
19787
+ const stepList = requiredSteps.map((s, i) => i + 1 + ". " + s).join(`
19788
+ `);
19789
+ msg += `
19790
+
19791
+ Required steps:
19792
+ ` + stepList;
19793
+ }
19794
+ return msg;
19795
+ },
19796
+ apiError(action, message, serverUrl) {
19797
+ let msg = `Action \`${action}\` failed: ${message}`;
19798
+ const hints = [
19799
+ serverUrl ? `SonarQube server is reachable at ${serverUrl}` : "SonarQube server is reachable",
19800
+ "Credentials are valid",
19801
+ 'Run with action: "setup" to initialize'
19802
+ ];
19803
+ const hintList = hints.map((h, i) => i + 1 + ". " + h).join(`
19804
+ `);
19805
+ msg += `
19806
+
19807
+ Please check:
19808
+ ` + hintList;
19809
+ return msg;
19810
+ },
19811
+ connectionError(serverUrl) {
19812
+ return `Cannot connect to SonarQube server at ${serverUrl}`;
19813
+ },
19814
+ authenticationError(reason) {
19815
+ return `Authentication failed: ${reason}`;
19816
+ },
19817
+ ruleNotFound(ruleKey) {
19818
+ return `Rule \`${ruleKey}\` was not found.
19819
+
19820
+ Please check:
19821
+ 1. The rule key is correct (e.g., "typescript:S1234")
19822
+ 2. The rule is available on your SonarQube server
19823
+ 3. The rule's language plugin is installed`;
19824
+ },
19825
+ missingRuleKey() {
19826
+ return `Missing \`ruleKey\` parameter. Please provide a rule key to explain.
19827
+
19828
+ Example usage:
19829
+ \`\`\`
19830
+ sonarqube({ action: "rule", ruleKey: "typescript:S1234" })
19831
+ \`\`\`
19832
+
19833
+ Common rule prefixes:
19834
+ - \`typescript:\` - TypeScript rules
19835
+ - \`javascript:\` - JavaScript rules
19836
+ - \`java:\` - Java rules
19837
+ - \`python:\` - Python rules
19838
+ - \`common-\` - Language-agnostic rules`;
19839
+ }
19840
+ };
19841
+
19655
19842
  // src/tools/base-handler.ts
19656
19843
  function createHandlerContext(config3, state, projectKey, directory) {
19657
19844
  return {
@@ -19757,63 +19944,11 @@ async function handleAnalyze(ctx, args) {
19757
19944
  }, directory);
19758
19945
  let output = formatAnalysisResult(result);
19759
19946
  if (args.fix && result.formattedIssues.length > 0) {
19760
- output += `
19761
-
19762
- ---
19763
-
19764
- ## Fix Suggestions
19765
-
19766
- `;
19767
- output += `Based on the issues found, here are recommended actions:
19768
-
19769
- `;
19770
- const blockers = result.formattedIssues.filter((i) => i.severity === "BLOCKER");
19771
- const critical = result.formattedIssues.filter((i) => i.severity === "CRITICAL");
19772
- if (blockers.length > 0) {
19773
- output += `### Immediate Fixes Required (Blockers)
19774
- `;
19775
- for (const issue2 of blockers.slice(0, 5)) {
19776
- output += `1. **${issue2.file}:${issue2.line ?? "?"}** - ${issue2.message}
19777
- `;
19778
- output += ` - Rule: \`${issue2.rule}\` - Review and fix this security/reliability issue
19779
-
19780
- `;
19781
- }
19782
- }
19783
- if (critical.length > 0) {
19784
- output += `### High Priority Fixes (Critical)
19785
- `;
19786
- for (const issue2 of critical.slice(0, 5)) {
19787
- output += `1. **${issue2.file}:${issue2.line ?? "?"}** - ${issue2.message}
19788
- `;
19789
- output += ` - Rule: \`${issue2.rule}\`
19790
-
19791
- `;
19792
- }
19793
- }
19947
+ output += formatFixSuggestions(result.formattedIssues);
19794
19948
  }
19795
19949
  return output;
19796
19950
  }
19797
19951
  // src/tools/handlers/issues.ts
19798
- init_logger();
19799
- var logger9 = new Logger("sonarqube-handler-issues");
19800
- function getSeveritiesFromLevel(level) {
19801
- switch (level.toLowerCase()) {
19802
- case "blocker":
19803
- return ["BLOCKER"];
19804
- case "critical":
19805
- return ["BLOCKER", "CRITICAL"];
19806
- case "major":
19807
- return ["BLOCKER", "CRITICAL", "MAJOR"];
19808
- case "minor":
19809
- return ["BLOCKER", "CRITICAL", "MAJOR", "MINOR"];
19810
- case "info":
19811
- case "all":
19812
- return ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
19813
- default:
19814
- return ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
19815
- }
19816
- }
19817
19952
  async function handleIssues(ctx, args) {
19818
19953
  const { api: api2, projectKey } = ctx;
19819
19954
  const severities = args.severity === "all" ? undefined : getSeveritiesFromLevel(args.severity ?? "all");
@@ -19836,7 +19971,7 @@ async function handleIssues(ctx, args) {
19836
19971
  message: i.message,
19837
19972
  component: `${projectKey}:${i.file}`,
19838
19973
  line: i.line
19839
- })), 5);
19974
+ })), API.SOURCE_CONTEXT_LINES);
19840
19975
  output += `
19841
19976
 
19842
19977
  ---
@@ -19853,9 +19988,7 @@ async function handleIssues(ctx, args) {
19853
19988
 
19854
19989
  `;
19855
19990
  }
19856
- } catch {
19857
- logger9.debug("Could not fetch source context for issues");
19858
- }
19991
+ } catch {}
19859
19992
  }
19860
19993
  return output;
19861
19994
  }
@@ -19867,7 +20000,7 @@ async function handleNewIssues(ctx, args) {
19867
20000
  severities,
19868
20001
  inNewCode: true,
19869
20002
  resolved: false,
19870
- pageSize: 500
20003
+ pageSize: API.DEFAULT_PAGE_SIZE
19871
20004
  });
19872
20005
  const issues = api2.issues.formatIssues(response.issues, response.components);
19873
20006
  if (issues.length === 0) {
@@ -19889,8 +20022,7 @@ These issues were introduced in your recent changes and should be fixed before m
19889
20022
  bySeverity[issue2.severity] = [];
19890
20023
  bySeverity[issue2.severity].push(issue2);
19891
20024
  }
19892
- const severityOrder = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"];
19893
- for (const sev of severityOrder) {
20025
+ for (const sev of SEVERITIES) {
19894
20026
  const sevIssues = bySeverity[sev];
19895
20027
  if (!sevIssues || sevIssues.length === 0)
19896
20028
  continue;
@@ -20066,31 +20198,14 @@ async function handleRule(ctx, ruleKey) {
20066
20198
  if (!ruleKey) {
20067
20199
  return `## SonarQube Error
20068
20200
 
20069
- Missing \`ruleKey\` parameter. Please provide a rule key to explain.
20070
-
20071
- Example usage:
20072
- \`\`\`
20073
- sonarqube({ action: "rule", ruleKey: "typescript:S1234" })
20074
- \`\`\`
20075
-
20076
- Common rule prefixes:
20077
- - \`typescript:\` - TypeScript rules
20078
- - \`javascript:\` - JavaScript rules
20079
- - \`java:\` - Java rules
20080
- - \`python:\` - Python rules
20081
- - \`common-\` - Language-agnostic rules`;
20201
+ ${ErrorMessages.missingRuleKey()}`;
20082
20202
  }
20083
20203
  const { api: api2 } = ctx;
20084
20204
  const rule = await api2.rules.getRule(ruleKey);
20085
20205
  if (!rule) {
20086
20206
  return `## SonarQube Rule Not Found
20087
20207
 
20088
- Rule \`${ruleKey}\` was not found.
20089
-
20090
- Please check:
20091
- 1. The rule key is correct (e.g., "typescript:S1234")
20092
- 2. The rule is available on your SonarQube server
20093
- 3. The rule's language plugin is installed`;
20208
+ ${ErrorMessages.ruleNotFound(ruleKey)}`;
20094
20209
  }
20095
20210
  return `## SonarQube Rule: ${rule.name}
20096
20211
 
@@ -20186,7 +20301,7 @@ Run \`sonarqube({ action: "analyze" })\` to generate metrics.`));
20186
20301
  return output;
20187
20302
  }
20188
20303
  // src/tools/sonarqube.ts
20189
- var logger10 = new Logger("sonarqube-tool");
20304
+ var logger9 = new Logger("sonarqube-tool");
20190
20305
  var SonarQubeToolArgsSchema = exports_external2.object({
20191
20306
  action: exports_external2.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform: analyze (run scanner), issues (all issues), newissues (only new code issues), status (quality gate), init/setup (initialize), validate (enterprise check), hotspots (security review), duplications (code duplicates), rule (explain rule), history (past analyses), profile (quality profile), branches (branch status), metrics (detailed metrics), worstfiles (files with most issues)"),
20192
20307
  scope: exports_external2.enum(["all", "new", "changed"]).optional().default("all").describe("Scope of analysis: all files, only new code, or changed files"),
@@ -20201,12 +20316,11 @@ async function executeSonarQubeTool(args, context) {
20201
20316
  const directory = context.directory ?? process.cwd();
20202
20317
  const config3 = loadConfig(context.config);
20203
20318
  if (!config3) {
20204
- return formatError2(`SonarQube configuration not found.
20205
-
20206
- Please ensure you have one of:
20207
- 1. Environment variables: SONAR_HOST_URL and SONAR_TOKEN
20208
- 2. A .sonarqube/config.json file in your project
20209
- 3. Plugin configuration in opencode.json
20319
+ return formatError2(ErrorMessages.configurationMissing("SonarQube configuration not found.", [
20320
+ "Set environment variables: SONAR_HOST_URL and SONAR_TOKEN",
20321
+ "Or create a .sonarqube/config.json file in your project",
20322
+ "Or add plugin configuration in opencode.json"
20323
+ ]) + `
20210
20324
 
20211
20325
  Required environment variables:
20212
20326
  - SONAR_HOST_URL (e.g., https://sonarqube.company.com)
@@ -20216,13 +20330,13 @@ Optional:
20216
20330
  - SONAR_USER
20217
20331
  - SONAR_PASSWORD`);
20218
20332
  }
20219
- logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
20333
+ logger9.info(`Executing SonarQube tool: ${args.action}`, { directory });
20220
20334
  try {
20221
20335
  if (args.action === "init" || args.action === "setup") {
20222
20336
  return await handleSetup(config3, directory, args.force);
20223
20337
  }
20224
20338
  if (await needsBootstrap(directory)) {
20225
- logger10.info("First run detected, running bootstrap");
20339
+ logger9.info("First run detected, running bootstrap");
20226
20340
  const { bootstrap: bootstrap2 } = await Promise.resolve().then(() => (init_bootstrap(), exports_bootstrap));
20227
20341
  const setupResult = await bootstrap2({ config: config3, directory });
20228
20342
  if (!setupResult.success) {
@@ -20230,7 +20344,7 @@ Optional:
20230
20344
 
20231
20345
  ${setupResult.message}`);
20232
20346
  }
20233
- logger10.info("Bootstrap completed", { projectKey: setupResult.projectKey });
20347
+ logger9.info("Bootstrap completed", { projectKey: setupResult.projectKey });
20234
20348
  }
20235
20349
  const state = await getProjectState(directory);
20236
20350
  if (!state) {
@@ -20270,13 +20384,8 @@ ${setupResult.message}`);
20270
20384
  }
20271
20385
  } catch (error45) {
20272
20386
  const errorMessage = error45 instanceof Error ? error45.message : String(error45);
20273
- logger10.error(`Tool execution failed: ${errorMessage}`);
20274
- return formatError2(`Action \`${args.action}\` failed: ${errorMessage}
20275
-
20276
- Please check:
20277
- 1. SonarQube server is reachable at ${config3.url}
20278
- 2. Credentials are valid
20279
- 3. Run with action: "setup" to initialize`);
20387
+ logger9.error(`Tool execution failed: ${errorMessage}`);
20388
+ return formatError2(ErrorMessages.apiError(args.action, errorMessage, config3.url));
20280
20389
  }
20281
20390
  }
20282
20391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "OpenCode Plugin for SonarQube integration - Enterprise-level code quality from the start",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",