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