/** * Reference Resolution Orchestrator * * Coordinates all reference resolution strategies. */ import * as fs from 'fs'; import * as path from 'path'; import { Node, UnresolvedReference, Edge } from '../types'; import { QueryBuilder } from '../db/queries'; import { captureException } from '../sentry'; import { UnresolvedRef, ResolvedRef, ResolutionResult, ResolutionContext, FrameworkResolver, } from './types'; import { matchReference } from './name-matcher'; import { resolveViaImport } from './import-resolver'; import { detectFrameworks } from './frameworks'; import { logDebug } from '../errors'; // Re-export types export * from './types'; /** * Reference Resolver * * Orchestrates reference resolution using multiple strategies. */ export class ReferenceResolver { private projectRoot: string; private queries: QueryBuilder; private context: ResolutionContext; private frameworks: FrameworkResolver[] = []; private nodeCache: Map = new Map(); private fileCache: Map = new Map(); private nameCache: Map = new Map(); private qualifiedNameCache: Map = new Map(); private kindCache: Map = new Map(); private nodeByIdCache: Map = new Map(); private cachesWarmed = false; constructor(projectRoot: string, queries: QueryBuilder) { this.projectRoot = projectRoot; this.queries = queries; this.context = this.createContext(); } /** * Initialize the resolver (detect frameworks, etc.) */ initialize(): void { this.frameworks = detectFrameworks(this.context); this.clearCaches(); } /** * Pre-load all nodes into memory maps for fast lookup during resolution. * This eliminates repeated SQLite queries and provides the core speedup. */ warmCaches(): void { if (this.cachesWarmed) return; const allNodes = this.queries.getAllNodes(); for (const node of allNodes) { // Index by name const byName = this.nameCache.get(node.name); if (byName) { byName.push(node); } else { this.nameCache.set(node.name, [node]); } // Index by qualified name const byQName = this.qualifiedNameCache.get(node.qualifiedName); if (byQName) { byQName.push(node); } else { this.qualifiedNameCache.set(node.qualifiedName, [node]); } // Index by kind const byKind = this.kindCache.get(node.kind); if (byKind) { byKind.push(node); } else { this.kindCache.set(node.kind, [node]); } // Index by ID this.nodeByIdCache.set(node.id, node); } this.cachesWarmed = true; } /** * Clear internal caches */ clearCaches(): void { this.nodeCache.clear(); this.fileCache.clear(); this.nameCache.clear(); this.qualifiedNameCache.clear(); this.kindCache.clear(); this.nodeByIdCache.clear(); this.cachesWarmed = false; } /** * Create the resolution context */ private createContext(): ResolutionContext { return { getNodesInFile: (filePath: string) => { if (!this.nodeCache.has(filePath)) { this.nodeCache.set(filePath, this.queries.getNodesByFile(filePath)); } return this.nodeCache.get(filePath)!; }, getNodesByName: (name: string) => { // Use warm cache if available, otherwise fall back to search if (this.cachesWarmed) { return this.nameCache.get(name) ?? []; } return this.queries.searchNodes(name, { limit: 100 }).map((r) => r.node); }, getNodesByQualifiedName: (qualifiedName: string) => { // Use warm cache if available, otherwise fall back to search + filter if (this.cachesWarmed) { return this.qualifiedNameCache.get(qualifiedName) ?? []; } return this.queries .searchNodes(qualifiedName, { limit: 50 }) .filter((r) => r.node.qualifiedName === qualifiedName) .map((r) => r.node); }, getNodesByKind: (kind: Node['kind']) => { if (this.cachesWarmed) { return this.kindCache.get(kind) ?? []; } return this.queries.getNodesByKind(kind); }, fileExists: (filePath: string) => { const fullPath = path.join(this.projectRoot, filePath); try { return fs.existsSync(fullPath); } catch (error) { captureException(error, { operation: 'resolution-file-exists', filePath }); logDebug('Error checking file existence', { filePath, error: String(error) }); return false; } }, readFile: (filePath: string) => { if (this.fileCache.has(filePath)) { return this.fileCache.get(filePath)!; } const fullPath = path.join(this.projectRoot, filePath); try { const content = fs.readFileSync(fullPath, 'utf-8'); this.fileCache.set(filePath, content); return content; } catch (error) { captureException(error, { operation: 'resolution-read-file', filePath }); logDebug('Failed to read file for resolution', { filePath, error: String(error) }); this.fileCache.set(filePath, null); return null; } }, getProjectRoot: () => this.projectRoot, getAllFiles: () => { return this.queries.getAllFiles().map((f) => f.path); }, }; } /** * Resolve all unresolved references */ resolveAll( unresolvedRefs: UnresolvedReference[], onProgress?: (current: number, total: number) => void ): ResolutionResult { // Pre-load all nodes into memory for fast lookups this.warmCaches(); const resolved: ResolvedRef[] = []; const unresolved: UnresolvedRef[] = []; const byMethod: Record = {}; // Convert to our internal format, using denormalized fields when available const refs: UnresolvedRef[] = unresolvedRefs.map((ref) => ({ fromNodeId: ref.fromNodeId, referenceName: ref.referenceName, referenceKind: ref.referenceKind, line: ref.line, column: ref.column, filePath: ref.filePath || this.getFilePathFromNodeId(ref.fromNodeId), language: ref.language || this.getLanguageFromNodeId(ref.fromNodeId), })); const total = refs.length; let lastReportedPercent = -1; for (let i = 0; i < refs.length; i++) { const ref = refs[i]!; // Array index is guaranteed to be in bounds const result = this.resolveOne(ref); if (result) { resolved.push(result); byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1; } else { unresolved.push(ref); } // Report progress every 1% to avoid too many updates if (onProgress) { const currentPercent = Math.floor((i / total) * 100); if (currentPercent > lastReportedPercent) { lastReportedPercent = currentPercent; onProgress(i + 1, total); } } } // Final progress report if (onProgress && total > 0) { onProgress(total, total); } return { resolved, unresolved, stats: { total: refs.length, resolved: resolved.length, unresolved: unresolved.length, byMethod, }, }; } /** * Resolve a single reference */ resolveOne(ref: UnresolvedRef): ResolvedRef | null { // Skip built-in/external references if (this.isBuiltInOrExternal(ref)) { return null; } const candidates: ResolvedRef[] = []; // Strategy 1: Try framework-specific resolution for (const framework of this.frameworks) { const result = framework.resolve(ref, this.context); if (result) { if (result.confidence >= 0.9) return result; // High confidence, return immediately candidates.push(result); } } // Strategy 2: Try import-based resolution const importResult = resolveViaImport(ref, this.context); if (importResult) { if (importResult.confidence >= 0.9) return importResult; candidates.push(importResult); } // Strategy 3: Try name matching const nameResult = matchReference(ref, this.context); if (nameResult) { candidates.push(nameResult); } if (candidates.length === 0) return null; // Return highest confidence candidate return candidates.reduce((best, curr) => curr.confidence > best.confidence ? curr : best ); } /** * Create edges from resolved references */ createEdges(resolved: ResolvedRef[]): Edge[] { return resolved.map((ref) => ({ source: ref.original.fromNodeId, target: ref.targetNodeId, kind: ref.original.referenceKind, line: ref.original.line, column: ref.original.column, metadata: { confidence: ref.confidence, resolvedBy: ref.resolvedBy, }, })); } /** * Resolve and persist edges to database */ resolveAndPersist( unresolvedRefs: UnresolvedReference[], onProgress?: (current: number, total: number) => void ): ResolutionResult { const result = this.resolveAll(unresolvedRefs, onProgress); // Create edges from resolved references const edges = this.createEdges(result.resolved); // Insert edges into database if (edges.length > 0) { this.queries.insertEdges(edges); } return result; } /** * Get detected frameworks */ getDetectedFrameworks(): string[] { return this.frameworks.map((f) => f.name); } /** * Check if reference is to a built-in or external symbol */ private isBuiltInOrExternal(ref: UnresolvedRef): boolean { const name = ref.referenceName; // JavaScript/TypeScript built-ins const jsBuiltIns = [ 'console', 'window', 'document', 'global', 'process', 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date', 'Math', 'JSON', 'RegExp', 'Error', 'Map', 'Set', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'fetch', 'require', 'module', 'exports', '__dirname', '__filename', ]; if (jsBuiltIns.includes(name)) { return true; } // Common library calls if (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.')) { return true; } // React hooks from React itself const reactHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue']; if (reactHooks.includes(name)) { return true; } // Python built-ins const pythonBuiltIns = [ 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', 'open', 'input', 'type', 'isinstance', 'hasattr', 'getattr', 'setattr', 'super', 'self', 'cls', 'None', 'True', 'False', ]; if (ref.language === 'python' && pythonBuiltIns.includes(name)) { return true; } // Pascal/Delphi built-ins and standard library units if (ref.language === 'pascal') { // Standard RTL/VCL/FMX unit prefixes — these are external dependencies const pascalUnitPrefixes = [ 'System.', 'Winapi.', 'Vcl.', 'Fmx.', 'Data.', 'Datasnap.', 'Soap.', 'Xml.', 'Web.', 'REST.', 'FireDAC.', 'IBX.', 'IdHTTP', 'IdTCP', 'IdSSL', 'Id', ]; if (pascalUnitPrefixes.some((p) => name.startsWith(p))) { return true; } // Common standalone RTL units and built-in identifiers const pascalBuiltIns = [ 'System', 'SysUtils', 'Classes', 'Types', 'Variants', 'StrUtils', 'Math', 'DateUtils', 'IOUtils', 'Generics.Collections', 'Generics.Defaults', 'Rtti', 'TypInfo', 'SyncObjs', 'RegularExpressions', 'SysInit', 'Windows', 'Messages', 'Graphics', 'Controls', 'Forms', 'Dialogs', 'StdCtrls', 'ExtCtrls', 'ComCtrls', 'Menus', 'ActnList', 'WriteLn', 'Write', 'ReadLn', 'Read', 'Inc', 'Dec', 'Ord', 'Chr', 'Length', 'SetLength', 'High', 'Low', 'Assigned', 'FreeAndNil', 'Format', 'IntToStr', 'StrToInt', 'FloatToStr', 'StrToFloat', 'Trim', 'UpperCase', 'LowerCase', 'Pos', 'Copy', 'Delete', 'Insert', 'Now', 'Date', 'Time', 'DateToStr', 'StrToDate', 'Raise', 'Exit', 'Break', 'Continue', 'Abort', 'True', 'False', 'nil', 'Self', 'Result', 'Create', 'Destroy', 'Free', 'TObject', 'TComponent', 'TPersistent', 'TInterfacedObject', 'TList', 'TStringList', 'TStrings', 'TStream', 'TMemoryStream', 'TFileStream', 'Exception', 'EAbort', 'EConvertError', 'EAccessViolation', 'IInterface', 'IUnknown', ]; if (pascalBuiltIns.includes(name)) { return true; } } return false; } /** * Get file path from node ID */ private getFilePathFromNodeId(nodeId: string): string { // Check warm cache first const cached = this.nodeByIdCache.get(nodeId); if (cached) return cached.filePath; const node = this.queries.getNodeById(nodeId); return node?.filePath || ''; } /** * Get language from node ID */ private getLanguageFromNodeId(nodeId: string): UnresolvedRef['language'] { // Check warm cache first const cached = this.nodeByIdCache.get(nodeId); if (cached) return cached.language; const node = this.queries.getNodeById(nodeId); return node?.language || 'unknown'; } } /** * Create a reference resolver instance */ export function createResolver(projectRoot: string, queries: QueryBuilder): ReferenceResolver { const resolver = new ReferenceResolver(projectRoot, queries); resolver.initialize(); return resolver; }