@colbymchenry/codegraph-win32-arm64 1.1.6 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/lib/dist/bin/codegraph.d.ts +1 -1
  2. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  3. package/lib/dist/bin/codegraph.js +232 -61
  4. package/lib/dist/bin/codegraph.js.map +1 -1
  5. package/lib/dist/bin/command-supervision.d.ts.map +1 -1
  6. package/lib/dist/bin/command-supervision.js +14 -1
  7. package/lib/dist/bin/command-supervision.js.map +1 -1
  8. package/lib/dist/db/migrations.d.ts +1 -1
  9. package/lib/dist/db/migrations.d.ts.map +1 -1
  10. package/lib/dist/db/migrations.js +19 -1
  11. package/lib/dist/db/migrations.js.map +1 -1
  12. package/lib/dist/db/queries.d.ts +51 -0
  13. package/lib/dist/db/queries.d.ts.map +1 -1
  14. package/lib/dist/db/queries.js +141 -0
  15. package/lib/dist/db/queries.js.map +1 -1
  16. package/lib/dist/db/schema.sql +19 -0
  17. package/lib/dist/directory.d.ts +9 -5
  18. package/lib/dist/directory.d.ts.map +1 -1
  19. package/lib/dist/directory.js +210 -19
  20. package/lib/dist/directory.js.map +1 -1
  21. package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
  22. package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
  23. package/lib/dist/extraction/cfml-extractor.js +494 -0
  24. package/lib/dist/extraction/cfml-extractor.js.map +1 -0
  25. package/lib/dist/extraction/grammars.d.ts +9 -0
  26. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  27. package/lib/dist/extraction/grammars.js +115 -3
  28. package/lib/dist/extraction/grammars.js.map +1 -1
  29. package/lib/dist/extraction/index.d.ts +46 -1
  30. package/lib/dist/extraction/index.d.ts.map +1 -1
  31. package/lib/dist/extraction/index.js +254 -3
  32. package/lib/dist/extraction/index.js.map +1 -1
  33. package/lib/dist/extraction/languages/arkts.d.ts +3 -0
  34. package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
  35. package/lib/dist/extraction/languages/arkts.js +127 -0
  36. package/lib/dist/extraction/languages/arkts.js.map +1 -0
  37. package/lib/dist/extraction/languages/c-cpp.d.ts +59 -0
  38. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  39. package/lib/dist/extraction/languages/c-cpp.js +499 -3
  40. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  41. package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
  42. package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
  43. package/lib/dist/extraction/languages/cfquery.js +28 -0
  44. package/lib/dist/extraction/languages/cfquery.js.map +1 -0
  45. package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
  46. package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
  47. package/lib/dist/extraction/languages/cfscript.js +73 -0
  48. package/lib/dist/extraction/languages/cfscript.js.map +1 -0
  49. package/lib/dist/extraction/languages/cobol.d.ts +33 -0
  50. package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
  51. package/lib/dist/extraction/languages/cobol.js +499 -0
  52. package/lib/dist/extraction/languages/cobol.js.map +1 -0
  53. package/lib/dist/extraction/languages/erlang.d.ts +3 -0
  54. package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
  55. package/lib/dist/extraction/languages/erlang.js +350 -0
  56. package/lib/dist/extraction/languages/erlang.js.map +1 -0
  57. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  58. package/lib/dist/extraction/languages/index.js +18 -0
  59. package/lib/dist/extraction/languages/index.js.map +1 -1
  60. package/lib/dist/extraction/languages/nix.d.ts +3 -0
  61. package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
  62. package/lib/dist/extraction/languages/nix.js +294 -0
  63. package/lib/dist/extraction/languages/nix.js.map +1 -0
  64. package/lib/dist/extraction/languages/solidity.d.ts +3 -0
  65. package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
  66. package/lib/dist/extraction/languages/solidity.js +293 -0
  67. package/lib/dist/extraction/languages/solidity.js.map +1 -0
  68. package/lib/dist/extraction/languages/terraform.d.ts +3 -0
  69. package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
  70. package/lib/dist/extraction/languages/terraform.js +641 -0
  71. package/lib/dist/extraction/languages/terraform.js.map +1 -0
  72. package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
  73. package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
  74. package/lib/dist/extraction/languages/vbnet.js +141 -0
  75. package/lib/dist/extraction/languages/vbnet.js.map +1 -0
  76. package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
  77. package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  78. package/lib/dist/extraction/mybatis-extractor.js +140 -38
  79. package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
  80. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  81. package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
  82. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  83. package/lib/dist/extraction/tree-sitter-types.d.ts +20 -1
  84. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  85. package/lib/dist/extraction/tree-sitter.d.ts +38 -0
  86. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  87. package/lib/dist/extraction/tree-sitter.js +845 -16
  88. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  89. package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  90. package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  91. package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  92. package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  93. package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  94. package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  95. package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  96. package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  97. package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  98. package/lib/dist/graph/traversal.d.ts.map +1 -1
  99. package/lib/dist/graph/traversal.js +76 -17
  100. package/lib/dist/graph/traversal.js.map +1 -1
  101. package/lib/dist/index.d.ts +63 -1
  102. package/lib/dist/index.d.ts.map +1 -1
  103. package/lib/dist/index.js +310 -6
  104. package/lib/dist/index.js.map +1 -1
  105. package/lib/dist/installer/index.d.ts.map +1 -1
  106. package/lib/dist/installer/index.js +3 -1
  107. package/lib/dist/installer/index.js.map +1 -1
  108. package/lib/dist/mcp/daemon.d.ts +25 -3
  109. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  110. package/lib/dist/mcp/daemon.js +39 -7
  111. package/lib/dist/mcp/daemon.js.map +1 -1
  112. package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  113. package/lib/dist/mcp/dynamic-boundaries.js +2 -1
  114. package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
  115. package/lib/dist/mcp/early-ppid.d.ts +26 -0
  116. package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
  117. package/lib/dist/mcp/early-ppid.js +29 -0
  118. package/lib/dist/mcp/early-ppid.js.map +1 -0
  119. package/lib/dist/mcp/index.d.ts.map +1 -1
  120. package/lib/dist/mcp/index.js +21 -4
  121. package/lib/dist/mcp/index.js.map +1 -1
  122. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  123. package/lib/dist/mcp/proxy.js +21 -3
  124. package/lib/dist/mcp/proxy.js.map +1 -1
  125. package/lib/dist/mcp/startup-handshake.d.ts +44 -0
  126. package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
  127. package/lib/dist/mcp/startup-handshake.js +73 -0
  128. package/lib/dist/mcp/startup-handshake.js.map +1 -0
  129. package/lib/dist/mcp/tools.d.ts +22 -0
  130. package/lib/dist/mcp/tools.d.ts.map +1 -1
  131. package/lib/dist/mcp/tools.js +74 -9
  132. package/lib/dist/mcp/tools.js.map +1 -1
  133. package/lib/dist/project-config.d.ts +38 -0
  134. package/lib/dist/project-config.d.ts.map +1 -1
  135. package/lib/dist/project-config.js +101 -2
  136. package/lib/dist/project-config.js.map +1 -1
  137. package/lib/dist/resolution/callback-synthesizer.d.ts +1 -1
  138. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  139. package/lib/dist/resolution/callback-synthesizer.js +743 -11
  140. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  141. package/lib/dist/resolution/cooperative-yield.d.ts +32 -0
  142. package/lib/dist/resolution/cooperative-yield.d.ts.map +1 -0
  143. package/lib/dist/resolution/cooperative-yield.js +42 -0
  144. package/lib/dist/resolution/cooperative-yield.js.map +1 -0
  145. package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
  146. package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
  147. package/lib/dist/resolution/frameworks/cics.js +90 -0
  148. package/lib/dist/resolution/frameworks/cics.js.map +1 -0
  149. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  150. package/lib/dist/resolution/frameworks/index.js +6 -0
  151. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  152. package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
  153. package/lib/dist/resolution/frameworks/java.js +14 -6
  154. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  155. package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
  156. package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  157. package/lib/dist/resolution/frameworks/terraform.js +277 -0
  158. package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
  159. package/lib/dist/resolution/import-resolver.d.ts +7 -0
  160. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  161. package/lib/dist/resolution/import-resolver.js +129 -4
  162. package/lib/dist/resolution/import-resolver.js.map +1 -1
  163. package/lib/dist/resolution/index.d.ts +44 -2
  164. package/lib/dist/resolution/index.d.ts.map +1 -1
  165. package/lib/dist/resolution/index.js +335 -30
  166. package/lib/dist/resolution/index.js.map +1 -1
  167. package/lib/dist/resolution/name-matcher.d.ts +22 -3
  168. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  169. package/lib/dist/resolution/name-matcher.js +484 -39
  170. package/lib/dist/resolution/name-matcher.js.map +1 -1
  171. package/lib/dist/resolution/strip-comments.d.ts +1 -1
  172. package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
  173. package/lib/dist/resolution/strip-comments.js +49 -0
  174. package/lib/dist/resolution/strip-comments.js.map +1 -1
  175. package/lib/dist/resolution/types.d.ts +20 -0
  176. package/lib/dist/resolution/types.d.ts.map +1 -1
  177. package/lib/dist/resolution/workspace-packages.d.ts +10 -0
  178. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
  179. package/lib/dist/resolution/workspace-packages.js +142 -4
  180. package/lib/dist/resolution/workspace-packages.js.map +1 -1
  181. package/lib/dist/search/identifier-segments.d.ts +60 -0
  182. package/lib/dist/search/identifier-segments.d.ts.map +1 -0
  183. package/lib/dist/search/identifier-segments.js +176 -0
  184. package/lib/dist/search/identifier-segments.js.map +1 -0
  185. package/lib/dist/sync/git-hooks.d.ts.map +1 -1
  186. package/lib/dist/sync/git-hooks.js +2 -0
  187. package/lib/dist/sync/git-hooks.js.map +1 -1
  188. package/lib/dist/sync/watcher.d.ts +10 -5
  189. package/lib/dist/sync/watcher.d.ts.map +1 -1
  190. package/lib/dist/sync/watcher.js +51 -14
  191. package/lib/dist/sync/watcher.js.map +1 -1
  192. package/lib/dist/sync/worktree.d.ts.map +1 -1
  193. package/lib/dist/sync/worktree.js +5 -0
  194. package/lib/dist/sync/worktree.js.map +1 -1
  195. package/lib/dist/types.d.ts +19 -1
  196. package/lib/dist/types.d.ts.map +1 -1
  197. package/lib/dist/types.js +10 -0
  198. package/lib/dist/types.js.map +1 -1
  199. package/lib/dist/upgrade/index.js +1 -1
  200. package/lib/dist/upgrade/index.js.map +1 -1
  201. package/lib/node_modules/.package-lock.json +1 -1
  202. package/lib/package.json +1 -1
  203. package/package.json +1 -1
  204. package/lib/dist/reasoning/config.d.ts +0 -45
  205. package/lib/dist/reasoning/config.d.ts.map +0 -1
  206. package/lib/dist/reasoning/config.js +0 -171
  207. package/lib/dist/reasoning/config.js.map +0 -1
  208. package/lib/dist/reasoning/credentials.d.ts +0 -5
  209. package/lib/dist/reasoning/credentials.d.ts.map +0 -1
  210. package/lib/dist/reasoning/credentials.js +0 -83
  211. package/lib/dist/reasoning/credentials.js.map +0 -1
  212. package/lib/dist/reasoning/login.d.ts +0 -21
  213. package/lib/dist/reasoning/login.d.ts.map +0 -1
  214. package/lib/dist/reasoning/login.js +0 -85
  215. package/lib/dist/reasoning/login.js.map +0 -1
  216. package/lib/dist/reasoning/reasoner.d.ts +0 -43
  217. package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
  218. package/lib/dist/reasoning/reasoner.js +0 -308
  219. package/lib/dist/reasoning/reasoner.js.map +0 -1
@@ -12,6 +12,8 @@ exports.crossesKnownFamily = crossesKnownFamily;
12
12
  exports.matchFunctionRef = matchFunctionRef;
13
13
  exports.matchByExactName = matchByExactName;
14
14
  exports.matchByQualifiedName = matchByQualifiedName;
15
+ exports.preferCallSiteFile = preferCallSiteFile;
16
+ exports.resolveMethodOnType = resolveMethodOnType;
15
17
  exports.matchCppCallChain = matchCppCallChain;
16
18
  exports.matchScopedCallChain = matchScopedCallChain;
17
19
  exports.matchDottedCallChain = matchDottedCallChain;
@@ -138,7 +140,9 @@ function pickClosestFileNode(candidates, ref) {
138
140
  const LANGUAGE_FAMILY = {
139
141
  java: 'jvm', kotlin: 'jvm', scala: 'jvm',
140
142
  swift: 'apple', objc: 'apple',
141
- typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web',
143
+ // ArkTS is a TS superset every HarmonyOS project mixes `.ets` UI with
144
+ // `.ts` logic modules, so refs must cross freely between them.
145
+ typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web', arkts: 'web',
142
146
  c: 'c', cpp: 'c',
143
147
  // Razor/Blazor markup names C# types — same family so `@model Foo` /
144
148
  // `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
@@ -220,6 +224,7 @@ function matchFunctionRef(ref, context) {
220
224
  // implicit-self, Java/Kotlin method references.
221
225
  const bareFnOnly = ref.language === 'typescript' || ref.language === 'tsx' ||
222
226
  ref.language === 'javascript' || ref.language === 'jsx' ||
227
+ ref.language === 'arkts' ||
223
228
  ref.language === 'cpp' || ref.language === 'python' ||
224
229
  ref.language === 'php';
225
230
  // Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
@@ -382,7 +387,17 @@ function matchByQualifiedName(ref, context) {
382
387
  if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
383
388
  return null;
384
389
  }
385
- const candidates = context.getNodesByQualifiedName(ref.referenceName);
390
+ // A method call `receiver.method()` can share an exact qualified name with a
391
+ // config-file key: `service.process()` (a `calls` ref named `service.process`)
392
+ // vs the yaml key `service.process`. Config keys are bound to their code refs
393
+ // upstream by the framework resolvers (`@Value` → `references`); a `calls` ref
394
+ // must never resolve to a yaml/properties config node — that's a wrong edge
395
+ // AND it hides the real callee. Drop those from both the exact and the partial
396
+ // candidate sets so resolution falls through to method resolution below (#1180).
397
+ const keepForRef = (nodes) => ref.referenceKind === 'calls'
398
+ ? nodes.filter((n) => !(n.kind === 'constant' && (n.language === 'yaml' || n.language === 'properties')))
399
+ : nodes;
400
+ const candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
386
401
  if (candidates.length === 1) {
387
402
  return {
388
403
  original: ref,
@@ -391,24 +406,63 @@ function matchByQualifiedName(ref, context) {
391
406
  resolvedBy: 'qualified-name',
392
407
  };
393
408
  }
394
- // Try partial qualified name match
409
+ // Several symbols share this exact qualified name (e.g. `Logger::log` declared
410
+ // in two files — an ODR clash or separate translation units): prefer the one
411
+ // in the call site's own file before the partial-match fallback below, else
412
+ // the first-indexed def wins and a call in `b/svc` targets `a/svc` (#1079).
413
+ if (candidates.length > 1) {
414
+ const ordered = preferCallSiteFile(candidates, ref.filePath);
415
+ if (ordered[0].filePath === ref.filePath) {
416
+ return {
417
+ original: ref,
418
+ targetNodeId: ordered[0].id,
419
+ confidence: 0.95,
420
+ resolvedBy: 'qualified-name',
421
+ };
422
+ }
423
+ }
424
+ // Try partial qualified name match — again preferring the call site's own
425
+ // file when more than one symbol's qualifiedName ends with the reference.
395
426
  const parts = ref.referenceName.split(/[:.]/);
396
427
  const lastName = parts[parts.length - 1];
397
428
  if (lastName) {
398
- const partialCandidates = context.getNodesByName(lastName);
399
- for (const candidate of partialCandidates) {
400
- if (candidate.qualifiedName.endsWith(ref.referenceName)) {
401
- return {
402
- original: ref,
403
- targetNodeId: candidate.id,
404
- confidence: 0.85,
405
- resolvedBy: 'qualified-name',
406
- };
407
- }
429
+ const partialCandidates = keepForRef(context.getNodesByName(lastName))
430
+ .filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
431
+ const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
432
+ if (chosen) {
433
+ return {
434
+ original: ref,
435
+ targetNodeId: chosen.id,
436
+ confidence: 0.85,
437
+ resolvedBy: 'qualified-name',
438
+ };
408
439
  }
409
440
  }
410
441
  return null;
411
442
  }
443
+ /**
444
+ * When a symbol name is ambiguous across files, prefer the candidate(s) declared
445
+ * in the call site's own file, keeping the rest in their original order (#1079).
446
+ * A same-file definition is the strongest language-agnostic signal for which of
447
+ * several same-named symbols a call means; without it, resolution collapses onto
448
+ * whichever was indexed first, so a call in `b/svc` wrongly targets `a/svc`.
449
+ * No-op when there are <2 candidates or none share the call site's file.
450
+ */
451
+ function preferCallSiteFile(nodes, callSiteFile) {
452
+ if (nodes.length < 2)
453
+ return nodes;
454
+ const same = [];
455
+ const other = [];
456
+ for (const n of nodes) {
457
+ if (n.filePath === callSiteFile)
458
+ same.push(n);
459
+ else
460
+ other.push(n);
461
+ }
462
+ return same.length ? [...same, ...other] : nodes;
463
+ }
464
+ // Exported for the precedence unit tests (#1079): they assert the
465
+ // preferredFqn → same-file → matches[0] ordering directly.
412
466
  function resolveMethodOnType(typeName, methodName, ref, context, confidence, resolvedBy,
413
467
  /**
414
468
  * Optional FQN that identifies WHICH class declaration `typeName`
@@ -426,17 +480,28 @@ depth = 0) {
426
480
  // in-class (`class Foo { int bar() { ... } }`) or out-of-line in a separate
427
481
  // file (`int Foo::bar() { ... }` in foo.cpp while class Foo is in foo.hpp).
428
482
  // The previous same-file approach missed the latter — the typical C++ layout.
429
- const methodCandidates = context.getNodesByName(methodName);
430
- const want = `${typeName}::${methodName}`;
431
- const matches = [];
432
- for (const m of methodCandidates) {
433
- if (m.kind !== 'method')
434
- continue;
435
- if (m.language !== ref.language)
436
- continue;
437
- const qn = m.qualifiedName;
438
- if (qn === want || qn.endsWith(`::${want}`)) {
439
- matches.push(m);
483
+ // Prefer the context's per-(type, method) memo: the raw name lookup fetches
484
+ // EVERY node sharing the method name — tens of thousands of rows for a
485
+ // collision-heavy Java name like `execute` — and re-filtering that per ref
486
+ // was a dominant term in the #1122 watchdog kill on large repos. Only the
487
+ // ref-independent filter is memoized; per-ref disambiguation stays below.
488
+ let matches;
489
+ if (context.getMethodMatches) {
490
+ matches = context.getMethodMatches(typeName, methodName, ref.language);
491
+ }
492
+ else {
493
+ const methodCandidates = context.getNodesByName(methodName);
494
+ const want = `${typeName}::${methodName}`;
495
+ matches = [];
496
+ for (const m of methodCandidates) {
497
+ if (m.kind !== 'method')
498
+ continue;
499
+ if (m.language !== ref.language)
500
+ continue;
501
+ const qn = m.qualifiedName;
502
+ if (qn === want || qn.endsWith(`::${want}`)) {
503
+ matches.push(m);
504
+ }
440
505
  }
441
506
  }
442
507
  if (matches.length === 0) {
@@ -472,9 +537,19 @@ depth = 0) {
472
537
  };
473
538
  }
474
539
  }
540
+ // Language-agnostic disambiguation: when several same-named methods survive
541
+ // (e.g. two files each declaring `class Logger { void log(); }` — an ODR
542
+ // clash, an anonymous-namespace type, or separate translation units), prefer
543
+ // the definition in the CALL SITE's own file. Without this, every ambiguous
544
+ // call collapses onto the first-indexed definition, so a call in `b/svc.cpp`
545
+ // wrongly points at `a/svc.cpp` (#1079). This runs AFTER the `preferredFqn`
546
+ // block, so Java/Kotlin import disambiguation — whose target is intentionally
547
+ // in ANOTHER file (#314) — is unaffected: that block returns early whenever
548
+ // an import FQN pins the class.
549
+ const ordered = preferCallSiteFile(matches, ref.filePath);
475
550
  return {
476
551
  original: ref,
477
- targetNodeId: matches[0].id,
552
+ targetNodeId: ordered[0].id,
478
553
  confidence,
479
554
  resolvedBy,
480
555
  };
@@ -513,10 +588,14 @@ function buildDeclaratorRegex(escapedReceiver) {
513
588
  return new RegExp(`([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`);
514
589
  }
515
590
  function inferCppReceiverType(receiverName, ref, context, depth = 0) {
516
- const source = context.readFile(ref.filePath);
517
- if (!source)
591
+ // Per-file lines cache when available — this runs per `receiver->method()`
592
+ // ref and re-splitting the file each time is the same quadratic as the
593
+ // shared inferrer's (#1122).
594
+ const lines = context.getFileLines
595
+ ? context.getFileLines(ref.filePath)
596
+ : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
597
+ if (!lines || lines.length === 0)
518
598
  return null;
519
- const lines = source.split(/\r?\n/);
520
599
  const callLineIndex = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
521
600
  const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
522
601
  const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
@@ -549,10 +628,12 @@ function inferCppReceiverType(receiverName, ref, context, depth = 0) {
549
628
  for (const headerPath of headerCandidates) {
550
629
  if (!context.fileExists(headerPath))
551
630
  continue;
552
- const headerSource = context.readFile(headerPath);
553
- if (!headerSource)
631
+ const headerLines = context.getFileLines
632
+ ? context.getFileLines(headerPath)
633
+ : (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
634
+ if (!headerLines)
554
635
  continue;
555
- for (const line of headerSource.split(/\r?\n/)) {
636
+ for (const line of headerLines) {
556
637
  if (!receiverPattern.test(line))
557
638
  continue;
558
639
  const declaratorMatch = line.match(declaratorRegex);
@@ -880,6 +961,283 @@ function inferJavaFieldReceiverType(receiverName, ref, context) {
880
961
  return null; // primitives / lowercase → skip
881
962
  return lastPart;
882
963
  }
964
+ // ── Local-variable receiver-type inference (#1108) ──────────────────────────
965
+ //
966
+ // Instance calls through a local variable (`const lg = new Logger(); lg.log()`)
967
+ // only resolved in C++ before this — no other language could learn the
968
+ // receiver's type. Local variables are not indexed as nodes (node-explosion),
969
+ // so, like the C++ inferrer above, we read the enclosing function's source and
970
+ // match the receiver's declaration/initializer to recover its type. The type is
971
+ // then handed to resolveMethodOnType, which VALIDATES that the type actually
972
+ // declares the method, so a mis-inference produces NO edge — the safety net
973
+ // that lets the patterns below stay simple. C++ keeps its dedicated inferrer
974
+ // (header scan + `auto`); this covers every other language.
975
+ // Tokens a loose pattern might capture that are never a user-defined type.
976
+ const NON_TYPE_RECEIVER_TOKENS = new Set([
977
+ 'this', 'self', 'super', 'new', 'return', 'await', 'yield', 'typeof',
978
+ 'null', 'nil', 'None', 'true', 'false', 'True', 'False', 'undefined',
979
+ ]);
980
+ /**
981
+ * Normalize a captured type expression to a simple type name: drop generic
982
+ * args and pointer/ref markers, take the last `.`/`::`-qualified segment, and
983
+ * reject obvious non-types.
984
+ */
985
+ function normalizeInferredTypeName(raw) {
986
+ const cleaned = raw.replace(/<[^>]*>/g, '').replace(/[&*]/g, '').trim();
987
+ const seg = cleaned.split(/[.:]+/).filter(Boolean).pop();
988
+ if (!seg)
989
+ return null;
990
+ if (NON_TYPE_RECEIVER_TOKENS.has(seg))
991
+ return null;
992
+ return seg;
993
+ }
994
+ /**
995
+ * Per-language patterns that recover a local variable's (or typed parameter's)
996
+ * type from its declaration/initializer. Each regex captures the type in group
997
+ * 1; `r` is the already-escaped receiver name. Ordered most-specific first.
998
+ * PascalCase is required in the capture where the language convention allows,
999
+ * as a cheap false-positive guard on top of resolveMethodOnType's validation.
1000
+ */
1001
+ function localReceiverTypePatterns(language, r) {
1002
+ switch (language) {
1003
+ case 'typescript':
1004
+ case 'javascript':
1005
+ case 'tsx':
1006
+ case 'jsx':
1007
+ case 'arkts':
1008
+ return [
1009
+ new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
1010
+ // No keyword requirement, so this matches BOTH a local annotation
1011
+ // (`const lg: Logger`) and a typed parameter (`function use(lg: Logger)`
1012
+ // / `(lg: Logger) =>`) — the parameter case the old `const|let|var`
1013
+ // prefix excluded (#1125). Mirrors Kotlin/Swift/Scala; the capture stops
1014
+ // at `<` so a generic-typed param (`repo: Repository<User>`) still yields
1015
+ // `Repository`. resolveMethodOnType validates the type actually declares
1016
+ // the method, so the looser match produces no edge on a mis-inference.
1017
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger (annotation or typed param)
1018
+ ];
1019
+ case 'python':
1020
+ return [
1021
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // lg = Logger(...)
1022
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // lg: Logger (PEP 526)
1023
+ ];
1024
+ case 'java':
1025
+ return [
1026
+ new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`), // = new Logger()
1027
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / param
1028
+ ];
1029
+ case 'kotlin':
1030
+ return [
1031
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // val lg = Logger(...)
1032
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // val lg: Logger / param
1033
+ ];
1034
+ case 'csharp':
1035
+ return [
1036
+ new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`), // = new Logger()
1037
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / param
1038
+ ];
1039
+ case 'swift':
1040
+ return [
1041
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // let lg = Logger(...)
1042
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // let lg: Logger / param
1043
+ ];
1044
+ case 'rust':
1045
+ return [
1046
+ new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger
1047
+ // No `let`, so this covers a `let lg: Logger` binding AND a typed
1048
+ // parameter (`fn use(lg: &Logger)`, a closure `|lg: Logger|`) — the
1049
+ // parameter case the old `let`-anchored pattern excluded (#1125).
1050
+ new RegExp(`\\b${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // lg: Logger (binding or typed param)
1051
+ ];
1052
+ case 'go':
1053
+ return [
1054
+ new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
1055
+ new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger
1056
+ // A typed parameter / method receiver (`func use(lg Logger)`,
1057
+ // `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125).
1058
+ // PascalCase-guarded (unlike the anchored patterns above) to keep the
1059
+ // keyword-free `ident Type` shape from matching unrelated pairs; the
1060
+ // enclosing-scope bound already excludes package-level struct fields.
1061
+ new RegExp(`\\b${r}\\s+\\*?([A-Z][\\w.]*)`), // func use(lg Logger) / (l Logger)
1062
+ ];
1063
+ case 'ruby':
1064
+ return [
1065
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w:]*)\\.new\\b`), // lg = Logger.new
1066
+ ];
1067
+ case 'scala':
1068
+ return [
1069
+ new RegExp(`\\b${r}\\b\\s*=\\s*(?:new\\s+)?([A-Z][\\w.]*)`), // val lg = new Logger / Logger(...)
1070
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // val lg: Logger / param
1071
+ ];
1072
+ case 'dart':
1073
+ return [
1074
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...)
1075
+ // Trailing `[=;,)]` (not just `[=;]`) so a typed parameter — `Logger lg)`
1076
+ // / `Logger lg,` — matches too, not only `Logger lg = ...` / `Logger lg;`
1077
+ // (#1125). Mirrors Java/C#.
1078
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg = ... / param
1079
+ ];
1080
+ case 'php':
1081
+ return [
1082
+ new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger()
1083
+ // A typed parameter (`function use(Logger $lg)`, `?Logger $lg`,
1084
+ // `\\App\\Logger $lg`, `&$lg` by-ref) and a typed `catch (E $e)` — the
1085
+ // type sits before the `$`-variable (#1125). Namespace `\\` allowed.
1086
+ new RegExp(`\\b([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // Logger $lg (typed param)
1087
+ ];
1088
+ case 'lua':
1089
+ case 'luau':
1090
+ return [
1091
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\.new\\b`), // local lg = Logger.new()
1092
+ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\s*\\(`), // local lg = Logger(...) (callable table)
1093
+ // Luau annotation (`local lg: Logger`) / typed param — but Lua's
1094
+ // method-call syntax is the IDENTICAL `receiver:Name` shape, and the
1095
+ // backward scan starts on the call's own line, so without a gate any
1096
+ // PascalCase method call (`lg:Log()`, the Roblox convention)
1097
+ // self-matches as "type = Log" before the scan reaches the real
1098
+ // declaration (#1124). The lookahead rejects a capture followed by
1099
+ // any of Lua's three call forms — `(args)`, `"s"`/`'s'`/`[[s]]`,
1100
+ // `{t}` — and its leading `[\w.]` alternative stops backtracking from
1101
+ // shrinking the capture to dodge the gate (`lg:Log()` would otherwise
1102
+ // still match, as `Lo`).
1103
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)(?![\\w.]|\\s*[({"'\\[])`), // local lg: Logger / typed param
1104
+ ];
1105
+ case 'r':
1106
+ return [
1107
+ new RegExp(`\\b${r}\\b\\s*(?:<-|<<-|=)\\s*([A-Z][\\w.]*)\\$new\\b`), // lg <- Logger$new() (R6)
1108
+ ];
1109
+ case 'pascal':
1110
+ return [
1111
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w]*)`), // var lg: TLogger / param lg: TLogger
1112
+ new RegExp(`\\b${r}\\b\\s*:=\\s*([A-Z][\\w.]*)\\.Create\\b`), // lg := TLogger.Create
1113
+ ];
1114
+ case 'cfml':
1115
+ case 'cfscript':
1116
+ return [
1117
+ // svc = new UserService() / new path.to.UserService() — dotted component
1118
+ // paths reduce to their final segment via normalizeInferredTypeName.
1119
+ // Also matches inside tag markup (`<cfset svc = new UserService()>`)
1120
+ // since the scan reads raw source lines.
1121
+ new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`),
1122
+ // The classic form: svc = createObject("component", "path.to.UserService")
1123
+ // (casing of createObject varies in the wild), plus the modern
1124
+ // single-argument form createObject("path.to.UserService").
1125
+ new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']component["']\\s*,\\s*["']([\\w.]+)["']`),
1126
+ new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']([\\w.]+)["']\\s*\\)`),
1127
+ // Typed cfscript parameter: `function save(UserService svc)` /
1128
+ // `required UserService svc` — CFML's built-in types (string, numeric,
1129
+ // any, struct…) are lowercase by convention, so the PascalCase guard
1130
+ // excludes them.
1131
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`),
1132
+ // Tag-form typed argument, either attribute order:
1133
+ // <cfargument name="svc" type="path.to.UserService">
1134
+ new RegExp(`\\bcfargument[^>\\n]*\\bname\\s*=\\s*["']${r}["'][^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["']`, 'i'),
1135
+ new RegExp(`\\bcfargument[^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["'][^>\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
1136
+ // Component property (incl. WireBox DI): `property name="svc"
1137
+ // inject="UserService";` / `<cfproperty name="svc" type="UserService">`,
1138
+ // either attribute order. An inject DSL value with a namespace
1139
+ // (`inject="svc@core"`) captures only the leading name and simply
1140
+ // fails type-validation — no edge, never a wrong one.
1141
+ new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\bname\\s*=\\s*["']${r}["'][^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["']`, 'i'),
1142
+ new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["'][^;\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
1143
+ ];
1144
+ default:
1145
+ return [];
1146
+ }
1147
+ }
1148
+ /** 1-based start line of the tightest function/method enclosing the call. */
1149
+ function enclosingScopeStartLine(ref, context) {
1150
+ let start = 1;
1151
+ for (const n of context.getNodesInFile(ref.filePath)) {
1152
+ if (n.kind !== 'function' && n.kind !== 'method')
1153
+ continue;
1154
+ if (n.language !== ref.language)
1155
+ continue;
1156
+ const end = n.endLine ?? n.startLine;
1157
+ if (n.startLine <= ref.line && end >= ref.line && n.startLine >= start) {
1158
+ start = n.startLine;
1159
+ }
1160
+ }
1161
+ return start;
1162
+ }
1163
+ /**
1164
+ * Infer a receiver's type from its local declaration/initializer in the
1165
+ * enclosing function body. Language-dispatched; returns null for languages
1166
+ * without patterns or when no declaration is found. Bounded to the enclosing
1167
+ * scope so a same-named variable in another function can't leak in.
1168
+ */
1169
+ function inferLocalReceiverType(receiverName, ref, context) {
1170
+ // CFML scope prefixes: `variables.svc` / `this.svc` name a COMPONENT-scoped
1171
+ // field whose assignment or `property` declaration usually lives outside the
1172
+ // calling function (the init-pseudoconstructor / WireBox-injection pattern),
1173
+ // and `local.svc` is an explicit function-local. Strip the prefix so the
1174
+ // declaration patterns match (`variables.svc = new X()`, `property
1175
+ // name="svc" …`, `var svc = …` all bind the bare name), and widen the scan
1176
+ // to the whole file for the component-scoped forms — nearest-declaration-
1177
+ // backward still wins, so a function-local shadowing the field is preferred.
1178
+ let scanReceiver = receiverName;
1179
+ let componentScoped = false;
1180
+ if (ref.language === 'cfml' || ref.language === 'cfscript') {
1181
+ const scoped = receiverName.match(/^(variables|this|local|arguments)\.(.+)$/i);
1182
+ if (scoped) {
1183
+ scanReceiver = scoped[2];
1184
+ const scope = scoped[1].toLowerCase();
1185
+ componentScoped = scope === 'variables' || scope === 'this';
1186
+ }
1187
+ }
1188
+ const patterns = localReceiverTypePatterns(ref.language, scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1189
+ if (patterns.length === 0)
1190
+ return null;
1191
+ // Split through the context's per-file lines cache when available: this runs
1192
+ // for EVERY `receiver.method()` ref, and re-splitting the whole file per ref
1193
+ // was ~20% of total index CPU on Java-heavy repos (#1122).
1194
+ const lines = context.getFileLines
1195
+ ? context.getFileLines(ref.filePath)
1196
+ : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
1197
+ if (!lines || lines.length === 0)
1198
+ return null;
1199
+ const callIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
1200
+ const startIdx = componentScoped
1201
+ ? 0
1202
+ : Math.max(0, enclosingScopeStartLine(ref, context) - 1);
1203
+ const matchLine = (i) => {
1204
+ const line = lines[i];
1205
+ if (!line)
1206
+ return null;
1207
+ // A generated/minified line (one multi-KB statement) is not something a
1208
+ // human-written local declaration lives on, and regexing it per ref is
1209
+ // pure waste — skip it rather than scan it.
1210
+ if (line.length > 10_000)
1211
+ return null;
1212
+ for (const re of patterns) {
1213
+ const m = line.match(re);
1214
+ if (m && m[1]) {
1215
+ const type = normalizeInferredTypeName(m[1]);
1216
+ if (type)
1217
+ return type;
1218
+ }
1219
+ }
1220
+ return null;
1221
+ };
1222
+ // Nearest declaration wins: scan backward from the call to the scope start.
1223
+ for (let i = callIdx; i >= startIdx; i--) {
1224
+ const type = matchLine(i);
1225
+ if (type)
1226
+ return type;
1227
+ }
1228
+ // A component-scoped field's declaration is position-independent — the
1229
+ // `variables.svc = new X()` pseudoconstructor assignment or `property`
1230
+ // declaration may sit BELOW the calling function in the file — so when the
1231
+ // backward pass finds nothing, sweep the remainder of the file too.
1232
+ if (componentScoped) {
1233
+ for (let i = callIdx + 1; i < lines.length; i++) {
1234
+ const type = matchLine(i);
1235
+ if (type)
1236
+ return type;
1237
+ }
1238
+ }
1239
+ return null;
1240
+ }
883
1241
  /**
884
1242
  * Try to resolve by method name on a class/object
885
1243
  */
@@ -895,15 +1253,42 @@ function matchMethodCall(ref, context) {
895
1253
  // `Guard.Against.X()`) matched no pattern and never resolved.
896
1254
  const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
897
1255
  const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
898
- const match = dotMatch || colonMatch;
1256
+ // Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
1257
+ // Recognize these receiver/method separators so local-variable receiver-type
1258
+ // inference (#1108) applies to them too — extraction already emits the ref in
1259
+ // this shape, but the resolver otherwise only understood `.` and `::`.
1260
+ const luaColonMatch = (ref.language === 'lua' || ref.language === 'luau')
1261
+ ? ref.referenceName.match(/^([\w.]+):(\w+)$/)
1262
+ : null;
1263
+ const rDollarMatch = ref.language === 'r'
1264
+ ? ref.referenceName.match(/^([\w.]+)\$(\w+)$/)
1265
+ : null;
1266
+ const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
899
1267
  if (!match) {
900
1268
  return null;
901
1269
  }
902
1270
  const [, objectOrClass, methodName] = match;
903
- if (ref.language === 'cpp' && dotMatch) {
904
- const inferredType = inferCppReceiverType(objectOrClass, ref, context);
1271
+ // A simple `receiver.method` / `receiver:method` / `receiver$method` shape whose
1272
+ // receiver type we can try to infer from its local declaration.
1273
+ const inferableReceiver = dotMatch || luaColonMatch || rDollarMatch;
1274
+ // Infer the receiver's type from its local declaration/initializer in the
1275
+ // enclosing scope, then resolve the method on that type (#1108). C++ keeps its
1276
+ // dedicated inferrer (header scan + `auto`); every other language uses the
1277
+ // shared source-based inferrer. resolveMethodOnType validates the method
1278
+ // exists on the inferred type, so a mis-inference produces no edge.
1279
+ if (inferableReceiver) {
1280
+ const inferredType = ref.language === 'cpp'
1281
+ ? inferCppReceiverType(objectOrClass, ref, context)
1282
+ : inferLocalReceiverType(objectOrClass, ref, context);
905
1283
  if (inferredType) {
906
- const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method');
1284
+ // Java/Kotlin: when two classes share the simple name, the file's import
1285
+ // pins WHICH one (#314). Other languages disambiguate by call-site file.
1286
+ const importedFqn = ref.language === 'java' || ref.language === 'kotlin'
1287
+ ? context
1288
+ .getImportMappings(ref.filePath, ref.language)
1289
+ .find((i) => i.localName === inferredType)?.source
1290
+ : undefined;
1291
+ const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
907
1292
  if (typedMatch) {
908
1293
  return typedMatch;
909
1294
  }
@@ -928,8 +1313,12 @@ function matchMethodCall(ref, context) {
928
1313
  }
929
1314
  }
930
1315
  }
931
- // Strategy 1: Direct class name match (existing logic)
932
- const classCandidates = context.getNodesByName(objectOrClass);
1316
+ // Strategy 1: Direct class name match (existing logic). When the receiver
1317
+ // names a class that exists in several files (`Logger.log()` / `Logger::log()`
1318
+ // with a `Logger` in both `a/` and `b/`), try the class in the call site's
1319
+ // own file first — otherwise the first-indexed class wins and a call in `b/`
1320
+ // resolves to `a/`'s method (#1079).
1321
+ const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
933
1322
  for (const classNode of classCandidates) {
934
1323
  if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
935
1324
  // Skip cross-language class matches
@@ -953,7 +1342,7 @@ function matchMethodCall(ref, context) {
953
1342
  // e.g., "permissionEngine" → look for classes containing "PermissionEngine"
954
1343
  const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
955
1344
  if (capitalizedReceiver !== objectOrClass) {
956
- const fuzzyClassCandidates = context.getNodesByName(capitalizedReceiver);
1345
+ const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
957
1346
  for (const classNode of fuzzyClassCandidates) {
958
1347
  if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
959
1348
  // Skip cross-language class matches
@@ -1006,7 +1395,10 @@ function matchMethodCall(ref, context) {
1006
1395
  const receiverWords = splitCamelCase(objectOrClass);
1007
1396
  let bestMatch;
1008
1397
  let bestScore = 0;
1009
- for (const method of targetMethods) {
1398
+ // Same-file candidates first, so a score tie (`score > bestScore` keeps
1399
+ // the first seen) resolves to the call site's own file rather than the
1400
+ // first-indexed duplicate (#1079).
1401
+ for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
1010
1402
  const classWords = splitCamelCase(method.qualifiedName);
1011
1403
  let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
1012
1404
  // Bonus for same language
@@ -1185,6 +1577,8 @@ function matchFuzzy(ref, context) {
1185
1577
  /**
1186
1578
  * Match all strategies in order of confidence
1187
1579
  */
1580
+ /** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
1581
+ const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
1188
1582
  function matchReference(ref, context) {
1189
1583
  // Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
1190
1584
  // never the fuzzy/qualified fallthrough below (a wrong callback edge is
@@ -1192,6 +1586,57 @@ function matchReference(ref, context) {
1192
1586
  if (ref.referenceKind === 'function_ref') {
1193
1587
  return matchFunctionRef(ref, context);
1194
1588
  }
1589
+ // ArkTS chained UI attributes — emitted with a leading dot (`.titleStyle`,
1590
+ // `.width`) by the extractor — resolve ONLY to decorator-marked attribute
1591
+ // helpers: `@Extend`/`@Styles`/`@AnimatableExtend` functions (and global
1592
+ // `@Builder`s used attribute-position). Framework attributes (`.width`,
1593
+ // `.fontSize` — on nearly every UI line) match no such helper and stay
1594
+ // unresolved, NEVER falling through to bare-name matching: on a samples
1595
+ // monorepo that fallthrough manufactured 36k wrong edges, giving single
1596
+ // same-named properties thousands of false callers. Ambiguity rule matches
1597
+ // the rest of the file: several same-named helpers → prefer the call-site
1598
+ // file, still ambiguous → drop the ref rather than guess.
1599
+ if (ref.language === 'arkts' && ref.referenceName.startsWith('.')) {
1600
+ const base = ref.referenceName.slice(1);
1601
+ const candidates = context
1602
+ .getNodesByName(base)
1603
+ .filter((n) => n.language === 'arkts' &&
1604
+ n.kind === 'function' &&
1605
+ (n.decorators ?? []).some((d) => ARKUI_ATTRIBUTE_DECORATORS.has(d)));
1606
+ const chosen = candidates.length > 1 ? preferCallSiteFile(candidates, ref.filePath) : candidates;
1607
+ if (chosen.length !== 1)
1608
+ return null;
1609
+ return {
1610
+ original: ref,
1611
+ targetNodeId: chosen[0].id,
1612
+ confidence: 0.85,
1613
+ resolvedBy: 'exact-match',
1614
+ };
1615
+ }
1616
+ // Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to
1617
+ // bare-name matching grabs any same-named symbol — on emqx,
1618
+ // `-behaviour(supervisor)` resolved to a `-define(supervisor, …)` macro
1619
+ // constant in an unrelated app. Resolve only to the behaviour module's
1620
+ // namespace; an out-of-repo behaviour (OTP's gen_server/supervisor) stays
1621
+ // unresolved rather than guessed. The same module-only rule applies to every
1622
+ // ref an `.app`/`.app.src` resource file emits — its `{mod, …}` callback and
1623
+ // `{applications, …}` dependency names can only mean modules, and on emqx
1624
+ // the `ssl` OTP app otherwise resolved to a test helper FUNCTION named ssl.
1625
+ if (ref.language === 'erlang' &&
1626
+ (ref.referenceKind === 'implements' || /\.app(?:\.src)?$/i.test(ref.filePath))) {
1627
+ const modules = context
1628
+ .getNodesByName(ref.referenceName)
1629
+ .filter((n) => n.language === 'erlang' && n.kind === 'namespace');
1630
+ const chosen = preferCallSiteFile(modules, ref.filePath)[0];
1631
+ if (!chosen)
1632
+ return null;
1633
+ return {
1634
+ original: ref,
1635
+ targetNodeId: chosen.id,
1636
+ confidence: 0.9,
1637
+ resolvedBy: 'exact-match',
1638
+ };
1639
+ }
1195
1640
  // Try strategies in order of confidence
1196
1641
  let result;
1197
1642
  // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)