arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
392 lines • 15.9 kB
JavaScript
/**
* Graph Builder - Constructs dependency graph from analyzed files
*/
import path from "path";
import fs from "fs";
/**
* Build the dependency graph from analyzed files
*/
export async function buildGraph(analyses, repoPath, db, onProgress) {
const totalFiles = analyses.length;
onProgress?.(`Building graph from ${totalFiles} analyzed files...`);
// Extract Go module name if this is a Go project
const goModuleName = extractGoModuleName(repoPath);
// Phase 1: Add all files to the database
const fileIdMap = new Map();
const functionIdMap = new Map();
db.beginTransaction();
try {
for (let i = 0; i < analyses.length; i++) {
const analysis = analyses[i];
onProgress?.(`Adding files to graph (${i + 1}/${totalFiles})...`);
// Add file
const fileNode = {
path: analysis.filePath,
repoPath,
type: analysis.type,
lines: analysis.lines,
};
const fileId = db.addFile(fileNode);
fileIdMap.set(analysis.filePath, fileId);
// Add functions from this file
for (const fn of analysis.functions) {
const functionId = db.addFunction(fileId, fn);
const functionKey = `${analysis.filePath}::${fn.name}`;
functionIdMap.set(functionKey, functionId);
}
// Add API endpoints
for (const endpoint of analysis.apiEndpoints) {
endpoint.fileId = fileId;
db.addApiEndpoint(endpoint);
}
// Add API calls
for (const call of analysis.apiCalls) {
db.addApiCall(fileId, call);
}
}
// Phase 2: Resolve and add import relationships
onProgress?.(`Resolving import relationships...`);
for (const analysis of analyses) {
const fromFileId = fileIdMap.get(analysis.filePath);
if (!fromFileId)
continue;
for (const imp of analysis.imports) {
// Try to resolve the import to a file
const resolvedPath = resolveImport(imp.from, analysis.filePath, repoPath, goModuleName, fileIdMap);
const toFileId = resolvedPath ? fileIdMap.get(resolvedPath) : null;
db.addImport(fromFileId, toFileId ?? null, imp.from, imp.type, imp.names, imp.line);
}
}
// Phase 3: Resolve and add function call relationships
onProgress?.(`Resolving function calls...`);
for (const analysis of analyses) {
const fromFileId = fileIdMap.get(analysis.filePath);
if (!fromFileId)
continue;
for (const fn of analysis.functions) {
const functionKey = `${analysis.filePath}::${fn.name}`;
const functionId = functionIdMap.get(functionKey);
if (!functionId)
continue;
// Analyze function body for calls (simple pattern matching)
// For now, we'll do a basic resolution based on function names
// A more sophisticated approach would parse the AST again
const potentialCalls = findPotentialFunctionCalls(fn.name, analysis);
for (const callInfo of potentialCalls) {
// Try to find the called function
let calledFunctionId = null;
// Check in same file first
const sameFunctionKey = `${analysis.filePath}::${callInfo.name}`;
calledFunctionId = functionIdMap.get(sameFunctionKey) ?? null;
// Try imported modules
if (!calledFunctionId) {
for (const imp of analysis.imports) {
const resolvedPath = resolveImport(imp.from, analysis.filePath, repoPath, goModuleName, fileIdMap);
if (resolvedPath && imp.names.includes(callInfo.name)) {
const importedFunctionKey = `${resolvedPath}::${callInfo.name}`;
calledFunctionId = functionIdMap.get(importedFunctionKey) ?? null;
if (calledFunctionId)
break;
}
}
}
db.addFunctionCall(functionId, calledFunctionId, callInfo.name, callInfo.line);
}
}
}
db.commit();
}
catch (error) {
db.rollback();
throw error;
}
}
/**
* Resolve an import path to an absolute file path
* Handles various import patterns: relative paths, node_modules, aliases
* Supports TypeScript, JavaScript, Python, Go, Rust, and more
*/
function resolveImport(importPath, fromFilePath, repoPath, goModuleName, fileIdMap) {
// Detect language from file extension
const fileExt = path.extname(fromFilePath);
const language = detectLanguage(fileExt);
// Handle Python imports
if (language === 'python') {
return resolvePythonImport(importPath, fromFilePath, repoPath);
}
// Handle Go imports
if (language === 'go') {
return resolveGoImport(importPath, fromFilePath, repoPath, goModuleName, fileIdMap);
}
// Handle Rust imports
if (language === 'rust') {
return resolveRustImport(importPath, fromFilePath, repoPath);
}
// Handle TypeScript/JavaScript imports
return resolveJsImport(importPath, fromFilePath, repoPath);
}
/**
* Extract Go module name from go.mod file
* Returns the module name (e.g., "zombie-survival" from "module zombie-survival")
* Returns null if go.mod doesn't exist or is not a Go project
*/
function extractGoModuleName(repoPath) {
try {
const goModPath = path.join(repoPath, 'go.mod');
if (!fs.existsSync(goModPath)) {
return null;
}
const content = fs.readFileSync(goModPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('module ')) {
// Extract module name (e.g., "module zombie-survival" -> "zombie-survival")
const moduleName = trimmed.substring(7).trim();
return moduleName;
}
}
}
catch (error) {
// If there's an error reading go.mod, just return null
// This allows other language projects to work without issues
}
return null;
}
/**
* Detect language from file extension
*/
function detectLanguage(ext) {
const langMap = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.mts': 'typescript',
'.py': 'python',
'.go': 'go',
'.rs': 'rust',
'.rb': 'ruby',
'.php': 'php',
'.java': 'java',
'.cs': 'csharp',
'.cpp': 'cpp',
'.c': 'c',
'.swift': 'swift',
'.kt': 'kotlin',
};
return langMap[ext] || 'unknown';
}
/**
* Resolve TypeScript/JavaScript imports
*/
function resolveJsImport(importPath, fromFilePath, repoPath) {
// Skip node_modules imports
if (!importPath.startsWith('.')) {
return null;
}
// Resolve relative imports
const fromDir = path.dirname(fromFilePath);
let resolvedPath = path.join(fromDir, importPath);
// Normalize to repo-relative path
const repoRelative = path.relative(repoPath, resolvedPath);
// Try different extensions
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '/index.ts', '/index.tsx', '/index.js'];
for (const ext of extensions) {
const candidatePath = repoRelative.endsWith(ext) ? repoRelative : `${repoRelative}${ext}`;
if (!repoRelative.includes('node_modules')) {
return candidatePath;
}
}
return null;
}
/**
* Resolve Python imports
* Handles: from .module import X, from ..parent import Y, import package.module
*/
function resolvePythonImport(importPath, fromFilePath, repoPath) {
// Skip standard library and external packages
const stdLibs = ['sys', 'os', 'json', 'datetime', 'typing', 'pathlib', 'collections', 're', 'argparse', 'logging'];
const externalPackages = ['django', 'flask', 'fastapi', 'requests', 'numpy', 'pandas', 'psycopg2', 'sqlalchemy', 'pydantic'];
if (stdLibs.includes(importPath.split('.')[0]) || externalPackages.includes(importPath.split('.')[0])) {
return null;
}
const fromDir = path.dirname(fromFilePath);
// Handle relative imports: .module or ..parent.module
if (importPath.startsWith('.')) {
const levels = importPath.match(/^\.+/)?.[0].length || 0;
const modulePath = importPath.substring(levels);
// Go up 'levels - 1' directories (one dot = same dir)
let targetDir = fromDir;
for (let i = 1; i < levels; i++) {
targetDir = path.dirname(targetDir);
}
// Convert module path to file path
const filePath = modulePath.replace(/\./g, path.sep);
const resolvedPath = path.join(targetDir, filePath);
const repoRelative = path.relative(repoPath, resolvedPath);
// Try .py and __init__.py
const candidates = [
`${repoRelative}.py`,
path.join(repoRelative, '__init__.py'),
];
for (const candidate of candidates) {
if (!candidate.includes('node_modules') && !candidate.includes('venv')) {
return candidate;
}
}
}
// Handle absolute imports: package.module.submodule
// Convert dots to path separators
const filePath = importPath.replace(/\./g, path.sep);
// Try from repo root
const candidates = [
`${filePath}.py`,
path.join(filePath, '__init__.py'),
];
for (const candidate of candidates) {
if (!candidate.includes('venv') && !candidate.includes('site-packages')) {
return candidate;
}
}
return null;
}
/**
* Resolve Go imports
* Handles: relative imports, module-prefixed imports, and external packages
*/
function resolveGoImport(importPath, fromFilePath, repoPath, goModuleName, fileIdMap) {
// Skip standard library imports (single word or stdlib packages)
if (!importPath.includes('/')) {
return null;
}
// Handle relative imports (e.g., ./sibling, ../parent)
if (importPath.startsWith('.')) {
const fromDir = path.dirname(fromFilePath);
const resolvedPath = path.join(fromDir, importPath);
const relPath = path.relative(repoPath, resolvedPath);
// Look for .go files in the resolved directory
if (fileIdMap) {
for (const [filePath] of fileIdMap.entries()) {
if (filePath.startsWith(relPath + '/') && filePath.endsWith('.go')) {
return filePath;
}
// Also check if it matches the directory directly
if (path.dirname(filePath) === relPath && filePath.endsWith('.go')) {
return filePath;
}
}
}
return null;
}
// Handle module-prefixed imports (e.g., module-name/package/subpackage)
if (goModuleName && importPath.startsWith(goModuleName + '/')) {
// Strip the module prefix to get the relative path
const relativePath = importPath.substring(goModuleName.length + 1);
// Look for any .go file in that directory
if (fileIdMap) {
for (const [filePath] of fileIdMap.entries()) {
// Check if this file is in the target directory
if (filePath.startsWith(relativePath + '/') && filePath.endsWith('.go')) {
return filePath;
}
// Check if the file is directly in the target directory (package-level)
if (path.dirname(filePath) === relativePath && filePath.endsWith('.go')) {
return filePath;
}
}
}
return null;
}
// Handle external module imports (e.g., github.com/user/repo/package)
// Only process internal paths if they look like they're part of this repo
const parts = importPath.split('/');
if (parts.length > 3 && !importPath.includes('github.com') && !importPath.includes('gitlab.com')) {
// Might be a monorepo or multi-module setup
const internalPath = parts.slice(3).join('/');
if (fileIdMap) {
for (const [filePath] of fileIdMap.entries()) {
if (filePath.startsWith(internalPath + '/') && filePath.endsWith('.go')) {
return filePath;
}
if (path.dirname(filePath) === internalPath && filePath.endsWith('.go')) {
return filePath;
}
}
}
return null;
}
// External packages (github.com, etc.) remain unresolved
return null;
}
/**
* Resolve Rust imports
*/
function resolveRustImport(importPath, fromFilePath, repoPath) {
// Skip external crates
if (!importPath.startsWith('crate::') && !importPath.startsWith('super::') && !importPath.startsWith('self::')) {
return null;
}
// Handle crate:: (from root)
if (importPath.startsWith('crate::')) {
const modulePath = importPath.substring(7).replace(/::/g, '/');
return `src/${modulePath}.rs`;
}
// Handle super:: (parent module)
if (importPath.startsWith('super::')) {
const fromDir = path.dirname(fromFilePath);
const parentDir = path.dirname(fromDir);
const modulePath = importPath.substring(7).replace(/::/g, '/');
const resolvedPath = path.join(parentDir, modulePath);
return `${path.relative(repoPath, resolvedPath)}.rs`;
}
// Handle self:: (current module)
if (importPath.startsWith('self::')) {
const fromDir = path.dirname(fromFilePath);
const modulePath = importPath.substring(6).replace(/::/g, '/');
const resolvedPath = path.join(fromDir, modulePath);
return `${path.relative(repoPath, resolvedPath)}.rs`;
}
return null;
}
/**
* Find potential function calls in a function (simple heuristic)
* In a real implementation, this would re-parse the function body
*/
function findPotentialFunctionCalls(functionName, analysis) {
// This is a simplified implementation
// A more sophisticated approach would re-analyze the function body
const calls = [];
// Look for common function call patterns
// This is a basic heuristic and should be improved
const localFunctionNames = analysis.functions.map(f => f.name);
// In a real implementation, we would:
// 1. Get the function body from the AST
// 2. Find all CallExpression nodes
// 3. Extract the function names being called
// For now, return empty array
return calls;
}
/**
* Get statistics about the built graph
*/
export function getGraphStats(db) {
const stats = {
modules: 0,
components: 0,
services: 0,
apiEndpoints: 0,
};
try {
stats.modules = (db.query('SELECT COUNT(DISTINCT path) as count FROM files')[0]?.count || 0);
stats.components = (db.query("SELECT COUNT(*) as count FROM files WHERE type = 'component'")[0]?.count || 0);
stats.services = (db.query("SELECT COUNT(*) as count FROM files WHERE type = 'service'")[0]?.count || 0);
stats.apiEndpoints = (db.query('SELECT COUNT(*) as count FROM api_endpoints')[0]?.count || 0);
}
catch (error) {
// Return partial stats if there's an error
}
return stats;
}
//# sourceMappingURL=graph-builder.js.map