name-matcher.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /**
  2. * Name Matcher
  3. *
  4. * Handles symbol name matching for reference resolution.
  5. */
  6. import { Node } from '../types';
  7. import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
  8. /**
  9. * Try to resolve a reference by exact name match
  10. */
  11. export function matchByExactName(
  12. ref: UnresolvedRef,
  13. context: ResolutionContext
  14. ): ResolvedRef | null {
  15. const candidates = context.getNodesByName(ref.referenceName);
  16. if (candidates.length === 0) {
  17. return null;
  18. }
  19. // If only one match, use it
  20. if (candidates.length === 1) {
  21. return {
  22. original: ref,
  23. targetNodeId: candidates[0]!.id,
  24. confidence: 0.9,
  25. resolvedBy: 'exact-match',
  26. };
  27. }
  28. // Multiple matches - try to narrow down
  29. const bestMatch = findBestMatch(ref, candidates, context);
  30. if (bestMatch) {
  31. return {
  32. original: ref,
  33. targetNodeId: bestMatch.id,
  34. confidence: 0.7,
  35. resolvedBy: 'exact-match',
  36. };
  37. }
  38. return null;
  39. }
  40. /**
  41. * Try to resolve by qualified name
  42. */
  43. export function matchByQualifiedName(
  44. ref: UnresolvedRef,
  45. context: ResolutionContext
  46. ): ResolvedRef | null {
  47. // Check if the reference name looks qualified (contains :: or .)
  48. if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
  49. return null;
  50. }
  51. const candidates = context.getNodesByQualifiedName(ref.referenceName);
  52. if (candidates.length === 1) {
  53. return {
  54. original: ref,
  55. targetNodeId: candidates[0]!.id,
  56. confidence: 0.95,
  57. resolvedBy: 'qualified-name',
  58. };
  59. }
  60. // Try partial qualified name match
  61. const parts = ref.referenceName.split(/[:.]/);
  62. const lastName = parts[parts.length - 1];
  63. if (lastName) {
  64. const partialCandidates = context.getNodesByName(lastName);
  65. for (const candidate of partialCandidates) {
  66. if (candidate.qualifiedName.endsWith(ref.referenceName)) {
  67. return {
  68. original: ref,
  69. targetNodeId: candidate.id,
  70. confidence: 0.85,
  71. resolvedBy: 'qualified-name',
  72. };
  73. }
  74. }
  75. }
  76. return null;
  77. }
  78. /**
  79. * Try to resolve by method name on a class/object
  80. */
  81. export function matchMethodCall(
  82. ref: UnresolvedRef,
  83. context: ResolutionContext
  84. ): ResolvedRef | null {
  85. // Parse method call patterns like "obj.method" or "Class::method"
  86. const dotMatch = ref.referenceName.match(/^(\w+)\.(\w+)$/);
  87. const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
  88. const match = dotMatch || colonMatch;
  89. if (!match) {
  90. return null;
  91. }
  92. const [, objectOrClass, methodName] = match;
  93. // Find the class/object first
  94. const classCandidates = context.getNodesByName(objectOrClass!);
  95. for (const classNode of classCandidates) {
  96. if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
  97. // Look for method in the same file
  98. const nodesInFile = context.getNodesInFile(classNode.filePath);
  99. const methodNode = nodesInFile.find(
  100. (n) =>
  101. n.kind === 'method' &&
  102. n.name === methodName &&
  103. n.qualifiedName.includes(classNode.name)
  104. );
  105. if (methodNode) {
  106. return {
  107. original: ref,
  108. targetNodeId: methodNode.id,
  109. confidence: 0.85,
  110. resolvedBy: 'qualified-name',
  111. };
  112. }
  113. }
  114. }
  115. return null;
  116. }
  117. /**
  118. * Find the best matching node when there are multiple candidates
  119. */
  120. function findBestMatch(
  121. ref: UnresolvedRef,
  122. candidates: Node[],
  123. _context: ResolutionContext
  124. ): Node | null {
  125. // Prioritization rules:
  126. // 1. Same file > different file
  127. // 2. Same language > different language
  128. // 3. Functions/methods > classes/types (for call references)
  129. // 4. Exported > non-exported
  130. let bestScore = -1;
  131. let bestNode: Node | null = null;
  132. for (const candidate of candidates) {
  133. let score = 0;
  134. // Same file bonus
  135. if (candidate.filePath === ref.filePath) {
  136. score += 100;
  137. }
  138. // Same language bonus
  139. if (candidate.language === ref.language) {
  140. score += 50;
  141. }
  142. // For call references, prefer functions/methods
  143. if (ref.referenceKind === 'calls') {
  144. if (candidate.kind === 'function' || candidate.kind === 'method') {
  145. score += 25;
  146. }
  147. }
  148. // Exported bonus
  149. if (candidate.isExported) {
  150. score += 10;
  151. }
  152. // Closer line number (within same file)
  153. if (candidate.filePath === ref.filePath && candidate.startLine) {
  154. const distance = Math.abs(candidate.startLine - ref.line);
  155. score += Math.max(0, 20 - distance / 10);
  156. }
  157. if (score > bestScore) {
  158. bestScore = score;
  159. bestNode = candidate;
  160. }
  161. }
  162. return bestNode;
  163. }
  164. /**
  165. * Fuzzy match - last resort with lower confidence
  166. */
  167. export function matchFuzzy(
  168. ref: UnresolvedRef,
  169. context: ResolutionContext
  170. ): ResolvedRef | null {
  171. const lowerName = ref.referenceName.toLowerCase();
  172. // Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
  173. const candidates = context.getNodesByLowerName(lowerName);
  174. // Filter to callable kinds only (function, method, class)
  175. const callableKinds = new Set(['function', 'method', 'class']);
  176. const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
  177. if (callableCandidates.length === 1) {
  178. return {
  179. original: ref,
  180. targetNodeId: callableCandidates[0]!.id,
  181. confidence: 0.5,
  182. resolvedBy: 'fuzzy',
  183. };
  184. }
  185. return null;
  186. }
  187. /**
  188. * Match all strategies in order of confidence
  189. */
  190. export function matchReference(
  191. ref: UnresolvedRef,
  192. context: ResolutionContext
  193. ): ResolvedRef | null {
  194. // Try strategies in order of confidence
  195. let result: ResolvedRef | null;
  196. // 1. Qualified name match (highest confidence)
  197. result = matchByQualifiedName(ref, context);
  198. if (result) return result;
  199. // 2. Method call pattern
  200. result = matchMethodCall(ref, context);
  201. if (result) return result;
  202. // 3. Exact name match
  203. result = matchByExactName(ref, context);
  204. if (result) return result;
  205. // 4. Fuzzy match (lowest confidence)
  206. result = matchFuzzy(ref, context);
  207. if (result) return result;
  208. return null;
  209. }