UNPKG

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
/** * 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