UNPKG

ai-index

Version:

AI-powered local code indexing and search system for any codebase

228 lines (190 loc) • 6.9 kB
#!/usr/bin/env node 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)); } }