homegraph 1.2.0 → 1.4.1

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 (284) hide show
  1. package/dist/bin/homegraph.js +3 -0
  2. package/dist/bin/homegraph.js.map +1 -1
  3. package/dist/context/index.d.ts.map +1 -1
  4. package/dist/context/index.js +85 -0
  5. package/dist/context/index.js.map +1 -1
  6. package/dist/db/index.d.ts +64 -5
  7. package/dist/db/index.d.ts.map +1 -1
  8. package/dist/db/index.js +178 -14
  9. package/dist/db/index.js.map +1 -1
  10. package/dist/db/migrations.d.ts +1 -1
  11. package/dist/db/migrations.d.ts.map +1 -1
  12. package/dist/db/migrations.js +19 -1
  13. package/dist/db/migrations.js.map +1 -1
  14. package/dist/db/queries.d.ts +117 -2
  15. package/dist/db/queries.d.ts.map +1 -1
  16. package/dist/db/queries.js +299 -6
  17. package/dist/db/queries.js.map +1 -1
  18. package/dist/db/schema.sql +32 -1
  19. package/dist/db/wal-valve.d.ts +106 -0
  20. package/dist/db/wal-valve.d.ts.map +1 -0
  21. package/dist/db/wal-valve.js +208 -0
  22. package/dist/db/wal-valve.js.map +1 -0
  23. package/dist/directory.d.ts +9 -5
  24. package/dist/directory.d.ts.map +1 -1
  25. package/dist/directory.js +210 -19
  26. package/dist/directory.js.map +1 -1
  27. package/dist/extraction/cfml-extractor.d.ts +107 -0
  28. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  29. package/dist/extraction/cfml-extractor.js +494 -0
  30. package/dist/extraction/cfml-extractor.js.map +1 -0
  31. package/dist/extraction/grammars.d.ts +25 -1
  32. package/dist/extraction/grammars.d.ts.map +1 -1
  33. package/dist/extraction/grammars.js +177 -24
  34. package/dist/extraction/grammars.js.map +1 -1
  35. package/dist/extraction/index.d.ts +43 -4
  36. package/dist/extraction/index.d.ts.map +1 -1
  37. package/dist/extraction/index.js +308 -5
  38. package/dist/extraction/index.js.map +1 -1
  39. package/dist/extraction/languages/c-cpp.d.ts +42 -0
  40. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  41. package/dist/extraction/languages/c-cpp.js +369 -4
  42. package/dist/extraction/languages/c-cpp.js.map +1 -1
  43. package/dist/extraction/languages/cfquery.d.ts +12 -0
  44. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  45. package/dist/extraction/languages/cfquery.js +28 -0
  46. package/dist/extraction/languages/cfquery.js.map +1 -0
  47. package/dist/extraction/languages/cfscript.d.ts +3 -0
  48. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  49. package/dist/extraction/languages/cfscript.js +73 -0
  50. package/dist/extraction/languages/cfscript.js.map +1 -0
  51. package/dist/extraction/languages/cobol.d.ts +33 -0
  52. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  53. package/dist/extraction/languages/cobol.js +499 -0
  54. package/dist/extraction/languages/cobol.js.map +1 -0
  55. package/dist/extraction/languages/erlang.d.ts +3 -0
  56. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  57. package/dist/extraction/languages/erlang.js +350 -0
  58. package/dist/extraction/languages/erlang.js.map +1 -0
  59. package/dist/extraction/languages/index.d.ts.map +1 -1
  60. package/dist/extraction/languages/index.js +16 -0
  61. package/dist/extraction/languages/index.js.map +1 -1
  62. package/dist/extraction/languages/nix.d.ts +3 -0
  63. package/dist/extraction/languages/nix.d.ts.map +1 -0
  64. package/dist/extraction/languages/nix.js +294 -0
  65. package/dist/extraction/languages/nix.js.map +1 -0
  66. package/dist/extraction/languages/solidity.d.ts +3 -0
  67. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  68. package/dist/extraction/languages/solidity.js +293 -0
  69. package/dist/extraction/languages/solidity.js.map +1 -0
  70. package/dist/extraction/languages/terraform.d.ts +3 -0
  71. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  72. package/dist/extraction/languages/terraform.js +641 -0
  73. package/dist/extraction/languages/terraform.js.map +1 -0
  74. package/dist/extraction/languages/vbnet.d.ts +11 -0
  75. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  76. package/dist/extraction/languages/vbnet.js +141 -0
  77. package/dist/extraction/languages/vbnet.js.map +1 -0
  78. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  79. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  80. package/dist/extraction/mybatis-extractor.js +140 -38
  81. package/dist/extraction/mybatis-extractor.js.map +1 -1
  82. package/dist/extraction/parse-pool.d.ts +27 -1
  83. package/dist/extraction/parse-pool.d.ts.map +1 -1
  84. package/dist/extraction/parse-pool.js +75 -7
  85. package/dist/extraction/parse-pool.js.map +1 -1
  86. package/dist/extraction/parse-worker.js +9 -2
  87. package/dist/extraction/parse-worker.js.map +1 -1
  88. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  89. package/dist/extraction/tree-sitter-helpers.js +1 -0
  90. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  91. package/dist/extraction/tree-sitter-types.d.ts +3 -1
  92. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  93. package/dist/extraction/tree-sitter.d.ts +38 -0
  94. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  95. package/dist/extraction/tree-sitter.js +754 -10
  96. package/dist/extraction/tree-sitter.js.map +1 -1
  97. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  98. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  99. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  100. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  101. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  102. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  103. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  104. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  105. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  106. package/dist/graph/traversal.d.ts.map +1 -1
  107. package/dist/graph/traversal.js +1 -1
  108. package/dist/graph/traversal.js.map +1 -1
  109. package/dist/index.d.ts +60 -2
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +348 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/installer/index.d.ts +42 -0
  114. package/dist/installer/index.d.ts.map +1 -1
  115. package/dist/installer/index.js +97 -2
  116. package/dist/installer/index.js.map +1 -1
  117. package/dist/mcp/daemon.d.ts +25 -3
  118. package/dist/mcp/daemon.d.ts.map +1 -1
  119. package/dist/mcp/daemon.js +62 -8
  120. package/dist/mcp/daemon.js.map +1 -1
  121. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  122. package/dist/mcp/dynamic-boundaries.js +2 -1
  123. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  124. package/dist/mcp/early-ppid.d.ts +26 -0
  125. package/dist/mcp/early-ppid.d.ts.map +1 -0
  126. package/dist/mcp/early-ppid.js +29 -0
  127. package/dist/mcp/early-ppid.js.map +1 -0
  128. package/dist/mcp/engine.d.ts +4 -5
  129. package/dist/mcp/engine.d.ts.map +1 -1
  130. package/dist/mcp/engine.js +27 -1
  131. package/dist/mcp/engine.js.map +1 -1
  132. package/dist/mcp/index.d.ts.map +1 -1
  133. package/dist/mcp/index.js +41 -6
  134. package/dist/mcp/index.js.map +1 -1
  135. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  136. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  137. package/dist/mcp/liveness-watchdog.js +73 -13
  138. package/dist/mcp/liveness-watchdog.js.map +1 -1
  139. package/dist/mcp/memory-budget.d.ts +30 -0
  140. package/dist/mcp/memory-budget.d.ts.map +1 -0
  141. package/dist/mcp/memory-budget.js +123 -0
  142. package/dist/mcp/memory-budget.js.map +1 -0
  143. package/dist/mcp/proxy.d.ts.map +1 -1
  144. package/dist/mcp/proxy.js +88 -4
  145. package/dist/mcp/proxy.js.map +1 -1
  146. package/dist/mcp/query-cache.d.ts +35 -4
  147. package/dist/mcp/query-cache.d.ts.map +1 -1
  148. package/dist/mcp/query-cache.js +89 -19
  149. package/dist/mcp/query-cache.js.map +1 -1
  150. package/dist/mcp/query-pool.d.ts +46 -12
  151. package/dist/mcp/query-pool.d.ts.map +1 -1
  152. package/dist/mcp/query-pool.js +100 -23
  153. package/dist/mcp/query-pool.js.map +1 -1
  154. package/dist/mcp/server-instructions.d.ts +4 -27
  155. package/dist/mcp/server-instructions.d.ts.map +1 -1
  156. package/dist/mcp/server-instructions.js +39 -84
  157. package/dist/mcp/server-instructions.js.map +1 -1
  158. package/dist/mcp/session.d.ts +14 -0
  159. package/dist/mcp/session.d.ts.map +1 -1
  160. package/dist/mcp/session.js +28 -1
  161. package/dist/mcp/session.js.map +1 -1
  162. package/dist/mcp/startup-handshake.d.ts +44 -0
  163. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  164. package/dist/mcp/startup-handshake.js +73 -0
  165. package/dist/mcp/startup-handshake.js.map +1 -0
  166. package/dist/mcp/tools.d.ts +104 -4
  167. package/dist/mcp/tools.d.ts.map +1 -1
  168. package/dist/mcp/tools.js +2390 -191
  169. package/dist/mcp/tools.js.map +1 -1
  170. package/dist/mcp/transport.d.ts.map +1 -1
  171. package/dist/mcp/transport.js +18 -0
  172. package/dist/mcp/transport.js.map +1 -1
  173. package/dist/project-config.d.ts +38 -0
  174. package/dist/project-config.d.ts.map +1 -1
  175. package/dist/project-config.js +101 -2
  176. package/dist/project-config.js.map +1 -1
  177. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  178. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  179. package/dist/resolution/c-fnptr-synthesizer.js +178 -146
  180. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  181. package/dist/resolution/callback-synthesizer.d.ts +9 -1
  182. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  183. package/dist/resolution/callback-synthesizer.js +1164 -176
  184. package/dist/resolution/callback-synthesizer.js.map +1 -1
  185. package/dist/resolution/frameworks/cics.d.ts +20 -0
  186. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  187. package/dist/resolution/frameworks/cics.js +90 -0
  188. package/dist/resolution/frameworks/cics.js.map +1 -0
  189. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  190. package/dist/resolution/frameworks/index.js +3 -0
  191. package/dist/resolution/frameworks/index.js.map +1 -1
  192. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  193. package/dist/resolution/frameworks/java.js +14 -6
  194. package/dist/resolution/frameworks/java.js.map +1 -1
  195. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  196. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  197. package/dist/resolution/frameworks/terraform.js +277 -0
  198. package/dist/resolution/frameworks/terraform.js.map +1 -0
  199. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  200. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  201. package/dist/resolution/goframe-synthesizer.js +8 -3
  202. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  203. package/dist/resolution/import-resolver.d.ts +7 -0
  204. package/dist/resolution/import-resolver.d.ts.map +1 -1
  205. package/dist/resolution/import-resolver.js +129 -4
  206. package/dist/resolution/import-resolver.js.map +1 -1
  207. package/dist/resolution/index.d.ts +60 -7
  208. package/dist/resolution/index.d.ts.map +1 -1
  209. package/dist/resolution/index.js +430 -80
  210. package/dist/resolution/index.js.map +1 -1
  211. package/dist/resolution/name-matcher.d.ts +0 -3
  212. package/dist/resolution/name-matcher.d.ts.map +1 -1
  213. package/dist/resolution/name-matcher.js +354 -33
  214. package/dist/resolution/name-matcher.js.map +1 -1
  215. package/dist/resolution/strip-comments.d.ts +1 -1
  216. package/dist/resolution/strip-comments.d.ts.map +1 -1
  217. package/dist/resolution/strip-comments.js +49 -0
  218. package/dist/resolution/strip-comments.js.map +1 -1
  219. package/dist/resolution/types.d.ts +29 -0
  220. package/dist/resolution/types.d.ts.map +1 -1
  221. package/dist/resolution/workspace-packages.d.ts +10 -0
  222. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  223. package/dist/resolution/workspace-packages.js +142 -4
  224. package/dist/resolution/workspace-packages.js.map +1 -1
  225. package/dist/search/identifier-segments.d.ts +60 -0
  226. package/dist/search/identifier-segments.d.ts.map +1 -0
  227. package/dist/search/identifier-segments.js +176 -0
  228. package/dist/search/identifier-segments.js.map +1 -0
  229. package/dist/search/query-utils.d.ts +201 -0
  230. package/dist/search/query-utils.d.ts.map +1 -1
  231. package/dist/search/query-utils.js +919 -2
  232. package/dist/search/query-utils.js.map +1 -1
  233. package/dist/sync/git-hooks.d.ts.map +1 -1
  234. package/dist/sync/git-hooks.js +2 -0
  235. package/dist/sync/git-hooks.js.map +1 -1
  236. package/dist/sync/watcher.d.ts +10 -5
  237. package/dist/sync/watcher.d.ts.map +1 -1
  238. package/dist/sync/watcher.js +51 -14
  239. package/dist/sync/watcher.js.map +1 -1
  240. package/dist/sync/worktree.d.ts.map +1 -1
  241. package/dist/sync/worktree.js +5 -0
  242. package/dist/sync/worktree.js.map +1 -1
  243. package/dist/types.d.ts +19 -1
  244. package/dist/types.d.ts.map +1 -1
  245. package/dist/types.js +10 -0
  246. package/dist/types.js.map +1 -1
  247. package/dist/upgrade/index.d.ts +32 -0
  248. package/dist/upgrade/index.d.ts.map +1 -1
  249. package/dist/upgrade/index.js +162 -14
  250. package/dist/upgrade/index.js.map +1 -1
  251. package/dist/upgrade/remove-binary.d.ts +87 -0
  252. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  253. package/dist/upgrade/remove-binary.js +289 -0
  254. package/dist/upgrade/remove-binary.js.map +1 -0
  255. package/dist/upgrade/update-check.d.ts +92 -0
  256. package/dist/upgrade/update-check.d.ts.map +1 -0
  257. package/dist/upgrade/update-check.js +258 -0
  258. package/dist/upgrade/update-check.js.map +1 -0
  259. package/package.json +2 -2
  260. package/scripts/agent-eval/run-all.sh +6 -0
  261. package/scripts/build-bundle.sh +5 -0
  262. package/scripts/npm-shim.js +8 -1
  263. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  264. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  265. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  266. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  267. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  268. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  269. package/scripts/exp_boundary_eval/data/agents.json +0 -109
  270. package/scripts/exp_boundary_eval/data/experiments.json +0 -140
  271. package/scripts/qa_eval/README.md +0 -407
  272. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  273. package/scripts/qa_eval/agent_runner.py +0 -526
  274. package/scripts/qa_eval/data/.gitignore +0 -4
  275. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  276. package/scripts/qa_eval/eval_metrics.py +0 -274
  277. package/scripts/qa_eval/external_agent.py +0 -976
  278. package/scripts/qa_eval/llm_config.py +0 -92
  279. package/scripts/qa_eval/memory_monitor.py +0 -132
  280. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  281. package/scripts/qa_eval/requirements.txt +0 -2
  282. package/scripts/qa_eval/run_pipeline.py +0 -804
  283. package/scripts/qa_eval/stats_efficiency.py +0 -279
  284. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -140,7 +140,9 @@ function pickClosestFileNode(candidates, ref) {
140
140
  const LANGUAGE_FAMILY = {
141
141
  java: 'jvm', kotlin: 'jvm', scala: 'jvm',
142
142
  swift: 'apple', objc: 'apple',
143
- 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',
144
146
  c: 'c', cpp: 'c',
145
147
  // Razor/Blazor markup names C# types — same family so `@model Foo` /
146
148
  // `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
@@ -222,6 +224,7 @@ function matchFunctionRef(ref, context) {
222
224
  // implicit-self, Java/Kotlin method references.
223
225
  const bareFnOnly = ref.language === 'typescript' || ref.language === 'tsx' ||
224
226
  ref.language === 'javascript' || ref.language === 'jsx' ||
227
+ ref.language === 'arkts' ||
225
228
  ref.language === 'cpp' || ref.language === 'python' ||
226
229
  ref.language === 'php';
227
230
  // Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
@@ -384,7 +387,17 @@ function matchByQualifiedName(ref, context) {
384
387
  if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
385
388
  return null;
386
389
  }
387
- 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));
388
401
  if (candidates.length === 1) {
389
402
  return {
390
403
  original: ref,
@@ -413,8 +426,7 @@ function matchByQualifiedName(ref, context) {
413
426
  const parts = ref.referenceName.split(/[:.]/);
414
427
  const lastName = parts[parts.length - 1];
415
428
  if (lastName) {
416
- const partialCandidates = context
417
- .getNodesByName(lastName)
429
+ const partialCandidates = keepForRef(context.getNodesByName(lastName))
418
430
  .filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
419
431
  const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
420
432
  if (chosen) {
@@ -468,17 +480,28 @@ depth = 0) {
468
480
  // in-class (`class Foo { int bar() { ... } }`) or out-of-line in a separate
469
481
  // file (`int Foo::bar() { ... }` in foo.cpp while class Foo is in foo.hpp).
470
482
  // The previous same-file approach missed the latter — the typical C++ layout.
471
- const methodCandidates = context.getNodesByName(methodName);
472
- const want = `${typeName}::${methodName}`;
473
- const matches = [];
474
- for (const m of methodCandidates) {
475
- if (m.kind !== 'method')
476
- continue;
477
- if (m.language !== ref.language)
478
- continue;
479
- const qn = m.qualifiedName;
480
- if (qn === want || qn.endsWith(`::${want}`)) {
481
- 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
+ }
482
505
  }
483
506
  }
484
507
  if (matches.length === 0) {
@@ -565,10 +588,14 @@ function buildDeclaratorRegex(escapedReceiver) {
565
588
  return new RegExp(`([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`);
566
589
  }
567
590
  function inferCppReceiverType(receiverName, ref, context, depth = 0) {
568
- const source = context.readFile(ref.filePath);
569
- 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)
570
598
  return null;
571
- const lines = source.split(/\r?\n/);
572
599
  const callLineIndex = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
573
600
  const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
574
601
  const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
@@ -601,10 +628,12 @@ function inferCppReceiverType(receiverName, ref, context, depth = 0) {
601
628
  for (const headerPath of headerCandidates) {
602
629
  if (!context.fileExists(headerPath))
603
630
  continue;
604
- const headerSource = context.readFile(headerPath);
605
- if (!headerSource)
631
+ const headerLines = context.getFileLines
632
+ ? context.getFileLines(headerPath)
633
+ : (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
634
+ if (!headerLines)
606
635
  continue;
607
- for (const line of headerSource.split(/\r?\n/)) {
636
+ for (const line of headerLines) {
608
637
  if (!receiverPattern.test(line))
609
638
  continue;
610
639
  const declaratorMatch = line.match(declaratorRegex);
@@ -975,9 +1004,17 @@ function localReceiverTypePatterns(language, r) {
975
1004
  case 'javascript':
976
1005
  case 'tsx':
977
1006
  case 'jsx':
1007
+ case 'arkts':
978
1008
  return [
979
1009
  new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
980
- new RegExp(`\\b(?:const|let|var)\\s+${r}\\s*:\\s*([A-Z][\\w.$]*)`), // lg: 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)
981
1018
  ];
982
1019
  case 'python':
983
1020
  return [
@@ -1007,12 +1044,21 @@ function localReceiverTypePatterns(language, r) {
1007
1044
  case 'rust':
1008
1045
  return [
1009
1046
  new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger
1010
- new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg: 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)
1011
1051
  ];
1012
1052
  case 'go':
1013
1053
  return [
1014
1054
  new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
1015
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)
1016
1062
  ];
1017
1063
  case 'ruby':
1018
1064
  return [
@@ -1026,18 +1072,35 @@ function localReceiverTypePatterns(language, r) {
1026
1072
  case 'dart':
1027
1073
  return [
1028
1074
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...)
1029
- new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;]`), // Logger lg = ...
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
1030
1079
  ];
1031
1080
  case 'php':
1032
1081
  return [
1033
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)
1034
1087
  ];
1035
1088
  case 'lua':
1036
1089
  case 'luau':
1037
1090
  return [
1038
1091
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\.new\\b`), // local lg = Logger.new()
1039
1092
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\s*\\(`), // local lg = Logger(...) (callable table)
1040
- new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // Luau: local lg: Logger / typed param
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
1041
1104
  ];
1042
1105
  case 'r':
1043
1106
  return [
@@ -1048,6 +1111,36 @@ function localReceiverTypePatterns(language, r) {
1048
1111
  new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w]*)`), // var lg: TLogger / param lg: TLogger
1049
1112
  new RegExp(`\\b${r}\\b\\s*:=\\s*([A-Z][\\w.]*)\\.Create\\b`), // lg := TLogger.Create
1050
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
+ ];
1051
1144
  default:
1052
1145
  return [];
1053
1146
  }
@@ -1074,20 +1167,70 @@ function enclosingScopeStartLine(ref, context) {
1074
1167
  * scope so a same-named variable in another function can't leak in.
1075
1168
  */
1076
1169
  function inferLocalReceiverType(receiverName, ref, context) {
1077
- const patterns = localReceiverTypePatterns(ref.language, receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
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
+ // PHP `$this->prop` receiver — the property's declaration lives outside the
1189
+ // calling method (a promoted constructor parameter `private readonly Foo $prop`,
1190
+ // a typed property `private Foo $prop;`, or a classic constructor parameter
1191
+ // `Foo $prop` assigned in __construct). Strip the prefix and widen the scan to
1192
+ // the whole file (the constructor may sit below the calling method), but —
1193
+ // unlike CFML's scopes above — switch to PROPERTY-shaped patterns: a plain
1194
+ // `$prop` local or parameter lives in a different namespace than `$this->prop`
1195
+ // and can never shadow it, so the generic local patterns would type the
1196
+ // property from unrelated same-named variables in other methods (a wrong
1197
+ // 0.9-confidence edge, not a missing one).
1198
+ let phpProperty = false;
1199
+ if (ref.language === 'php') {
1200
+ const scoped = receiverName.match(/^this->(.+)$/);
1201
+ if (scoped) {
1202
+ scanReceiver = scoped[1];
1203
+ componentScoped = true;
1204
+ phpProperty = true;
1205
+ }
1206
+ }
1207
+ const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1208
+ const patterns = phpProperty
1209
+ ? phpPropertyTypePatterns(escapedReceiver)
1210
+ : localReceiverTypePatterns(ref.language, escapedReceiver);
1078
1211
  if (patterns.length === 0)
1079
1212
  return null;
1080
- const source = context.readFile(ref.filePath);
1081
- if (!source)
1213
+ // Split through the context's per-file lines cache when available: this runs
1214
+ // for EVERY `receiver.method()` ref, and re-splitting the whole file per ref
1215
+ // was ~20% of total index CPU on Java-heavy repos (#1122).
1216
+ const lines = context.getFileLines
1217
+ ? context.getFileLines(ref.filePath)
1218
+ : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
1219
+ if (!lines || lines.length === 0)
1082
1220
  return null;
1083
- const lines = source.split(/\r?\n/);
1084
1221
  const callIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
1085
- const startIdx = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
1086
- // Nearest declaration wins: scan backward from the call to the scope start.
1087
- for (let i = callIdx; i >= startIdx; i--) {
1222
+ const startIdx = componentScoped
1223
+ ? 0
1224
+ : Math.max(0, enclosingScopeStartLine(ref, context) - 1);
1225
+ const matchLine = (i) => {
1088
1226
  const line = lines[i];
1089
1227
  if (!line)
1090
- continue;
1228
+ return null;
1229
+ // A generated/minified line (one multi-KB statement) is not something a
1230
+ // human-written local declaration lives on, and regexing it per ref is
1231
+ // pure waste — skip it rather than scan it.
1232
+ if (line.length > 10_000)
1233
+ return null;
1091
1234
  for (const re of patterns) {
1092
1235
  const m = line.match(re);
1093
1236
  if (m && m[1]) {
@@ -1096,6 +1239,113 @@ function inferLocalReceiverType(receiverName, ref, context) {
1096
1239
  return type;
1097
1240
  }
1098
1241
  }
1242
+ return null;
1243
+ };
1244
+ // Nearest declaration wins: scan backward from the call to the scope start.
1245
+ for (let i = callIdx; i >= startIdx; i--) {
1246
+ const type = matchLine(i);
1247
+ if (type)
1248
+ return type;
1249
+ }
1250
+ // A component-scoped field's declaration is position-independent — the
1251
+ // `variables.svc = new X()` pseudoconstructor assignment or `property`
1252
+ // declaration may sit BELOW the calling function in the file — so when the
1253
+ // backward pass finds nothing, sweep the remainder of the file too.
1254
+ if (componentScoped) {
1255
+ for (let i = callIdx + 1; i < lines.length; i++) {
1256
+ const type = matchLine(i);
1257
+ if (type)
1258
+ return type;
1259
+ }
1260
+ }
1261
+ // A PHP property with no statically-typed declaration (classic pre-7.4
1262
+ // style) may still be typed by what gets ASSIGNED to it — follow the
1263
+ // `$this->prop = $var` assignment to the assigned variable's own typed
1264
+ // declaration (a classic or multi-line constructor parameter, or a typed
1265
+ // setter's parameter).
1266
+ if (phpProperty) {
1267
+ return inferPhpAssignedPropertyType(escapedReceiver, lines, callIdx);
1268
+ }
1269
+ return null;
1270
+ }
1271
+ /**
1272
+ * Patterns that recover a PHP class property's declared type for a
1273
+ * `$this->prop` receiver. Deliberately NOT localReceiverTypePatterns: only
1274
+ * property-shaped declarations qualify —
1275
+ * 1. a modifier-prefixed typed declaration, which covers both a typed
1276
+ * property (`private ?Foo $prop;`) and a promoted constructor parameter
1277
+ * (`private readonly Foo $prop`), and
1278
+ * 2. the pseudoconstructor assignment (`$this->prop = new Foo(...)`).
1279
+ * A bare `X $prop` parameter or `$prop = new X()` local elsewhere in the
1280
+ * file must NOT match: those variables can never alias `$this->prop`.
1281
+ * Union-typed properties (`Foo|Bar $prop`) yield no match and thus no edge —
1282
+ * silent beats wrong. The classic untyped-property-assigned-in-constructor
1283
+ * shape is handled by inferPhpAssignedPropertyType instead.
1284
+ */
1285
+ function phpPropertyTypePatterns(r) {
1286
+ return [
1287
+ new RegExp(`\\b(?:(?:private|protected|public|readonly|static|final)(?:\\(set\\))?\\s+)+\\??([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // private readonly ?Foo $prop (typed property / promoted param)
1288
+ new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
1289
+ ];
1290
+ }
1291
+ /**
1292
+ * Second-chance typing for a PHP `$this->prop` receiver whose property
1293
+ * declaration carries no static type (classic pre-7.4 style): find the
1294
+ * `$this->prop = $var` assignment, then recover `$var`'s type from its own
1295
+ * declaration WITHIN the assignment's function — the constructor's (possibly
1296
+ * multi-line) parameter list, a typed setter's parameter, or a `= new X()`
1297
+ * local. The backward scan stops at the enclosing `function` line (checked
1298
+ * for a match first — a single-line `__construct(Foo $var) { ... }` carries
1299
+ * the typed parameter itself), so a same-named variable in another method
1300
+ * can never type the property.
1301
+ */
1302
+ function inferPhpAssignedPropertyType(escapedProp, lines, callIdx) {
1303
+ const assignRe = new RegExp(`\\$this->${escapedProp}\\b\\s*=\\s*\\$(\\w+)\\b`);
1304
+ const assignAt = (i) => {
1305
+ const line = lines[i];
1306
+ if (!line || line.length > 10_000)
1307
+ return null;
1308
+ return line.match(assignRe);
1309
+ };
1310
+ // The assignment is position-independent relative to the call — nearest-
1311
+ // backward first, then sweep forward, same order as the componentScoped scan.
1312
+ let assignIdx = -1;
1313
+ let varName = null;
1314
+ for (let i = callIdx; i >= 0; i--) {
1315
+ const m = assignAt(i);
1316
+ if (m) {
1317
+ assignIdx = i;
1318
+ varName = m[1];
1319
+ break;
1320
+ }
1321
+ }
1322
+ if (varName === null) {
1323
+ for (let i = callIdx + 1; i < lines.length; i++) {
1324
+ const m = assignAt(i);
1325
+ if (m) {
1326
+ assignIdx = i;
1327
+ varName = m[1];
1328
+ break;
1329
+ }
1330
+ }
1331
+ }
1332
+ if (varName === null)
1333
+ return null;
1334
+ const varPatterns = localReceiverTypePatterns('php', varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1335
+ for (let i = assignIdx; i >= 0; i--) {
1336
+ const line = lines[i];
1337
+ if (line && line.length <= 10_000) {
1338
+ for (const re of varPatterns) {
1339
+ const m = line.match(re);
1340
+ if (m && m[1]) {
1341
+ const type = normalizeInferredTypeName(m[1]);
1342
+ if (type)
1343
+ return type;
1344
+ }
1345
+ }
1346
+ }
1347
+ if (line && /\bfunction\b/.test(line))
1348
+ break;
1099
1349
  }
1100
1350
  return null;
1101
1351
  }
@@ -1124,6 +1374,24 @@ function matchMethodCall(ref, context) {
1124
1374
  const rDollarMatch = ref.language === 'r'
1125
1375
  ? ref.referenceName.match(/^([\w.]+)\$(\w+)$/)
1126
1376
  : null;
1377
+ // PHP property receiver: `$this->prop->method()` reaches the resolver as
1378
+ // `this->prop.method` (the extractor records the receiver's raw text with the
1379
+ // leading `$` stripped). Resolve it EXCLUSIVELY through declared-type
1380
+ // inference + resolveMethodOnType validation — the name-similarity strategies
1381
+ // below must never see this shape, so a property whose type can't be
1382
+ // recovered stays unlinked rather than guessed (a wrong inference produces no
1383
+ // edge rather than a wrong one). Deeper chains (`this->a->b.method`) don't
1384
+ // match the single-property pattern and stay unlinked, same as before.
1385
+ const phpThisPropMatch = ref.language === 'php'
1386
+ ? ref.referenceName.match(/^(this->\w+)\.(\w+)$/)
1387
+ : null;
1388
+ if (phpThisPropMatch) {
1389
+ const [, receiver, phpMethodName] = phpThisPropMatch;
1390
+ const inferredType = inferLocalReceiverType(receiver, ref, context);
1391
+ if (!inferredType)
1392
+ return null;
1393
+ return resolveMethodOnType(inferredType, phpMethodName, ref, context, 0.9, 'instance-method', importedFqnOf(inferredType, ref, context));
1394
+ }
1127
1395
  const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
1128
1396
  if (!match) {
1129
1397
  return null;
@@ -1438,6 +1706,8 @@ function matchFuzzy(ref, context) {
1438
1706
  /**
1439
1707
  * Match all strategies in order of confidence
1440
1708
  */
1709
+ /** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
1710
+ const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
1441
1711
  function matchReference(ref, context) {
1442
1712
  // Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
1443
1713
  // never the fuzzy/qualified fallthrough below (a wrong callback edge is
@@ -1445,6 +1715,57 @@ function matchReference(ref, context) {
1445
1715
  if (ref.referenceKind === 'function_ref') {
1446
1716
  return matchFunctionRef(ref, context);
1447
1717
  }
1718
+ // ArkTS chained UI attributes — emitted with a leading dot (`.titleStyle`,
1719
+ // `.width`) by the extractor — resolve ONLY to decorator-marked attribute
1720
+ // helpers: `@Extend`/`@Styles`/`@AnimatableExtend` functions (and global
1721
+ // `@Builder`s used attribute-position). Framework attributes (`.width`,
1722
+ // `.fontSize` — on nearly every UI line) match no such helper and stay
1723
+ // unresolved, NEVER falling through to bare-name matching: on a samples
1724
+ // monorepo that fallthrough manufactured 36k wrong edges, giving single
1725
+ // same-named properties thousands of false callers. Ambiguity rule matches
1726
+ // the rest of the file: several same-named helpers → prefer the call-site
1727
+ // file, still ambiguous → drop the ref rather than guess.
1728
+ if (ref.language === 'arkts' && ref.referenceName.startsWith('.')) {
1729
+ const base = ref.referenceName.slice(1);
1730
+ const candidates = context
1731
+ .getNodesByName(base)
1732
+ .filter((n) => n.language === 'arkts' &&
1733
+ n.kind === 'function' &&
1734
+ (n.decorators ?? []).some((d) => ARKUI_ATTRIBUTE_DECORATORS.has(d)));
1735
+ const chosen = candidates.length > 1 ? preferCallSiteFile(candidates, ref.filePath) : candidates;
1736
+ if (chosen.length !== 1)
1737
+ return null;
1738
+ return {
1739
+ original: ref,
1740
+ targetNodeId: chosen[0].id,
1741
+ confidence: 0.85,
1742
+ resolvedBy: 'exact-match',
1743
+ };
1744
+ }
1745
+ // Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to
1746
+ // bare-name matching grabs any same-named symbol — on emqx,
1747
+ // `-behaviour(supervisor)` resolved to a `-define(supervisor, …)` macro
1748
+ // constant in an unrelated app. Resolve only to the behaviour module's
1749
+ // namespace; an out-of-repo behaviour (OTP's gen_server/supervisor) stays
1750
+ // unresolved rather than guessed. The same module-only rule applies to every
1751
+ // ref an `.app`/`.app.src` resource file emits — its `{mod, …}` callback and
1752
+ // `{applications, …}` dependency names can only mean modules, and on emqx
1753
+ // the `ssl` OTP app otherwise resolved to a test helper FUNCTION named ssl.
1754
+ if (ref.language === 'erlang' &&
1755
+ (ref.referenceKind === 'implements' || /\.app(?:\.src)?$/i.test(ref.filePath))) {
1756
+ const modules = context
1757
+ .getNodesByName(ref.referenceName)
1758
+ .filter((n) => n.language === 'erlang' && n.kind === 'namespace');
1759
+ const chosen = preferCallSiteFile(modules, ref.filePath)[0];
1760
+ if (!chosen)
1761
+ return null;
1762
+ return {
1763
+ original: ref,
1764
+ targetNodeId: chosen.id,
1765
+ confidence: 0.9,
1766
+ resolvedBy: 'exact-match',
1767
+ };
1768
+ }
1448
1769
  // Try strategies in order of confidence
1449
1770
  let result;
1450
1771
  // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)