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.
- package/AGENTS.md +164 -0
- package/README.md +2 -2
- package/config.example.json +3 -1
- package/package.json +6 -2
- package/scripts/publish-all.js +211 -0
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -197
- package/packages/cli/index.js +0 -132
- package/packages/cli/package.json +0 -23
- package/packages/core/index.js +0 -475
- package/packages/core/package.json +0 -22
- package/packages/git-client/index.js +0 -371
- package/packages/git-client/package.json +0 -17
- package/packages/github-action/action.yml +0 -71
- package/packages/github-action/index.js +0 -86
- package/packages/github-action/package.json +0 -17
- package/packages/llm-client/index.js +0 -419
- package/packages/llm-client/package.json +0 -17
- package/packages/sonar-client/index.js +0 -343
- package/packages/sonar-client/package.json +0 -20
- package/packages/validator/index.js +0 -170
- package/packages/validator/package.json +0 -17
package/packages/cli/index.js
DELETED
|
@@ -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
|
-
}
|
package/packages/core/index.js
DELETED
|
@@ -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
|
-
}
|