ai-index
Version:
AI-powered local code indexing and search system for any codebase
228 lines (190 loc) ⢠6.9 kB
JavaScript
import { globby } from 'globby';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { CodeAnalyzer } from './code-analyzer.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class CallChainAnalyzer {
constructor(options = {}) {
this.rootPath = options.rootPath || process.cwd();
this.analyzer = new CodeAnalyzer();
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
console.log('š Analyzing function call chains...');
// Index all JavaScript files to build call graph
const patterns = ['**/*.{js,mjs}'];
const ignorePatterns = [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/*.min.js',
'**/*.test.js',
'**/*.spec.js'
];
const files = await globby(patterns, {
cwd: this.rootPath,
ignore: ignorePatterns,
absolute: false,
gitignore: true
});
console.log(`Found ${files.length} JavaScript files to analyze`);
let processed = 0;
for (const file of files) {
const fullPath = path.join(this.rootPath, file);
try {
const content = await fs.readFile(fullPath, 'utf-8');
await this.analyzer.analyzeFile(fullPath, content);
processed++;
if (processed % 10 === 0) {
console.log(`Processed ${processed}/${files.length} files...`);
}
} catch (error) {
console.error(`Error analyzing ${file}:`, error.message);
}
}
console.log(`ā
Analyzed ${processed} files, built call graph`);
this.initialized = true;
}
async getCallChain(functionName) {
await this.initialize();
const callChain = this.analyzer.getFunctionCallChain(functionName);
if (!callChain) {
return {
error: `Function '${functionName}' not found in codebase`,
suggestions: this.suggestSimilarFunctions(functionName)
};
}
return {
function: callChain.function,
downstream: {
title: `Functions called by '${functionName}'`,
calls: callChain.calls
},
upstream: {
title: `Functions that call '${functionName}'`,
callers: callChain.calledBy
},
callChain: callChain.callChain
};
}
suggestSimilarFunctions(searchName) {
const allFunctions = [];
for (const [key, data] of this.analyzer.globalCallGraph) {
if (data.name && data.type !== 'external_or_unknown') {
allFunctions.push(data.name);
}
}
// Simple similarity matching
const suggestions = allFunctions.filter(name =>
name.toLowerCase().includes(searchName.toLowerCase()) ||
searchName.toLowerCase().includes(name.toLowerCase())
).slice(0, 5);
return suggestions;
}
async getAllFunctions() {
await this.initialize();
const functions = [];
for (const [key, data] of this.analyzer.globalCallGraph) {
if (data.name && data.type !== 'external_or_unknown') {
functions.push({
name: data.name,
file: data.file,
line: data.line,
type: data.type,
calls: data.calls?.length || 0,
calledBy: data.calledBy?.length || 0
});
}
}
return functions.sort((a, b) => a.name.localeCompare(b.name));
}
formatCallChain(result) {
if (result.error) {
let output = `ā ${result.error}\n`;
if (result.suggestions.length > 0) {
output += `\nš” Did you mean one of these?\n`;
result.suggestions.forEach(name => {
output += ` - ${name}\n`;
});
}
return output;
}
let output = `\nš Call Chain Analysis for '${result.function.name}'\n`;
output += `š File: ${result.function.file}:${result.function.line}\n`;
output += `ā” Type: ${result.function.type}\n`;
if (result.function.params.length > 0) {
output += `š Parameters: ${result.function.params.join(', ')}\n`;
}
output += `\nā¬ļø ${result.downstream.title}:\n`;
if (result.downstream.calls.length === 0) {
output += ` (no function calls detected)\n`;
} else {
result.downstream.calls.forEach(call => {
output += ` š ${call.name}() at line ${call.line} (${call.type})\n`;
});
}
output += `\nā¬ļø ${result.upstream.title}:\n`;
if (result.upstream.callers.length === 0) {
output += ` (not called by any detected functions)\n`;
} else {
result.upstream.callers.forEach(caller => {
output += ` š ${caller.name}() in ${caller.file}:${caller.line}\n`;
});
}
// Add deep call chain if available
if (result.callChain.downstream.length > 0 || result.callChain.upstream.length > 0) {
output += `\nš Extended Call Chain (up to 3 levels):\n`;
if (result.callChain.downstream.length > 0) {
output += ` Downstream:\n`;
this.formatChainLevel(result.callChain.downstream, ' ', output);
}
if (result.callChain.upstream.length > 0) {
output += ` Upstream:\n`;
this.formatChainLevel(result.callChain.upstream, ' ', output);
}
}
return output;
}
formatChainLevel(chain, indent, output) {
chain.forEach(item => {
output += `${indent}ā ${item.name}() [depth ${item.depth}]\n`;
if (item.chain.downstream.length > 0 || item.chain.upstream.length > 0) {
this.formatChainLevel([...item.chain.downstream, ...item.chain.upstream], indent + ' ', output);
}
});
}
}
// CLI support
if (import.meta.url === `file://${process.argv[1]}`) {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(`
Call Chain Analyzer - Function dependency analysis
Usage:
call-chain <function-name> Show call chain for specific function
call-chain --list List all functions in codebase
call-chain --help Show this help message
Examples:
call-chain calculateProfit # Show what calls calculateProfit and what it calls
call-chain --list # List all functions
`);
process.exit(0);
}
const analyzer = new CallChainAnalyzer();
if (args.includes('--list')) {
const functions = await analyzer.getAllFunctions();
console.log(`\nš All Functions (${functions.length} found):\n`);
functions.forEach(func => {
console.log(`š ${func.name} (${func.file}:${func.line})`);
console.log(` Calls ${func.calls} functions, called by ${func.calledBy} functions`);
});
} else {
const functionName = args[0];
const result = await analyzer.getCallChain(functionName);
console.log(analyzer.formatCallChain(result));
}
}