@vizalice/codegraph 1.5.0 → 1.6.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 (131) hide show
  1. package/README.md +25 -1
  2. package/dist/bin/codegraph.js +126 -24
  3. package/dist/bin/codegraph.js.map +1 -1
  4. package/dist/context/index.d.ts +12 -0
  5. package/dist/context/index.d.ts.map +1 -1
  6. package/dist/context/index.js +47 -8
  7. package/dist/context/index.js.map +1 -1
  8. package/dist/db/index.d.ts +2 -0
  9. package/dist/db/index.d.ts.map +1 -1
  10. package/dist/db/index.js +23 -0
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/db/queries.d.ts +39 -2
  13. package/dist/db/queries.d.ts.map +1 -1
  14. package/dist/db/queries.js +104 -19
  15. package/dist/db/queries.js.map +1 -1
  16. package/dist/directory.d.ts +29 -0
  17. package/dist/directory.d.ts.map +1 -1
  18. package/dist/directory.js +52 -0
  19. package/dist/directory.js.map +1 -1
  20. package/dist/extraction/grammars.d.ts.map +1 -1
  21. package/dist/extraction/grammars.js +38 -2
  22. package/dist/extraction/grammars.js.map +1 -1
  23. package/dist/extraction/index.d.ts +59 -0
  24. package/dist/extraction/index.d.ts.map +1 -1
  25. package/dist/extraction/index.js +175 -53
  26. package/dist/extraction/index.js.map +1 -1
  27. package/dist/extraction/languages/erlang.d.ts.map +1 -1
  28. package/dist/extraction/languages/erlang.js +58 -22
  29. package/dist/extraction/languages/erlang.js.map +1 -1
  30. package/dist/extraction/languages/rust.d.ts +20 -0
  31. package/dist/extraction/languages/rust.d.ts.map +1 -1
  32. package/dist/extraction/languages/rust.js +44 -20
  33. package/dist/extraction/languages/rust.js.map +1 -1
  34. package/dist/extraction/parse-pool.d.ts +7 -0
  35. package/dist/extraction/parse-pool.d.ts.map +1 -1
  36. package/dist/extraction/parse-pool.js +20 -1
  37. package/dist/extraction/parse-pool.js.map +1 -1
  38. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  39. package/dist/extraction/tree-sitter.js +66 -35
  40. package/dist/extraction/tree-sitter.js.map +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +68 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/installer/index.d.ts +2 -2
  45. package/dist/installer/index.d.ts.map +1 -1
  46. package/dist/installer/index.js +4 -3
  47. package/dist/installer/index.js.map +1 -1
  48. package/dist/installer/targets/codex.d.ts +22 -8
  49. package/dist/installer/targets/codex.d.ts.map +1 -1
  50. package/dist/installer/targets/codex.js +65 -48
  51. package/dist/installer/targets/codex.js.map +1 -1
  52. package/dist/installer/targets/types.d.ts +4 -4
  53. package/dist/mcp/daemon-manager.d.ts +1 -1
  54. package/dist/mcp/daemon-manager.d.ts.map +1 -1
  55. package/dist/mcp/daemon-manager.js +1 -1
  56. package/dist/mcp/daemon-manager.js.map +1 -1
  57. package/dist/mcp/daemon-paths.d.ts +6 -0
  58. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  59. package/dist/mcp/daemon-paths.js +54 -0
  60. package/dist/mcp/daemon-paths.js.map +1 -1
  61. package/dist/mcp/daemon-registry.d.ts +10 -0
  62. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  63. package/dist/mcp/daemon-registry.js +51 -3
  64. package/dist/mcp/daemon-registry.js.map +1 -1
  65. package/dist/mcp/daemon.d.ts +9 -6
  66. package/dist/mcp/daemon.d.ts.map +1 -1
  67. package/dist/mcp/daemon.js +10 -8
  68. package/dist/mcp/daemon.js.map +1 -1
  69. package/dist/mcp/engine.d.ts +3 -0
  70. package/dist/mcp/engine.d.ts.map +1 -1
  71. package/dist/mcp/engine.js +45 -3
  72. package/dist/mcp/engine.js.map +1 -1
  73. package/dist/mcp/explore-diagnostics.d.ts +2 -0
  74. package/dist/mcp/explore-diagnostics.d.ts.map +1 -1
  75. package/dist/mcp/explore-diagnostics.js +3 -0
  76. package/dist/mcp/explore-diagnostics.js.map +1 -1
  77. package/dist/mcp/index.d.ts.map +1 -1
  78. package/dist/mcp/index.js +17 -4
  79. package/dist/mcp/index.js.map +1 -1
  80. package/dist/mcp/session.d.ts.map +1 -1
  81. package/dist/mcp/session.js +17 -12
  82. package/dist/mcp/session.js.map +1 -1
  83. package/dist/mcp/tools.d.ts +17 -0
  84. package/dist/mcp/tools.d.ts.map +1 -1
  85. package/dist/mcp/tools.js +164 -25
  86. package/dist/mcp/tools.js.map +1 -1
  87. package/dist/project-config.d.ts +23 -0
  88. package/dist/project-config.d.ts.map +1 -1
  89. package/dist/project-config.js +39 -2
  90. package/dist/project-config.js.map +1 -1
  91. package/dist/resolution/c-fnptr-synthesizer.js +2 -2
  92. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  93. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  94. package/dist/resolution/callback-synthesizer.js +38 -10
  95. package/dist/resolution/callback-synthesizer.js.map +1 -1
  96. package/dist/resolution/frameworks/swift.d.ts.map +1 -1
  97. package/dist/resolution/frameworks/swift.js +11 -1
  98. package/dist/resolution/frameworks/swift.js.map +1 -1
  99. package/dist/resolution/import-resolver.d.ts.map +1 -1
  100. package/dist/resolution/import-resolver.js +16 -1
  101. package/dist/resolution/import-resolver.js.map +1 -1
  102. package/dist/resolution/index.d.ts.map +1 -1
  103. package/dist/resolution/index.js +5 -1
  104. package/dist/resolution/index.js.map +1 -1
  105. package/dist/resolution/name-matcher.d.ts +30 -0
  106. package/dist/resolution/name-matcher.d.ts.map +1 -1
  107. package/dist/resolution/name-matcher.js +278 -0
  108. package/dist/resolution/name-matcher.js.map +1 -1
  109. package/dist/resolution/path-aliases.d.ts +6 -4
  110. package/dist/resolution/path-aliases.d.ts.map +1 -1
  111. package/dist/resolution/path-aliases.js +125 -17
  112. package/dist/resolution/path-aliases.js.map +1 -1
  113. package/dist/search/identifier-segments.d.ts +10 -0
  114. package/dist/search/identifier-segments.d.ts.map +1 -1
  115. package/dist/search/identifier-segments.js +29 -0
  116. package/dist/search/identifier-segments.js.map +1 -1
  117. package/dist/search/query-paths.d.ts +57 -0
  118. package/dist/search/query-paths.d.ts.map +1 -0
  119. package/dist/search/query-paths.js +298 -0
  120. package/dist/search/query-paths.js.map +1 -0
  121. package/dist/search/query-utils.d.ts +1 -1
  122. package/dist/search/query-utils.d.ts.map +1 -1
  123. package/dist/search/query-utils.js +12 -3
  124. package/dist/search/query-utils.js.map +1 -1
  125. package/dist/sync/watcher.d.ts +22 -0
  126. package/dist/sync/watcher.d.ts.map +1 -1
  127. package/dist/sync/watcher.js +51 -5
  128. package/dist/sync/watcher.js.map +1 -1
  129. package/dist/types.d.ts +8 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/package.json +1 -1
@@ -13,6 +13,7 @@ exports.matchFunctionRef = matchFunctionRef;
13
13
  exports.matchByExactName = matchByExactName;
14
14
  exports.matchByQualifiedName = matchByQualifiedName;
15
15
  exports.preferCallSiteFile = preferCallSiteFile;
16
+ exports.resolveObjectLiteralMember = resolveObjectLiteralMember;
16
17
  exports.resolveMethodOnType = resolveMethodOnType;
17
18
  exports.matchCppCallChain = matchCppCallChain;
18
19
  exports.matchScopedCallChain = matchScopedCallChain;
@@ -21,6 +22,7 @@ exports.normalizeInferredTypeName = normalizeInferredTypeName;
21
22
  exports.clearNameMatcherMemos = clearNameMatcherMemos;
22
23
  exports.localReceiverTypePatterns = localReceiverTypePatterns;
23
24
  exports.matchMethodCall = matchMethodCall;
25
+ exports.rustFieldTypeName = rustFieldTypeName;
24
26
  exports.matchFuzzy = matchFuzzy;
25
27
  exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
26
28
  exports.matchReference = matchReference;
@@ -467,6 +469,32 @@ function matchByQualifiedName(ref, context) {
467
469
  };
468
470
  }
469
471
  }
472
+ // Erlang qualified refs (#1610): every erlang function's qualifiedName
473
+ // carries its arity (`mod::f/2`), and refs carry the call-site arity when it
474
+ // is statically known.
475
+ if (ref.language === 'erlang' && ref.referenceName.includes('::')) {
476
+ // A ref WITH arity that missed the exact lookup names an arity that isn't
477
+ // defined (or a module out of repo). Never fall through to the partial
478
+ // match — its "last segment" would be the arity digits — and never settle
479
+ // for a sibling arity: silent beats wrong.
480
+ if (/\/\d{1,3}$/.test(ref.referenceName))
481
+ return null;
482
+ // An arity-LESS qualified ref (dynamic MFA whose args list wasn't a
483
+ // static literal): resolve only when the module defines exactly ONE arity
484
+ // of that function; several arities with no signal is a guess.
485
+ const base = ref.referenceName.slice(ref.referenceName.lastIndexOf('::') + 2);
486
+ const prefix = `${ref.referenceName}/`;
487
+ const arityCands = keepForRef(context.getNodesByName(base)).filter((n) => n.qualifiedName.startsWith(prefix) && /^\d{1,3}$/.test(n.qualifiedName.slice(prefix.length)));
488
+ if (arityCands.length === 1) {
489
+ return {
490
+ original: ref,
491
+ targetNodeId: arityCands[0].id,
492
+ confidence: 0.85,
493
+ resolvedBy: 'qualified-name',
494
+ };
495
+ }
496
+ return null;
497
+ }
470
498
  // Try partial qualified name match — again preferring the call site's own
471
499
  // file when more than one symbol's qualifiedName ends with the reference.
472
500
  const parts = ref.referenceName.split(/[:.]/);
@@ -507,6 +535,82 @@ function preferCallSiteFile(nodes, callSiteFile) {
507
535
  }
508
536
  return same.length ? [...same, ...other] : nodes;
509
537
  }
538
+ /**
539
+ * Languages whose object literals declare callable members — `export const
540
+ * api = { call() {…}, get: () => {…} }` used as a namespace (#1573).
541
+ */
542
+ const OBJECT_LITERAL_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
543
+ /** True when `inner`'s source range lies within `outer`'s (lines, then columns on a shared line). */
544
+ function rangeWithin(inner, outer) {
545
+ const innerEnd = inner.endLine ?? inner.startLine;
546
+ const outerEnd = outer.endLine ?? outer.startLine;
547
+ if (inner.startLine < outer.startLine || innerEnd > outerEnd)
548
+ return false;
549
+ if (inner.startLine === outer.startLine && inner.startColumn < outer.startColumn)
550
+ return false;
551
+ if (innerEnd === outerEnd && inner.endColumn > outer.endColumn)
552
+ return false;
553
+ return true;
554
+ }
555
+ function sameRange(a, b) {
556
+ return (a.startLine === b.startLine &&
557
+ a.startColumn === b.startColumn &&
558
+ (a.endLine ?? a.startLine) === (b.endLine ?? b.startLine) &&
559
+ a.endColumn === b.endColumn);
560
+ }
561
+ /**
562
+ * Resolve `container.member` where `container` is a VALUE holding an object
563
+ * literal — `export const api = { call() {…}, get: () => {…} }` used as the
564
+ * module's namespace (#1573). The members are extracted as plain functions
565
+ * with BARE qualified names inside the constant's source extent (there is no
566
+ * `api::call`), so neither the `Container::member` lookup the class-shaped
567
+ * kinds use (#825) nor the declared-type inference for singleton instances
568
+ * (#1292) can reach them, and every such call resolved to nothing — or, via
569
+ * an import, to the constant itself. This looks the member up by CONTAINMENT:
570
+ * a node named `member` whose range lies inside the container's, in the
571
+ * container's own file. A helper declared inside a member's body is not a
572
+ * member and is skipped; nothing else in the file can donate a match. Calls
573
+ * take callable kinds only; other references accept value members too.
574
+ */
575
+ function resolveObjectLiteralMember(container, member, ref, context, confidence, resolvedBy) {
576
+ if (container.kind !== 'constant' && container.kind !== 'variable')
577
+ return null;
578
+ if (!OBJECT_LITERAL_LANGUAGES.has(container.language))
579
+ return null;
580
+ if (!sameLanguageFamily(container.language, ref.language))
581
+ return null;
582
+ const inFile = context.getNodesInFile(container.filePath);
583
+ const callable = (n) => n.kind === 'function' || n.kind === 'method';
584
+ const valueMember = (n) => callable(n) || n.kind === 'property' || n.kind === 'variable' || n.kind === 'constant';
585
+ const accepts = ref.referenceKind === 'calls' ? callable : valueMember;
586
+ const inside = inFile.filter((n) => n.id !== container.id && rangeWithin(n, container));
587
+ let candidates = inside.filter((n) => n.name === member && accepts(n));
588
+ if (candidates.length === 0)
589
+ return null;
590
+ // Drop a candidate nested inside ANOTHER callable's body within the literal
591
+ // (`{ run() { const call = () => {}; } }` — `call` is `run`'s local, not a
592
+ // member). Strict containment: an identically-ranged sibling node for the
593
+ // same member (a property node over an arrow function) is not a body.
594
+ const bodies = inside.filter(callable);
595
+ candidates = candidates.filter((c) => !bodies.some((b) => b.id !== c.id && !sameRange(b, c) && rangeWithin(c, b)));
596
+ if (candidates.length === 0)
597
+ return null;
598
+ // Several survivors (a property AND a function for one arrow member, say):
599
+ // a callable first, then the earliest in source order.
600
+ candidates.sort((a, b) => {
601
+ const ca = callable(a) ? 0 : 1;
602
+ const cb = callable(b) ? 0 : 1;
603
+ if (ca !== cb)
604
+ return ca - cb;
605
+ return a.startLine - b.startLine || a.startColumn - b.startColumn;
606
+ });
607
+ return {
608
+ original: ref,
609
+ targetNodeId: candidates[0].id,
610
+ confidence,
611
+ resolvedBy,
612
+ };
613
+ }
510
614
  // Exported for the precedence unit tests (#1079): they assert the
511
615
  // preferredFqn → same-file → matches[0] ordering directly.
512
616
  function resolveMethodOnType(typeName, methodName, ref, context, confidence, resolvedBy,
@@ -1592,6 +1696,17 @@ function matchMethodCall(ref, context) {
1592
1696
  if (ref.language === 'go' && dotMatch && objectOrClass.includes('.')) {
1593
1697
  return matchGoFieldChainCall(objectOrClass, methodName, ref, context);
1594
1698
  }
1699
+ // Rust call through a field of the enclosing type — `self.inner.run()`,
1700
+ // emitted as `self.inner.run` (#1585). Same discipline as the Go branch
1701
+ // above, and EXCLUSIVE for the same reason: validated field-type inference
1702
+ // or nothing. Letting this shape reach the bare-name strategies below is
1703
+ // how `self.inner.run()` resolved to a same-named method on an unrelated
1704
+ // type — or to the calling method itself, a self-edge the source doesn't
1705
+ // contain — whenever the field's type was external or merely shared a
1706
+ // method name with something nearby.
1707
+ if (ref.language === 'rust' && dotMatch && objectOrClass.startsWith('self.')) {
1708
+ return matchRustSelfFieldCall(objectOrClass.slice('self.'.length), methodName, ref, context);
1709
+ }
1595
1710
  // Java/Kotlin: receiver may be a field whose name doesn't match the type by
1596
1711
  // Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
1597
1712
  // the field in the enclosing class to get its declared type, then resolve
@@ -1611,6 +1726,26 @@ function matchMethodCall(ref, context) {
1611
1726
  }
1612
1727
  }
1613
1728
  }
1729
+ // Object-literal namespace receiver (#1573): `api.call()` where `api` is a
1730
+ // same-file `const api = { call() {…}, get: () => {…} }`. Its members are
1731
+ // plain functions with bare names inside the constant's extent — no
1732
+ // `Container::member` qualified name — so none of the class-shaped
1733
+ // strategies below can see them (Strategy 3 only considers `method`
1734
+ // kinds) and the call resolved to nothing at all. Same file only: a
1735
+ // cross-file use reaches the same helper through the import path.
1736
+ if (dotMatch && !objectOrClass.includes('.') && OBJECT_LITERAL_LANGUAGES.has(ref.language)) {
1737
+ const literalMatch = nmTimedT('mc-literal', ref, () => {
1738
+ const holders = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath).filter((n) => (n.kind === 'constant' || n.kind === 'variable') && n.filePath === ref.filePath);
1739
+ for (const holder of holders) {
1740
+ const hit = resolveObjectLiteralMember(holder, methodName, ref, context, 0.85, 'instance-method');
1741
+ if (hit)
1742
+ return hit;
1743
+ }
1744
+ return null;
1745
+ });
1746
+ if (literalMatch)
1747
+ return literalMatch;
1748
+ }
1614
1749
  // Strategy 1: Direct class name match (existing logic). When the receiver
1615
1750
  // names a class that exists in several files (`Logger.log()` / `Logger::log()`
1616
1751
  // with a `Logger` in both `a/` and `b/`), try the class in the call site's
@@ -1814,6 +1949,109 @@ function matchGoFieldChainCall(receiverChain, methodName, ref, context) {
1814
1949
  }
1815
1950
  return null;
1816
1951
  }
1952
+ // Rust primitives and the prelude's own types: a field of one of these never
1953
+ // names a project type, so a `self.<field>.<method>()` on it stays unresolved.
1954
+ const RUST_NON_PROJECT_FIELD_TYPES = new Set([
1955
+ 'bool', 'char', 'str', 'String',
1956
+ 'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
1957
+ 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
1958
+ 'f32', 'f64',
1959
+ 'Self', 'self',
1960
+ ]);
1961
+ /**
1962
+ * Reduce a Rust field's declared type text to the simple name of the type a
1963
+ * method call on that field auto-derefs to, or null when there is none we can
1964
+ * name. Only the layers Rust's method-call auto-deref looks through are
1965
+ * unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
1966
+ * (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
1967
+ * `Inner::run`. Containers that do NOT auto-deref to their parameter
1968
+ * (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
1969
+ * own name and, having no project node, resolve to nothing — `self.items.push()`
1970
+ * must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
1971
+ * the trait, whose method node the interface-impl synthesizer fans out. A
1972
+ * generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
1973
+ * type, or a non-identifier yields null.
1974
+ */
1975
+ function rustFieldTypeName(raw) {
1976
+ let t = raw.trim();
1977
+ for (;;) {
1978
+ const before = t;
1979
+ t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
1980
+ t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
1981
+ t = t.replace(/^(?:dyn|impl)\s+/, '');
1982
+ if (t === before)
1983
+ break;
1984
+ }
1985
+ // Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
1986
+ // bounds (`dyn Source + Send`); keep the last path segment.
1987
+ t = t.replace(/[<>+].*$/, '').trim();
1988
+ const seg = t.split('::').filter(Boolean).pop();
1989
+ if (!seg || !/^[A-Za-z_]\w*$/.test(seg))
1990
+ return null;
1991
+ if (RUST_NON_PROJECT_FIELD_TYPES.has(seg))
1992
+ return null;
1993
+ if (/^[A-Z]$/.test(seg))
1994
+ return null; // bare single-letter generic parameter
1995
+ return seg;
1996
+ }
1997
+ /**
1998
+ * Resolve a Rust call through a field of the enclosing type —
1999
+ * `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
2000
+ * Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
2001
+ * method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
2002
+ * type comes from the owner struct's OWN declaration lines, and the method is
2003
+ * resolved AND VALIDATED on that type by resolveMethodOnType. The caller
2004
+ * treats this branch as exclusive for `self.<field>` receivers: a field whose
2005
+ * type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
2006
+ * parameter, or not declared where we can see it yields null and the ref stays
2007
+ * unresolved. Rust struct fields are not graph nodes, so the declaration text
2008
+ * is the only place the type lives.
2009
+ */
2010
+ function matchRustSelfFieldCall(field, methodName, ref, context) {
2011
+ // The extractor only ever emits a single field hop; anything else is not ours.
2012
+ if (!field || field.includes('.'))
2013
+ return null;
2014
+ const caller = context.getNodeById?.(ref.fromNodeId);
2015
+ if (!caller)
2016
+ return null;
2017
+ const sep = caller.qualifiedName.lastIndexOf('::');
2018
+ if (sep <= 0)
2019
+ return null; // a free fn has no `self`
2020
+ const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
2021
+ if (!owner)
2022
+ return null;
2023
+ const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter((n) => (n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
2024
+ n.language === 'rust');
2025
+ const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2026
+ // `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
2027
+ // the type text runs to the field separator. A comma inside generic args
2028
+ // (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
2029
+ // reduces to the container's own name — exactly the non-deref case it
2030
+ // refuses anyway.
2031
+ const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
2032
+ for (const s of owners) {
2033
+ const source = context.readFile(s.filePath);
2034
+ if (!source)
2035
+ continue;
2036
+ // Only the struct's own declaration lines, comment-stripped line by line —
2037
+ // same discipline as the Go helper: prose or a same-named identifier
2038
+ // elsewhere in the file can never donate a type.
2039
+ const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
2040
+ for (const rawLine of declLines) {
2041
+ const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
2042
+ const m = line.match(fieldRe);
2043
+ if (!m || !m[1])
2044
+ continue;
2045
+ const fieldType = rustFieldTypeName(m[1]);
2046
+ // The field is declared here; whether or not its type names a project
2047
+ // symbol, this owner is the answer — no other same-named struct applies.
2048
+ if (!fieldType)
2049
+ return null;
2050
+ return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
2051
+ }
2052
+ }
2053
+ return null;
2054
+ }
1817
2055
  /**
1818
2056
  * Split a camelCase or PascalCase string into words.
1819
2057
  */
@@ -2070,6 +2308,46 @@ function matchReference(ref, context) {
2070
2308
  resolvedBy: 'exact-match',
2071
2309
  };
2072
2310
  }
2311
+ // Erlang call/fun refs carry the call-site arity (`f/1` — #1610) because
2312
+ // arity is part of the function's identity and every erlang function's
2313
+ // qualifiedName carries it (`mod::f/1`). Resolve ONLY to a definition of
2314
+ // that exact arity: the call site's own file first (a local call targets its
2315
+ // own module by language semantics; `-import`ed functions ride the
2316
+ // cross-file branch), and when no definition of that arity exists anywhere,
2317
+ // resolve to NOTHING rather than a sibling arity — the real target may be
2318
+ // macro-generated or out of repo, and a wrong-arity edge is worse than none.
2319
+ if (ref.language === 'erlang' &&
2320
+ !ref.referenceName.includes('::') &&
2321
+ (ref.referenceKind === 'calls' || ref.referenceKind === 'references')) {
2322
+ const am = /^(.+)\/(\d{1,3})$/.exec(ref.referenceName);
2323
+ if (am) {
2324
+ // endsWith is length-anchored, so `/1` cannot match `…/11`.
2325
+ const arityTail = `/${am[2]}`;
2326
+ const candidates = context
2327
+ .getNodesByName(am[1])
2328
+ .filter((n) => n.language === 'erlang' && n.kind === 'function' && n.qualifiedName.endsWith(arityTail));
2329
+ if (candidates.length > 0) {
2330
+ const sameFile = candidates.find((n) => n.filePath === ref.filePath);
2331
+ if (sameFile) {
2332
+ return { original: ref, targetNodeId: sameFile.id, confidence: 0.95, resolvedBy: 'exact-match' };
2333
+ }
2334
+ if (candidates.length === 1) {
2335
+ return { original: ref, targetNodeId: candidates[0].id, confidence: 0.8, resolvedBy: 'exact-match' };
2336
+ }
2337
+ const best = findBestMatch(ref, candidates, context);
2338
+ if (best) {
2339
+ const proximity = computePathProximity(ref.filePath, best.filePath);
2340
+ return {
2341
+ original: ref,
2342
+ targetNodeId: best.id,
2343
+ confidence: proximity >= 30 ? 0.7 : 0.4,
2344
+ resolvedBy: 'exact-match',
2345
+ };
2346
+ }
2347
+ }
2348
+ return null;
2349
+ }
2350
+ }
2073
2351
  // Try strategies in order of confidence
2074
2352
  let result;
2075
2353
  // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)