UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

339 lines (334 loc) • 14.9 kB
import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; /** * AST-based tool for analyzing TypeScript files and identifying natural module boundaries * Follows the proven TDD extraction pattern from successful modular extractions */ export class ExtractionAnalyzer { fileSystem; constructor(fileSystem) { this.fileSystem = fileSystem || { readFileSync: (path, encoding = 'utf-8') => fs.readFileSync(path, { encoding: encoding }), statSync: (path) => fs.statSync(path) }; } /** * Analyze a TypeScript file and identify potential module extraction candidates */ async analyzeFile(filePath) { try { const sourceCode = this.fileSystem.readFileSync(filePath, 'utf-8'); // Check for syntax errors by attempting to parse const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true); // Simple check for obvious syntax errors - if we can't find any valid TypeScript constructs, it's likely invalid const hasValidContent = this.hasValidTypeScriptContent(sourceCode); if (!hasValidContent) { return { extractionCandidates: [], metrics: { cyclomaticComplexity: 0, totalLines: 0, functionCount: 0 }, recommendations: [], reason: `parsing error: Invalid TypeScript syntax` }; } const analysis = this.performASTAnalysis(sourceFile, sourceCode); return analysis; } catch (error) { if (error instanceof Error) { throw error; // Re-throw the error for proper testing } throw error; } } /** * Generate module template code following proven patterns from successful extractions */ generateModuleTemplate(candidate) { const moduleCode = this.generateModuleCode(candidate); const testCode = this.generateTestCode(candidate); const integrationCode = this.generateIntegrationCode(candidate); return { moduleCode, testCode, integrationCode }; } /** * Analyze extraction progress across multiple files */ async analyzeExtractionProgress(filePaths) { const violations = []; let totalExcess = 0; for (const filePath of filePaths) { try { const stats = this.fileSystem.statSync(filePath); const lines = this.countLines(filePath); const target = 300; if (lines > target) { const excess = lines - target; violations.push({ file: path.basename(filePath), excess }); totalExcess += excess; } } catch (error) { // File doesn't exist or other error, skip continue; } } // Sort by excess lines (largest first) const priorityOrder = violations .sort((a, b) => b.excess - a.excess) .map(v => v.file); return { totalViolations: violations.length, totalExcessLines: totalExcess, priorityOrder }; } performASTAnalysis(sourceFile, sourceCode) { const functions = this.extractFunctions(sourceFile); const dependencies = this.analyzeDependencies(sourceFile, functions); const metrics = this.calculateMetrics(sourceFile, sourceCode); // Group functions by potential modules using heuristics const functionGroups = this.groupFunctionsByModule(functions, dependencies); if (functionGroups.length === 0) { return { extractionCandidates: [], metrics, recommendations: [], reason: 'No clear module boundaries found - functions are too interdependent' }; } const candidates = functionGroups.map(group => this.createExtractionCandidate(group, dependencies)); const recommendations = candidates.map(candidate => this.createRecommendation(candidate)); return { extractionCandidates: candidates, metrics, recommendations }; } extractFunctions(sourceFile) { const functions = []; const visit = (node) => { if (ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name)) { functions.push({ name: node.name.text, node }); } else if (ts.isFunctionDeclaration(node) && node.name) { functions.push({ name: node.name.text, node }); } ts.forEachChild(node, visit); }; visit(sourceFile); return functions; } analyzeDependencies(sourceFile, functions) { const dependencies = new Map(); const functionNames = new Set(functions.map(f => f.name)); functions.forEach(func => { const deps = []; const visit = (node) => { if (ts.isCallExpression(node)) { // Handle method calls like this.utilA() if (ts.isPropertyAccessExpression(node.expression)) { const methodName = node.expression.name.text; if (functionNames.has(methodName) && methodName !== func.name) { deps.push(methodName); } } // Handle direct function calls else if (ts.isIdentifier(node.expression)) { const funcName = node.expression.text; if (functionNames.has(funcName) && funcName !== func.name) { deps.push(funcName); } } } ts.forEachChild(node, visit); }; visit(func.node); dependencies.set(func.name, [...new Set(deps)]); }); return dependencies; } calculateMetrics(sourceFile, sourceCode) { let complexity = 1; // Base complexity let functionCount = 0; const visit = (node) => { // Count control flow statements for cyclomatic complexity if (ts.isIfStatement(node) || ts.isWhileStatement(node) || ts.isForStatement(node) || ts.isCaseClause(node) || ts.isConditionalExpression(node)) { complexity++; } if (ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) { functionCount++; } ts.forEachChild(node, visit); }; visit(sourceFile); return { cyclomaticComplexity: complexity, totalLines: sourceCode.split('\n').length, functionCount }; } groupFunctionsByModule(functions, dependencies) { const groups = []; const processed = new Set(); functions.forEach(func => { if (processed.has(func.name)) return; const group = this.findConnectedFunctions(func.name, dependencies, processed); if (group.length >= 2) { // Only consider groups with at least 2 functions groups.push(group); group.forEach(name => processed.add(name)); } }); return groups; } findConnectedFunctions(startFunction, dependencies, processed) { const group = [startFunction]; const toProcess = [startFunction]; const visited = new Set([startFunction]); while (toProcess.length > 0) { const current = toProcess.pop(); const deps = dependencies.get(current) || []; // Add direct dependencies to the group deps.forEach(dep => { if (!visited.has(dep) && !processed.has(dep)) { visited.add(dep); group.push(dep); toProcess.push(dep); } }); // Be more selective about reverse dependencies - only add if it's clearly a helper for (const [funcName, funcDeps] of dependencies.entries()) { if (funcDeps.includes(current) && !visited.has(funcName) && !processed.has(funcName)) { // Only include if it has similar naming pattern and is likely a helper const isSimilarNaming = this.hasSimilarNaming(funcName, current); const isHelperFunction = funcName.toLowerCase().includes('helper') || funcName.toLowerCase().includes('util') || current.toLowerCase().includes('helper') || current.toLowerCase().includes('util'); if (isSimilarNaming && isHelperFunction) { visited.add(funcName); group.push(funcName); toProcess.push(funcName); } } } } return group; } createExtractionCandidate(functionGroup, dependencies) { // Calculate cohesion score based on internal vs external dependencies const internalDeps = functionGroup.flatMap(func => (dependencies.get(func) || []).filter(dep => functionGroup.includes(dep))).length; const externalDeps = functionGroup.flatMap(func => (dependencies.get(func) || []).filter(dep => !functionGroup.includes(dep))).length; const cohesionScore = internalDeps / Math.max(internalDeps + externalDeps, 1); // Generate module name based on function names const moduleName = this.generateModuleName(functionGroup); // Determine extraction priority const extractionPriority = this.determineExtractionPriority(functionGroup.length, cohesionScore); return { moduleName, functions: functionGroup, dependencies: [...new Set(functionGroup.flatMap(func => dependencies.get(func) || []))], cohesionScore, extractionPriority }; } generateModuleName(functionGroup) { // Simple heuristic: look for common prefixes/themes const commonTerms = ['browser', 'event', 'state', 'debug', 'framework', 'user', 'action', 'monitor']; for (const term of commonTerms) { if (functionGroup.some(func => func.toLowerCase().includes(term))) { return term.charAt(0).toUpperCase() + term.slice(1) + 'Manager'; } } // Fallback: use first function name as basis const firstFunc = functionGroup[0]; const baseName = firstFunc.replace(/([a-z])([A-Z])/g, '$1 $2').split(' ')[0]; return baseName.charAt(0).toUpperCase() + baseName.slice(1) + 'Helper'; } determineExtractionPriority(functionCount, cohesionScore) { if (functionCount >= 4 && cohesionScore >= 0.7) return 'high'; if (functionCount >= 3 && cohesionScore >= 0.5) return 'medium'; return 'low'; } createRecommendation(candidate) { const estimatedLines = candidate.functions.length * 15; // Rough estimate const reasoning = `Module ${candidate.moduleName} has ${candidate.functions.length} functions with ${(candidate.cohesionScore * 100).toFixed(0)}% cohesion`; return { moduleName: candidate.moduleName, priority: candidate.extractionPriority, estimatedLines, reasoning }; } generateModuleCode(candidate) { const dependencies = candidate.dependencies.filter(dep => !candidate.functions.includes(dep)); const hasExternalDeps = dependencies.length > 0; return `/** * ${candidate.moduleName} - Extracted module following proven TDD pattern * Cohesion Score: ${(candidate.cohesionScore * 100).toFixed(1)}% * Functions: ${candidate.functions.join(', ')} */ export class ${candidate.moduleName} {${hasExternalDeps ? ` constructor(${dependencies.map(dep => `private ${dep.toLowerCase()}: ${dep}`).join(', ')}) {}` : ''} ${candidate.functions.map(func => ` async ${func}(): Promise<any> { // TODO: Implement ${func} - extracted from original class throw new Error('${func} not yet implemented'); }`).join('\n\n')} }`; } generateTestCode(candidate) { return `import { describe, beforeEach, it, expect, jest } from '@jest/globals'; import { ${candidate.moduleName} } from '../src/${candidate.moduleName.toLowerCase()}.js'; describe('${candidate.moduleName}', () => { let ${candidate.moduleName.toLowerCase()}: ${candidate.moduleName}; beforeEach(() => { ${candidate.moduleName.toLowerCase()} = new ${candidate.moduleName}(); }); ${candidate.functions.map(func => ` describe('${func}', () => { it('should ${func.replace(/([A-Z])/g, ' $1').toLowerCase()}', async () => { // TODO: Implement test for ${func} await expect(${candidate.moduleName.toLowerCase()}.${func}()).rejects.toThrow('${func} not yet implemented'); }); });`).join('\n\n')} });`; } generateIntegrationCode(candidate) { const varName = candidate.moduleName.replace(/([A-Z])/g, (match, p1, offset) => offset === 0 ? p1.toLowerCase() : p1.toLowerCase()); return `// Integration code for ${candidate.moduleName} // Add to constructor: this.${varName} = new ${candidate.moduleName}(${candidate.dependencies.map(dep => `this.${dep.toLowerCase()}`).join(', ')}); // Replace method calls: ${candidate.functions.map(func => `// Replace: this.${func}() // With: this.${varName}.${func}()`).join('\n')}`; } countLines(filePath) { try { const content = this.fileSystem.readFileSync(filePath, 'utf-8'); return content.split('\n').length; } catch { return 0; } } hasValidTypeScriptContent(sourceCode) { // Simple heuristic: look for common TypeScript/JavaScript keywords and syntax const keywords = ['class', 'function', 'const', 'let', 'var', 'interface', 'type', 'export', 'import']; const hasKeyword = keywords.some(keyword => sourceCode.includes(keyword)); const hasBraces = sourceCode.includes('{') && sourceCode.includes('}'); return hasKeyword && hasBraces; } hasSimilarNaming(funcName1, funcName2) { // Simple heuristic: check if functions have similar prefixes const commonPrefixes = ['util', 'helper', 'db', 'browser', 'event', 'state']; return commonPrefixes.some(prefix => funcName1.toLowerCase().includes(prefix) && funcName2.toLowerCase().includes(prefix)); } } //# sourceMappingURL=extraction-analyzer.js.map