UNPKG

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
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}.`; } }