UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

294 lines 14.3 kB
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { basename, join, relative } from 'node:path'; import { performAdvancedAstAnalysis } from './codeAnalysisTools.js'; const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo']); export function createRefactoringTools(workingDir) { return [ { name: 'detect_refactoring_hotspots', description: 'Scan a file or directory for large/complex functions that merit refactoring.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File or directory to inspect (relative to workspace).', }, maxResults: { type: 'number', description: 'Limit the number of hotspots returned (default: 10).', }, }, required: ['path'], additionalProperties: false, }, handler: async (args) => { try { const targetPath = resolveFilePath(workingDir, args['path']); if (!existsSync(targetPath)) { return `Error: Path not found: ${targetPath}`; } const files = collectSourceFiles(targetPath); if (files.length === 0) { return `No source files (.ts/.tsx/.js/.jsx) discovered under ${targetPath}.`; } const hotspots = []; for (const file of files) { const content = readFileSync(file, 'utf-8'); const ast = performAdvancedAstAnalysis(content, file); for (const symbol of ast.symbols) { if (symbol.kind === 'class') { continue; } const reasons = []; if (symbol.statementCount > 40) { reasons.push(`long body (${symbol.statementCount} statements)`); } if (symbol.cyclomaticComplexity > 12) { reasons.push(`complex (CC ${symbol.cyclomaticComplexity})`); } const span = symbol.endLine - symbol.startLine + 1; if (span > 120) { reasons.push(`spans ${span} lines`); } if (reasons.length > 0) { hotspots.push({ file, symbol, reasons, score: symbol.cyclomaticComplexity * 2 + symbol.statementCount, }); } } } if (hotspots.length === 0) { return 'No refactoring hotspots detected.'; } const maxResultsArg = args['maxResults']; const limit = typeof maxResultsArg === 'number' && Number.isFinite(maxResultsArg) && maxResultsArg > 0 ? Math.floor(maxResultsArg) : 10; hotspots.sort((a, b) => b.score - a.score); const selection = hotspots.slice(0, limit); const output = []; output.push(`# Refactoring hotspots (${selection.length}/${hotspots.length})`); output.push(''); selection.forEach((record, index) => { const relPath = relative(workingDir, record.file); const span = record.symbol.endLine - record.symbol.startLine + 1; output.push(`${index + 1}. ${record.symbol.name} in ${relPath} (lines ${record.symbol.startLine}-${record.symbol.endLine})`); output.push(` - Severity score: ${record.score}`); output.push(` - Reasons: ${record.reasons.join(', ')}`); output.push(` - Statements: ${record.symbol.statementCount}, CC: ${record.symbol.cyclomaticComplexity}, span ${span} lines`); }); return output.join('\n'); } catch (error) { return `Error detecting hotspots: ${error instanceof Error ? error.message : String(error)}`; } }, }, { name: 'generate_refactor_plan', description: 'Create a structured refactor plan for the most complex symbol in a file (or a specific symbol).', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File to analyze.', }, symbol: { type: 'string', description: 'Optional symbol to target (defaults to the most complex function).', }, }, required: ['path'], additionalProperties: false, }, handler: async (args) => { try { const filePath = resolveFilePath(workingDir, args['path']); if (!existsSync(filePath)) { return `Error: File not found: ${filePath}`; } const content = readFileSync(filePath, 'utf-8'); const ast = performAdvancedAstAnalysis(content, filePath); if (ast.symbols.length === 0) { return `No analyzable symbols found in ${filePath}.`; } const targetSymbol = selectTargetSymbol(ast.symbols, args['symbol']); if (!targetSymbol) { return `Symbol "${String(args['symbol'])}" not found in ${filePath}.`; } const callOutputs = ast.callGraph.filter((edge) => edge.from === targetSymbol.name || edge.to === targetSymbol.name); const relPath = relative(workingDir, filePath); const plan = []; plan.push(`# Refactor plan for ${targetSymbol.name}`); plan.push(`File: ${relPath}`); plan.push(''); plan.push('## Current metrics'); plan.push(`- Statements: ${targetSymbol.statementCount}`); plan.push(`- Cyclomatic complexity: ${targetSymbol.cyclomaticComplexity}`); plan.push(`- Span: lines ${targetSymbol.startLine}-${targetSymbol.endLine}`); plan.push(''); plan.push('## Recommended steps'); plan.push('- Break the function into cohesive helpers grouped by responsibility.'); if (targetSymbol.statementCount > 60) { plan.push('- Extract the initialization / setup logic into a dedicated helper.'); } if (targetSymbol.cyclomaticComplexity > 14) { plan.push('- Replace deeply nested branching with guard clauses or strategy objects.'); } plan.push('- Introduce descriptive interfaces/types to clarify inputs and return values.'); plan.push('- Write focused unit tests for each new helper and for the refactored entry point.'); plan.push(''); plan.push('## Dependency considerations'); if (callOutputs.length === 0) { plan.push('This symbol does not interact with other tracked functions inside the file.'); } else { plan.push('Calls & referenced symbols:'); callOutputs.forEach((edge) => { if (edge.from === targetSymbol.name) { plan.push(`- Calls ${edge.to} (${edge.count} time${edge.count === 1 ? '' : 's'})`); } else { plan.push(`- Invoked by ${edge.from} (${edge.count} call${edge.count === 1 ? '' : 's'})`); } }); } return plan.join('\n'); } catch (error) { return `Error generating refactor plan: ${error instanceof Error ? error.message : String(error)}`; } }, }, { name: 'analyze_refactor_impact', description: 'Summarize inbound/outbound calls for a symbol to estimate refactor blast radius.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File containing the symbol.', }, symbol: { type: 'string', description: 'Symbol name to inspect. Defaults to the most connected symbol.', }, }, required: ['path'], additionalProperties: false, }, handler: async (args) => { try { const filePath = resolveFilePath(workingDir, args['path']); if (!existsSync(filePath)) { return `Error: File not found: ${filePath}`; } const content = readFileSync(filePath, 'utf-8'); const ast = performAdvancedAstAnalysis(content, filePath); if (ast.symbols.length === 0) { return `No analyzable symbols found in ${filePath}.`; } const fallbackSymbol = ast.symbols[0]?.name ?? ''; const symbolArg = args['symbol']; const symbolName = typeof symbolArg === 'string' && symbolArg.trim() ? symbolArg.trim() : findMostConnectedSymbol(ast) ?? fallbackSymbol; const incoming = ast.callGraph.filter((edge) => edge.to === symbolName); const outgoing = ast.callGraph.filter((edge) => edge.from === symbolName); const summary = []; summary.push(`# Refactor impact for ${symbolName}`); summary.push(''); summary.push('## Incoming references'); if (incoming.length === 0) { summary.push('No recorded inbound calls inside this file.'); } else { incoming.forEach((edge) => { summary.push(`- ${edge.from} (${edge.count} call${edge.count === 1 ? '' : 's'})`); }); } summary.push(''); summary.push('## Outgoing calls'); if (outgoing.length === 0) { summary.push('No outbound calls recorded.'); } else { outgoing.forEach((edge) => { summary.push(`- ${edge.to} (${edge.count} call${edge.count === 1 ? '' : 's'})`); }); } return summary.join('\n'); } catch (error) { return `Error analyzing refactor impact: ${error instanceof Error ? error.message : String(error)}`; } }, }, ]; } function resolveFilePath(workingDir, path) { if (typeof path !== 'string' || !path.trim()) { throw new Error('Path must be a non-empty string.'); } const value = path.trim(); return value.startsWith('/') ? value : join(workingDir, value); } function collectSourceFiles(targetPath) { const stats = statSync(targetPath); if (stats.isDirectory()) { const directoryName = basename(targetPath); if (IGNORED_DIRECTORIES.has(directoryName)) { return []; } const entries = readdirSync(targetPath); const files = []; for (const entry of entries) { files.push(...collectSourceFiles(join(targetPath, entry))); } return files; } if (!stats.isFile()) { return []; } if (SOURCE_EXTENSIONS.some((ext) => targetPath.endsWith(ext))) { return [targetPath]; } return []; } function selectTargetSymbol(symbols, requestedSymbol) { if (typeof requestedSymbol === 'string' && requestedSymbol.trim()) { const match = symbols.find((symbol) => symbol.name === requestedSymbol.trim()); if (match) { return match; } return null; } const sorted = [...symbols] .filter((symbol) => symbol.kind !== 'class') .sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity); return sorted[0] ?? null; } function findMostConnectedSymbol(ast) { const connectionWeights = new Map(); for (const edge of ast.callGraph) { connectionWeights.set(edge.from, (connectionWeights.get(edge.from) ?? 0) + edge.count); connectionWeights.set(edge.to, (connectionWeights.get(edge.to) ?? 0) + edge.count); } let bestSymbol = null; let bestScore = -1; for (const [symbol, weight] of connectionWeights.entries()) { if (weight > bestScore) { bestSymbol = symbol; bestScore = weight; } } return bestSymbol; } //# sourceMappingURL=refactoringTools.js.map