index.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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 {
  11. UnresolvedRef,
  12. ResolvedRef,
  13. ResolutionResult,
  14. ResolutionContext,
  15. FrameworkResolver,
  16. } from './types';
  17. import { matchReference } from './name-matcher';
  18. import { resolveViaImport } from './import-resolver';
  19. import { detectFrameworks } from './frameworks';
  20. import { logDebug } from '../errors';
  21. // Re-export types
  22. export * from './types';
  23. /**
  24. * Reference Resolver
  25. *
  26. * Orchestrates reference resolution using multiple strategies.
  27. */
  28. export class ReferenceResolver {
  29. private projectRoot: string;
  30. private queries: QueryBuilder;
  31. private context: ResolutionContext;
  32. private frameworks: FrameworkResolver[] = [];
  33. private nodeCache: Map<string, Node[]> = new Map();
  34. private fileCache: Map<string, string | null> = new Map();
  35. constructor(projectRoot: string, queries: QueryBuilder) {
  36. this.projectRoot = projectRoot;
  37. this.queries = queries;
  38. this.context = this.createContext();
  39. }
  40. /**
  41. * Initialize the resolver (detect frameworks, etc.)
  42. */
  43. initialize(): void {
  44. this.frameworks = detectFrameworks(this.context);
  45. this.clearCaches();
  46. }
  47. /**
  48. * Clear internal caches
  49. */
  50. clearCaches(): void {
  51. this.nodeCache.clear();
  52. this.fileCache.clear();
  53. }
  54. /**
  55. * Create the resolution context
  56. */
  57. private createContext(): ResolutionContext {
  58. return {
  59. getNodesInFile: (filePath: string) => {
  60. if (!this.nodeCache.has(filePath)) {
  61. this.nodeCache.set(filePath, this.queries.getNodesByFile(filePath));
  62. }
  63. return this.nodeCache.get(filePath)!;
  64. },
  65. getNodesByName: (name: string) => {
  66. return this.queries.searchNodes(name, { limit: 100 }).map((r) => r.node);
  67. },
  68. getNodesByQualifiedName: (qualifiedName: string) => {
  69. // Search for exact qualified name match
  70. return this.queries
  71. .searchNodes(qualifiedName, { limit: 50 })
  72. .filter((r) => r.node.qualifiedName === qualifiedName)
  73. .map((r) => r.node);
  74. },
  75. getNodesByKind: (kind: Node['kind']) => {
  76. return this.queries.getNodesByKind(kind);
  77. },
  78. fileExists: (filePath: string) => {
  79. const fullPath = path.join(this.projectRoot, filePath);
  80. try {
  81. return fs.existsSync(fullPath);
  82. } catch (error) {
  83. logDebug('Error checking file existence', { filePath, error: String(error) });
  84. return false;
  85. }
  86. },
  87. readFile: (filePath: string) => {
  88. if (this.fileCache.has(filePath)) {
  89. return this.fileCache.get(filePath)!;
  90. }
  91. const fullPath = path.join(this.projectRoot, filePath);
  92. try {
  93. const content = fs.readFileSync(fullPath, 'utf-8');
  94. this.fileCache.set(filePath, content);
  95. return content;
  96. } catch (error) {
  97. logDebug('Failed to read file for resolution', { filePath, error: String(error) });
  98. this.fileCache.set(filePath, null);
  99. return null;
  100. }
  101. },
  102. getProjectRoot: () => this.projectRoot,
  103. getAllFiles: () => {
  104. return this.queries.getAllFiles().map((f) => f.path);
  105. },
  106. };
  107. }
  108. /**
  109. * Resolve all unresolved references
  110. */
  111. resolveAll(unresolvedRefs: UnresolvedReference[]): ResolutionResult {
  112. const resolved: ResolvedRef[] = [];
  113. const unresolved: UnresolvedRef[] = [];
  114. const byMethod: Record<string, number> = {};
  115. // Convert to our internal format
  116. const refs: UnresolvedRef[] = unresolvedRefs.map((ref) => ({
  117. fromNodeId: ref.fromNodeId,
  118. referenceName: ref.referenceName,
  119. referenceKind: ref.referenceKind,
  120. line: ref.line,
  121. column: ref.column,
  122. filePath: this.getFilePathFromNodeId(ref.fromNodeId),
  123. language: this.getLanguageFromNodeId(ref.fromNodeId),
  124. }));
  125. for (const ref of refs) {
  126. const result = this.resolveOne(ref);
  127. if (result) {
  128. resolved.push(result);
  129. byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
  130. } else {
  131. unresolved.push(ref);
  132. }
  133. }
  134. return {
  135. resolved,
  136. unresolved,
  137. stats: {
  138. total: refs.length,
  139. resolved: resolved.length,
  140. unresolved: unresolved.length,
  141. byMethod,
  142. },
  143. };
  144. }
  145. /**
  146. * Resolve a single reference
  147. */
  148. resolveOne(ref: UnresolvedRef): ResolvedRef | null {
  149. // Skip built-in/external references
  150. if (this.isBuiltInOrExternal(ref)) {
  151. return null;
  152. }
  153. // Strategy 1: Try framework-specific resolution first
  154. for (const framework of this.frameworks) {
  155. const result = framework.resolve(ref, this.context);
  156. if (result) {
  157. return result;
  158. }
  159. }
  160. // Strategy 2: Try import-based resolution
  161. const importResult = resolveViaImport(ref, this.context);
  162. if (importResult) {
  163. return importResult;
  164. }
  165. // Strategy 3: Try name matching
  166. const nameResult = matchReference(ref, this.context);
  167. if (nameResult) {
  168. return nameResult;
  169. }
  170. return null;
  171. }
  172. /**
  173. * Create edges from resolved references
  174. */
  175. createEdges(resolved: ResolvedRef[]): Edge[] {
  176. return resolved.map((ref) => ({
  177. source: ref.original.fromNodeId,
  178. target: ref.targetNodeId,
  179. kind: ref.original.referenceKind,
  180. line: ref.original.line,
  181. column: ref.original.column,
  182. metadata: {
  183. confidence: ref.confidence,
  184. resolvedBy: ref.resolvedBy,
  185. },
  186. }));
  187. }
  188. /**
  189. * Resolve and persist edges to database
  190. */
  191. resolveAndPersist(unresolvedRefs: UnresolvedReference[]): ResolutionResult {
  192. const result = this.resolveAll(unresolvedRefs);
  193. // Create edges from resolved references
  194. const edges = this.createEdges(result.resolved);
  195. // Insert edges into database
  196. if (edges.length > 0) {
  197. this.queries.insertEdges(edges);
  198. }
  199. return result;
  200. }
  201. /**
  202. * Get detected frameworks
  203. */
  204. getDetectedFrameworks(): string[] {
  205. return this.frameworks.map((f) => f.name);
  206. }
  207. /**
  208. * Check if reference is to a built-in or external symbol
  209. */
  210. private isBuiltInOrExternal(ref: UnresolvedRef): boolean {
  211. const name = ref.referenceName;
  212. // JavaScript/TypeScript built-ins
  213. const jsBuiltIns = [
  214. 'console', 'window', 'document', 'global', 'process',
  215. 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
  216. 'Date', 'Math', 'JSON', 'RegExp', 'Error', 'Map', 'Set',
  217. 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
  218. 'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
  219. ];
  220. if (jsBuiltIns.includes(name)) {
  221. return true;
  222. }
  223. // Common library calls
  224. if (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.')) {
  225. return true;
  226. }
  227. // React hooks from React itself
  228. const reactHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue'];
  229. if (reactHooks.includes(name)) {
  230. return true;
  231. }
  232. // Python built-ins
  233. const pythonBuiltIns = [
  234. 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
  235. 'open', 'input', 'type', 'isinstance', 'hasattr', 'getattr', 'setattr',
  236. 'super', 'self', 'cls', 'None', 'True', 'False',
  237. ];
  238. if (ref.language === 'python' && pythonBuiltIns.includes(name)) {
  239. return true;
  240. }
  241. return false;
  242. }
  243. /**
  244. * Get file path from node ID
  245. */
  246. private getFilePathFromNodeId(nodeId: string): string {
  247. const node = this.queries.getNodeById(nodeId);
  248. return node?.filePath || '';
  249. }
  250. /**
  251. * Get language from node ID
  252. */
  253. private getLanguageFromNodeId(nodeId: string): UnresolvedRef['language'] {
  254. const node = this.queries.getNodeById(nodeId);
  255. return node?.language || 'unknown';
  256. }
  257. }
  258. /**
  259. * Create a reference resolver instance
  260. */
  261. export function createResolver(projectRoot: string, queries: QueryBuilder): ReferenceResolver {
  262. const resolver = new ReferenceResolver(projectRoot, queries);
  263. resolver.initialize();
  264. return resolver;
  265. }