| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- /**
- * 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<string, Node[]> = new Map();
- private fileCache: Map<string, string | null> = new Map();
- private nameCache: Map<string, Node[]> = new Map();
- private qualifiedNameCache: Map<string, Node[]> = new Map();
- private kindCache: Map<string, Node[]> = new Map();
- private nodeByIdCache: Map<string, Node> = 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<string, number> = {};
- // 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;
- }
|