erosolar-cli
Version:
Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning
354 lines • 13.3 kB
JavaScript
/**
* Automated Refactoring Engine
*
* Identifies refactoring opportunities and applies safe transformations.
*
* Principal Investigator: Bo Shang
* Framework: erosolar-cli
*/
import * as fs from 'fs';
import * as path from 'path';
const REFACTORING_RULES = [
// Convert .then() chains to async/await
{
type: 'convert-to-async-await',
detect: /(\w+)\s*\(\s*\)\s*\.then\s*\(\s*(?:async\s*)?\(?\s*(\w+)\s*\)?\s*=>\s*\{([^}]+)\}\s*\)/g,
risk: 'low',
impact: 'medium',
description: 'Convert Promise .then() chain to async/await',
transform: (match) => {
const [full, func, param, body] = match;
if (!body)
return null;
return {
before: full,
after: `const ${param} = await ${func}();\n${body.trim()}`,
explanation: 'Async/await is more readable than .then() chains',
};
},
},
// Convert function expressions to arrow functions
{
type: 'convert-to-arrow',
detect: /function\s*\(([^)]*)\)\s*\{([^}]{1,100}return\s+[^}]+)\}/g,
risk: 'safe',
impact: 'low',
description: 'Convert function expression to arrow function',
transform: (match) => {
const [full, params, body] = match;
if (!body)
return null;
// Simple case: single return statement
const returnMatch = body.match(/^\s*return\s+(.+?);?\s*$/);
if (returnMatch) {
return {
before: full,
after: `(${params}) => ${returnMatch[1]}`,
explanation: 'Arrow functions are more concise for simple returns',
};
}
return {
before: full,
after: `(${params}) => {${body}}`,
explanation: 'Arrow functions are more concise',
};
},
},
// Simplify boolean expressions
{
type: 'simplify-conditional',
detect: /if\s*\(\s*(\w+)\s*===?\s*true\s*\)/g,
risk: 'safe',
impact: 'low',
description: 'Simplify boolean comparison',
transform: (match) => {
const [full, variable] = match;
return {
before: full,
after: `if (${variable})`,
explanation: 'Comparing to true is redundant',
};
},
},
// Simplify negated boolean
{
type: 'simplify-conditional',
detect: /if\s*\(\s*(\w+)\s*===?\s*false\s*\)/g,
risk: 'safe',
impact: 'low',
description: 'Simplify negated boolean comparison',
transform: (match) => {
const [full, variable] = match;
return {
before: full,
after: `if (!${variable})`,
explanation: 'Use negation instead of comparing to false',
};
},
},
// Extract repeated string to constant
{
type: 'extract-constant',
detect: /["']([A-Z][A-Z_]{10,})["']/g,
risk: 'safe',
impact: 'medium',
description: 'Extract string constant',
transform: (match) => {
const [full, value] = match;
if (!value)
return null;
const constName = value.replace(/[^A-Z_]/g, '_');
return {
before: full,
after: constName,
explanation: `Add: const ${constName} = ${full}; at module level`,
};
},
},
// Consolidate duplicate imports
{
type: 'consolidate-imports',
detect: /import\s+\{\s*(\w+)\s*\}\s+from\s+['"]([^'"]+)['"]\s*;\s*import\s+\{\s*(\w+)\s*\}\s+from\s+['"]\2['"]/g,
risk: 'safe',
impact: 'low',
description: 'Consolidate imports from same module',
transform: (match) => {
const [full, import1, module, import2] = match;
return {
before: full,
after: `import { ${import1}, ${import2} } from '${module}'`,
explanation: 'Combine imports from the same module',
};
},
},
// Remove unused variables (simple cases)
{
type: 'remove-dead-code',
detect: /(?:const|let|var)\s+(\w+)\s*=\s*[^;]+;(?![^]*\1)/g,
risk: 'medium',
impact: 'medium',
description: 'Potentially unused variable',
transform: (match) => {
const [full, varName] = match;
return {
before: full,
after: `// REMOVED: ${full}`,
explanation: `Variable '${varName}' appears to be unused`,
};
},
},
// Convert ternary to nullish coalescing
{
type: 'simplify-conditional',
detect: /(\w+)\s*\?\s*\1\s*:\s*(\w+|["'][^"']+["']|\d+)/g,
risk: 'safe',
impact: 'low',
description: 'Convert ternary to nullish coalescing',
transform: (match) => {
const [full, variable, fallback] = match;
return {
before: full,
after: `${variable} ?? ${fallback}`,
explanation: 'Nullish coalescing is cleaner for default values',
};
},
},
];
// ============================================================================
// Refactoring Engine
// ============================================================================
export class RefactoringEngine {
workingDir;
fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
excludeDirs = ['node_modules', '.git', 'dist', 'build'];
constructor(workingDir) {
this.workingDir = workingDir;
}
findOpportunities(targetPath) {
const target = targetPath || this.workingDir;
const files = this.collectFiles(target);
const opportunities = [];
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
const fileOpportunities = this.analyzeFile(file, content);
opportunities.push(...fileOpportunities);
}
return opportunities.sort((a, b) => {
const impactOrder = { high: 0, medium: 1, low: 2 };
const riskOrder = { safe: 0, low: 1, medium: 2, high: 3 };
return (impactOrder[a.impact] - impactOrder[b.impact] ||
riskOrder[a.risk] - riskOrder[b.risk]);
});
}
collectFiles(dir) {
const files = [];
if (fs.statSync(dir).isFile()) {
return this.fileExtensions.some(ext => dir.endsWith(ext)) ? [dir] : [];
}
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!this.excludeDirs.includes(entry.name)) {
files.push(...this.collectFiles(fullPath));
}
}
else if (entry.isFile()) {
if (this.fileExtensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
}
return files;
}
analyzeFile(file, content) {
const opportunities = [];
for (const rule of REFACTORING_RULES) {
const matches = content.matchAll(rule.detect);
for (const match of matches) {
const preview = rule.transform(match, content);
if (preview) {
const position = this.getPosition(content, match.index || 0);
opportunities.push({
type: rule.type,
file: path.relative(this.workingDir, file),
line: position.line,
description: rule.description,
impact: rule.impact,
risk: rule.risk,
preview,
});
}
}
}
return opportunities;
}
getPosition(content, index) {
const before = content.substring(0, index);
const lines = before.split('\n');
const lastLine = lines[lines.length - 1] || '';
return {
line: lines.length,
column: lastLine.length + 1,
};
}
applyRefactoring(opportunity) {
if (!opportunity.preview) {
return {
success: false,
filesModified: [],
changes: [],
errors: ['No preview available for this refactoring'],
};
}
const filePath = path.join(this.workingDir, opportunity.file);
let content = fs.readFileSync(filePath, 'utf-8');
const originalLength = content.length;
content = content.replace(opportunity.preview.before, opportunity.preview.after);
if (content.length === originalLength && !content.includes(opportunity.preview.after)) {
return {
success: false,
filesModified: [],
changes: [],
errors: ['Could not apply refactoring - pattern not found'],
};
}
fs.writeFileSync(filePath, content);
return {
success: true,
filesModified: [opportunity.file],
changes: [
{
file: opportunity.file,
type: opportunity.type,
description: opportunity.description,
linesChanged: opportunity.preview.after.split('\n').length,
},
],
errors: [],
};
}
applySafeRefactorings(opportunities) {
const safeOpportunities = opportunities.filter(o => o.risk === 'safe');
const results = {
success: true,
filesModified: [],
changes: [],
errors: [],
};
for (const opportunity of safeOpportunities) {
const result = this.applyRefactoring(opportunity);
results.filesModified.push(...result.filesModified);
results.changes.push(...result.changes);
results.errors.push(...result.errors);
if (!result.success) {
results.success = false;
}
}
// Deduplicate files
results.filesModified = [...new Set(results.filesModified)];
return results;
}
formatReport(opportunities) {
const lines = [];
lines.push('═'.repeat(60));
lines.push('REFACTORING OPPORTUNITIES REPORT');
lines.push('═'.repeat(60));
lines.push('');
// Summary by type
const byType = new Map();
for (const opp of opportunities) {
const list = byType.get(opp.type) || [];
list.push(opp);
byType.set(opp.type, list);
}
lines.push('📊 SUMMARY BY TYPE');
lines.push('─'.repeat(40));
for (const [type, opps] of byType) {
const safeCount = opps.filter(o => o.risk === 'safe').length;
lines.push(` ${type}: ${opps.length} (${safeCount} safe)`);
}
lines.push('');
// Summary by risk
const safeCount = opportunities.filter(o => o.risk === 'safe').length;
const lowRiskCount = opportunities.filter(o => o.risk === 'low').length;
const mediumRiskCount = opportunities.filter(o => o.risk === 'medium').length;
const highRiskCount = opportunities.filter(o => o.risk === 'high').length;
lines.push('⚠️ RISK ASSESSMENT');
lines.push('─'.repeat(40));
lines.push(` ✅ Safe to apply automatically: ${safeCount}`);
lines.push(` 🟢 Low risk (review recommended): ${lowRiskCount}`);
lines.push(` 🟡 Medium risk (careful review): ${mediumRiskCount}`);
lines.push(` 🔴 High risk (manual only): ${highRiskCount}`);
lines.push('');
// Detailed opportunities
lines.push('📝 OPPORTUNITIES');
lines.push('─'.repeat(40));
for (const opp of opportunities.slice(0, 15)) {
const riskIcon = {
safe: '✅',
low: '🟢',
medium: '🟡',
high: '🔴',
}[opp.risk];
lines.push(`${riskIcon} ${opp.file}:${opp.line}`);
lines.push(` [${opp.type}] ${opp.description}`);
if (opp.preview) {
lines.push(` Before: ${opp.preview.before.substring(0, 50)}...`);
lines.push(` After: ${opp.preview.after.substring(0, 50)}...`);
}
lines.push('');
}
if (opportunities.length > 15) {
lines.push(`... and ${opportunities.length - 15} more opportunities`);
lines.push('');
}
lines.push('═'.repeat(60));
return lines.join('\n');
}
}
// ============================================================================
// Exports
// ============================================================================
export default RefactoringEngine;
//# sourceMappingURL=refactoring.js.map