index.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. /**
  2. * Reference Resolution Orchestrator
  3. *
  4. * Coordinates all reference resolution strategies.
  5. */
  6. import * as fs from 'fs';
  7. import * as path from 'path';
  8. import { Node, UnresolvedReference, Edge } from '../types';
  9. import { QueryBuilder } from '../db/queries';
  10. import { captureException } from '../sentry';
  11. import {
  12. UnresolvedRef,
  13. ResolvedRef,
  14. ResolutionResult,
  15. ResolutionContext,
  16. FrameworkResolver,
  17. } from './types';
  18. import { matchReference } from './name-matcher';
  19. import { resolveViaImport } from './import-resolver';
  20. import { detectFrameworks } from './frameworks';
  21. import { logDebug } from '../errors';
  22. // Re-export types
  23. export * from './types';
  24. /**
  25. * Reference Resolver
  26. *
  27. * Orchestrates reference resolution using multiple strategies.
  28. */
  29. export class ReferenceResolver {
  30. private projectRoot: string;
  31. private queries: QueryBuilder;
  32. private context: ResolutionContext;
  33. private frameworks: FrameworkResolver[] = [];
  34. private nodeCache: Map<string, Node[]> = new Map();
  35. private fileCache: Map<string, string | null> = new Map();
  36. private nameCache: Map<string, Node[]> = new Map();
  37. private qualifiedNameCache: Map<string, Node[]> = new Map();
  38. private kindCache: Map<string, Node[]> = new Map();
  39. private nodeByIdCache: Map<string, Node> = new Map();
  40. private cachesWarmed = false;
  41. constructor(projectRoot: string, queries: QueryBuilder) {
  42. this.projectRoot = projectRoot;
  43. this.queries = queries;
  44. this.context = this.createContext();
  45. }
  46. /**
  47. * Initialize the resolver (detect frameworks, etc.)
  48. */
  49. initialize(): void {
  50. this.frameworks = detectFrameworks(this.context);
  51. this.clearCaches();
  52. }
  53. /**
  54. * Pre-load all nodes into memory maps for fast lookup during resolution.
  55. * This eliminates repeated SQLite queries and provides the core speedup.
  56. */
  57. warmCaches(): void {
  58. if (this.cachesWarmed) return;
  59. const allNodes = this.queries.getAllNodes();
  60. for (const node of allNodes) {
  61. // Index by name
  62. const byName = this.nameCache.get(node.name);
  63. if (byName) {
  64. byName.push(node);
  65. } else {
  66. this.nameCache.set(node.name, [node]);
  67. }
  68. // Index by qualified name
  69. const byQName = this.qualifiedNameCache.get(node.qualifiedName);
  70. if (byQName) {
  71. byQName.push(node);
  72. } else {
  73. this.qualifiedNameCache.set(node.qualifiedName, [node]);
  74. }
  75. // Index by kind
  76. const byKind = this.kindCache.get(node.kind);
  77. if (byKind) {
  78. byKind.push(node);
  79. } else {
  80. this.kindCache.set(node.kind, [node]);
  81. }
  82. // Index by ID
  83. this.nodeByIdCache.set(node.id, node);
  84. }
  85. this.cachesWarmed = true;
  86. }
  87. /**
  88. * Clear internal caches
  89. */
  90. clearCaches(): void {
  91. this.nodeCache.clear();
  92. this.fileCache.clear();
  93. this.nameCache.clear();
  94. this.qualifiedNameCache.clear();
  95. this.kindCache.clear();
  96. this.nodeByIdCache.clear();
  97. this.cachesWarmed = false;
  98. }
  99. /**
  100. * Create the resolution context
  101. */
  102. private createContext(): ResolutionContext {
  103. return {
  104. getNodesInFile: (filePath: string) => {
  105. if (!this.nodeCache.has(filePath)) {
  106. this.nodeCache.set(filePath, this.queries.getNodesByFile(filePath));
  107. }
  108. return this.nodeCache.get(filePath)!;
  109. },
  110. getNodesByName: (name: string) => {
  111. // Use warm cache if available, otherwise fall back to search
  112. if (this.cachesWarmed) {
  113. return this.nameCache.get(name) ?? [];
  114. }
  115. return this.queries.searchNodes(name, { limit: 100 }).map((r) => r.node);
  116. },
  117. getNodesByQualifiedName: (qualifiedName: string) => {
  118. // Use warm cache if available, otherwise fall back to search + filter
  119. if (this.cachesWarmed) {
  120. return this.qualifiedNameCache.get(qualifiedName) ?? [];
  121. }
  122. return this.queries
  123. .searchNodes(qualifiedName, { limit: 50 })
  124. .filter((r) => r.node.qualifiedName === qualifiedName)
  125. .map((r) => r.node);
  126. },
  127. getNodesByKind: (kind: Node['kind']) => {
  128. if (this.cachesWarmed) {
  129. return this.kindCache.get(kind) ?? [];
  130. }
  131. return this.queries.getNodesByKind(kind);
  132. },
  133. fileExists: (filePath: string) => {
  134. const fullPath = path.join(this.projectRoot, filePath);
  135. try {
  136. return fs.existsSync(fullPath);
  137. } catch (error) {
  138. captureException(error, { operation: 'resolution-file-exists', filePath });
  139. logDebug('Error checking file existence', { filePath, error: String(error) });
  140. return false;
  141. }
  142. },
  143. readFile: (filePath: string) => {
  144. if (this.fileCache.has(filePath)) {
  145. return this.fileCache.get(filePath)!;
  146. }
  147. const fullPath = path.join(this.projectRoot, filePath);
  148. try {
  149. const content = fs.readFileSync(fullPath, 'utf-8');
  150. this.fileCache.set(filePath, content);
  151. return content;
  152. } catch (error) {
  153. captureException(error, { operation: 'resolution-read-file', filePath });
  154. logDebug('Failed to read file for resolution', { filePath, error: String(error) });
  155. this.fileCache.set(filePath, null);
  156. return null;
  157. }
  158. },
  159. getProjectRoot: () => this.projectRoot,
  160. getAllFiles: () => {
  161. return this.queries.getAllFiles().map((f) => f.path);
  162. },
  163. };
  164. }
  165. /**
  166. * Resolve all unresolved references
  167. */
  168. resolveAll(
  169. unresolvedRefs: UnresolvedReference[],
  170. onProgress?: (current: number, total: number) => void
  171. ): ResolutionResult {
  172. // Pre-load all nodes into memory for fast lookups
  173. this.warmCaches();
  174. const resolved: ResolvedRef[] = [];
  175. const unresolved: UnresolvedRef[] = [];
  176. const byMethod: Record<string, number> = {};
  177. // Convert to our internal format, using denormalized fields when available
  178. const refs: UnresolvedRef[] = unresolvedRefs.map((ref) => ({
  179. fromNodeId: ref.fromNodeId,
  180. referenceName: ref.referenceName,
  181. referenceKind: ref.referenceKind,
  182. line: ref.line,
  183. column: ref.column,
  184. filePath: ref.filePath || this.getFilePathFromNodeId(ref.fromNodeId),
  185. language: ref.language || this.getLanguageFromNodeId(ref.fromNodeId),
  186. }));
  187. const total = refs.length;
  188. let lastReportedPercent = -1;
  189. for (let i = 0; i < refs.length; i++) {
  190. const ref = refs[i]!; // Array index is guaranteed to be in bounds
  191. const result = this.resolveOne(ref);
  192. if (result) {
  193. resolved.push(result);
  194. byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
  195. } else {
  196. unresolved.push(ref);
  197. }
  198. // Report progress every 1% to avoid too many updates
  199. if (onProgress) {
  200. const currentPercent = Math.floor((i / total) * 100);
  201. if (currentPercent > lastReportedPercent) {
  202. lastReportedPercent = currentPercent;
  203. onProgress(i + 1, total);
  204. }
  205. }
  206. }
  207. // Final progress report
  208. if (onProgress && total > 0) {
  209. onProgress(total, total);
  210. }
  211. return {
  212. resolved,
  213. unresolved,
  214. stats: {
  215. total: refs.length,
  216. resolved: resolved.length,
  217. unresolved: unresolved.length,
  218. byMethod,
  219. },
  220. };
  221. }
  222. /**
  223. * Resolve a single reference
  224. */
  225. resolveOne(ref: UnresolvedRef): ResolvedRef | null {
  226. // Skip built-in/external references
  227. if (this.isBuiltInOrExternal(ref)) {
  228. return null;
  229. }
  230. const candidates: ResolvedRef[] = [];
  231. // Strategy 1: Try framework-specific resolution
  232. for (const framework of this.frameworks) {
  233. const result = framework.resolve(ref, this.context);
  234. if (result) {
  235. if (result.confidence >= 0.9) return result; // High confidence, return immediately
  236. candidates.push(result);
  237. }
  238. }
  239. // Strategy 2: Try import-based resolution
  240. const importResult = resolveViaImport(ref, this.context);
  241. if (importResult) {
  242. if (importResult.confidence >= 0.9) return importResult;
  243. candidates.push(importResult);
  244. }
  245. // Strategy 3: Try name matching
  246. const nameResult = matchReference(ref, this.context);
  247. if (nameResult) {
  248. candidates.push(nameResult);
  249. }
  250. if (candidates.length === 0) return null;
  251. // Return highest confidence candidate
  252. return candidates.reduce((best, curr) =>
  253. curr.confidence > best.confidence ? curr : best
  254. );
  255. }
  256. /**
  257. * Create edges from resolved references
  258. */
  259. createEdges(resolved: ResolvedRef[]): Edge[] {
  260. return resolved.map((ref) => ({
  261. source: ref.original.fromNodeId,
  262. target: ref.targetNodeId,
  263. kind: ref.original.referenceKind,
  264. line: ref.original.line,
  265. column: ref.original.column,
  266. metadata: {
  267. confidence: ref.confidence,
  268. resolvedBy: ref.resolvedBy,
  269. },
  270. }));
  271. }
  272. /**
  273. * Resolve and persist edges to database
  274. */
  275. resolveAndPersist(
  276. unresolvedRefs: UnresolvedReference[],
  277. onProgress?: (current: number, total: number) => void
  278. ): ResolutionResult {
  279. const result = this.resolveAll(unresolvedRefs, onProgress);
  280. // Create edges from resolved references
  281. const edges = this.createEdges(result.resolved);
  282. // Insert edges into database
  283. if (edges.length > 0) {
  284. this.queries.insertEdges(edges);
  285. }
  286. return result;
  287. }
  288. /**
  289. * Get detected frameworks
  290. */
  291. getDetectedFrameworks(): string[] {
  292. return this.frameworks.map((f) => f.name);
  293. }
  294. /**
  295. * Check if reference is to a built-in or external symbol
  296. */
  297. private isBuiltInOrExternal(ref: UnresolvedRef): boolean {
  298. const name = ref.referenceName;
  299. // JavaScript/TypeScript built-ins
  300. const jsBuiltIns = [
  301. 'console', 'window', 'document', 'global', 'process',
  302. 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
  303. 'Date', 'Math', 'JSON', 'RegExp', 'Error', 'Map', 'Set',
  304. 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
  305. 'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
  306. ];
  307. if (jsBuiltIns.includes(name)) {
  308. return true;
  309. }
  310. // Common library calls
  311. if (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.')) {
  312. return true;
  313. }
  314. // React hooks from React itself
  315. const reactHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue'];
  316. if (reactHooks.includes(name)) {
  317. return true;
  318. }
  319. // Python built-ins
  320. const pythonBuiltIns = [
  321. 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
  322. 'open', 'input', 'type', 'isinstance', 'hasattr', 'getattr', 'setattr',
  323. 'super', 'self', 'cls', 'None', 'True', 'False',
  324. ];
  325. if (ref.language === 'python' && pythonBuiltIns.includes(name)) {
  326. return true;
  327. }
  328. // Pascal/Delphi built-ins and standard library units
  329. if (ref.language === 'pascal') {
  330. // Standard RTL/VCL/FMX unit prefixes — these are external dependencies
  331. const pascalUnitPrefixes = [
  332. 'System.', 'Winapi.', 'Vcl.', 'Fmx.', 'Data.', 'Datasnap.',
  333. 'Soap.', 'Xml.', 'Web.', 'REST.', 'FireDAC.', 'IBX.',
  334. 'IdHTTP', 'IdTCP', 'IdSSL', 'Id',
  335. ];
  336. if (pascalUnitPrefixes.some((p) => name.startsWith(p))) {
  337. return true;
  338. }
  339. // Common standalone RTL units and built-in identifiers
  340. const pascalBuiltIns = [
  341. 'System', 'SysUtils', 'Classes', 'Types', 'Variants', 'StrUtils',
  342. 'Math', 'DateUtils', 'IOUtils', 'Generics.Collections', 'Generics.Defaults',
  343. 'Rtti', 'TypInfo', 'SyncObjs', 'RegularExpressions',
  344. 'SysInit', 'Windows', 'Messages', 'Graphics', 'Controls', 'Forms',
  345. 'Dialogs', 'StdCtrls', 'ExtCtrls', 'ComCtrls', 'Menus', 'ActnList',
  346. 'WriteLn', 'Write', 'ReadLn', 'Read', 'Inc', 'Dec', 'Ord', 'Chr',
  347. 'Length', 'SetLength', 'High', 'Low', 'Assigned', 'FreeAndNil',
  348. 'Format', 'IntToStr', 'StrToInt', 'FloatToStr', 'StrToFloat',
  349. 'Trim', 'UpperCase', 'LowerCase', 'Pos', 'Copy', 'Delete', 'Insert',
  350. 'Now', 'Date', 'Time', 'DateToStr', 'StrToDate',
  351. 'Raise', 'Exit', 'Break', 'Continue', 'Abort',
  352. 'True', 'False', 'nil', 'Self', 'Result',
  353. 'Create', 'Destroy', 'Free',
  354. 'TObject', 'TComponent', 'TPersistent', 'TInterfacedObject',
  355. 'TList', 'TStringList', 'TStrings', 'TStream', 'TMemoryStream', 'TFileStream',
  356. 'Exception', 'EAbort', 'EConvertError', 'EAccessViolation',
  357. 'IInterface', 'IUnknown',
  358. ];
  359. if (pascalBuiltIns.includes(name)) {
  360. return true;
  361. }
  362. }
  363. return false;
  364. }
  365. /**
  366. * Get file path from node ID
  367. */
  368. private getFilePathFromNodeId(nodeId: string): string {
  369. // Check warm cache first
  370. const cached = this.nodeByIdCache.get(nodeId);
  371. if (cached) return cached.filePath;
  372. const node = this.queries.getNodeById(nodeId);
  373. return node?.filePath || '';
  374. }
  375. /**
  376. * Get language from node ID
  377. */
  378. private getLanguageFromNodeId(nodeId: string): UnresolvedRef['language'] {
  379. // Check warm cache first
  380. const cached = this.nodeByIdCache.get(nodeId);
  381. if (cached) return cached.language;
  382. const node = this.queries.getNodeById(nodeId);
  383. return node?.language || 'unknown';
  384. }
  385. }
  386. /**
  387. * Create a reference resolver instance
  388. */
  389. export function createResolver(projectRoot: string, queries: QueryBuilder): ReferenceResolver {
  390. const resolver = new ReferenceResolver(projectRoot, queries);
  391. resolver.initialize();
  392. return resolver;
  393. }