mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
372 lines • 14.6 kB
JavaScript
/**
* 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