sonar-autofix 0.1.1 → 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.
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env node
2
- // CLI entry point for Sonar Autofix
3
- const CoreOrchestrator = require('@sonar-autofix/core');
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- /**
8
- * CLI interface for the Sonar Autofix tool
9
- * Supports configuration via JSON file and/or environment variables.
10
- */
11
- async function main() {
12
- console.log('Sonar Autofix CLI');
13
- console.log('=========================');
14
-
15
- // Parse command line arguments
16
- const args = process.argv.slice(2);
17
-
18
- // Handle help
19
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
20
- console.log('');
21
- console.log('This tool automatically fixes Sonar issues using LLMs.');
22
- console.log('');
23
- console.log('Usage:');
24
- console.log(' npx sonar-autofix --config <path> [options]');
25
- console.log('');
26
- console.log('Options:');
27
- console.log(' --help, -h Show this help message');
28
- console.log(' --version, -v Show version information');
29
- console.log(' --config <path> Path to configuration JSON file');
30
- console.log(' --dry-run Run without making changes');
31
- console.log('');
32
- console.log('Environment Variables:');
33
- console.log(' SONAR_HOST SonarQube/SonarCloud host URL');
34
- console.log(' SONAR_PROJECT Sonar project key');
35
- console.log(' SONAR_TOKEN Sonar authentication token');
36
- console.log(' LLM_PROVIDER LLM provider (lmstudio, openai, azure, anthropic, ollama, openrouter)');
37
- console.log(' LLM_ENDPOINT LLM API endpoint URL');
38
- console.log(' LLM_MODEL LLM model name');
39
- console.log(' LLM_API_KEY LLM API key');
40
- console.log(' GIT_BRANCH Branch name for autofix commits (default: sonar-autofix)');
41
- console.log('');
42
- console.log('Environment variables override config file values.');
43
- return;
44
- }
45
-
46
- // Handle version
47
- if (args.includes('--version') || args.includes('-v')) {
48
- console.log('sonar-autofix v0.0.1');
49
- return;
50
- }
51
-
52
- // Handle dry-run flag
53
- const dryRun = args.includes('--dry-run');
54
-
55
- // Load configuration from file
56
- let config = {};
57
- const configIndex = args.indexOf('--config');
58
- if (configIndex !== -1 && configIndex + 1 < args.length) {
59
- const configPath = args[configIndex + 1];
60
- try {
61
- const configFileContent = fs.readFileSync(path.resolve(configPath), 'utf8');
62
- config = JSON.parse(configFileContent);
63
- } catch (error) {
64
- console.error('❌ Failed to read configuration file:', error.message);
65
- process.exit(1);
66
- }
67
- }
68
-
69
- // Apply environment variable overrides
70
- if (process.env.SONAR_HOST || process.env.SONAR_PROJECT || process.env.SONAR_TOKEN) {
71
- config.sonar = config.sonar || {};
72
- if (process.env.SONAR_HOST) config.sonar.host = process.env.SONAR_HOST;
73
- if (process.env.SONAR_PROJECT) config.sonar.project = process.env.SONAR_PROJECT;
74
- if (process.env.SONAR_TOKEN) config.sonar.token = process.env.SONAR_TOKEN;
75
- }
76
-
77
- if (process.env.LLM_PROVIDER || process.env.LLM_ENDPOINT || process.env.LLM_MODEL || process.env.LLM_API_KEY) {
78
- config.llm = config.llm || {};
79
- if (process.env.LLM_PROVIDER) config.llm.provider = process.env.LLM_PROVIDER;
80
- if (process.env.LLM_ENDPOINT) config.llm.endpoint = process.env.LLM_ENDPOINT;
81
- if (process.env.LLM_MODEL) config.llm.model = process.env.LLM_MODEL;
82
- if (process.env.LLM_API_KEY) config.llm.apiKey = process.env.LLM_API_KEY;
83
- }
84
-
85
- if (process.env.GIT_BRANCH) {
86
- config.git = config.git || {};
87
- config.git.branch = process.env.GIT_BRANCH;
88
- }
89
-
90
- // Apply dry-run
91
- if (dryRun) {
92
- config.dryRun = true;
93
- }
94
-
95
- // Validate required configuration
96
- if (!config.sonar?.project) {
97
- console.error('❌ Missing required configuration: sonar.project (or SONAR_PROJECT env var)');
98
- process.exit(1);
99
- }
100
- if (!config.sonar?.token) {
101
- console.error('❌ Missing required configuration: sonar.token (or SONAR_TOKEN env var)');
102
- process.exit(1);
103
- }
104
- if (!config.llm?.provider) {
105
- console.error('❌ Missing required configuration: llm.provider (or LLM_PROVIDER env var)');
106
- process.exit(1);
107
- }
108
-
109
- // Run the core orchestrator
110
- try {
111
- const orchestrator = new CoreOrchestrator(config);
112
-
113
- console.log('🚀 Starting Sonar Autofix...');
114
- if (dryRun) {
115
- console.log(' (dry-run mode — no changes will be made)');
116
- }
117
- const summary = await orchestrator.run();
118
-
119
- console.log('✅ Done!');
120
- return summary;
121
- } catch (error) {
122
- console.error('❌ Error:', error.message);
123
- process.exit(1);
124
- }
125
- }
126
-
127
- // Run the CLI if this file is executed directly
128
- if (require.main === module) {
129
- main();
130
- }
131
-
132
- module.exports = { main };
@@ -1,23 +0,0 @@
1
- {
2
- "name": "@sonar-autofix/cli",
3
- "version": "0.0.6",
4
- "description": "CLI tool for the Sonar Autofix tool",
5
- "main": "index.js",
6
- "bin": {
7
- "sonar-autofix": "./index.js"
8
- },
9
- "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
12
- "keywords": [
13
- "sonar",
14
- "autofix",
15
- "github",
16
- "cli"
17
- ],
18
- "author": "Sonar Autofix Team",
19
- "license": "MIT",
20
- "dependencies": {
21
- "@sonar-autofix/core": "^0.0.3"
22
- }
23
- }
@@ -1,475 +0,0 @@
1
- // Core orchestrator for Sonar Autofix
2
- // Implements the complete workflow: Sonar → Fetch → Prioritize → Group → Fix → Validate → Repeat → Commit → Push → PR
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- // Priority and type ordering as specified in PROJECT.md
8
- const SEVERITY_ORDER = { BLOCKER: 0, CRITICAL: 1, MAJOR: 2, MINOR: 3, INFO: 4 };
9
- const TYPE_ORDER = { BUG: 0, VULNERABILITY: 1, SECURITY_HOTSPOT: 2, CODE_SMELL: 3 };
10
-
11
- const DEFAULT_RETRY_LIMIT = 3;
12
- const DEFAULT_MAX_ITERATIONS = 5;
13
-
14
- class CoreOrchestrator {
15
- /**
16
- * @param {object} config - Full configuration object
17
- * @param {object} [deps] - Dependency injection for clients
18
- * @param {object} [deps.sonarClient]
19
- * @param {object} [deps.llmClient]
20
- * @param {object} [deps.gitClient]
21
- * @param {object} [deps.validator]
22
- */
23
- constructor(config, deps = {}) {
24
- this.config = config;
25
- this.dryRun = config.dryRun || false;
26
- this.restoreOnFail = config.restoreOnFail || false;
27
- this.retryLimit = config.retry?.maxRetries || DEFAULT_RETRY_LIMIT;
28
- this.maxIterations = config.retry?.maxIterations || DEFAULT_MAX_ITERATIONS;
29
- this.cwd = config.cwd || process.cwd();
30
-
31
- // Dependency injection — lazily initialize if not provided
32
- this._sonarClient = deps.sonarClient || null;
33
- this._llmClient = deps.llmClient || null;
34
- this._gitClient = deps.gitClient || null;
35
- this._validator = deps.validator || null;
36
-
37
- // Plugin hooks
38
- this.hooks = {
39
- beforeRun: config.hooks?.beforeRun || [],
40
- beforeFile: config.hooks?.beforeFile || [],
41
- afterFile: config.hooks?.afterFile || [],
42
- afterRun: config.hooks?.afterRun || [],
43
- };
44
-
45
- // Load prompt template
46
- this.promptTemplate = null;
47
- this.retryPromptTemplate = null;
48
- }
49
-
50
- // Lazy client initialization
51
- get sonarClient() {
52
- if (!this._sonarClient) {
53
- const SonarClient = require('@sonar-autofix/sonar-client');
54
- this._sonarClient = new SonarClient(this.config);
55
- }
56
- return this._sonarClient;
57
- }
58
-
59
- get llmClient() {
60
- if (!this._llmClient) {
61
- const LLMClient = require('@sonar-autofix/llm-client');
62
- this._llmClient = new LLMClient(this.config);
63
- }
64
- return this._llmClient;
65
- }
66
-
67
- get gitClient() {
68
- if (!this._gitClient) {
69
- const GitClient = require('@sonar-autofix/git-client');
70
- this._gitClient = new GitClient(this.config, this.cwd);
71
- }
72
- return this._gitClient;
73
- }
74
-
75
- get validator() {
76
- if (!this._validator) {
77
- // const Validator = require('@sonar-autofix/validator');
78
- // this._validator = new Validator(this.config, this.cwd);
79
- }
80
- return this._validator;
81
- }
82
-
83
- /**
84
- * Main orchestration — runs the complete workflow
85
- */
86
- async run() {
87
- console.log('✓ Starting Sonar Autofix workflow...');
88
-
89
- await this.runHooks('beforeRun');
90
-
91
- try {
92
- // Step 1: Create a working branch
93
- const branchName = this.config.git?.branch || 'sonar-autofix';
94
- if (!this.dryRun) {
95
- await this.gitClient.createBranch(branchName);
96
- console.log(`✓ Created branch: ${branchName}`);
97
- }
98
-
99
- let iteration = 0;
100
- let totalFixed = 0;
101
-
102
- while (iteration < this.maxIterations) {
103
- iteration++;
104
- console.log(`\n--- Iteration ${iteration}/${this.maxIterations} ---`);
105
-
106
- // Step 2: Run Sonar analysis
107
- console.log('✓ Running Sonar analysis...');
108
- if (!this.dryRun) {
109
- await this.sonarClient.runAnalysis(this.config.sonar?.project);
110
- }
111
-
112
- // Step 3: Fetch issues
113
- console.log('✓ Fetching issues from Sonar...');
114
- const issues = await this.fetchAndNormalizeIssues();
115
- console.log(`✓ ${issues.length} issues found`);
116
-
117
- if (issues.length === 0) {
118
- console.log('✓ No issues remaining — workflow complete');
119
- break;
120
- }
121
-
122
- // Step 4: Prioritize
123
- const prioritized = this.prioritizeIssues(issues);
124
-
125
- // Step 5: Group by file
126
- const grouped = this.groupByFile(prioritized);
127
- console.log(`✓ Grouped into ${Object.keys(grouped).length} files`);
128
-
129
- // Step 6: Fix files
130
- let fixedInIteration = 0;
131
- for (const [filePath, fileIssues] of Object.entries(grouped)) {
132
- await this.runHooks('beforeFile', { filePath, issues: fileIssues });
133
-
134
- console.log(`✓ Fixing ${filePath} (${fileIssues.length} issues)...`);
135
- console.log(issues[0]);
136
- const result = await this.fixFileWithRetry(filePath, fileIssues);
137
-
138
- if (result.success) {
139
- fixedInIteration++;
140
- console.log(` ✓ ${filePath} fixed successfully`);
141
- } else {
142
- console.log(` ✗ ${filePath} could not be fixed`);
143
- if (!this.dryRun && this.restoreOnFail) {
144
- console.log(` ✗ ${filePath} could not be fixed — restoring original`);
145
- await this.gitClient.restoreFile(filePath);
146
- }
147
- }
148
-
149
- await this.runHooks('afterFile', { filePath, issues: fileIssues, result });
150
- }
151
-
152
- totalFixed += fixedInIteration;
153
-
154
- if (fixedInIteration === 0) {
155
- console.log('✓ No files fixed in this iteration — stopping');
156
- break;
157
- }
158
-
159
- //TODO
160
- // Step 7: Validate the batch
161
- // console.log('✓ Running batch validation...');
162
- // const batchResult = await this.validator.validateBatch();
163
- // if (!batchResult.success) {
164
- // console.log('✗ Batch validation failed — restoring all changes');
165
- // if (!this.dryRun) {
166
- // await this.gitClient.reset('hard');
167
- // }
168
- // break;
169
- // }
170
- // console.log('✓ Batch validation passed');
171
- }
172
-
173
- // Step 8: Commit and push
174
- if (totalFixed > 0 && !this.dryRun) {
175
- const hasChanges = await this.gitClient.hasChanges();
176
- if (hasChanges) {
177
- console.log('✓ Committing changes...');
178
- await this.gitClient.commit();
179
-
180
- console.log('✓ Pushing branch...');
181
- await this.gitClient.push(branchName);
182
-
183
- // Step 9: Create PR
184
- console.log('✓ Creating Pull Request...');
185
- const prUrl = await this.gitClient.createPullRequest(
186
- 'fix(sonar): automatically resolve Sonar issues',
187
- this.buildPRBody(totalFixed, iteration),
188
- );
189
- console.log(`✓ Pull Request created: ${prUrl}`);
190
- }
191
- } else if (this.dryRun) {
192
- console.log('✓ Dry-run mode — skipping commit/push/PR');
193
- } else {
194
- console.log('✓ No changes to commit');
195
- }
196
-
197
- const summary = { totalFixed, iterations: iteration };
198
- await this.runHooks('afterRun', summary);
199
-
200
- console.log(`\n✓ Workflow complete — ${totalFixed} files fixed in ${iteration} iteration(s)`);
201
- return summary;
202
- } catch (error) {
203
- console.error('✗ Workflow failed:', error.message);
204
- throw error;
205
- }
206
- }
207
-
208
- /**
209
- * Fetch issues from Sonar and normalize them into a consistent format
210
- */
211
- async fetchAndNormalizeIssues() {
212
- const projectKey = this.config.sonar?.project;
213
- const response = await this.sonarClient.getIssues(projectKey);
214
-
215
- // Sonar API returns { issues: [...], ... }
216
- const rawIssues = response.issues || response || [];
217
-
218
- return rawIssues.map(issue => ({
219
- key: issue.key,
220
- rule: issue.rule,
221
- severity: issue.severity || 'MAJOR',
222
- type: issue.type || 'CODE_SMELL',
223
- message: issue.message,
224
- component: issue.component,
225
- file: this.extractFilePath(issue.component, projectKey),
226
- line: issue.line || (issue.textRange && issue.textRange.startLine) || 0,
227
- effort: issue.effort,
228
- status: issue.status,
229
- }));
230
- }
231
-
232
- /**
233
- * Extract the relative file path from a Sonar component key
234
- * Component keys look like: "project-key:src/main/File.ts"
235
- */
236
- extractFilePath(component, projectKey) {
237
- if (!component) return '';
238
- const prefix = projectKey + ':';
239
- if (component.startsWith(prefix)) {
240
- return component.substring(prefix.length);
241
- }
242
- return component;
243
- }
244
-
245
- /**
246
- * Prioritize issues per PROJECT.md ordering:
247
- * 1. By severity: BLOCKER > CRITICAL > MAJOR > MINOR > INFO
248
- * 2. By type: Bug > Vulnerability > Security Hotspot > Code Smell
249
- */
250
- prioritizeIssues(issues) {
251
- return [...issues].sort((a, b) => {
252
- const sevA = SEVERITY_ORDER[a.severity] ?? 99;
253
- const sevB = SEVERITY_ORDER[b.severity] ?? 99;
254
- if (sevA !== sevB) return sevA - sevB;
255
-
256
- const typeA = TYPE_ORDER[a.type] ?? 99;
257
- const typeB = TYPE_ORDER[b.type] ?? 99;
258
- return typeA - typeB;
259
- });
260
- }
261
-
262
- /**
263
- * Group issues by file path — one LLM call per file, not per issue
264
- */
265
- groupByFile(issues) {
266
- const grouped = {};
267
- for (const issue of issues) {
268
- const file = issue.file;
269
- if (!file) continue;
270
- if (!grouped[file]) grouped[file] = [];
271
- grouped[file].push(issue);
272
- }
273
- return grouped;
274
- }
275
-
276
- /**
277
- * Fix a file with retry logic
278
- * On failure: retry up to retryLimit times, then restore original
279
- */
280
- async fixFileWithRetry(filePath, issues) {
281
- const absPath = path.resolve(this.cwd, filePath);
282
-
283
- // Read original file content
284
- let originalContent;
285
- try {
286
- originalContent = fs.readFileSync(absPath, 'utf8');
287
- } catch (err) {
288
- console.log(` ✗ Cannot read file: ${err.message}`);
289
- return { success: false, error: err.message };
290
- }
291
-
292
- let currentContent = originalContent;
293
- let lastErrors = '';
294
-
295
- for (let attempt = 1; attempt <= this.retryLimit; attempt++) {
296
- if (attempt > 1) {
297
- console.log(` ↻ Retry attempt ${attempt}/${this.retryLimit}...`);
298
- }
299
-
300
- try {
301
- // Build prompt and call LLM
302
- const fixedContent = await this.requestFix(filePath, currentContent, issues, lastErrors, attempt);
303
-
304
- if (!fixedContent || fixedContent.trim() === currentContent.trim()) {
305
- console.log(' ✗ LLM returned unchanged content');
306
- return { success: false, error: 'No changes produced' };
307
- }
308
-
309
- // Write the fixed content
310
- if (!this.dryRun) {
311
- fs.writeFileSync(absPath, fixedContent, 'utf8');
312
- }
313
-
314
- // Validate the file
315
- // const validation = await this.validator.validateFile(filePath);
316
- // if (validation.success) {
317
- return { success: true, attempt };
318
- // }
319
-
320
- // Validation failed — prepare for retry
321
- // lastErrors = validation.errors.join('\n');
322
- // currentContent = fixedContent;
323
- // console.log(` ✗ Validation failed: ${validation.errors[0]}`);
324
- } catch (err) {
325
- console.log(` ✗ Fix attempt failed: ${err.message}`);
326
- lastErrors = err.message;
327
- }
328
- }
329
-
330
- // All retries exhausted — restore original
331
- if (!this.dryRun) {
332
- fs.writeFileSync(absPath, originalContent, 'utf8');
333
- }
334
- return { success: false, error: 'All retry attempts exhausted' };
335
- }
336
-
337
- /**
338
- * Request a fix from the LLM
339
- */
340
- async requestFix(filePath, fileContent, issues, validationErrors, attempt) {
341
- // Truncate file content if too long
342
-
343
- const issueDescriptions = issues.map((issue, i) =>
344
- `Issue ${i + 1}:\n- Rule: ${issue.rule}\n- Severity: ${issue.severity}\n- Type: ${issue.type}\n- Message: ${issue.message}\n- Line: ${issue.line}`
345
- ).join('\n\n');
346
-
347
- let systemPrompt = 'You are an expert software engineer tasked with fixing code quality issues identified by SonarQube/SonarCloud.';
348
-
349
- let userPrompt;
350
- if (attempt > 1 && validationErrors) {
351
- // Retry prompt
352
- userPrompt = `A previous attempt to fix this file failed validation. Please fix the file again, addressing both the original Sonar issues AND the validation errors.
353
-
354
- File: ${filePath}
355
-
356
- \`\`\`
357
- ${fileContent}
358
- \`\`\`
359
-
360
- Original Sonar Issues:
361
- ${issueDescriptions}
362
-
363
- Validation Errors from Previous Attempt:
364
- \`\`\`
365
- ${validationErrors}
366
- \`\`\`
367
-
368
- Rules:
369
- - Modify only the supplied file.
370
- - Do not rename public APIs.
371
- - Preserve formatting and code style.
372
- - Do not introduce unrelated changes.
373
- - Return the complete file content.
374
-
375
- Return ONLY the complete fixed file content, wrapped in a code block.`;
376
- } else {
377
- // First attempt prompt
378
- userPrompt = `Fix the following Sonar issues in this file.
379
-
380
- File: ${filePath}
381
-
382
- \`\`\`
383
- ${fileContent}
384
- \`\`\`
385
-
386
- Issues to Fix:
387
- ${issueDescriptions}
388
-
389
- ${this.config.prompt?.codingGuidelines ? `Coding Guidelines:\n${this.config.prompt.codingGuidelines}\n` : ''}
390
- ${this.config.prompt?.projectInstructions ? `Project Instructions:\n${this.config.prompt.projectInstructions}\n` : ''}
391
-
392
- Rules:
393
- - Modify only the supplied file.
394
- - Do not rename public APIs.
395
- - Preserve formatting and code style.
396
- - Do not introduce unrelated changes.
397
- - Return the complete file content.
398
-
399
- Return ONLY the complete fixed file content, wrapped in a code block.`;
400
- }
401
-
402
- const messages = [
403
- { role: 'system', content: systemPrompt },
404
- { role: 'user', content: userPrompt },
405
- ];
406
-
407
- const response = await this.llmClient.chat(messages, {
408
- temperature: 0.1
409
- });
410
-
411
- // Extract code from the response (strip markdown code block markers)
412
- return this.extractCodeFromResponse(response);
413
- }
414
-
415
- /**
416
- * Extract code content from LLM response, stripping markdown fences
417
- */
418
- extractCodeFromResponse(response) {
419
- if (!response) {
420
- console.log('No response from LLM');
421
- return null;
422
- }
423
-
424
- // Try to extract from code block
425
- const codeBlockMatch = response.match(/```[\w]*\n([\s\S]*?)```/);
426
- if (codeBlockMatch) {
427
- return codeBlockMatch[1];
428
- }
429
-
430
- // If no code block found, return the response as-is (it might be raw code)
431
- return response.trim();
432
- }
433
-
434
- /**
435
- * Build a descriptive PR body
436
- */
437
- buildPRBody(totalFixed, iterations) {
438
- return `## Automated Sonar Issue Fixes
439
-
440
- This PR was automatically generated by **sonar-autofix**.
441
-
442
- ### Summary
443
- - **Files fixed:** ${totalFixed}
444
- - **Iterations:** ${iterations}
445
-
446
- ### What was done
447
- 1. Ran SonarQube/SonarCloud analysis
448
- 2. Fetched and prioritized open issues
449
- 3. Used LLM to generate fixes grouped by file
450
- 4. Validated each fix (syntax, lint, build)
451
- 5. Re-ran Sonar to verify improvement
452
- 6. Committed all successful fixes
453
-
454
- ### Review Notes
455
- - All changes have been validated against the project's build, lint, and test configuration.
456
- - Please review the changes carefully before merging.
457
-
458
- ---
459
- *Generated by [sonar-autofix](https://github.com/sonar-autofix)*`;
460
- }
461
-
462
- /**
463
- * Run plugin hooks
464
- */
465
- async runHooks(hookName, context = {}) {
466
- const hooks = this.hooks[hookName] || [];
467
- for (const hook of hooks) {
468
- if (typeof hook === 'function') {
469
- await hook(context);
470
- }
471
- }
472
- }
473
- }
474
-
475
- module.exports = CoreOrchestrator;
@@ -1,22 +0,0 @@
1
- {
2
- "name": "@sonar-autofix/core",
3
- "version": "0.0.4",
4
- "description": "Core orchestrator for the Sonar Autofix tool",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [
10
- "sonar",
11
- "autofix",
12
- "github",
13
- "action"
14
- ],
15
- "author": "Sonar Autofix Team",
16
- "license": "MIT",
17
- "dependencies": {
18
- "@sonar-autofix/sonar-client": "^0.0.3",
19
- "@sonar-autofix/llm-client": "^0.0.2",
20
- "@sonar-autofix/git-client": "^0.0.1"
21
- }
22
- }