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
JavaScript
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