UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

372 lines 14.6 kB
/** * Call Graph Builder * Builds and manages the call graph for identifying unused code */ import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; import { LanguageConfigManager } from './LanguageConfigManager.js'; export class CallGraphBuilder { projectRoot; languageManager; callGraph = new Map(); fileMetadata = new Map(); entryPoints = new Set(); constructor(projectRoot) { this.projectRoot = projectRoot; this.languageManager = new LanguageConfigManager(); } async buildCallGraph() { // Get all source files for all supported languages const allExtensions = new Set(); for (const language of this.languageManager.getAllLanguages()) { const config = this.languageManager.getConfig(language); if (config) { config.extensions.forEach(ext => allExtensions.add(ext)); } } const extensionPattern = Array.from(allExtensions).join(','); const sourceFiles = await glob(`**/*.{${extensionPattern}}`, { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'], absolute: false }); // Build file metadata first for (const file of sourceFiles) { await this.analyzeFile(file); } // Build call relationships for (const file of sourceFiles) { await this.buildFileCallGraph(file); } // Identify entry points await this.identifyEntryPoints(); return { callGraph: this.callGraph, fileMetadata: this.fileMetadata, entryPoints: this.entryPoints }; } async analyzeFile(file) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const stats = await fs.stat(filePath); const ext = path.extname(file).toLowerCase().substring(1); const language = this.languageManager.getLanguageFromExtension(ext); const metadata = { path: file, size: stats.size, lines: content.split('\n').length, lastModified: stats.mtime, language: language || 'unknown', functions: this.extractFunctionNames(content, language), imports: this.extractImports(content, language), exports: this.extractExports(content, language), dependencies: [], isEntryPoint: false }; this.fileMetadata.set(file, metadata); } catch (error) { // Skip files that can't be read } } async buildFileCallGraph(file) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const ext = path.extname(file).toLowerCase().substring(1); const language = this.languageManager.getLanguageFromExtension(ext); if (!language) return; // Extract functions, classes, and variables const functions = this.extractFunctions(content, file, language); const classes = this.extractClasses(content, file, language); const variables = this.extractVariables(content, file, language); // Add to call graph [...functions, ...classes, ...variables].forEach(node => { this.callGraph.set(`${file}:${node.name}`, node); }); // Build call relationships this.buildCallRelationships(content, file, language); } catch (error) { // Skip files that can't be read } } extractFunctionNames(content, language) { if (!language) return []; const config = this.languageManager.getConfig(language); if (!config) return []; const functions = []; for (const pattern of config.functionPatterns) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { if (match[1] && !functions.includes(match[1])) { functions.push(match[1]); } } } return functions; } extractImports(content, language) { if (!language) return []; const config = this.languageManager.getConfig(language); if (!config) return []; const imports = []; for (const pattern of config.importPatterns) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { if (match[1] && !imports.includes(match[1])) { imports.push(match[1]); } } } return imports; } extractExports(content, language) { if (!language) return []; const config = this.languageManager.getConfig(language); if (!config) return []; const exports = []; for (const pattern of config.exportPatterns) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { if (match[1] && !exports.includes(match[1])) { exports.push(match[1]); } } } return exports; } extractFunctions(content, file, language) { const config = this.languageManager.getConfig(language); if (!config) return []; const functions = []; const lines = content.split('\n'); for (const pattern of config.functionPatterns) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { if (match[1]) { const lineNumber = this.getLineNumber(content, match.index); functions.push({ name: match[1], file, line: lineNumber, type: 'function', calls: new Set(), calledBy: new Set(), isExported: this.isExported(content, match[1], language), isExternal: false }); } } } return functions; } extractClasses(content, file, language) { const config = this.languageManager.getConfig(language); if (!config) return []; const classes = []; for (const pattern of config.classPatterns) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { if (match[1]) { const lineNumber = this.getLineNumber(content, match.index); classes.push({ name: match[1], file, line: lineNumber, type: 'class', calls: new Set(), calledBy: new Set(), isExported: this.isExported(content, match[1], language), isExternal: false }); } } } return classes; } extractVariables(content, file, language) { // Simple variable extraction for major languages const variables = []; if (language === 'javascript') { const varPattern = /(?:const|let|var)\s+(\w+)\s*=/g; let match; while ((match = varPattern.exec(content)) !== null) { if (match[1]) { const lineNumber = this.getLineNumber(content, match.index); variables.push({ name: match[1], file, line: lineNumber, type: 'variable', calls: new Set(), calledBy: new Set(), isExported: this.isExported(content, match[1], language), isExternal: false }); } } } return variables; } buildCallRelationships(content, file, language) { // This is a simplified implementation - in reality, this would be much more sophisticated const lines = content.split('\n'); // For each function call pattern, find what it calls const callPattern = /(\w+)\s*\(/g; let match; while ((match = callPattern.exec(content)) !== null) { const callerLineNumber = this.getLineNumber(content, match.index); const functionName = match[1]; // Find the function that contains this call const caller = this.findContainingFunction(content, match.index, file); if (caller) { const callerKey = `${file}:${caller}`; const calleeKey = `${file}:${functionName}`; const callerNode = this.callGraph.get(callerKey); const calleeNode = this.callGraph.get(calleeKey); if (callerNode && calleeNode) { callerNode.calls.add(calleeKey); calleeNode.calledBy.add(callerKey); } } } } getLineNumber(content, index) { const beforeMatch = content.substring(0, index); return beforeMatch.split('\n').length; } isExported(content, name, language) { const config = this.languageManager.getConfig(language); if (!config) return false; // Check if the name appears in any export pattern for (const pattern of config.exportPatterns) { pattern.lastIndex = 0; // Reset regex let match; while ((match = pattern.exec(content)) !== null) { if (match[1] === name) { return true; } } } return false; } findContainingFunction(content, index, file) { // Simple heuristic: find the closest function declaration before this index const beforeContent = content.substring(0, index); const functionPattern = /(?:function\s+(\w+)|(\w+)\s*=\s*(?:async\s+)?(?:function|\(.*?\)\s*=>))/g; let lastFunction = null; let match; while ((match = functionPattern.exec(beforeContent)) !== null) { lastFunction = match[1] || match[2]; } return lastFunction; } async identifyEntryPoints() { // Check package.json for entry points const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); // Main entry point if (packageJson.main) { this.entryPoints.add(packageJson.main); } // Binary entry points if (packageJson.bin) { if (typeof packageJson.bin === 'string') { this.entryPoints.add(packageJson.bin); } else { Object.values(packageJson.bin).forEach(bin => { this.entryPoints.add(bin); }); } } // Script entry points if (packageJson.scripts) { Object.values(packageJson.scripts).forEach(script => { const scriptStr = script; const fileMatch = scriptStr.match(/(?:node|ts-node|tsx)\s+([^\s]+)/); if (fileMatch) { this.entryPoints.add(fileMatch[1]); } }); } } catch (error) { // Skip if can't read package.json } } // Check for common framework entry points await this.identifyFrameworkEntryPoints(); // Check for test files (they're entry points for testing) const testFiles = await glob('**/*.{test,spec}.{ts,tsx,js,jsx}', { cwd: this.projectRoot, ignore: ['node_modules/**'], absolute: false }); testFiles.forEach(file => this.entryPoints.add(file)); // Mark entry points in metadata for (const entryPoint of this.entryPoints) { const metadata = this.fileMetadata.get(entryPoint); if (metadata) { metadata.isEntryPoint = true; } } } async identifyFrameworkEntryPoints() { // React/Next.js entry points const reactFiles = ['pages/**/*.tsx', 'pages/**/*.ts', 'src/pages/**/*.tsx', 'app/**/*.tsx']; for (const pattern of reactFiles) { const files = await glob(pattern, { cwd: this.projectRoot, ignore: ['node_modules/**'], absolute: false }); files.forEach(file => this.entryPoints.add(file)); } // Express/API routes const apiFiles = ['routes/**/*.js', 'routes/**/*.ts', 'api/**/*.js', 'api/**/*.ts']; for (const pattern of apiFiles) { const files = await glob(pattern, { cwd: this.projectRoot, ignore: ['node_modules/**'], absolute: false }); files.forEach(file => this.entryPoints.add(file)); } // Common entry point patterns const entryPointPatterns = [ 'index.*', 'main.*', 'app.*', 'server.*', 'src/index.*', '*.config.*', 'webpack.config.*', 'vite.config.*' ]; for (const pattern of entryPointPatterns) { const files = await glob(pattern, { cwd: this.projectRoot, ignore: ['node_modules/**'], absolute: false }); files.forEach(file => this.entryPoints.add(file)); } } } //# sourceMappingURL=CallGraphBuilder.js.map