UNPKG

@context-sync/server

Version:

MCP server for AI context sync with persistent memory, workspace file access, and intelligent code operations

373 lines 13.8 kB
import * as fs from 'fs'; import * as path from 'path'; export class CallGraphAnalyzer { workspacePath; fileCache; functionCache; callCache; constructor(workspacePath) { this.workspacePath = workspacePath; this.fileCache = new Map(); this.functionCache = new Map(); this.callCache = new Map(); } /** * Main method: Analyze call graph for a function */ analyzeCallGraph(functionName) { // Find the function definition const funcDef = this.findFunctionDefinition(functionName); if (!funcDef) { return null; } // Find all callers (who calls this function) const callers = this.findCallers(functionName); // Find all callees (what this function calls) const callees = this.findCallees(funcDef); // Check if recursive const isRecursive = callees.some(call => call.callee === functionName); return { function: funcDef, callers, callees, callDepth: this.calculateCallDepth(functionName), isRecursive }; } /** * Find all functions that call the given function */ findCallers(functionName) { const callers = []; const allFiles = this.getAllProjectFiles(); for (const file of allFiles) { const content = this.readFile(file); const functions = this.extractFunctions(file); for (const func of functions) { const calls = this.extractFunctionCalls(file, func.name); for (const call of calls) { if (call.callee === functionName) { callers.push({ caller: func.name, callee: functionName, line: call.line, filePath: file, isAsync: call.isAsync, callExpression: call.callExpression }); } } } } return callers; } /** * Find all functions that this function calls */ findCallees(funcDef) { return this.extractFunctionCalls(funcDef.filePath, funcDef.name); } /** * Trace execution path from start function to end function */ traceExecutionPath(startFunction, endFunction, maxDepth = 10) { const paths = []; const visited = new Set(); const dfs = (current, currentPath, currentFiles, depth) => { if (depth > maxDepth) return; // Found the target if (current === endFunction) { const hasAsync = currentPath.some(fn => { const def = this.findFunctionDefinition(fn); return def?.type === 'async'; }); paths.push({ path: [...currentPath, current], files: [...currentFiles], description: `${currentPath.join(' → ')} → ${current}`, isAsync: hasAsync, depth: depth + 1 }); return; } const key = `${current}-${depth}`; if (visited.has(key)) return; visited.add(key); const graph = this.analyzeCallGraph(current); if (!graph) return; for (const callee of graph.callees) { dfs(callee.callee, [...currentPath, current], [...currentFiles, callee.filePath], depth + 1); } }; dfs(startFunction, [], [], 0); return paths; } /** * Get call tree showing nested function calls */ getCallTree(functionName, maxDepth = 3) { const funcDef = this.findFunctionDefinition(functionName); if (!funcDef) return null; const visited = new Set(); const buildTree = (funcName, depth) => { if (depth > maxDepth) return null; const def = this.findFunctionDefinition(funcName); if (!def) return null; const tree = { function: funcName, file: this.getRelativePath(def.filePath), line: def.line, depth, calls: [], isRecursive: visited.has(funcName), isAsync: def.type === 'async' }; if (visited.has(funcName)) { return tree; } visited.add(funcName); const callees = this.findCallees(def); for (const callee of callees) { const subtree = buildTree(callee.callee, depth + 1); if (subtree) { tree.calls.push(subtree); } } return tree; }; return buildTree(functionName, 0); } /** * Find function definition across all files */ findFunctionDefinition(functionName) { const allFiles = this.getAllProjectFiles(); for (const file of allFiles) { const functions = this.extractFunctions(file); const found = functions.find(f => f.name === functionName); if (found) return found; } return null; } /** * Extract all function definitions from a file */ extractFunctions(filePath) { // Check cache if (this.functionCache.has(filePath)) { return this.functionCache.get(filePath); } const content = this.readFile(filePath); const functions = []; const lines = content.split('\n'); let currentClass; lines.forEach((line, lineNumber) => { const trimmed = line.trim(); // Class detection const classMatch = /class\s+(\w+)/.exec(trimmed); if (classMatch) { currentClass = classMatch[1]; } // Regular function: function name() {} const funcMatch = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/.exec(trimmed); if (funcMatch) { functions.push({ name: funcMatch[1], filePath, line: lineNumber + 1, type: trimmed.includes('async') ? 'async' : 'function', params: this.parseParams(funcMatch[2]), isExported: trimmed.includes('export'), className: currentClass }); return; } // Arrow function: const name = () => {} const arrowMatch = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/.exec(trimmed); if (arrowMatch) { functions.push({ name: arrowMatch[1], filePath, line: lineNumber + 1, type: 'arrow', params: this.parseParams(arrowMatch[2]), isExported: trimmed.includes('export') }); return; } // Method: methodName() {} or async methodName() {} const methodMatch = /(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*[:{]/.exec(trimmed); if (methodMatch && currentClass && !trimmed.includes('function')) { const methodName = methodMatch[1]; // Skip constructors and common keywords if (methodName !== 'constructor' && methodName !== 'if' && methodName !== 'while' && methodName !== 'for') { functions.push({ name: methodName, filePath, line: lineNumber + 1, type: trimmed.includes('async') ? 'async' : 'method', params: this.parseParams(methodMatch[2]), isExported: false, className: currentClass }); } } }); this.functionCache.set(filePath, functions); return functions; } /** * Extract function calls from a specific function */ extractFunctionCalls(filePath, functionName) { const content = this.readFile(filePath); const lines = content.split('\n'); const calls = []; // Pre-compile regex for better performance (fixes regex-in-loops) const escapedFunctionName = functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const funcRegex = new RegExp(`(?:function\\s+${escapedFunctionName}|(?:const|let|var)\\s+${escapedFunctionName}\\s*=|${escapedFunctionName}\\s*\\()`); // Find the function's body let inFunction = false; let braceCount = 0; let functionStartLine = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // Check if we're entering the target function if (!inFunction) { if (funcRegex.test(trimmed)) { inFunction = true; functionStartLine = i; braceCount = 0; } continue; } // Track braces braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; // Extract function calls in this line const callRegex = /(\w+)\s*\(/g; let match; while ((match = callRegex.exec(trimmed)) !== null) { const calledFunc = match[1]; // Skip keywords and common patterns if (this.isKeyword(calledFunc)) continue; calls.push({ caller: functionName, callee: calledFunc, line: i + 1, filePath, isAsync: trimmed.includes('await'), callExpression: trimmed }); } // Exit function when braces are balanced if (braceCount === 0 && inFunction && i > functionStartLine) { break; } } return calls; } /** * Calculate call depth (longest chain from this function) */ calculateCallDepth(functionName, visited = new Set()) { if (visited.has(functionName)) return 0; visited.add(functionName); const funcDef = this.findFunctionDefinition(functionName); if (!funcDef) return 0; const callees = this.findCallees(funcDef); if (callees.length === 0) return 1; let maxDepth = 0; for (const callee of callees) { const depth = this.calculateCallDepth(callee.callee, new Set(visited)); maxDepth = Math.max(maxDepth, depth); } return maxDepth + 1; } // Helper methods parseParams(paramString) { if (!paramString || !paramString.trim()) return []; return paramString.split(',').map(p => p.trim().split(/[:=]/)[0].trim()); } isKeyword(word) { const keywords = [ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally', 'new', 'typeof', 'instanceof', 'this', 'super', 'class', 'extends', 'import', 'export', 'default', 'const', 'let', 'var', 'function', 'async', 'await', 'yield', 'delete', 'in', 'of' ]; return keywords.includes(word); } readFile(filePath) { if (this.fileCache.has(filePath)) { return this.fileCache.get(filePath); } try { const content = fs.readFileSync(filePath, 'utf-8'); this.fileCache.set(filePath, content); return content; } catch (error) { return ''; } } resolveFilePath(filePath) { if (path.isAbsolute(filePath)) { return filePath; } return path.resolve(this.workspacePath, filePath); } getRelativePath(filePath) { return path.relative(this.workspacePath, filePath); } getAllProjectFiles() { const files = []; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; const walk = (dir) => { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!['node_modules', 'dist', 'build', '.git', '.next', 'out', 'coverage'].includes(entry.name)) { walk(fullPath); } } else { const ext = path.extname(entry.name); if (extensions.includes(ext)) { files.push(fullPath); } } } } catch (error) { // Skip directories we can't read } }; walk(this.workspacePath); return files; } /** * Clear caches */ clearCache() { this.fileCache.clear(); this.functionCache.clear(); this.callCache.clear(); } } //# sourceMappingURL=call-graph-analyzer.js.map