opencode-sonarqube 1.5.1 → 2.0.1
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/dist/index.js +177 -66
- 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().
|
|
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
|
});
|
|
@@ -4234,7 +4234,8 @@ async function deriveProjectKey(directory) {
|
|
|
4234
4234
|
return `project-${Date.now()}`;
|
|
4235
4235
|
}
|
|
4236
4236
|
function sanitizeProjectKey(input) {
|
|
4237
|
-
|
|
4237
|
+
const sanitized = input.toLowerCase().replaceAll(/[^a-z0-9-_]/g, "-").replaceAll(/-+/g, "-").replaceAll(/(?:^-)|(?:-$)/g, "").slice(0, 400);
|
|
4238
|
+
return sanitized || "project";
|
|
4238
4239
|
}
|
|
4239
4240
|
var configLogger, DEFAULT_CONFIG;
|
|
4240
4241
|
var init_config = __esm(() => {
|
|
@@ -4244,8 +4245,7 @@ var init_config = __esm(() => {
|
|
|
4244
4245
|
DEFAULT_CONFIG = {
|
|
4245
4246
|
level: "enterprise",
|
|
4246
4247
|
autoAnalyze: true,
|
|
4247
|
-
newCodeDefinition: "previous_version"
|
|
4248
|
-
sources: "src"
|
|
4248
|
+
newCodeDefinition: "previous_version"
|
|
4249
4249
|
};
|
|
4250
4250
|
});
|
|
4251
4251
|
|
|
@@ -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
|
|
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)}
|
|
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)) {
|
|
@@ -19401,7 +19419,7 @@ function extractTaskId(output) {
|
|
|
19401
19419
|
return altMatch?.[1];
|
|
19402
19420
|
}
|
|
19403
19421
|
function sanitizeArgValue(value) {
|
|
19404
|
-
return value.replaceAll(/[
|
|
19422
|
+
return value.replaceAll(/[\n\r]/g, "");
|
|
19405
19423
|
}
|
|
19406
19424
|
async function runScanner(config3, state, options, directory) {
|
|
19407
19425
|
const dir = directory ?? process.cwd();
|
|
@@ -19441,7 +19459,7 @@ async function runScanner(config3, state, options, directory) {
|
|
|
19441
19459
|
stderr: "pipe",
|
|
19442
19460
|
env: {
|
|
19443
19461
|
...process.env,
|
|
19444
|
-
NODE_OPTIONS: "--max-old-space-size=4096
|
|
19462
|
+
NODE_OPTIONS: `${process.env["NODE_OPTIONS"] ?? ""} --max-old-space-size=4096`.trim()
|
|
19445
19463
|
}
|
|
19446
19464
|
});
|
|
19447
19465
|
const stdout = await new Response(proc.stdout).text();
|
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
20000
|
-
|
|
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:
|
|
20005
|
-
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 =
|
|
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
|
|
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"),
|
|
@@ -20384,26 +20498,23 @@ async function executeSonarQubeTool(args, context) {
|
|
|
20384
20498
|
const config3 = loadConfig(context.config);
|
|
20385
20499
|
if (!config3) {
|
|
20386
20500
|
return formatError2(ErrorMessages.configurationMissing("SonarQube configuration not found.", [
|
|
20387
|
-
"Set environment variables: SONAR_HOST_URL and
|
|
20501
|
+
"Set environment variables: SONAR_HOST_URL, SONAR_USER, and SONAR_PASSWORD",
|
|
20388
20502
|
"Or create a .sonarqube/config.json file in your project",
|
|
20389
20503
|
"Or add plugin configuration in opencode.json"
|
|
20390
20504
|
]) + `
|
|
20391
20505
|
|
|
20392
20506
|
Required environment variables:
|
|
20393
20507
|
- SONAR_HOST_URL (e.g., https://sonarqube.company.com)
|
|
20394
|
-
-
|
|
20395
|
-
|
|
20396
|
-
Optional:
|
|
20397
|
-
- SONAR_USER
|
|
20398
|
-
- SONAR_PASSWORD`);
|
|
20508
|
+
- SONAR_USER (e.g., admin)
|
|
20509
|
+
- SONAR_PASSWORD (password or token)`);
|
|
20399
20510
|
}
|
|
20400
|
-
|
|
20511
|
+
logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
|
|
20401
20512
|
try {
|
|
20402
20513
|
if (args.action === "init" || args.action === "setup") {
|
|
20403
20514
|
return await handleSetup(config3, directory, args.force);
|
|
20404
20515
|
}
|
|
20405
20516
|
if (await needsBootstrap(directory)) {
|
|
20406
|
-
|
|
20517
|
+
logger10.info("First run detected, running bootstrap");
|
|
20407
20518
|
const { bootstrap: bootstrap2 } = await Promise.resolve().then(() => (init_bootstrap(), exports_bootstrap));
|
|
20408
20519
|
const setupResult = await bootstrap2({ config: config3, directory });
|
|
20409
20520
|
if (!setupResult.success) {
|
|
@@ -20411,7 +20522,7 @@ Optional:
|
|
|
20411
20522
|
|
|
20412
20523
|
${setupResult.message}`);
|
|
20413
20524
|
}
|
|
20414
|
-
|
|
20525
|
+
logger10.info("Bootstrap completed", { projectKey: setupResult.projectKey });
|
|
20415
20526
|
}
|
|
20416
20527
|
const state = await getProjectState(directory);
|
|
20417
20528
|
if (!state) {
|
|
@@ -20451,7 +20562,7 @@ ${setupResult.message}`);
|
|
|
20451
20562
|
}
|
|
20452
20563
|
} catch (error45) {
|
|
20453
20564
|
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
20454
|
-
|
|
20565
|
+
logger10.error(`Tool execution failed: ${errorMessage}`);
|
|
20455
20566
|
return formatError2(ErrorMessages.apiError(args.action, errorMessage, config3.url));
|
|
20456
20567
|
}
|
|
20457
20568
|
}
|