opencode-sonarqube 1.5.1 → 2.0.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 +169 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4039,7 +4039,7 @@ var init_types2 = __esm(() => {
4039
4039
  projectName: exports_external2.string().optional().describe("SonarQube project display name"),
4040
4040
  qualityGate: exports_external2.string().optional().describe("Quality gate to use"),
4041
4041
  newCodeDefinition: exports_external2.enum(["previous_version", "number_of_days", "reference_branch", "specific_analysis"]).default("previous_version").describe("How to define 'new code' for analysis"),
4042
- sources: exports_external2.string().default("src").describe("Source directories to analyze"),
4042
+ sources: exports_external2.string().optional().describe("Source directories to analyze (auto-detected if not set)"),
4043
4043
  tests: exports_external2.string().optional().describe("Test directories"),
4044
4044
  exclusions: exports_external2.string().optional().describe("File exclusion patterns")
4045
4045
  });
@@ -4422,11 +4422,29 @@ function getSubdirectories(path) {
4422
4422
  const output = result.stdout.toString().trim();
4423
4423
  if (!output)
4424
4424
  return [];
4425
+ const SKIP_DIRS = new Set([
4426
+ "node_modules",
4427
+ "dist",
4428
+ "build",
4429
+ "out",
4430
+ "coverage",
4431
+ ".next",
4432
+ ".nuxt",
4433
+ ".output",
4434
+ "target",
4435
+ "__pycache__",
4436
+ ".venv",
4437
+ "venv",
4438
+ "vendor",
4439
+ "tmp",
4440
+ ".turbo",
4441
+ ".cache"
4442
+ ]);
4425
4443
  return output.split(`
4426
4444
  `).filter((name) => {
4427
4445
  if (name.startsWith("."))
4428
4446
  return false;
4429
- if (name === "node_modules")
4447
+ if (SKIP_DIRS.has(name))
4430
4448
  return false;
4431
4449
  return directoryExists(`${path}/${name}`);
4432
4450
  });
@@ -4576,7 +4594,7 @@ async function generatePropertiesContent(options, config2, directory) {
4576
4594
  if (detection.languages.includes("go")) {
4577
4595
  lines.push("", "# Go", "sonar.go.coverage.reportPaths=coverage.out");
4578
4596
  }
4579
- lines.push("", "# Analysis Settings", "sonar.sourceEncoding=UTF-8", "sonar.scm.provider=git", `sonar.cpd.exclusions=${getTestPatterns(detection.languages)}`, "sonar.coverage.exclusions=src/bootstrap/index.ts,src/index.ts,src/scanner/runner.ts,src/tools/handlers/setup.ts,src/cli.ts");
4597
+ lines.push("", "# Analysis Settings", "sonar.sourceEncoding=UTF-8", "sonar.scm.provider=git", `sonar.cpd.exclusions=${getTestPatterns(detection.languages)}`);
4580
4598
  if (options.additionalProperties) {
4581
4599
  lines.push("", "# Additional Properties");
4582
4600
  for (const [key, value] of Object.entries(options.additionalProperties)) {
@@ -19917,9 +19935,118 @@ function createHandlerContext(config3, state, projectKey, directory) {
19917
19935
  // src/tools/handlers/setup.ts
19918
19936
  init_bootstrap();
19919
19937
  init_detection();
19920
- init_logger();
19921
19938
  import { mkdir } from "node:fs/promises";
19922
- var logger8 = new Logger("sonarqube-handler-setup");
19939
+
19940
+ // src/scanner/config/validation.ts
19941
+ init_logger();
19942
+ init_patterns();
19943
+ init_detection();
19944
+ var logger8 = new Logger("config-validation");
19945
+ function sourcesNeedRepair(sources) {
19946
+ if (!sources)
19947
+ return false;
19948
+ const parts = sources.split(",").map((s) => s.trim());
19949
+ return parts.some((part) => MONOREPO_PATTERNS.includes(part));
19950
+ }
19951
+ async function validateAndRepairConfig(directory) {
19952
+ const configPath = `${directory}/.sonarqube/config.json`;
19953
+ const repairs = [];
19954
+ try {
19955
+ const file2 = Bun.file(configPath);
19956
+ if (!await file2.exists()) {
19957
+ return { valid: true, repairs };
19958
+ }
19959
+ const config3 = await file2.json();
19960
+ let changed = false;
19961
+ if (config3.sources && sourcesNeedRepair(config3.sources)) {
19962
+ const oldSources = config3.sources;
19963
+ const newSources = detectSourceDirectories(directory);
19964
+ if (newSources !== oldSources) {
19965
+ config3.sources = newSources;
19966
+ changed = true;
19967
+ repairs.push(`Updated sources: "${oldSources}" → "${newSources}" (avoid scanning build artifacts)`);
19968
+ }
19969
+ }
19970
+ if (config3.sources === "" || config3.sources === ".") {
19971
+ const detected = detectSourceDirectories(directory);
19972
+ if (detected !== ".") {
19973
+ config3.sources = detected;
19974
+ changed = true;
19975
+ repairs.push(`Detected sources: "${detected}"`);
19976
+ }
19977
+ }
19978
+ if (changed) {
19979
+ await Bun.write(configPath, JSON.stringify(config3, null, 2));
19980
+ logger8.info("Repaired config.json", { repairs });
19981
+ }
19982
+ return { valid: !changed, repairs };
19983
+ } catch (error45) {
19984
+ logger8.warn("Could not validate config.json", { error: error45 });
19985
+ return { valid: true, repairs };
19986
+ }
19987
+ }
19988
+ async function validateAndRepairProperties(directory) {
19989
+ const propertiesPath = `${directory}/sonar-project.properties`;
19990
+ const repairs = [];
19991
+ try {
19992
+ const file2 = Bun.file(propertiesPath);
19993
+ if (!await file2.exists()) {
19994
+ return { valid: true, repairs };
19995
+ }
19996
+ let content = await file2.text();
19997
+ let changed = false;
19998
+ const sourcesRegex = /^sonar\.sources=(.+)$/m;
19999
+ const sourcesMatch = sourcesRegex.exec(content);
20000
+ if (sourcesMatch) {
20001
+ const currentSources = sourcesMatch[1];
20002
+ if (sourcesNeedRepair(currentSources)) {
20003
+ const newSources = detectSourceDirectories(directory);
20004
+ if (newSources !== currentSources) {
20005
+ content = content.replace(sourcesRegex, `sonar.sources=${newSources}`);
20006
+ changed = true;
20007
+ repairs.push(`Updated sonar.sources: "${currentSources}" → "${newSources}"`);
20008
+ }
20009
+ }
20010
+ }
20011
+ const testsRegex = /^sonar\.tests=(.+)$/m;
20012
+ const testsMatch = testsRegex.exec(content);
20013
+ if (testsMatch) {
20014
+ const currentTests = testsMatch[1];
20015
+ if (sourcesNeedRepair(currentTests)) {
20016
+ const newTests = detectSourceDirectories(directory);
20017
+ if (newTests !== currentTests) {
20018
+ content = content.replace(testsRegex, `sonar.tests=${newTests}`);
20019
+ changed = true;
20020
+ repairs.push(`Updated sonar.tests: "${currentTests}" → "${newTests}"`);
20021
+ }
20022
+ }
20023
+ }
20024
+ if (changed) {
20025
+ await Bun.write(propertiesPath, content);
20026
+ logger8.info("Repaired sonar-project.properties", { repairs });
20027
+ }
20028
+ return { valid: !changed, repairs };
20029
+ } catch (error45) {
20030
+ logger8.warn("Could not validate sonar-project.properties", { error: error45 });
20031
+ return { valid: true, repairs };
20032
+ }
20033
+ }
20034
+ async function validateAndRepairAll(directory) {
20035
+ const configResult = await validateAndRepairConfig(directory);
20036
+ const propertiesResult = await validateAndRepairProperties(directory);
20037
+ const allRepairs = [...configResult.repairs, ...propertiesResult.repairs];
20038
+ const allValid = configResult.valid && propertiesResult.valid;
20039
+ if (allRepairs.length > 0) {
20040
+ logger8.info("Config validation completed with repairs", {
20041
+ repairCount: allRepairs.length
20042
+ });
20043
+ }
20044
+ return { valid: allValid, repairs: allRepairs };
20045
+ }
20046
+
20047
+ // src/tools/handlers/setup.ts
20048
+ init_logger();
20049
+ var logger9 = new Logger("sonarqube-handler-setup");
19923
20050
  async function handleSetup(config3, directory, force = false) {
19924
20051
  const result = await bootstrap({ config: config3, directory, force });
19925
20052
  if (!result.success) {
@@ -19930,7 +20057,12 @@ ${result.message}`;
19930
20057
  const sonarqubeDir = `${directory}/.sonarqube`;
19931
20058
  const configPath = `${sonarqubeDir}/config.json`;
19932
20059
  const configExists = await Bun.file(configPath).exists();
19933
- if (!configExists) {
20060
+ if (configExists) {
20061
+ const validation = await validateAndRepairAll(directory);
20062
+ if (validation.repairs.length > 0) {
20063
+ logger9.info("Repaired existing config", { repairs: validation.repairs });
20064
+ }
20065
+ } else {
19934
20066
  await mkdir(sonarqubeDir, { recursive: true });
19935
20067
  const detectedSources = detectSourceDirectories(directory);
19936
20068
  const defaultConfig = {
@@ -19940,7 +20072,7 @@ ${result.message}`;
19940
20072
  newCodeDefinition: "previous_version"
19941
20073
  };
19942
20074
  await Bun.write(configPath, JSON.stringify(defaultConfig, null, 2));
19943
- logger8.info("Created default config.json", { path: configPath });
20075
+ logger9.info("Created default config.json", { path: configPath });
19944
20076
  }
19945
20077
  const lines = [
19946
20078
  "## SonarQube Project Initialized",
@@ -19960,56 +20092,38 @@ ${result.message}`;
19960
20092
  `);
19961
20093
  }
19962
20094
  // src/tools/handlers/analyze.ts
19963
- var COVERAGE_EXCLUDED_FILES = [
19964
- "src/bootstrap/index.ts",
19965
- "src/index.ts",
19966
- "src/scanner/runner.ts",
19967
- "src/tools/handlers/setup.ts",
19968
- "src/cli.ts",
19969
- "src/utils/config.ts",
19970
- "src/utils/shared-state.ts"
19971
- ];
19972
- async function filterLcovForCoverage(directory) {
19973
- const lcovPath = `${directory}/coverage/lcov.info`;
19974
- try {
19975
- const content = await Bun.file(lcovPath).text();
19976
- const lines = content.split(`
19977
- `);
19978
- const filteredLines = [];
19979
- let skipCurrentFile = false;
19980
- for (const line of lines) {
19981
- if (line.startsWith("SF:")) {
19982
- const filePath = line.substring(3);
19983
- skipCurrentFile = COVERAGE_EXCLUDED_FILES.some((excluded) => filePath.endsWith(excluded) || filePath.includes(`/${excluded}`));
19984
- }
19985
- if (!skipCurrentFile) {
19986
- filteredLines.push(line);
19987
- }
19988
- if (line === "end_of_record") {
19989
- skipCurrentFile = false;
19990
- }
19991
- }
19992
- await Bun.write(lcovPath, filteredLines.join(`
19993
- `));
19994
- } catch {}
19995
- }
19996
20095
  async function handleAnalyze(ctx, args) {
19997
20096
  const { config: config3, state, projectKey, directory } = ctx;
19998
20097
  const projectName = config3.projectName ?? projectKey;
19999
- await filterLcovForCoverage(directory);
20000
- const coverageExclusions = COVERAGE_EXCLUDED_FILES.map((f) => `**/${f.split("/").pop()}`).join(",");
20098
+ const validation = await validateAndRepairAll(directory);
20099
+ let effectiveSources = config3.sources;
20100
+ let effectiveTests = config3.tests;
20101
+ if (validation.repairs.length > 0) {
20102
+ try {
20103
+ const repairedConfig = await Bun.file(`${directory}/.sonarqube/config.json`).json();
20104
+ effectiveSources = repairedConfig.sources ?? effectiveSources;
20105
+ effectiveTests = repairedConfig.tests ?? effectiveTests;
20106
+ } catch {}
20107
+ }
20001
20108
  const result = await runAnalysis(config3, state, {
20002
20109
  projectKey,
20003
20110
  projectName,
20004
- sources: config3.sources,
20005
- tests: config3.tests,
20006
- exclusions: config3.exclusions,
20007
- additionalProperties: {
20008
- "sonar.coverage.exclusions": coverageExclusions,
20009
- "sonar.javascript.lcov.reportPaths": "coverage/lcov.info"
20010
- }
20111
+ sources: effectiveSources,
20112
+ tests: effectiveTests,
20113
+ exclusions: config3.exclusions
20011
20114
  }, directory);
20012
- let output = formatAnalysisResult(result);
20115
+ let output = "";
20116
+ if (validation.repairs.length > 0) {
20117
+ output += `### Config Auto-Repair
20118
+ `;
20119
+ for (const repair of validation.repairs) {
20120
+ output += `- ${repair}
20121
+ `;
20122
+ }
20123
+ output += `
20124
+ `;
20125
+ }
20126
+ output += formatAnalysisResult(result);
20013
20127
  if (args.fix && result.formattedIssues.length > 0) {
20014
20128
  output += formatFixSuggestions(result.formattedIssues);
20015
20129
  }
@@ -20368,7 +20482,7 @@ Run \`sonarqube({ action: "analyze" })\` to generate metrics.`));
20368
20482
  return output;
20369
20483
  }
20370
20484
  // src/tools/sonarqube.ts
20371
- var logger9 = new Logger("sonarqube-tool");
20485
+ var logger10 = new Logger("sonarqube-tool");
20372
20486
  var SonarQubeToolArgsSchema = exports_external2.object({
20373
20487
  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)"),
20374
20488
  scope: exports_external2.enum(["all", "new", "changed"]).optional().default("all").describe("Scope of analysis: all files, only new code, or changed files"),
@@ -20397,13 +20511,13 @@ Optional:
20397
20511
  - SONAR_USER
20398
20512
  - SONAR_PASSWORD`);
20399
20513
  }
20400
- logger9.info(`Executing SonarQube tool: ${args.action}`, { directory });
20514
+ logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
20401
20515
  try {
20402
20516
  if (args.action === "init" || args.action === "setup") {
20403
20517
  return await handleSetup(config3, directory, args.force);
20404
20518
  }
20405
20519
  if (await needsBootstrap(directory)) {
20406
- logger9.info("First run detected, running bootstrap");
20520
+ logger10.info("First run detected, running bootstrap");
20407
20521
  const { bootstrap: bootstrap2 } = await Promise.resolve().then(() => (init_bootstrap(), exports_bootstrap));
20408
20522
  const setupResult = await bootstrap2({ config: config3, directory });
20409
20523
  if (!setupResult.success) {
@@ -20411,7 +20525,7 @@ Optional:
20411
20525
 
20412
20526
  ${setupResult.message}`);
20413
20527
  }
20414
- logger9.info("Bootstrap completed", { projectKey: setupResult.projectKey });
20528
+ logger10.info("Bootstrap completed", { projectKey: setupResult.projectKey });
20415
20529
  }
20416
20530
  const state = await getProjectState(directory);
20417
20531
  if (!state) {
@@ -20451,7 +20565,7 @@ ${setupResult.message}`);
20451
20565
  }
20452
20566
  } catch (error45) {
20453
20567
  const errorMessage = error45 instanceof Error ? error45.message : String(error45);
20454
- logger9.error(`Tool execution failed: ${errorMessage}`);
20568
+ logger10.error(`Tool execution failed: ${errorMessage}`);
20455
20569
  return formatError2(ErrorMessages.apiError(args.action, errorMessage, config3.url));
20456
20570
  }
20457
20571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "1.5.1",
3
+ "version": "2.0.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",