vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
701 lines (700 loc) • 36.8 kB
JavaScript
import path from 'path';
import fs from 'fs/promises';
import logger from '../../logger.js';
import { writeFileSecure, readFileSecure } from './fsUtils.js';
import { getOutputDirectory, getBaseOutputDir } from './directoryUtils.js';
export async function buildFileDependencyGraph(allFilesInfo, config, jobId) {
const nodes = [];
const edges = [];
const filePaths = new Set(allFilesInfo.map(f => f.relativePath));
const useIntermediateStorage = config && jobId && config.processing?.batchSize && allFilesInfo.length > config.processing.batchSize;
let tempDir;
if (useIntermediateStorage) {
tempDir = path.join(config.output?.outputDir || getOutputDirectory(config), '.cache', 'temp', jobId);
try {
await fs.mkdir(tempDir, { recursive: true });
logger.debug(`Created temporary directory for file dependency graph: ${tempDir}`);
}
catch (error) {
logger.warn(`Failed to create temporary directory for file dependency graph: ${error instanceof Error ? error.message : String(error)}`);
tempDir = undefined;
}
}
if (useIntermediateStorage && tempDir && config.processing?.batchSize) {
const batchSize = config.processing.batchSize;
const batches = [];
for (let i = 0; i < allFilesInfo.length; i += batchSize) {
batches.push(allFilesInfo.slice(i, i + batchSize));
}
logger.info(`Processing ${allFilesInfo.length} files in ${batches.length} batches for file dependency graph`);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchNodes = [];
const batchEdges = [];
batch.forEach(fileInfo => {
batchNodes.push({
id: fileInfo.relativePath,
label: `${fileInfo.relativePath} — ${fileInfo.comment || generateHeuristicComment(path.basename(fileInfo.relativePath), 'file')}`.substring(0, 80),
type: 'file',
comment: fileInfo.comment || generateHeuristicComment(path.basename(fileInfo.relativePath), 'file'),
});
fileInfo.imports.forEach(imp => {
let resolvedPath = null;
if (imp.path.startsWith('.')) {
resolvedPath = path.normalize(path.join(path.dirname(fileInfo.relativePath), imp.path));
if (resolvedPath && !filePaths.has(resolvedPath) && resolvedPath.endsWith('.js')) {
const tsPath = resolvedPath.replace(/\.js$/, '.ts');
if (filePaths.has(tsPath)) {
resolvedPath = tsPath;
}
}
}
else {
if (filePaths.has(imp.path) || filePaths.has(`${imp.path}.js`) || filePaths.has(`${imp.path}.ts`)) {
resolvedPath = filePaths.has(imp.path) ? imp.path : (filePaths.has(`${imp.path}.js`) ? `${imp.path}.js` : `${imp.path}.ts`);
}
}
if (resolvedPath && filePaths.has(resolvedPath)) {
batchEdges.push({
from: fileInfo.relativePath,
to: resolvedPath,
label: 'imports',
});
}
else if (resolvedPath) {
logger.debug(`Import from ${fileInfo.relativePath} to ${imp.path} (resolved: ${resolvedPath}) could not be matched to a project file.`);
}
});
});
const batchNodesFile = path.join(tempDir, `file-dep-nodes-batch-${i}.json`);
const batchEdgesFile = path.join(tempDir, `file-dep-edges-batch-${i}.json`);
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(batchNodesFile, JSON.stringify(batchNodes), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
await writeFileSecure(batchEdgesFile, JSON.stringify(batchEdges), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved batch ${i + 1}/${batches.length} results for file dependency graph`);
}
catch (error) {
logger.error(`Failed to save batch results for file dependency graph: ${error instanceof Error ? error.message : String(error)}`);
nodes.push(...batchNodes);
edges.push(...batchEdges);
}
}
try {
const batchFiles = await fs.readdir(tempDir);
const nodeFiles = batchFiles.filter(file => file.startsWith('file-dep-nodes-batch-'));
const edgeFiles = batchFiles.filter(file => file.startsWith('file-dep-edges-batch-'));
for (const file of nodeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchNodes = JSON.parse(content);
nodes.push(...batchNodes);
}
for (const file of edgeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchEdges = JSON.parse(content);
edges.push(...batchEdges);
}
logger.info(`Combined ${nodeFiles.length} batches for file dependency graph`);
}
catch (error) {
logger.error(`Failed to combine batch results for file dependency graph: ${error instanceof Error ? error.message : String(error)}`);
if (nodes.length === 0 || edges.length === 0) {
return processFileDependencyGraphDirectly(allFilesInfo);
}
}
}
else {
return processFileDependencyGraphDirectly(allFilesInfo);
}
return { nodes, edges };
}
function processFileDependencyGraphDirectly(allFilesInfo) {
const nodes = [];
const edges = [];
const filePaths = new Set(allFilesInfo.map(f => f.relativePath));
allFilesInfo.forEach(fileInfo => {
nodes.push({
id: fileInfo.relativePath,
label: `${fileInfo.relativePath} — ${fileInfo.comment || generateHeuristicComment(path.basename(fileInfo.relativePath), 'file')}`.substring(0, 80),
type: 'file',
comment: fileInfo.comment || generateHeuristicComment(path.basename(fileInfo.relativePath), 'file'),
});
fileInfo.imports.forEach(imp => {
let resolvedPath = null;
if (imp.path.startsWith('.')) {
resolvedPath = path.normalize(path.join(path.dirname(fileInfo.relativePath), imp.path));
if (resolvedPath && !filePaths.has(resolvedPath) && resolvedPath.endsWith('.js')) {
const tsPath = resolvedPath.replace(/\.js$/, '.ts');
if (filePaths.has(tsPath)) {
resolvedPath = tsPath;
}
}
}
else {
if (filePaths.has(imp.path) || filePaths.has(`${imp.path}.js`) || filePaths.has(`${imp.path}.ts`)) {
resolvedPath = filePaths.has(imp.path) ? imp.path : (filePaths.has(`${imp.path}.js`) ? `${imp.path}.js` : `${imp.path}.ts`);
}
}
if (resolvedPath && filePaths.has(resolvedPath)) {
edges.push({
from: fileInfo.relativePath,
to: resolvedPath,
label: 'imports',
});
}
else if (resolvedPath) {
logger.debug(`Import from ${fileInfo.relativePath} to ${imp.path} (resolved: ${resolvedPath}) could not be matched to a project file.`);
}
});
});
return { nodes, edges };
}
export async function buildClassInheritanceGraph(allFilesInfo, config, jobId) {
const useIntermediateStorage = config && jobId && config.processing?.batchSize && allFilesInfo.length > config.processing.batchSize;
let tempDir;
if (useIntermediateStorage) {
tempDir = path.join(config.output?.outputDir || getOutputDirectory(config), '.cache', 'temp', jobId);
try {
await fs.mkdir(tempDir, { recursive: true });
logger.debug(`Created temporary directory for class inheritance graph: ${tempDir}`);
}
catch (error) {
logger.warn(`Failed to create temporary directory for class inheritance graph: ${error instanceof Error ? error.message : String(error)}`);
tempDir = undefined;
}
}
if (useIntermediateStorage && tempDir && config.processing?.batchSize) {
return await processClassInheritanceGraphWithStorage(allFilesInfo, config, tempDir);
}
else {
return processClassInheritanceGraphDirectly(allFilesInfo);
}
}
async function processClassInheritanceGraphWithStorage(allFilesInfo, config, tempDir) {
const nodes = [];
const edges = [];
const classMap = new Map();
const classMapFile = path.join(tempDir, 'class-map.json');
const batchSize = config.processing?.batchSize || 100;
const batches = [];
for (let i = 0; i < allFilesInfo.length; i += batchSize) {
batches.push(allFilesInfo.slice(i, i + batchSize));
}
logger.info(`Processing ${allFilesInfo.length} files in ${batches.length} batches for class inheritance graph`);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchNodes = [];
const batchClassMap = {};
batch.forEach(fileInfo => {
fileInfo.classes.forEach(classInfo => {
const classId = `${fileInfo.relativePath}::${classInfo.name}`;
batchNodes.push({
id: classId,
label: `${classInfo.name} — ${classInfo.comment || generateHeuristicComment(classInfo.name, 'class')}`.substring(0, 80),
type: 'class',
comment: classInfo.comment || generateHeuristicComment(classInfo.name, 'class'),
filePath: fileInfo.relativePath,
});
batchClassMap[classInfo.name] = { classInfo, filePath: fileInfo.relativePath };
batchClassMap[classId] = { classInfo, filePath: fileInfo.relativePath };
classMap.set(classInfo.name, { classInfo, filePath: fileInfo.relativePath });
classMap.set(classId, { classInfo, filePath: fileInfo.relativePath });
});
});
const batchNodesFile = path.join(tempDir, `class-nodes-batch-${i}.json`);
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(batchNodesFile, JSON.stringify(batchNodes), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved batch ${i + 1}/${batches.length} nodes for class inheritance graph`);
}
catch (error) {
logger.error(`Failed to save batch nodes for class inheritance graph: ${error instanceof Error ? error.message : String(error)}`);
nodes.push(...batchNodes);
}
}
try {
const classMapObj = {};
classMap.forEach((value, key) => {
classMapObj[key] = {
classInfo: {
name: value.classInfo.name,
parentClass: value.classInfo.parentClass,
methods: value.classInfo.methods || [],
properties: value.classInfo.properties || [],
startLine: value.classInfo.startLine || 1,
endLine: value.classInfo.endLine || 1
},
filePath: value.filePath
};
});
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(classMapFile, JSON.stringify(classMapObj), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved class map for class inheritance graph`);
}
catch (error) {
logger.error(`Failed to save class map for class inheritance graph: ${error instanceof Error ? error.message : String(error)}`);
}
try {
const baseOutputDir = getBaseOutputDir();
const classMapContent = await readFileSecure(classMapFile, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const classMapObj = JSON.parse(classMapContent);
const classEntries = Object.entries(classMapObj);
const classBatchSize = batchSize;
const classBatches = [];
for (let i = 0; i < classEntries.length; i += classBatchSize) {
classBatches.push(classEntries.slice(i, i + classBatchSize));
}
logger.info(`Processing ${classEntries.length} class entries in ${classBatches.length} batches for inheritance edges`);
for (let i = 0; i < classBatches.length; i++) {
const batch = classBatches[i];
const batchEdges = [];
batch.forEach(([classId, { classInfo, filePath }]) => {
if (classInfo.parentClass) {
const parentEntry = classMapObj[classInfo.parentClass];
let parentClassId = classInfo.parentClass;
if (parentEntry) {
parentClassId = `${parentEntry.filePath}::${parentEntry.classInfo.name}`;
}
else {
const sameFileParentId = `${filePath}::${classInfo.parentClass}`;
if (classMapObj[sameFileParentId]) {
parentClassId = sameFileParentId;
}
else {
if (!Object.keys(classMapObj).some(k => k.endsWith(`::${classInfo.parentClass}`))) {
nodes.push({
id: classInfo.parentClass,
label: `${classInfo.parentClass} — (External or Unresolved)`,
type: 'class',
comment: '(External or Unresolved)'
});
}
parentClassId = classInfo.parentClass;
}
}
batchEdges.push({
from: parentClassId,
to: classId,
label: 'inherits',
});
}
});
const batchEdgesFile = path.join(tempDir, `class-edges-batch-${i}.json`);
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(batchEdgesFile, JSON.stringify(batchEdges), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved batch ${i + 1}/${classBatches.length} edges for class inheritance graph`);
}
catch (error) {
logger.error(`Failed to save batch edges for class inheritance graph: ${error instanceof Error ? error.message : String(error)}`);
edges.push(...batchEdges);
}
}
const batchFiles = await fs.readdir(tempDir);
const nodeFiles = batchFiles.filter(file => file.startsWith('class-nodes-batch-'));
const edgeFiles = batchFiles.filter(file => file.startsWith('class-edges-batch-'));
for (const file of nodeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchNodes = JSON.parse(content);
nodes.push(...batchNodes);
}
for (const file of edgeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchEdges = JSON.parse(content);
edges.push(...batchEdges);
}
logger.info(`Combined ${nodeFiles.length} node batches and ${edgeFiles.length} edge batches for class inheritance graph`);
}
catch (error) {
logger.error(`Failed to process class inheritance graph with intermediate storage: ${error instanceof Error ? error.message : String(error)}`);
if (nodes.length === 0 || edges.length === 0) {
return processClassInheritanceGraphDirectly(allFilesInfo);
}
}
return { nodes, edges };
}
function processClassInheritanceGraphDirectly(allFilesInfo) {
const nodes = [];
const edges = [];
const classMap = new Map();
allFilesInfo.forEach(fileInfo => {
fileInfo.classes.forEach(classInfo => {
const classId = `${fileInfo.relativePath}::${classInfo.name}`;
nodes.push({
id: classId,
label: `${classInfo.name} — ${classInfo.comment || generateHeuristicComment(classInfo.name, 'class')}`.substring(0, 80),
type: 'class',
comment: classInfo.comment || generateHeuristicComment(classInfo.name, 'class'),
filePath: fileInfo.relativePath,
});
classMap.set(classInfo.name, { classInfo, filePath: fileInfo.relativePath });
classMap.set(classId, { classInfo, filePath: fileInfo.relativePath });
});
});
classMap.forEach(({ classInfo, filePath }, classId) => {
if (classInfo.parentClass) {
const parentEntry = classMap.get(classInfo.parentClass);
let parentClassId = classInfo.parentClass;
if (parentEntry) {
parentClassId = `${parentEntry.filePath}::${parentEntry.classInfo.name}`;
}
else {
const sameFileParentId = `${filePath}::${classInfo.parentClass}`;
if (classMap.has(sameFileParentId)) {
parentClassId = sameFileParentId;
}
else {
if (!Array.from(classMap.keys()).some(k => k.endsWith(`::${classInfo.parentClass}`))) {
nodes.push({ id: classInfo.parentClass, label: `${classInfo.parentClass} — (External or Unresolved)`, type: 'class', comment: '(External or Unresolved)' });
}
parentClassId = classInfo.parentClass;
}
}
edges.push({
from: parentClassId,
to: classId,
label: 'inherits',
});
}
});
return { nodes, edges };
}
export async function buildFunctionCallGraph(allFilesInfo, sourceCodeCache, config, jobId) {
const useIntermediateStorage = config && jobId && config.processing?.batchSize && allFilesInfo.length > config.processing.batchSize;
let tempDir;
if (useIntermediateStorage) {
tempDir = path.join(config.output?.outputDir || getOutputDirectory(config), '.cache', 'temp', jobId);
try {
await fs.mkdir(tempDir, { recursive: true });
logger.debug(`Created temporary directory for function call graph: ${tempDir}`);
}
catch (error) {
logger.warn(`Failed to create temporary directory for function call graph: ${error instanceof Error ? error.message : String(error)}`);
tempDir = undefined;
}
}
if (useIntermediateStorage && tempDir && config.processing?.batchSize) {
return await processFunctionCallGraphWithStorage(allFilesInfo, sourceCodeCache, config, tempDir);
}
else {
return processFunctionCallGraphDirectly(allFilesInfo, sourceCodeCache);
}
}
async function processFunctionCallGraphWithStorage(allFilesInfo, sourceCodeCache, config, tempDir) {
const nodes = [];
const edges = [];
const functionsMapFile = path.join(tempDir, 'functions-map.json');
const batchSize = config.processing?.batchSize || 100;
const batches = [];
for (let i = 0; i < allFilesInfo.length; i += batchSize) {
batches.push(allFilesInfo.slice(i, i + batchSize));
}
logger.info(`Processing ${allFilesInfo.length} files in ${batches.length} batches for function call graph`);
const allKnownFunctionsObj = {};
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchNodes = [];
batch.forEach(fileInfo => {
fileInfo.functions.forEach(funcInfo => {
const funcId = `${fileInfo.relativePath}::${funcInfo.name}`;
batchNodes.push({
id: funcId,
label: `${funcInfo.name} — ${funcInfo.comment || generateHeuristicComment(funcInfo.name, 'function')}`.substring(0, 80),
type: 'function',
comment: funcInfo.comment,
filePath: fileInfo.relativePath,
});
allKnownFunctionsObj[funcInfo.name] = {
funcInfo: {
name: funcInfo.name,
startLine: funcInfo.startLine,
endLine: funcInfo.endLine
},
filePath: fileInfo.relativePath
};
allKnownFunctionsObj[funcId] = {
funcInfo: {
name: funcInfo.name,
startLine: funcInfo.startLine,
endLine: funcInfo.endLine
},
filePath: fileInfo.relativePath
};
});
fileInfo.classes.forEach(classInfo => {
classInfo.methods.forEach(methodInfo => {
const methodId = `${fileInfo.relativePath}::${classInfo.name}.${methodInfo.name}`;
batchNodes.push({
id: methodId,
label: `${classInfo.name}.${methodInfo.name} — ${methodInfo.comment || generateHeuristicComment(methodInfo.name, 'method', undefined, classInfo.name)}`.substring(0, 80),
type: 'method',
comment: methodInfo.comment,
filePath: fileInfo.relativePath,
});
allKnownFunctionsObj[`${classInfo.name}.${methodInfo.name}`] = {
funcInfo: {
name: methodInfo.name,
startLine: methodInfo.startLine,
endLine: methodInfo.endLine
},
filePath: fileInfo.relativePath,
className: classInfo.name
};
allKnownFunctionsObj[methodId] = {
funcInfo: {
name: methodInfo.name,
startLine: methodInfo.startLine,
endLine: methodInfo.endLine
},
filePath: fileInfo.relativePath,
className: classInfo.name
};
});
});
});
const batchNodesFile = path.join(tempDir, `func-nodes-batch-${i}.json`);
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(batchNodesFile, JSON.stringify(batchNodes), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved batch ${i + 1}/${batches.length} nodes for function call graph`);
}
catch (error) {
logger.error(`Failed to save batch nodes for function call graph: ${error instanceof Error ? error.message : String(error)}`);
nodes.push(...batchNodes);
}
}
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(functionsMapFile, JSON.stringify(allKnownFunctionsObj), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved functions map for function call graph`);
}
catch (error) {
logger.error(`Failed to save functions map for function call graph: ${error instanceof Error ? error.message : String(error)}`);
return processFunctionCallGraphDirectly(allFilesInfo, sourceCodeCache);
}
try {
const baseOutputDir = getBaseOutputDir();
const functionsMapContent = await readFileSecure(functionsMapFile, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const functionsMap = JSON.parse(functionsMapContent);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchEdges = [];
batch.forEach(fileInfo => {
const sourceCode = sourceCodeCache.get(fileInfo.filePath);
if (!sourceCode)
return;
const processSymbolList = (symbols, currentSymbolType, currentClassName) => {
symbols.forEach(callerInfo => {
const callerId = currentClassName
? `${fileInfo.relativePath}::${currentClassName}.${callerInfo.name}`
: `${fileInfo.relativePath}::${callerInfo.name}`;
const functionBody = sourceCode.substring(sourceCode.indexOf('{', callerInfo.startLine > 0 ? sourceCode.indexOf('\n', callerInfo.startLine - 1) : 0), sourceCode.lastIndexOf('}', callerInfo.endLine > 0 ? sourceCode.indexOf('\n', callerInfo.endLine) : sourceCode.length));
if (!functionBody)
return;
const functionEntries = Object.entries(functionsMap);
const functionBatchSize = 100;
for (let j = 0; j < functionEntries.length; j += functionBatchSize) {
const functionBatch = functionEntries.slice(j, j + functionBatchSize);
functionBatch.forEach(([key, { funcInfo: calleeInfo, filePath: calleeFilePath, className: calleeClassName }]) => {
if (!key.includes('::'))
return;
const calleeName = calleeInfo.name;
const calleeId = calleeClassName
? `${calleeFilePath}::${calleeClassName}.${calleeName}`
: `${calleeFilePath}::${calleeName}`;
if (callerId === calleeId)
return;
const callRegex = new RegExp(`\\b${escapeRegExp(calleeName)}\\b\\s*(?:\\(|\\.)`);
if (callRegex.test(functionBody)) {
batchEdges.push({
from: callerId,
to: calleeId,
label: 'calls?',
});
}
});
}
});
};
processSymbolList(fileInfo.functions, 'function');
fileInfo.classes.forEach(classInfo => {
processSymbolList(classInfo.methods, 'method', classInfo.name);
});
});
const batchEdgesFile = path.join(tempDir, `func-edges-batch-${i}.json`);
try {
const baseOutputDir = getBaseOutputDir();
await writeFileSecure(batchEdgesFile, JSON.stringify(batchEdges), config.allowedMappingDirectory, 'utf-8', baseOutputDir);
logger.debug(`Saved batch ${i + 1}/${batches.length} edges for function call graph`);
}
catch (error) {
logger.error(`Failed to save batch edges for function call graph: ${error instanceof Error ? error.message : String(error)}`);
edges.push(...batchEdges);
}
}
const batchFiles = await fs.readdir(tempDir);
const nodeFiles = batchFiles.filter(file => file.startsWith('func-nodes-batch-'));
const edgeFiles = batchFiles.filter(file => file.startsWith('func-edges-batch-'));
for (const file of nodeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchNodes = JSON.parse(content);
nodes.push(...batchNodes);
}
for (const file of edgeFiles) {
const filePath = path.join(tempDir, file);
const baseOutputDir = getBaseOutputDir();
const content = await readFileSecure(filePath, config.allowedMappingDirectory, 'utf-8', baseOutputDir);
const batchEdges = JSON.parse(content);
edges.push(...batchEdges);
}
logger.info(`Combined ${nodeFiles.length} node batches and ${edgeFiles.length} edge batches for function call graph`);
}
catch (error) {
logger.error(`Failed to process function call graph with intermediate storage: ${error instanceof Error ? error.message : String(error)}`);
if (nodes.length === 0 || edges.length === 0) {
return processFunctionCallGraphDirectly(allFilesInfo, sourceCodeCache);
}
}
const uniqueNodeIds = new Set(nodes.map(n => n.id));
const uniqueNodes = Array.from(uniqueNodeIds).map(id => nodes.find(n => n.id === id));
return { nodes: uniqueNodes, edges };
}
function processFunctionCallGraphDirectly(allFilesInfo, sourceCodeCache) {
const nodes = [];
const edges = [];
const allKnownFunctions = new Map();
allFilesInfo.forEach(fileInfo => {
fileInfo.functions.forEach(funcInfo => {
const funcId = `${fileInfo.relativePath}::${funcInfo.name}`;
nodes.push({
id: funcId,
label: `${funcInfo.name} — ${funcInfo.comment || generateHeuristicComment(funcInfo.name, 'function')}`.substring(0, 80),
type: 'function',
comment: funcInfo.comment,
filePath: fileInfo.relativePath,
});
allKnownFunctions.set(funcInfo.name, { funcInfo, filePath: fileInfo.relativePath });
allKnownFunctions.set(funcId, { funcInfo, filePath: fileInfo.relativePath });
});
fileInfo.classes.forEach(classInfo => {
classInfo.methods.forEach(methodInfo => {
const methodId = `${fileInfo.relativePath}::${classInfo.name}.${methodInfo.name}`;
nodes.push({
id: methodId,
label: `${classInfo.name}.${methodInfo.name} — ${methodInfo.comment || generateHeuristicComment(methodInfo.name, 'method', undefined, classInfo.name)}`.substring(0, 80),
type: 'method',
comment: methodInfo.comment,
filePath: fileInfo.relativePath,
});
allKnownFunctions.set(`${classInfo.name}.${methodInfo.name}`, { funcInfo: methodInfo, filePath: fileInfo.relativePath, className: classInfo.name });
allKnownFunctions.set(methodId, { funcInfo: methodInfo, filePath: fileInfo.relativePath, className: classInfo.name });
});
});
});
allFilesInfo.forEach(fileInfo => {
const sourceCode = sourceCodeCache.get(fileInfo.filePath);
if (!sourceCode)
return;
const processSymbolList = (symbols, currentSymbolType, currentClassName) => {
symbols.forEach(callerInfo => {
const callerId = currentClassName
? `${fileInfo.relativePath}::${currentClassName}.${callerInfo.name}`
: `${fileInfo.relativePath}::${callerInfo.name}`;
const functionBody = sourceCode.substring(sourceCode.indexOf('{', callerInfo.startLine > 0 ? sourceCode.indexOf('\n', callerInfo.startLine - 1) : 0), sourceCode.lastIndexOf('}', callerInfo.endLine > 0 ? sourceCode.indexOf('\n', callerInfo.endLine) : sourceCode.length));
if (!functionBody)
return;
allKnownFunctions.forEach(({ funcInfo: calleeInfo, filePath: calleeFilePath, className: calleeClassName }) => {
const calleeName = calleeInfo.name;
const calleeId = calleeClassName
? `${calleeFilePath}::${calleeClassName}.${calleeName}`
: `${calleeFilePath}::${calleeName}`;
if (callerId === calleeId)
return;
const callRegex = new RegExp(`\\b${escapeRegExp(calleeName)}\\b\\s*(?:\\(|\\.)`);
if (callRegex.test(functionBody)) {
edges.push({
from: callerId,
to: calleeId,
label: 'calls?',
});
}
});
});
};
processSymbolList(fileInfo.functions, 'function');
fileInfo.classes.forEach(classInfo => {
processSymbolList(classInfo.methods, 'method', classInfo.name);
});
});
const uniqueNodeIds = new Set(nodes.map(n => n.id));
const uniqueNodes = Array.from(uniqueNodeIds).map(id => nodes.find(n => n.id === id));
return { nodes: uniqueNodes, edges };
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export async function buildMethodCallSequenceGraph(allFilesInfo, sourceCodeCache, config, jobId) {
const useIntermediateStorage = config && jobId && config.processing?.batchSize && allFilesInfo.length > config.processing.batchSize;
let tempDir;
if (useIntermediateStorage) {
tempDir = path.join(config.output?.outputDir || getOutputDirectory(config), '.cache', 'temp', jobId);
try {
await fs.mkdir(tempDir, { recursive: true });
logger.debug(`Created temporary directory for method call sequence graph: ${tempDir}`);
}
catch (error) {
logger.warn(`Failed to create temporary directory for method call sequence graph: ${error instanceof Error ? error.message : String(error)}`);
tempDir = undefined;
}
}
if (useIntermediateStorage && tempDir && config.processing?.batchSize) {
return await processMethodCallSequenceGraphWithStorage(allFilesInfo, sourceCodeCache, config, tempDir);
}
else {
return processMethodCallSequenceGraphDirectly(allFilesInfo, sourceCodeCache);
}
}
async function processMethodCallSequenceGraphWithStorage(allFilesInfo, sourceCodeCache, config, tempDir) {
const { nodes, edges } = await processFunctionCallGraphWithStorage(allFilesInfo, sourceCodeCache, config, tempDir);
const sequenceEdges = edges.map((edge, index) => {
return {
...edge,
sequenceOrder: index,
};
});
return { nodes, edges: sequenceEdges };
}
function processMethodCallSequenceGraphDirectly(allFilesInfo, sourceCodeCache) {
return buildFunctionCallGraph(allFilesInfo, sourceCodeCache);
}
function generateHeuristicComment(name, type, signature, parentClass) {
const A_AN = ['a', 'e', 'i', 'o', 'u'].includes(name.charAt(0).toLowerCase()) ? 'An' : 'A';
const nameParts = name.replace(/([A-Z])/g, ' $1').toLowerCase().split(/[\s_]+/).filter(Boolean);
const readableName = nameParts.join(' ');
switch (type) {
case 'function':
return `Performs an action related to ${readableName}.`;
case 'method':
return `Method ${readableName} of class ${parentClass || 'N/A'}.`;
case 'class':
return `${A_AN} ${readableName} class definition.`;
case 'property':
return `Property ${readableName} of class ${parentClass || 'N/A'}.`;
case 'import':
return `Imports module or items from '${readableName}'.`;
case 'file':
return `File containing code related to ${readableName}.`;
default:
return `Symbol ${readableName}.`;
}
}