ucn 4.2.2 → 5.0.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 (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +445 -300
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -131
  13. package/core/cache.js +533 -11
  14. package/core/callers.js +5533 -494
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +421 -20
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +204 -42
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +216 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -177
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +371 -116
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +428 -16
  65. package/languages/javascript.js +452 -49
  66. package/languages/python.js +1041 -32
  67. package/languages/rust.js +1415 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +41 -24
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
package/languages/rust.js CHANGED
@@ -20,6 +20,105 @@ function parseTree(parser, code) {
20
20
  return safeParse(parser, code, undefined, PARSE_OPTIONS);
21
21
  }
22
22
 
23
+ const MACRO_ITEM_TOKENS = new Set([
24
+ 'fn', 'struct', 'enum', 'union', 'trait', 'impl', 'type', 'const',
25
+ 'static', 'mod', 'use', 'extern',
26
+ ]);
27
+ const MACRO_ITEM_NODES = new Set([
28
+ 'function_item', 'struct_item', 'enum_item', 'union_item', 'trait_item',
29
+ 'impl_item', 'type_item', 'const_item', 'static_item', 'mod_item',
30
+ 'use_declaration', 'foreign_mod_item',
31
+ ]);
32
+
33
+ let lastDeclarationParser = null;
34
+ let lastDeclarationCode = null;
35
+ let lastDeclarationTrees = null;
36
+
37
+ function macroInvocationIsItemPosition(node) {
38
+ if (node.parent?.type === 'source_file') return true;
39
+ if (node.parent?.type !== 'declaration_list') return false;
40
+ // A macro directly inside a module can emit items. An invocation inside
41
+ // an impl/trait body emits associated items and must not be reinterpreted
42
+ // as free functions or top-level types.
43
+ return node.parent.parent?.type === 'mod_item';
44
+ }
45
+
46
+ function tokenTreeMayDeclareItem(tokenTree) {
47
+ const pending = [tokenTree];
48
+ while (pending.length > 0) {
49
+ const current = pending.pop();
50
+ for (let index = 0; index < current.childCount; index++) {
51
+ const child = current.child(index);
52
+ if (MACRO_ITEM_TOKENS.has(child.type)) return true;
53
+ if (child.type === 'token_tree') pending.push(child);
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ function buildMacroItemRecoveryTree(code, parser, tree) {
60
+ const ranges = [];
61
+ traverseTreeCached(tree.rootNode, node => {
62
+ if (node.type !== 'macro_invocation' ||
63
+ !macroInvocationIsItemPosition(node)) return true;
64
+ const tokenTree = node.namedChildren.find(child => child.type === 'token_tree');
65
+ if (tokenTree && tokenTreeMayDeclareItem(tokenTree) &&
66
+ tokenTree.endIndex - tokenTree.startIndex > 2) {
67
+ ranges.push([tokenTree.startIndex + 1, tokenTree.endIndex - 1]);
68
+ }
69
+ // Its contents are opaque tokens in the primary tree; no nested AST
70
+ // invocation can be discovered by descending here.
71
+ return false;
72
+ });
73
+ if (ranges.length === 0) return null;
74
+
75
+ const masked = code.replace(/[^\r\n]/g, ' ').split('');
76
+ for (const [start, end] of ranges) {
77
+ for (let index = start; index < end; index++) masked[index] = code[index];
78
+ // Bound malformed macro DSL so it cannot absorb the next invocation.
79
+ if (end < masked.length && masked[end] !== '\n' && masked[end] !== '\r') {
80
+ masked[end] = ';';
81
+ }
82
+ }
83
+ const recovered = parseTree(parser, masked.join(''));
84
+ let itemCount = 0;
85
+ const declarationNameStarts = new Set();
86
+ traverseTreeCached(recovered.rootNode, node => {
87
+ if (MACRO_ITEM_NODES.has(node.type)) {
88
+ itemCount++;
89
+ const name = node.childForFieldName('name');
90
+ if (name) declarationNameStarts.add(name.startIndex);
91
+ }
92
+ return true;
93
+ });
94
+ if (itemCount === 0) return null;
95
+ return { tree: recovered, itemCount, declarationNameStarts };
96
+ }
97
+
98
+ /**
99
+ * Rust macro invocation bodies are token trees, even when they contain item
100
+ * declarations verbatim. Reparse only item-position bodies in a byte- and
101
+ * line-preserving synthetic source. The primary AST remains authoritative;
102
+ * the recovery tree contributes declarations the grammar otherwise hides.
103
+ */
104
+ function declarationTrees(code, parser) {
105
+ if (parser === lastDeclarationParser && code === lastDeclarationCode &&
106
+ lastDeclarationTrees) return lastDeclarationTrees;
107
+ const primary = parseTree(parser, code);
108
+ const macro = buildMacroItemRecoveryTree(code, parser, primary);
109
+ const result = {
110
+ primary,
111
+ trees: macro ? [primary, macro.tree] : [primary],
112
+ macroItemRecovery: !!macro,
113
+ macroItemCount: macro?.itemCount || 0,
114
+ macroDeclarationNameStarts: macro?.declarationNameStarts || new Set(),
115
+ };
116
+ lastDeclarationParser = parser;
117
+ lastDeclarationCode = code;
118
+ lastDeclarationTrees = result;
119
+ return result;
120
+ }
121
+
23
122
  /**
24
123
  * Extract return type from Rust function
25
124
  */
@@ -35,6 +134,58 @@ function extractReturnType(node) {
35
134
  return null;
36
135
  }
37
136
 
137
+ /**
138
+ * Extract the compiler-declared associated item of an iterator return:
139
+ * `impl Iterator<Item = &Arg>` → `Arg`. Tuple/dyn/opaque item shapes abstain.
140
+ */
141
+ function extractRustIteratorItemTypeFromTypeNode(typeNode) {
142
+ if (!typeNode) return null;
143
+ const pending = [typeNode];
144
+ while (pending.length > 0) {
145
+ const current = pending.pop();
146
+ if (current.type === 'type_binding') {
147
+ const nameNode = current.namedChild(0);
148
+ const valueNode = current.childForFieldName('type') || current.namedChild(1);
149
+ if (nameNode?.text === 'Item') return aliasBaseTypeName(valueNode);
150
+ }
151
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
152
+ pending.push(current.namedChild(i));
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+
158
+ function extractRustIteratorItemType(node) {
159
+ return extractRustIteratorItemTypeFromTypeNode(
160
+ node.childForFieldName('return_type'));
161
+ }
162
+
163
+ /**
164
+ * A turbofish on `Iterator::collect` fixes both the concrete collection and
165
+ * its item type at the call site: `collect::<Vec<Haystack>>()`. Keep that
166
+ * compiler-declared result shape so a later collection callback
167
+ * (`sort_by(|a, b| ...)`) can type its closure parameters without guessing.
168
+ */
169
+ function extractCollectResultContract(genericFunctionNode) {
170
+ if (genericFunctionNode?.type !== 'generic_function') return null;
171
+ const functionNode = genericFunctionNode.childForFieldName('function');
172
+ if (functionNode?.type !== 'field_expression' ||
173
+ functionNode.childForFieldName('field')?.text !== 'collect') {
174
+ return null;
175
+ }
176
+ const args = genericFunctionNode.namedChildren
177
+ .find(child => child.type === 'type_arguments');
178
+ if (!args || args.namedChildCount !== 1) return null;
179
+ const result = args.namedChild(0);
180
+ if (result.type !== 'generic_type') return null;
181
+ const outer = aliasBaseTypeName(result.childForFieldName('type') || result.namedChild(0));
182
+ const resultArgs = result.namedChildren
183
+ .find(child => child.type === 'type_arguments');
184
+ if (!outer || !resultArgs || resultArgs.namedChildCount !== 1) return null;
185
+ const item = aliasBaseTypeName(resultArgs.namedChild(0));
186
+ return item ? { type: outer, itemType: item } : null;
187
+ }
188
+
38
189
  /**
39
190
  * Extract Rust parameters
40
191
  */
@@ -48,6 +199,46 @@ function extractRustParams(paramsNode) {
48
199
  return text.replace(/^\(|\)$/g, '').trim();
49
200
  }
50
201
 
202
+ function extractRustCallbackParamTypes(paramsNode) {
203
+ if (!paramsNode) return undefined;
204
+ const callbacks = {};
205
+ let callArgumentIndex = 0;
206
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
207
+ const parameter = paramsNode.namedChild(i);
208
+ if (parameter.type === 'self_parameter') continue;
209
+ if (parameter.type !== 'parameter') continue;
210
+ const typeNode = parameter.childForFieldName('type');
211
+ let functionType = null;
212
+ const pending = typeNode ? [typeNode] : [];
213
+ while (pending.length > 0 && !functionType) {
214
+ const current = pending.pop();
215
+ if (current.type === 'function_type') {
216
+ functionType = current;
217
+ break;
218
+ }
219
+ for (let j = 0; j < current.namedChildCount; j++) {
220
+ pending.push(current.namedChild(j));
221
+ }
222
+ }
223
+ const callbackParams = functionType?.childForFieldName('parameters');
224
+ if (callbackParams) {
225
+ const types = [];
226
+ let complete = true;
227
+ for (let j = 0; j < callbackParams.namedChildCount; j++) {
228
+ const name = aliasBaseTypeName(callbackParams.namedChild(j));
229
+ if (!name) {
230
+ complete = false;
231
+ break;
232
+ }
233
+ types.push(name);
234
+ }
235
+ if (complete && types.length > 0) callbacks[callArgumentIndex] = types;
236
+ }
237
+ callArgumentIndex++;
238
+ }
239
+ return Object.keys(callbacks).length > 0 ? callbacks : undefined;
240
+ }
241
+
51
242
  /**
52
243
  * Base type name from a type-alias target (fix #208): SpannedString<Style>
53
244
  * → SpannedString, module::Type → Type, &T → T. dyn/impl/tuple/fn shapes
@@ -250,11 +441,13 @@ function _processFunction(node, functions, processedRanges, lines, code) {
250
441
  const isExtern = firstLine.includes('extern ');
251
442
  const visibility = extractVisibility(text);
252
443
  const returnType = extractReturnType(node);
444
+ const iteratorItemType = extractRustIteratorItemType(node);
253
445
  const docstring = extractRustDocstring(lines, startLine);
254
446
  const generics = extractGenerics(node);
255
447
  const attributes = extractAttributes(node, lines);
256
448
  const attributesWithArgs = extractAttributesWithArgs(node, lines);
257
449
  const inCfgTest = _isInsideCfgTestModule(node, lines);
450
+ const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
258
451
 
259
452
  const modifiers = [];
260
453
  if (visibility) modifiers.push(visibility);
@@ -274,11 +467,13 @@ function _processFunction(node, functions, processedRanges, lines, code) {
274
467
  name: nameNode.text,
275
468
  params: extractRustParams(paramsNode),
276
469
  paramsStructured: parseStructuredParams(paramsNode, 'rust'),
470
+ ...(callbackParamTypes && { callbackParamTypes }),
277
471
  startLine,
278
472
  endLine,
279
473
  indent,
280
474
  modifiers,
281
475
  ...(returnType && { returnType }),
476
+ ...(iteratorItemType && { iteratorItemType }),
282
477
  ...(docstring && { docstring }),
283
478
  ...(generics && { generics }),
284
479
  ...(attributesWithArgs.length > 0 && { attributesWithArgs })
@@ -305,6 +500,8 @@ function _processFunction(node, functions, processedRanges, lines, code) {
305
500
  const visibility = extractVisibility(child.text);
306
501
  const returnType = extractReturnType(child);
307
502
  const docstring = extractRustDocstring(lines, startLine);
503
+ const callbackParamTypes = extractRustCallbackParamTypes(fParams);
504
+ const iteratorItemType = extractRustIteratorItemType(child);
308
505
  const modifiers = ['extern'];
309
506
  if (visibility) modifiers.push(visibility);
310
507
 
@@ -312,6 +509,8 @@ function _processFunction(node, functions, processedRanges, lines, code) {
312
509
  name: fName.text,
313
510
  params: extractRustParams(fParams),
314
511
  paramsStructured: parseStructuredParams(fParams, 'rust'),
512
+ ...(callbackParamTypes && { callbackParamTypes }),
513
+ ...(iteratorItemType && { iteratorItemType }),
315
514
  startLine,
316
515
  endLine,
317
516
  indent,
@@ -329,6 +528,182 @@ function _processFunction(node, functions, processedRanges, lines, code) {
329
528
  return false;
330
529
  }
331
530
 
531
+ function _macroBodyTree(tree) {
532
+ let current = tree;
533
+ for (;;) {
534
+ const named = [];
535
+ for (let i = 0; i < current.namedChildCount; i++) {
536
+ named.push(current.namedChild(i));
537
+ }
538
+ if (named.length !== 1 || named[0].type !== 'token_tree') return current;
539
+ current = named[0];
540
+ }
541
+ }
542
+
543
+ function _macroTreeChildren(tree) {
544
+ const children = [];
545
+ for (let i = 0; i < tree.childCount; i++) {
546
+ const child = tree.child(i);
547
+ if (['{', '}', '(', ')', '[', ']'].includes(child.type)) continue;
548
+ children.push(child);
549
+ }
550
+ return children;
551
+ }
552
+
553
+ function _macroPathConstruction(children, methodIndex) {
554
+ const method = children[methodIndex];
555
+ if (!method || !['new', 'default'].includes(method.text) ||
556
+ children[methodIndex - 1]?.type !== '::' ||
557
+ children[methodIndex + 1]?.type !== 'token_tree') {
558
+ return null;
559
+ }
560
+ const typeNode = children[methodIndex - 2];
561
+ if (!typeNode || !['identifier', 'type_identifier'].includes(typeNode.type) ||
562
+ !/^[A-Z]/.test(typeNode.text)) {
563
+ return null;
564
+ }
565
+ const path = [typeNode.text];
566
+ let start = methodIndex - 2;
567
+ while (start >= 2 && children[start - 1]?.type === '::') {
568
+ const segment = children[start - 2];
569
+ if (!segment || ![
570
+ 'identifier', 'type_identifier', 'metavariable', 'crate', 'self', 'super',
571
+ ].includes(segment.type)) {
572
+ break;
573
+ }
574
+ path.unshift(segment.text === '$crate' ? 'crate' : segment.text);
575
+ start -= 2;
576
+ }
577
+ let end = methodIndex + 1;
578
+ while (children[end + 1]?.type === '.' &&
579
+ children[end + 2]?.type === 'identifier' &&
580
+ children[end + 3]?.type === 'token_tree') {
581
+ end += 3;
582
+ }
583
+ return {
584
+ type: typeNode.text,
585
+ qualifier: path.length > 1 ? path.slice(0, -1).join('::') : undefined,
586
+ start,
587
+ end,
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Infer a macro's value type only from its transcriber's final expression.
593
+ * This remains AST/token-tree driven: a constructor used merely as a temporary
594
+ * is not enough. Supported compiler-stable shapes are:
595
+ * Type::new(...).builder_chain()
596
+ * let value = Type::new(...); ...; value
597
+ * Recursive same-name rules delegate to the constructive rule, while a rule
598
+ * ending in compile_error! is divergent and cannot introduce another value.
599
+ */
600
+ function _inferMacroReturn(node, macroName) {
601
+ const results = [];
602
+ let sawRule = false;
603
+ for (let i = 0; i < node.namedChildCount; i++) {
604
+ const rule = node.namedChild(i);
605
+ if (rule.type !== 'macro_rule') continue;
606
+ sawRule = true;
607
+ const right = rule.childForFieldName('right');
608
+ if (!right) return {};
609
+ const body = _macroBodyTree(right);
610
+ const children = _macroTreeChildren(body);
611
+ if (children.length === 0) {
612
+ results.push({ kind: 'unknown' });
613
+ continue;
614
+ }
615
+
616
+ const bindings = new Map();
617
+ for (let j = 0; j < children.length; j++) {
618
+ if (children[j].type !== 'let') continue;
619
+ let nameIndex = j + 1;
620
+ if (children[nameIndex]?.type === 'mutable_specifier') nameIndex++;
621
+ const nameNode = children[nameIndex];
622
+ if (nameNode?.type !== 'identifier') continue;
623
+ let eq = nameIndex + 1;
624
+ while (eq < children.length && children[eq].type !== '=' &&
625
+ children[eq].type !== ';') eq++;
626
+ if (children[eq]?.type !== '=') continue;
627
+ let construction = null;
628
+ let semi = eq + 1;
629
+ for (; semi < children.length && children[semi].type !== ';'; semi++) {
630
+ const candidate = _macroPathConstruction(children, semi);
631
+ if (candidate && candidate.start === eq + 1) {
632
+ construction = candidate;
633
+ break;
634
+ }
635
+ }
636
+ if (!construction) continue;
637
+ const prior = bindings.get(nameNode.text);
638
+ const identity = `${construction.qualifier || ''}\0${construction.type}`;
639
+ if (prior && prior.identity !== identity) {
640
+ bindings.set(nameNode.text, { ambiguous: true });
641
+ } else if (!prior) {
642
+ bindings.set(nameNode.text, { ...construction, identity });
643
+ }
644
+ }
645
+
646
+ const tail = children[children.length - 1];
647
+ if (tail.type === 'identifier' && bindings.has(tail.text) &&
648
+ !bindings.get(tail.text).ambiguous) {
649
+ const binding = bindings.get(tail.text);
650
+ results.push({ kind: 'return', type: binding.type, qualifier: binding.qualifier });
651
+ continue;
652
+ }
653
+
654
+ let direct = null;
655
+ for (let j = 0; j < children.length; j++) {
656
+ const candidate = _macroPathConstruction(children, j);
657
+ if (candidate && candidate.end === children.length - 1) {
658
+ direct = candidate;
659
+ break;
660
+ }
661
+ }
662
+ if (direct) {
663
+ results.push({ kind: 'return', type: direct.type, qualifier: direct.qualifier });
664
+ continue;
665
+ }
666
+
667
+ // Diverging compile_error!(...); commonly carries a trailing
668
+ // semicolon. A recursive value-delegating rule may not: `foo!();`
669
+ // returns unit, so only compile_error receives this allowance.
670
+ const effectiveTailIndex = tail.type === ';'
671
+ ? children.length - 2 : children.length - 1;
672
+ const effectiveTail = children[effectiveTailIndex];
673
+ const bang = children[effectiveTailIndex - 1];
674
+ const macro = children[effectiveTailIndex - 2];
675
+ if (bang?.type === '!' && effectiveTail?.type === 'token_tree' &&
676
+ macro?.type === 'identifier') {
677
+ if (macro.text === macroName && tail.type !== ';') {
678
+ results.push({ kind: 'delegate' });
679
+ } else if (macro.text === 'compile_error') results.push({ kind: 'never' });
680
+ else results.push({ kind: 'unknown' });
681
+ continue;
682
+ }
683
+ results.push({ kind: 'unknown' });
684
+ }
685
+
686
+ if (!sawRule) return {};
687
+ const returns = results.filter(result => result.kind === 'return');
688
+ const blockers = results.filter(result => !['return', 'delegate', 'never'].includes(result.kind));
689
+ if (returns.length > 0 && blockers.length === 0) {
690
+ const identities = new Set(returns.map(result =>
691
+ `${result.qualifier || ''}\0${result.type}`));
692
+ if (identities.size === 1) {
693
+ return {
694
+ returnType: returns[0].type,
695
+ ...(returns[0].qualifier && {
696
+ returnTypeQualifier: returns[0].qualifier,
697
+ }),
698
+ };
699
+ }
700
+ }
701
+ if (results.every(result => result.kind === 'never')) {
702
+ return { macroNeverReturns: true };
703
+ }
704
+ return {};
705
+ }
706
+
332
707
  /**
333
708
  * Process a node for type/class extraction (single-pass helper)
334
709
  * Returns true if node was matched, false otherwise
@@ -432,6 +807,7 @@ function _processClass(node, types, processedRanges, lines, code) {
432
807
  const { startLine, endLine } = nodeToLocation(node, lines);
433
808
  const implInfo = extractImplInfo(node);
434
809
  const docstring = extractRustDocstring(lines, startLine);
810
+ const derefTarget = extractDerefTarget(node, implInfo.traitName);
435
811
 
436
812
  types.push({
437
813
  name: implInfo.name,
@@ -442,6 +818,7 @@ function _processClass(node, types, processedRanges, lines, code) {
442
818
  typeName: implInfo.typeName,
443
819
  members: extractImplMembers(node, lines, implInfo.typeName),
444
820
  modifiers: [],
821
+ ...(derefTarget && { derefTarget }),
445
822
  ...(docstring && { docstring })
446
823
  });
447
824
  return true; // matched
@@ -485,6 +862,7 @@ function _processClass(node, types, processedRanges, lines, code) {
485
862
  if (nameNode) {
486
863
  const { startLine, endLine } = nodeToLocation(node, lines);
487
864
  const docstring = extractRustDocstring(lines, startLine);
865
+ const inferred = _inferMacroReturn(node, nameNode.text);
488
866
 
489
867
  types.push({
490
868
  name: nameNode.text,
@@ -493,6 +871,7 @@ function _processClass(node, types, processedRanges, lines, code) {
493
871
  type: 'macro',
494
872
  members: [],
495
873
  modifiers: [],
874
+ ...inferred,
496
875
  ...(docstring && { docstring })
497
876
  });
498
877
  }
@@ -548,19 +927,41 @@ function _processClass(node, types, processedRanges, lines, code) {
548
927
  */
549
928
  function _postProcessTraitImpls(types) {
550
929
  const implTraits = new Map(); // typeName → [traitName, ...]
930
+ const derefTargets = new Map(); // typeName -> Set<Target>
551
931
  for (const t of types) {
552
932
  if (t.type === 'impl' && t.traitName && t.typeName) {
553
933
  if (!implTraits.has(t.typeName)) implTraits.set(t.typeName, []);
554
934
  implTraits.get(t.typeName).push(t.traitName);
935
+ if (t.derefTarget) {
936
+ if (!derefTargets.has(t.typeName)) derefTargets.set(t.typeName, new Set());
937
+ derefTargets.get(t.typeName).add(t.derefTarget);
938
+ }
555
939
  }
556
940
  }
557
941
  for (const t of types) {
558
942
  if ((t.type === 'struct' || t.type === 'enum') && implTraits.has(t.name)) {
559
943
  t.implements = implTraits.get(t.name);
944
+ const targets = derefTargets.get(t.name);
945
+ if (targets?.size === 1) t.derefTarget = [...targets][0];
560
946
  }
561
947
  }
562
948
  }
563
949
 
950
+ function extractDerefTarget(implNode, traitName) {
951
+ if (!traitName || !/(^|::)Deref(?:Mut)?$/.test(traitName)) return null;
952
+ let found = null;
953
+ const walk = node => {
954
+ if (found) return;
955
+ if (node.type === 'type_item' && node.childForFieldName('name')?.text === 'Target') {
956
+ found = aliasBaseTypeName(node.childForFieldName('type'));
957
+ return;
958
+ }
959
+ for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
960
+ };
961
+ walk(implNode);
962
+ return found;
963
+ }
964
+
564
965
  /**
565
966
  * Process a node for state object extraction (single-pass helper)
566
967
  * Returns true if node was matched, false otherwise
@@ -603,14 +1004,16 @@ function _processState(node, objects, lines) {
603
1004
  * Find all functions in Rust code using tree-sitter
604
1005
  */
605
1006
  function findFunctions(code, parser) {
606
- const tree = parseTree(parser, code);
1007
+ const { trees } = declarationTrees(code, parser);
607
1008
  const lines = code.split('\n');
608
1009
  const functions = [];
609
1010
  const processedRanges = new Set();
610
- traverseTreeCached(tree.rootNode, (node) => {
611
- _processFunction(node, functions, processedRanges, lines, code);
612
- return true;
613
- });
1011
+ for (const tree of trees) {
1012
+ traverseTreeCached(tree.rootNode, (node) => {
1013
+ _processFunction(node, functions, processedRanges, lines, code);
1014
+ return true;
1015
+ });
1016
+ }
614
1017
  functions.sort((a, b) => a.startLine - b.startLine);
615
1018
  return functions;
616
1019
  }
@@ -630,16 +1033,18 @@ function extractGenerics(node) {
630
1033
  * Find all types (structs, enums, traits, impls) in Rust code
631
1034
  */
632
1035
  function findClasses(code, parser) {
633
- const tree = parseTree(parser, code);
1036
+ const { trees } = declarationTrees(code, parser);
634
1037
  const lines = code.split('\n');
635
1038
  const types = [];
636
1039
  const processedRanges = new Set();
637
- traverseTreeCached(tree.rootNode, (node) => {
638
- const matched = _processClass(node, types, processedRanges, lines, code);
639
- // For impl_item, don't traverse into impl body (original behavior)
640
- if (matched && node.type === 'impl_item') return false;
641
- return true;
642
- });
1040
+ for (const tree of trees) {
1041
+ traverseTreeCached(tree.rootNode, (node) => {
1042
+ const matched = _processClass(node, types, processedRanges, lines, code);
1043
+ // For impl_item, don't traverse into impl body (original behavior)
1044
+ if (matched && node.type === 'impl_item') return false;
1045
+ return true;
1046
+ });
1047
+ }
643
1048
  _postProcessTraitImpls(types);
644
1049
  types.sort((a, b) => a.startLine - b.startLine);
645
1050
  return types;
@@ -654,6 +1059,28 @@ function extractStructFields(structNode, codeOrLines) {
654
1059
  const bodyNode = structNode.childForFieldName('body');
655
1060
  if (!bodyNode) return fields;
656
1061
 
1062
+ if (bodyNode.type === 'ordered_field_declaration_list') {
1063
+ let position = 0;
1064
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
1065
+ const field = bodyNode.namedChild(i);
1066
+ // Visibility modifiers and field attributes (`#[serde(..)] u32`)
1067
+ // are separate children; the type node owns the tuple position.
1068
+ // Numeric member names let the shared declared-field hop resolve
1069
+ // `self.0.method()` exactly.
1070
+ if (field.type === 'visibility_modifier' ||
1071
+ field.type === 'attribute_item') continue;
1072
+ const { startLine, endLine } = nodeToLocation(field, code);
1073
+ fields.push({
1074
+ name: String(position++),
1075
+ startLine,
1076
+ endLine,
1077
+ memberType: 'field',
1078
+ fieldType: field.text,
1079
+ });
1080
+ }
1081
+ return fields;
1082
+ }
1083
+
657
1084
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
658
1085
  const field = bodyNode.namedChild(i);
659
1086
  if (field.type === 'field_declaration') {
@@ -702,11 +1129,15 @@ function extractImplInfo(implNode) {
702
1129
  typeName = typeNode.text;
703
1130
  }
704
1131
 
705
- // Strip generic type arguments from typeName and traitName for lookup
706
- // e.g., "CacheService<T>" → "CacheService", "Entity" stays "Entity"
707
- const stripGenerics = (s) => s ? s.replace(/<[^>]*>/g, '').trim() : s;
708
- const bareTypeName = stripGenerics(typeName);
709
- const bareTraitName = stripGenerics(traitName);
1132
+ // Resolve the AST head instead of stripping generic text with a regex.
1133
+ // Nested type arguments (`Deserializer<read::StrRead<'a>>`) defeated the
1134
+ // old `<[^>]*>` expression and left the impossible owner
1135
+ // `Deserializer>`. Reference impls (`impl Trait for &'a mut Writer<T>`)
1136
+ // likewise need the referent owner so declared-field receiver evidence
1137
+ // and method definitions use the same identity.
1138
+ const stripGenerics = (s) => s ? s.replace(/<.*$/s, '').trim() : s;
1139
+ const bareTypeName = extractImplTypeHead(typeNode) || stripGenerics(typeName);
1140
+ const bareTraitName = extractImplTypeHead(traitNode) || stripGenerics(traitName);
710
1141
 
711
1142
  let name;
712
1143
  if (bareTraitName && bareTypeName) {
@@ -723,6 +1154,37 @@ function extractImplInfo(implNode) {
723
1154
  return { name, traitName, typeName: bareTypeName, generics: typeParams || undefined };
724
1155
  }
725
1156
 
1157
+ /**
1158
+ * Concrete lookup head of a Rust impl type.
1159
+ *
1160
+ * Generic wrappers remain their outer owner (`Box<Foo>` → `Box`), while
1161
+ * transparent references unwrap (`&mut Foo` → `Foo`). Scoped types use their
1162
+ * terminal name. Returning null for shapes without a named owner preserves
1163
+ * the conservative text fallback in extractImplInfo.
1164
+ */
1165
+ function extractImplTypeHead(typeNode) {
1166
+ if (!typeNode) return null;
1167
+ if (typeNode.type === 'type_identifier' ||
1168
+ typeNode.type === 'primitive_type') {
1169
+ return typeNode.text;
1170
+ }
1171
+ if (typeNode.type === 'scoped_type_identifier') {
1172
+ return typeNode.childForFieldName('name')?.text || null;
1173
+ }
1174
+ if (typeNode.type === 'reference_type' ||
1175
+ typeNode.type === 'parenthesized_type') {
1176
+ const inner = typeNode.childForFieldName('type') ||
1177
+ typeNode.namedChildren?.find(child =>
1178
+ !['lifetime', 'mutable_specifier'].includes(child.type));
1179
+ return extractImplTypeHead(inner);
1180
+ }
1181
+ if (typeNode.type === 'generic_type') {
1182
+ return extractImplTypeHead(typeNode.childForFieldName('type') ||
1183
+ typeNode.namedChild(0));
1184
+ }
1185
+ return null;
1186
+ }
1187
+
726
1188
  /**
727
1189
  * Extract enum variants
728
1190
  */
@@ -776,7 +1238,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
776
1238
  const { startLine, endLine } = nodeToLocation(child, code);
777
1239
  const paramsNode = child.childForFieldName('parameters');
778
1240
  const returnType = extractReturnType(child);
1241
+ const iteratorItemType = extractRustIteratorItemType(child);
779
1242
  const hasSelf = paramsNode && paramsNode.text.includes('self');
1243
+ const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
780
1244
 
781
1245
  // Rust vocabulary (fix #248): trait members carry the trait's
782
1246
  // OWN visibility — a method of a private trait is not `pub`,
@@ -791,7 +1255,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
791
1255
  modifiers: traitVisibility ? [traitVisibility] : [],
792
1256
  ...(paramsNode && { params: extractRustParams(paramsNode) }),
793
1257
  ...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
1258
+ ...(callbackParamTypes && { callbackParamTypes }),
794
1259
  ...(returnType && { returnType }),
1260
+ ...(iteratorItemType && { iteratorItemType }),
795
1261
  ...(hasSelf && { receiver: 'self' })
796
1262
  });
797
1263
  }
@@ -811,6 +1277,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
811
1277
  const members = [];
812
1278
  const bodyNode = implNode.childForFieldName('body');
813
1279
  if (!bodyNode) return members;
1280
+ const implAttributes = extractAttributes(implNode, codeOrLines);
814
1281
 
815
1282
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
816
1283
  const child = bodyNode.namedChild(i);
@@ -824,6 +1291,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
824
1291
  const text = child.text;
825
1292
  const firstLine = text.split('\n')[0];
826
1293
  const returnType = extractReturnType(child);
1294
+ const iteratorItemType = extractRustIteratorItemType(child);
827
1295
  const docstring = extractRustDocstring(code, startLine);
828
1296
  const visibility = extractVisibility(text);
829
1297
 
@@ -843,13 +1311,18 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
843
1311
  if (firstLine.includes('const fn')) modifiers.push('const');
844
1312
  if (firstLine.includes('extern ')) modifiers.push('extern');
845
1313
  for (const attr of attributes) modifiers.push(attr);
1314
+ for (const attr of implAttributes) {
1315
+ if (!modifiers.includes(attr)) modifiers.push(attr);
1316
+ }
846
1317
  if (inCfgTest) modifiers.push('cfg_test_module');
847
1318
 
848
1319
  const memberGenerics = extractGenerics(child);
1320
+ const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
849
1321
  members.push({
850
1322
  name: nameNode.text,
851
1323
  params: extractRustParams(paramsNode),
852
1324
  paramsStructured: parseStructuredParams(paramsNode, 'rust'),
1325
+ ...(callbackParamTypes && { callbackParamTypes }),
853
1326
  startLine,
854
1327
  endLine,
855
1328
  memberType: 'method',
@@ -858,6 +1331,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
858
1331
  modifiers,
859
1332
  ...(typeName && { receiver: typeName }), // All impl members get receiver for findMethodsForType
860
1333
  ...(returnType && { returnType }),
1334
+ ...(iteratorItemType && { iteratorItemType }),
861
1335
  ...(docstring && { docstring }),
862
1336
  // Method-level type params (fix #229): generic-param receiver
863
1337
  // types inside the method resolve against this declaration.
@@ -874,13 +1348,15 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
874
1348
  * Find state objects (const/static) in Rust code
875
1349
  */
876
1350
  function findStateObjects(code, parser) {
877
- const tree = parseTree(parser, code);
1351
+ const { trees } = declarationTrees(code, parser);
878
1352
  const lines = code.split('\n');
879
1353
  const objects = [];
880
- traverseTreeCached(tree.rootNode, (node) => {
881
- _processState(node, objects, lines);
882
- return true;
883
- });
1354
+ for (const tree of trees) {
1355
+ traverseTreeCached(tree.rootNode, (node) => {
1356
+ _processState(node, objects, lines);
1357
+ return true;
1358
+ });
1359
+ }
884
1360
  objects.sort((a, b) => a.startLine - b.startLine);
885
1361
  return objects;
886
1362
  }
@@ -889,17 +1365,20 @@ function findStateObjects(code, parser) {
889
1365
  * Parse a Rust file completely
890
1366
  */
891
1367
  function parse(code, parser) {
892
- const tree = parseTree(parser, code);
1368
+ const declaration = declarationTrees(code, parser);
1369
+ const tree = declaration.primary;
893
1370
  const lines = code.split('\n');
894
1371
  const functions = [], classes = [], stateObjects = [];
895
1372
  const processedFn = new Set(), processedCls = new Set();
896
1373
 
897
- traverseTreeCached(tree.rootNode, (node) => {
898
- _processFunction(node, functions, processedFn, lines, code);
899
- _processClass(node, classes, processedCls, lines, code);
900
- _processState(node, stateObjects, lines);
901
- return true; // always continue, never skip subtrees
902
- });
1374
+ for (const declarationTree of declaration.trees) {
1375
+ traverseTreeCached(declarationTree.rootNode, (node) => {
1376
+ _processFunction(node, functions, processedFn, lines, code);
1377
+ _processClass(node, classes, processedCls, lines, code);
1378
+ _processState(node, stateObjects, lines);
1379
+ return true; // always continue, never skip subtrees
1380
+ });
1381
+ }
903
1382
 
904
1383
  _postProcessTraitImpls(classes);
905
1384
 
@@ -909,7 +1388,9 @@ function parse(code, parser) {
909
1388
 
910
1389
  return {
911
1390
  language: 'rust', totalLines: lines.length, functions, classes, stateObjects,
912
- ...(tree.rootNode.hasError && { parseRecovery: true }),
1391
+ ...((tree.rootNode.hasError || declaration.macroItemRecovery) && {
1392
+ parseRecovery: true,
1393
+ }),
913
1394
  imports: [], exports: [],
914
1395
  };
915
1396
  }
@@ -1007,14 +1488,22 @@ function _tokenTreeCallArgsAfter(children, nameIndex) {
1007
1488
  }
1008
1489
 
1009
1490
  function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType,
1010
- isPatternShadow, isFlowInvalidated) {
1491
+ isPatternShadow, isFlowInvalidated, context = 'invocation') {
1492
+ const contextKind = typeof context === 'string' ? context : context.kind;
1493
+ const containerMacro = typeof context === 'object' ? context.containerMacro : undefined;
1011
1494
  const children = [];
1012
1495
  for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
1496
+ let lastProducer = null;
1497
+ const macroFields = {
1498
+ inMacro: true,
1499
+ ...(contextKind === 'definition' && { inMacroDefinition: true }),
1500
+ ...(containerMacro && { macroContainer: containerMacro }),
1501
+ };
1013
1502
  for (let i = 0; i < children.length; i++) {
1014
1503
  const tok = children[i];
1015
1504
  if (tok.type === 'token_tree') {
1016
1505
  extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType,
1017
- isPatternShadow, isFlowInvalidated);
1506
+ isPatternShadow, isFlowInvalidated, context);
1018
1507
  continue;
1019
1508
  }
1020
1509
  // `default` is tokenized as the Rust keyword even in the valid
@@ -1029,14 +1518,34 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1029
1518
  // Nested macro invocation: name!(...)
1030
1519
  if (next && next.type === '!' &&
1031
1520
  children[i + 2] && children[i + 2].type === 'token_tree') {
1032
- calls.push({
1521
+ const segments = [];
1522
+ let startNode = tok;
1523
+ let j = i - 1;
1524
+ while (j >= 1 && children[j].type === '::') {
1525
+ const segment = children[j - 1];
1526
+ if (!segment || ![
1527
+ 'identifier', 'metavariable', 'crate', 'self', 'super',
1528
+ ].includes(segment.type)) break;
1529
+ segments.unshift(segment.text === '$crate' ? 'crate' : segment.text);
1530
+ startNode = segment;
1531
+ j -= 2;
1532
+ }
1533
+ const record = {
1033
1534
  name: tok.text,
1034
1535
  line: tok.startPosition.row + 1,
1536
+ callStart: startNode.startIndex,
1537
+ callEnd: children[i + 2].endIndex,
1035
1538
  isMethod: false,
1036
1539
  isMacro: true,
1037
- inMacro: true,
1540
+ ...(segments.length > 0 && {
1541
+ receiver: segments.join('::'),
1542
+ isPathMacro: true,
1543
+ }),
1544
+ ...macroFields,
1038
1545
  enclosingFunction
1039
- });
1546
+ };
1547
+ calls.push(record);
1548
+ lastProducer = record;
1040
1549
  continue;
1041
1550
  }
1042
1551
  const callArgs = _tokenTreeCallArgsAfter(children, i);
@@ -1044,8 +1553,11 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1044
1553
  if (prev && prev.type === '::') {
1045
1554
  // Path call: Type::func(...) / module::sub::func(...) — segments
1046
1555
  // can be identifiers, primitives (char::from), or path keywords
1047
- const isSegment = (n) => n && ['identifier', 'primitive_type', 'self', 'super', 'crate'].includes(n.type);
1556
+ const isSegment = (n) => n && [
1557
+ 'identifier', 'primitive_type', 'metavariable', 'self', 'super', 'crate',
1558
+ ].includes(n.type);
1048
1559
  const segments = [];
1560
+ let startNode = tok;
1049
1561
  let j = i - 1;
1050
1562
  while (j >= 1 && children[j].type === '::') {
1051
1563
  let k = j - 1;
@@ -1069,23 +1581,39 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1069
1581
  k -= 2;
1070
1582
  }
1071
1583
  if (!isSegment(children[k])) break;
1072
- segments.unshift(children[k].text);
1584
+ segments.unshift(children[k].text === '$crate' ? 'crate' : children[k].text);
1585
+ startNode = children[k];
1073
1586
  j = k - 1;
1074
1587
  }
1075
- calls.push({
1588
+ const record = {
1076
1589
  name: tok.text,
1077
1590
  line: tok.startPosition.row + 1,
1591
+ callStart: startNode.startIndex,
1592
+ callEnd: callArgs.endIndex,
1078
1593
  isMethod: segments.length > 0,
1079
1594
  isPathCall: true,
1080
1595
  receiver: segments.length > 0 ? segments.join('::') : undefined,
1081
- inMacro: true,
1596
+ ...macroFields,
1082
1597
  enclosingFunction
1083
- });
1598
+ };
1599
+ calls.push(record);
1600
+ lastProducer = record;
1084
1601
  } else if (prev && prev.type === '.') {
1085
1602
  // Method call: recv.method(...)
1086
1603
  const recvTok = children[i - 2];
1087
1604
  const receiver = recvTok && (recvTok.type === 'identifier' || recvTok.type === 'self')
1088
1605
  ? recvTok.text : undefined;
1606
+ // Token trees flatten `root.field.method(...)`. Retain the same
1607
+ // one-hop field contract as the regular AST path so query-time
1608
+ // analysis can type `args.separator.into_bytes()` from the
1609
+ // declared type of `LowArgs.separator`.
1610
+ let receiverRoot, receiverField;
1611
+ if (receiver && children[i - 3]?.type === '.' &&
1612
+ (children[i - 4]?.type === 'identifier' ||
1613
+ children[i - 4]?.type === 'self')) {
1614
+ receiverRoot = children[i - 4].text;
1615
+ receiverField = receiver;
1616
+ }
1089
1617
  // Literal receivers type as builtins inside macros too (fix #220,
1090
1618
  // ripgrep-measured: assert_eq!(.., vec!["match:fg".parse()...]))
1091
1619
  const litType = recvTok
@@ -1096,26 +1624,47 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1096
1624
  ? getReceiverType(receiver, tok) : litType;
1097
1625
  const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
1098
1626
  const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
1099
- calls.push({
1627
+ const iterationSource = rustIterationSourceOf(tok, receiver);
1628
+ const producer = !receiver && lastProducer &&
1629
+ lastProducer.callEnd === recvTok?.endIndex ? lastProducer : null;
1630
+ const record = {
1100
1631
  name: tok.text,
1101
1632
  line: tok.startPosition.row + 1,
1633
+ callStart: producer?.callStart ?? recvTok?.startIndex ?? tok.startIndex,
1634
+ callEnd: callArgs.endIndex,
1102
1635
  isMethod: true,
1103
- receiver,
1636
+ receiver: receiverField ? undefined : receiver,
1637
+ ...(receiverField && { receiverRoot, receiverField }),
1104
1638
  ...(receiverType && { receiverType }),
1105
1639
  ...(receiverPatternShadow && { receiverPatternShadow: true }),
1106
1640
  ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
1107
- inMacro: true,
1641
+ ...(iterationSource || {}),
1642
+ ...(producer && {
1643
+ receiverCall: producer.name,
1644
+ ...(producer.isMethod && { receiverCallIsMethod: true }),
1645
+ ...(producer.isMacro && { receiverCallIsMacro: true }),
1646
+ receiverCallLine: producer.line,
1647
+ receiverCallStart: producer.callStart,
1648
+ receiverCallEnd: producer.callEnd,
1649
+ }),
1650
+ ...macroFields,
1108
1651
  enclosingFunction
1109
- });
1652
+ };
1653
+ calls.push(record);
1654
+ lastProducer = record;
1110
1655
  } else {
1111
1656
  // Plain call: func(...) — includes enum-variant constructors
1112
- calls.push({
1657
+ const record = {
1113
1658
  name: tok.text,
1114
1659
  line: tok.startPosition.row + 1,
1660
+ callStart: tok.startIndex,
1661
+ callEnd: callArgs.endIndex,
1115
1662
  isMethod: false,
1116
- inMacro: true,
1663
+ ...macroFields,
1117
1664
  enclosingFunction
1118
- });
1665
+ };
1666
+ calls.push(record);
1667
+ lastProducer = record;
1119
1668
  }
1120
1669
  }
1121
1670
  }
@@ -1131,6 +1680,38 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1131
1680
  * Result<T, _>/Option<T> from the producer's return annotation. `let mut x`
1132
1681
  * works too — the pattern field is the plain identifier.
1133
1682
  */
1683
+ function rustDivergingExpression(node) {
1684
+ if (!node) return false;
1685
+ if (['return_expression', 'break_expression', 'continue_expression']
1686
+ .includes(node.type)) return true;
1687
+ if (node.type === 'expression_statement' && node.namedChildCount === 1) {
1688
+ return rustDivergingExpression(node.namedChild(0));
1689
+ }
1690
+ if (node.type !== 'block') return false;
1691
+ const last = node.namedChildCount > 0
1692
+ ? node.namedChild(node.namedChildCount - 1) : null;
1693
+ return rustDivergingExpression(last);
1694
+ }
1695
+
1696
+ function rustMatchCallProducer(matchExpression) {
1697
+ if (matchExpression?.type !== 'match_expression') return null;
1698
+ const body = matchExpression.childForFieldName('body');
1699
+ if (!body) return null;
1700
+ const calls = [];
1701
+ for (let i = 0; i < body.namedChildCount; i++) {
1702
+ const arm = body.namedChild(i);
1703
+ if (arm.type !== 'match_arm') continue;
1704
+ const value = arm.childForFieldName('value');
1705
+ if (rustDivergingExpression(value)) continue;
1706
+ if (value?.type !== 'call_expression') return null;
1707
+ calls.push(value);
1708
+ }
1709
+ if (calls.length === 0) return null;
1710
+ const identities = new Set(calls.map(call =>
1711
+ call.childForFieldName('function')?.text || ''));
1712
+ return identities.size === 1 ? calls : null;
1713
+ }
1714
+
1134
1715
  function rustAssignmentTargetOf(callNode) {
1135
1716
  let n = callNode;
1136
1717
  let p = n.parent;
@@ -1148,12 +1729,43 @@ function rustAssignmentTargetOf(callNode) {
1148
1729
  }
1149
1730
  break;
1150
1731
  }
1732
+ if (p?.type === 'match_arm' &&
1733
+ p.childForFieldName('value')?.id === n.id) {
1734
+ let matchExpression = p.parent;
1735
+ while (matchExpression && matchExpression.type !== 'match_expression') {
1736
+ matchExpression = matchExpression.parent;
1737
+ }
1738
+ const producers = rustMatchCallProducer(matchExpression);
1739
+ if (producers?.some(producer => producer.id === callNode.id)) {
1740
+ const declaration = matchExpression.parent;
1741
+ if (declaration?.type === 'let_declaration' &&
1742
+ declaration.childForFieldName('value')?.id === matchExpression.id) {
1743
+ const pattern = declaration.childForFieldName('pattern');
1744
+ if (pattern?.type === 'identifier') {
1745
+ return { assignedTo: pattern.text };
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1151
1750
  if (p.type === 'let_declaration') {
1152
1751
  const value = p.childForFieldName('value');
1153
1752
  const pattern = p.childForFieldName('pattern');
1154
1753
  if (value && value.id === n.id && pattern?.type === 'identifier') {
1155
1754
  return { assignedTo: pattern.text, ...(unwrapped && { unwrapped: true }) };
1156
1755
  }
1756
+ if (value && value.id === n.id && pattern?.type === 'tuple_pattern') {
1757
+ const bindings = pattern.namedChildren
1758
+ .filter(child => child.type === 'identifier')
1759
+ .map(child => child.text);
1760
+ if (bindings.length === pattern.namedChildCount && bindings.length > 0) {
1761
+ return {
1762
+ assignedTo: bindings[0],
1763
+ tuple: true,
1764
+ ...(bindings.length > 1 && { tupleRest: bindings.slice(1) }),
1765
+ ...(unwrapped && { unwrapped: true }),
1766
+ };
1767
+ }
1768
+ }
1157
1769
  return undefined;
1158
1770
  }
1159
1771
  if (p.type === 'assignment_expression') {
@@ -1166,6 +1778,170 @@ function rustAssignmentTargetOf(callNode) {
1166
1778
  return undefined;
1167
1779
  }
1168
1780
 
1781
+ function rustCallIdentity(callNode) {
1782
+ if (!callNode || callNode.type !== 'call_expression') return null;
1783
+ const fn = callNode.childForFieldName('function');
1784
+ if (!fn) return null;
1785
+ if (fn.type === 'identifier') {
1786
+ return { name: fn.text, isMethod: false };
1787
+ }
1788
+ if (fn.type === 'scoped_identifier' || fn.type === 'generic_function') {
1789
+ const parts = fn.text.split('::').filter(Boolean);
1790
+ const name = parts.pop()?.replace(/::<.*$/, '');
1791
+ return name ? { name, isMethod: false } : null;
1792
+ }
1793
+ if (fn.type === 'field_expression') {
1794
+ const field = fn.childForFieldName('field');
1795
+ return field ? { name: field.text, isMethod: true } : null;
1796
+ }
1797
+ return null;
1798
+ }
1799
+
1800
+ /**
1801
+ * If this receiver is a for-loop binding, retain the exact iterator-source
1802
+ * call so query-time analysis can apply its declared Item contract.
1803
+ */
1804
+ function rustIterationSourceOf(node, receiver) {
1805
+ if (!receiver) return null;
1806
+ let current = node.parent;
1807
+ while (current) {
1808
+ if (current.type === 'for_expression') {
1809
+ const pattern = current.childForFieldName('pattern');
1810
+ const value = current.childForFieldName('value');
1811
+ if (pattern?.type === 'identifier' && pattern.text === receiver) {
1812
+ if (value?.type === 'identifier') {
1813
+ return { receiverIterationVariable: value.text };
1814
+ }
1815
+ if (value?.type === 'call_expression') {
1816
+ const identity = rustCallIdentity(value);
1817
+ if (!identity) return null;
1818
+ return {
1819
+ receiverIterationCall: identity.name,
1820
+ ...(identity.isMethod && { receiverIterationCallIsMethod: true }),
1821
+ receiverIterationCallLine: value.startPosition.row + 1,
1822
+ receiverIterationCallStart: value.startIndex,
1823
+ receiverIterationCallEnd: value.endIndex,
1824
+ };
1825
+ }
1826
+ }
1827
+ }
1828
+ current = current.parent;
1829
+ }
1830
+ return null;
1831
+ }
1832
+
1833
+ /**
1834
+ * Retain the enum-variant contract that binds a match-arm receiver:
1835
+ * `DirEntryInner::Raw(ref entry) => entry.path()`. The variant's indexed
1836
+ * payload type is resolved query-time, where cross-file identity is known.
1837
+ * Destructured/nested payloads abstain unless the receiver is the whole
1838
+ * positional field.
1839
+ */
1840
+ function rustPatternBindingOf(node, receiver) {
1841
+ if (!receiver) return null;
1842
+ const directBindingName = pattern => {
1843
+ if (!pattern) return null;
1844
+ if (pattern.type === 'identifier') return pattern.text;
1845
+ if (!['ref_pattern', 'mut_pattern', 'reference_pattern']
1846
+ .includes(pattern.type)) return null;
1847
+ const identifiers = [];
1848
+ const pending = [pattern];
1849
+ while (pending.length > 0) {
1850
+ const current = pending.pop();
1851
+ if (current.type === 'identifier') {
1852
+ identifiers.push(current.text);
1853
+ continue;
1854
+ }
1855
+ if (current !== pattern &&
1856
+ ['tuple_pattern', 'tuple_struct_pattern', 'struct_pattern']
1857
+ .includes(current.type)) {
1858
+ return null;
1859
+ }
1860
+ for (let i = 0; i < current.namedChildCount; i++) {
1861
+ pending.push(current.namedChild(i));
1862
+ }
1863
+ }
1864
+ return identifiers.length === 1 ? identifiers[0] : null;
1865
+ };
1866
+
1867
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
1868
+ if (ancestor.type === 'function_item') break;
1869
+ if (ancestor.type === 'closure_expression') {
1870
+ const params = ancestor.childForFieldName('parameters');
1871
+ if (params && patternContainsIdentifier(params, receiver)) break;
1872
+ continue; // captured binding from an outer match arm
1873
+ }
1874
+ if (ancestor.type !== 'match_arm') continue;
1875
+ let matchExpression = ancestor.parent;
1876
+ while (matchExpression && matchExpression.type !== 'match_expression' &&
1877
+ matchExpression.type !== 'function_item') {
1878
+ matchExpression = matchExpression.parent;
1879
+ }
1880
+ const matchValue = matchExpression?.type === 'match_expression'
1881
+ ? matchExpression.childForFieldName('value')
1882
+ : null;
1883
+ const source = matchValue?.type === 'identifier'
1884
+ ? { receiverPatternSourceVariable: matchValue.text }
1885
+ : {};
1886
+ const root = ancestor.childForFieldName('pattern');
1887
+ const pending = root ? [root] : [];
1888
+ while (pending.length > 0) {
1889
+ const pattern = pending.pop();
1890
+ if (pattern.type === 'tuple_struct_pattern') {
1891
+ const typeNode = pattern.childForFieldName('type');
1892
+ const positional = pattern.namedChildren
1893
+ .filter(child => !typeNode || child.id !== typeNode.id);
1894
+ for (let i = 0; i < positional.length; i++) {
1895
+ if (directBindingName(positional[i]) !== receiver) continue;
1896
+ const pathText = typeNode?.text;
1897
+ if (!pathText) return null;
1898
+ const segments = pathText.split('::').filter(Boolean);
1899
+ const variant = segments.pop();
1900
+ if (!variant) return null;
1901
+ return {
1902
+ receiverPatternVariant: variant,
1903
+ receiverPatternIndex: i,
1904
+ ...source,
1905
+ ...(segments.length > 0 && {
1906
+ receiverPatternOwner: segments.join('::'),
1907
+ }),
1908
+ };
1909
+ }
1910
+ }
1911
+ for (let i = 0; i < pattern.namedChildCount; i++) {
1912
+ pending.push(pattern.namedChild(i));
1913
+ }
1914
+ }
1915
+ return null;
1916
+ }
1917
+ return null;
1918
+ }
1919
+
1920
+ function patternContainsIdentifier(pattern, name) {
1921
+ const pending = pattern ? [pattern] : [];
1922
+ while (pending.length > 0) {
1923
+ const current = pending.pop();
1924
+ if (current.type === 'identifier' && current.text === name) return true;
1925
+ for (let i = 0; i < current.namedChildCount; i++) {
1926
+ pending.push(current.namedChild(i));
1927
+ }
1928
+ }
1929
+ return false;
1930
+ }
1931
+
1932
+ function rustMacroCallIdentity(macroNode) {
1933
+ if (!macroNode) return null;
1934
+ const parts = macroNode.text.replace(/!$/, '').split('::').filter(Boolean);
1935
+ const name = parts.pop();
1936
+ if (!name) return null;
1937
+ return {
1938
+ name,
1939
+ ...(parts.length > 0 && {
1940
+ receiver: parts.map(part => part === '$crate' ? 'crate' : part).join('::'),
1941
+ }),
1942
+ };
1943
+ }
1944
+
1169
1945
  function findCallsInCode(code, parser) {
1170
1946
  const tree = parseTree(parser, code);
1171
1947
  const calls = [];
@@ -1210,7 +1986,10 @@ function findCallsInCode(code, parser) {
1210
1986
  // Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
1211
1987
  const extractTypeName = (typeNode) => {
1212
1988
  if (!typeNode) return null;
1213
- if (typeNode.type === 'type_identifier') return typeNode.text;
1989
+ if (typeNode.type === 'type_identifier' ||
1990
+ typeNode.type === 'primitive_type') {
1991
+ return typeNode.text;
1992
+ }
1214
1993
  if (typeNode.type === 'reference_type') {
1215
1994
  // &Filter or &mut Filter -> Filter
1216
1995
  for (let i = 0; i < typeNode.namedChildCount; i++) {
@@ -1218,6 +1997,12 @@ function findCallsInCode(code, parser) {
1218
1997
  if (r) return r;
1219
1998
  }
1220
1999
  }
2000
+ if (typeNode.type === 'abstract_type' || typeNode.type === 'dynamic_type') {
2001
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
2002
+ const r = extractTypeName(typeNode.namedChild(i));
2003
+ if (r) return r;
2004
+ }
2005
+ }
1221
2006
  if (typeNode.type === 'generic_type') {
1222
2007
  // Box<Filter> -> Filter (or get the outer type)
1223
2008
  return extractTypeName(typeNode.namedChild(0));
@@ -1230,22 +2015,74 @@ function findCallsInCode(code, parser) {
1230
2015
  return null;
1231
2016
  };
1232
2017
 
2018
+ const extractTypeQualifier = (typeNode) => {
2019
+ if (!typeNode) return null;
2020
+ if (typeNode.type === 'reference_type') {
2021
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
2022
+ const qualifier = extractTypeQualifier(typeNode.namedChild(i));
2023
+ if (qualifier) return qualifier;
2024
+ }
2025
+ return null;
2026
+ }
2027
+ if (typeNode.type === 'scoped_type_identifier') {
2028
+ const nameNode = typeNode.childForFieldName('name');
2029
+ const text = typeNode.text;
2030
+ const suffix = nameNode ? `::${nameNode.text}` : '';
2031
+ return suffix && text.endsWith(suffix)
2032
+ ? text.slice(0, -suffix.length) : null;
2033
+ }
2034
+ return null;
2035
+ };
2036
+
1233
2037
  // Build type map from function parameters (including self receiver for impl methods)
1234
2038
  const buildScopeTypeMap = (node) => {
1235
2039
  const typeMap = new Map();
2040
+ typeMap.qualifiers = new Map();
2041
+ typeMap.iteratorItems = new Map();
2042
+ typeMap.annotationTexts = new Map();
2043
+ typeMap.boundNames = new Set();
2044
+ const retainBoundNames = (pattern) => {
2045
+ if (!pattern) return;
2046
+ const pending = [pattern];
2047
+ while (pending.length > 0) {
2048
+ const current = pending.pop();
2049
+ if (current.type === 'identifier') {
2050
+ typeMap.boundNames.add(current.text);
2051
+ continue;
2052
+ }
2053
+ for (let i = 0; i < current.namedChildCount; i++) {
2054
+ pending.push(current.namedChild(i));
2055
+ }
2056
+ }
2057
+ };
1236
2058
  const paramsNode = node.childForFieldName('parameters');
1237
2059
  if (paramsNode) {
1238
2060
  for (let i = 0; i < paramsNode.namedChildCount; i++) {
1239
2061
  const param = paramsNode.namedChild(i);
1240
2062
  if (param.type === 'parameter') {
1241
2063
  const patternNode = param.childForFieldName('pattern');
2064
+ retainBoundNames(patternNode);
1242
2065
  const typeNode = param.childForFieldName('type');
1243
2066
  const typeName = extractTypeName(typeNode);
2067
+ const qualifier = extractTypeQualifier(typeNode);
2068
+ const iteratorItem = extractRustIteratorItemTypeFromTypeNode(typeNode);
1244
2069
  if (patternNode && typeName) {
1245
2070
  // Pattern can be identifier or _
1246
2071
  const name = patternNode.type === 'identifier' ? patternNode.text : null;
1247
- if (name) typeMap.set(name, typeName);
2072
+ if (name) {
2073
+ typeMap.set(name, typeName);
2074
+ typeMap.annotationTexts.set(name, typeNode.text);
2075
+ if (qualifier) typeMap.qualifiers.set(name, qualifier);
2076
+ if (iteratorItem) typeMap.iteratorItems.set(name, iteratorItem);
2077
+ }
1248
2078
  }
2079
+ } else {
2080
+ // Closure parameters normally have no annotation. They
2081
+ // still bind the name and must stop lookup before an
2082
+ // identically-named outer parameter (`arg: &str`;
2083
+ // `.any(|arg| arg.method())`) leaks its unrelated type
2084
+ // into the closure call record.
2085
+ retainBoundNames(param);
1249
2086
  }
1250
2087
  }
1251
2088
  }
@@ -1267,7 +2104,10 @@ function findCallsInCode(code, parser) {
1267
2104
  // Helper to get current enclosing function
1268
2105
  const getCurrentEnclosingFunction = () => {
1269
2106
  return functionStack.length > 0
1270
- ? { ...functionStack[functionStack.length - 1] }
2107
+ ? {
2108
+ ...functionStack[functionStack.length - 1],
2109
+ scopeChain: functionStack.map(scope => scope.startLine),
2110
+ }
1271
2111
  : null;
1272
2112
  };
1273
2113
 
@@ -1305,10 +2145,11 @@ function findCallsInCode(code, parser) {
1305
2145
  while (n && ['try_expression', 'await_expression', 'parenthesized_expression'].includes(n.type)) {
1306
2146
  n = n.namedChildCount === 1 ? n.namedChild(0) : null;
1307
2147
  }
1308
- return n?.type === 'call_expression';
2148
+ return n?.type === 'call_expression' || n?.type === 'macro_invocation' ||
2149
+ !!rustMatchCallProducer(n);
1309
2150
  };
1310
2151
 
1311
- const flowInvalidatedAt = (node, varName) => {
2152
+ const flowEventAt = (node, varName) => {
1312
2153
  const pos = node?.startIndex ?? -1;
1313
2154
  for (let i = functionStack.length - 1; i >= 0; i--) {
1314
2155
  const byName = scopeFlowEvents.get(functionStack[i].startLine);
@@ -1319,21 +2160,125 @@ function findCallsInCode(code, parser) {
1319
2160
  if (event.at <= pos && pos <= event.until &&
1320
2161
  (!latest || event.at > latest.at)) latest = event;
1321
2162
  }
1322
- if (latest) return latest.invalidated;
2163
+ if (latest) return latest;
1323
2164
  }
1324
- return false;
2165
+ return null;
1325
2166
  };
1326
2167
 
2168
+ const flowInvalidatedAt = (node, varName) =>
2169
+ !!flowEventAt(node, varName)?.invalidated;
2170
+
1327
2171
  // Look up variable type from scope chain
1328
2172
  const getReceiverType = (varName, atNode) => {
1329
2173
  if (atNode && patternShadowsAt(atNode, varName)) return undefined;
2174
+ const flow = flowEventAt(atNode, varName);
2175
+ if (flow?.type) return flow.type;
1330
2176
  for (let i = functionStack.length - 1; i >= 0; i--) {
1331
2177
  const typeMap = scopeTypes.get(functionStack[i].startLine);
1332
2178
  if (typeMap?.has(varName)) return typeMap.get(varName);
2179
+ if (typeMap?.boundNames?.has(varName)) return undefined;
2180
+ }
2181
+ return undefined;
2182
+ };
2183
+
2184
+ const getReceiverTypeQualifier = (varName, atNode) => {
2185
+ if (atNode && patternShadowsAt(atNode, varName)) return undefined;
2186
+ const flow = flowEventAt(atNode, varName);
2187
+ if (flow?.qualifier) return flow.qualifier;
2188
+ for (let i = functionStack.length - 1; i >= 0; i--) {
2189
+ const typeMap = scopeTypes.get(functionStack[i].startLine);
2190
+ if (typeMap?.qualifiers?.has(varName)) {
2191
+ return typeMap.qualifiers.get(varName);
2192
+ }
2193
+ if (typeMap?.boundNames?.has(varName)) return undefined;
2194
+ }
2195
+ return undefined;
2196
+ };
2197
+
2198
+ const getReceiverIteratorItemType = (varName, atNode) => {
2199
+ if (atNode && patternShadowsAt(atNode, varName)) return undefined;
2200
+ for (let i = functionStack.length - 1; i >= 0; i--) {
2201
+ const typeMap = scopeTypes.get(functionStack[i].startLine);
2202
+ if (typeMap?.iteratorItems?.has(varName)) {
2203
+ return typeMap.iteratorItems.get(varName);
2204
+ }
2205
+ if (typeMap?.boundNames?.has(varName)) return undefined;
1333
2206
  }
1334
2207
  return undefined;
1335
2208
  };
1336
2209
 
2210
+ const getReceiverAnnotationText = (varName, atNode) => {
2211
+ if (atNode && patternShadowsAt(atNode, varName)) return undefined;
2212
+ for (let i = functionStack.length - 1; i >= 0; i--) {
2213
+ const typeMap = scopeTypes.get(functionStack[i].startLine);
2214
+ if (typeMap?.annotationTexts?.has(varName)) {
2215
+ return typeMap.annotationTexts.get(varName);
2216
+ }
2217
+ if (typeMap?.boundNames?.has(varName)) return undefined;
2218
+ }
2219
+ return undefined;
2220
+ };
2221
+
2222
+ const matchBindingType = (value, atNode) => {
2223
+ if (value?.type !== 'match_expression') return null;
2224
+ const source = value.childForFieldName('value');
2225
+ if (source?.type !== 'identifier') return null;
2226
+ const sourceType = getReceiverAnnotationText(source.text, atNode);
2227
+ if (!sourceType) return null;
2228
+ const body = value.childForFieldName('body');
2229
+ if (!body) return null;
2230
+ let variant = null;
2231
+ let binding = null;
2232
+ for (let i = 0; i < body.namedChildCount; i++) {
2233
+ const arm = body.namedChild(i);
2234
+ if (arm.type !== 'match_arm') continue;
2235
+ const armValue = arm.childForFieldName('value');
2236
+ if (['return_expression', 'break_expression', 'continue_expression']
2237
+ .includes(armValue?.type)) {
2238
+ continue;
2239
+ }
2240
+ if (armValue?.type !== 'identifier') return null;
2241
+ const pattern = arm.childForFieldName('pattern');
2242
+ const tuples = [];
2243
+ const pending = pattern ? [pattern] : [];
2244
+ while (pending.length > 0) {
2245
+ const current = pending.pop();
2246
+ if (current.type === 'tuple_struct_pattern') tuples.push(current);
2247
+ for (let j = 0; j < current.namedChildCount; j++) {
2248
+ pending.push(current.namedChild(j));
2249
+ }
2250
+ }
2251
+ const tuple = tuples.find(candidate =>
2252
+ candidate.namedChildren.some(child =>
2253
+ child.type === 'identifier' && child.text === armValue.text));
2254
+ const typeNode = tuple?.childForFieldName('type');
2255
+ if (!tuple || !typeNode) return null;
2256
+ const parts = typeNode.text.split('::').filter(Boolean);
2257
+ const currentVariant = parts.pop();
2258
+ if (!currentVariant || (variant && variant !== currentVariant) ||
2259
+ (binding && binding !== armValue.text)) {
2260
+ return null;
2261
+ }
2262
+ variant = currentVariant;
2263
+ binding = armValue.text;
2264
+ }
2265
+ if (!variant || !binding) return null;
2266
+ const wrapper = sourceType.trim().match(
2267
+ /^(?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*)*(Option|Result)\s*<(.*)>$/s);
2268
+ if (!wrapper) return null;
2269
+ const args = wrapper[2].split(',');
2270
+ const raw = args[variant === 'Err' ? 1 : 0]?.trim();
2271
+ if (!raw || [...'<>()[]'].some(character => raw.includes(character))) {
2272
+ return null;
2273
+ }
2274
+ const match = raw.match(/^(?:(.*)::)?([A-Za-z_][A-Za-z0-9_]*)$/);
2275
+ if (!match) return null;
2276
+ return {
2277
+ type: match[2],
2278
+ ...(match[1] && { qualifier: match[1] }),
2279
+ };
2280
+ };
2281
+
1337
2282
  // Walk up to the enclosing impl block's target type (impl<T> Foo<T> → Foo).
1338
2283
  const findEnclosingImplType = (n) => {
1339
2284
  for (let p = n.parent; p; p = p.parent) {
@@ -1345,13 +2290,96 @@ function findCallsInCode(code, parser) {
1345
2290
  return undefined;
1346
2291
  };
1347
2292
 
2293
+ const closureContractSource = (node) => {
2294
+ if (node.type !== 'closure_expression') return null;
2295
+ const argumentsNode = node.parent;
2296
+ const outerCall = argumentsNode?.type === 'arguments'
2297
+ ? argumentsNode.parent : null;
2298
+ if (outerCall?.type !== 'call_expression') return null;
2299
+ let argumentIndex = 0;
2300
+ let found = false;
2301
+ for (let i = 0; i < argumentsNode.namedChildCount; i++) {
2302
+ const argument = argumentsNode.namedChild(i);
2303
+ if (argument.type.endsWith('comment')) continue;
2304
+ if (argument.id === node.id) {
2305
+ found = true;
2306
+ break;
2307
+ }
2308
+ argumentIndex++;
2309
+ }
2310
+ if (!found) return null;
2311
+ let functionNode = outerCall.childForFieldName('function');
2312
+ if (functionNode?.type === 'generic_function') {
2313
+ functionNode = functionNode.childForFieldName('function') || functionNode;
2314
+ }
2315
+ let callName;
2316
+ let callIsMethod = false;
2317
+ if (functionNode?.type === 'field_expression') {
2318
+ callName = functionNode.childForFieldName('field')?.text;
2319
+ callIsMethod = true;
2320
+ } else if (functionNode?.type === 'identifier') {
2321
+ callName = functionNode.text;
2322
+ } else if (functionNode?.type === 'scoped_identifier') {
2323
+ callName = functionNode.childForFieldName('name')?.text;
2324
+ callIsMethod = true;
2325
+ }
2326
+ if (!callName) return null;
2327
+ const parameters = node.childForFieldName('parameters');
2328
+ const parameterNames = [];
2329
+ let parametersComplete = true;
2330
+ if (parameters) {
2331
+ for (let i = 0; i < parameters.namedChildCount; i++) {
2332
+ const parameter = parameters.namedChild(i);
2333
+ if (parameter.type === 'identifier') {
2334
+ parameterNames.push(parameter.text);
2335
+ continue;
2336
+ }
2337
+ // `|ref a, ref b|` and `|mut value|` bind the same callback
2338
+ // parameter as their identifier child. Destructuring patterns
2339
+ // intentionally abstain: a tuple member is not the callback's
2340
+ // whole declared type.
2341
+ if (['ref_pattern', 'mut_pattern', 'reference_pattern']
2342
+ .includes(parameter.type)) {
2343
+ const identifiers = [];
2344
+ const pending = [parameter];
2345
+ while (pending.length > 0) {
2346
+ const current = pending.pop();
2347
+ if (current.type === 'identifier') {
2348
+ identifiers.push(current.text);
2349
+ continue;
2350
+ }
2351
+ for (let j = 0; j < current.namedChildCount; j++) {
2352
+ pending.push(current.namedChild(j));
2353
+ }
2354
+ }
2355
+ if (identifiers.length === 1) {
2356
+ parameterNames.push(identifiers[0]);
2357
+ continue;
2358
+ }
2359
+ }
2360
+ parametersComplete = false;
2361
+ break;
2362
+ }
2363
+ }
2364
+ if (!parametersComplete || parameterNames.length === 0) return null;
2365
+ return {
2366
+ closureSourceCall: callName,
2367
+ closureSourceCallStart: outerCall.startIndex,
2368
+ closureSourceCallEnd: outerCall.endIndex,
2369
+ closureSourceCallIsMethod: callIsMethod,
2370
+ closureArgumentIndex: argumentIndex,
2371
+ closureParameterNames: parameterNames,
2372
+ };
2373
+ };
2374
+
1348
2375
  traverseTree(tree.rootNode, (node) => {
1349
2376
  // Track function entry
1350
2377
  if (isFunctionNode(node)) {
1351
2378
  const entry = {
1352
2379
  name: extractFunctionName(node),
1353
2380
  startLine: node.startPosition.row + 1,
1354
- endLine: node.endPosition.row + 1
2381
+ endLine: node.endPosition.row + 1,
2382
+ ...closureContractSource(node),
1355
2383
  };
1356
2384
  functionStack.push(entry);
1357
2385
  scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
@@ -1382,7 +2410,12 @@ function findCallsInCode(code, parser) {
1382
2410
  byName.get(pattern.text).push({
1383
2411
  at: node.endIndex,
1384
2412
  until,
1385
- invalidated: !valueHasFlowProducer(value),
2413
+ ...(() => {
2414
+ const inferred = matchBindingType(value, node);
2415
+ return inferred
2416
+ ? { invalidated: false, ...inferred }
2417
+ : { invalidated: !valueHasFlowProducer(value) };
2418
+ })(),
1386
2419
  });
1387
2420
  }
1388
2421
  }
@@ -1394,6 +2427,7 @@ function findCallsInCode(code, parser) {
1394
2427
  if (!funcNode) return true;
1395
2428
 
1396
2429
  // Unwrap turbofish: parse::<i32>() has generic_function wrapping the actual function
2430
+ const collectResult = extractCollectResultContract(funcNode);
1397
2431
  if (funcNode.type === 'generic_function') {
1398
2432
  funcNode = funcNode.childForFieldName('function') || funcNode;
1399
2433
  }
@@ -1423,10 +2457,14 @@ function findCallsInCode(code, parser) {
1423
2457
  calls.push({
1424
2458
  name: funcNode.text,
1425
2459
  line: node.startPosition.row + 1,
2460
+ callStart: node.startIndex,
2461
+ callEnd: node.endIndex,
1426
2462
  isMethod: false,
1427
2463
  argCount,
1428
2464
  ...(assigned && { assignedTo: assigned.assignedTo }),
1429
2465
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
2466
+ ...(assigned?.tuple && { assignedTuple: true }),
2467
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
1430
2468
  enclosingFunction,
1431
2469
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1432
2470
  });
@@ -1437,6 +2475,18 @@ function findCallsInCode(code, parser) {
1437
2475
 
1438
2476
  if (fieldNode) {
1439
2477
  let receiver = (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined;
2478
+ // A range index preserves the receiver's collection/slice
2479
+ // type (`doc[start..].find(...)` still dispatches on
2480
+ // `str`). A single-element index does NOT: `items[i]`
2481
+ // dispatches on the element type, so only the
2482
+ // range-expression shape may reuse the root binding.
2483
+ if (!receiver && valueNode?.type === 'index_expression' &&
2484
+ valueNode.namedChild(1)?.type === 'range_expression') {
2485
+ const indexed = valueNode.namedChild(0);
2486
+ if (indexed?.type === 'identifier' || indexed?.type === 'self') {
2487
+ receiver = indexed.text;
2488
+ }
2489
+ }
1440
2490
  // Detect chained Router::new()-rooted method calls. axum's canonical
1441
2491
  // idiom is `Router::new().route("/p", get(h)).route(...)` where the
1442
2492
  // receiver of `.route(...)` is itself a call_expression. Walk the
@@ -1461,7 +2511,8 @@ function findCallsInCode(code, parser) {
1461
2511
  // low.sep.into_bytes() — with .clone() transparency (clone()
1462
2512
  // returns Self by stdlib convention). receiverRoot/Field/RootType
1463
2513
  // let findCallers hop to the field's declared type cross-file.
1464
- let receiverRoot, receiverField, receiverRootType;
2514
+ let receiverRoot, receiverField, receiverFields, receiverRootType;
2515
+ let receiverFieldCallRoot;
1465
2516
  if (!receiver) {
1466
2517
  let obj = valueNode;
1467
2518
  while (obj?.type === 'call_expression') {
@@ -1472,15 +2523,32 @@ function findCallsInCode(code, parser) {
1472
2523
  } else break;
1473
2524
  }
1474
2525
  if (obj?.type === 'field_expression') {
1475
- const rootNode = obj.childForFieldName('value');
1476
- const fldNode = obj.childForFieldName('field');
1477
- if (fldNode?.type === 'field_identifier' && rootNode &&
2526
+ const fields = [];
2527
+ let rootNode = obj;
2528
+ while (rootNode?.type === 'field_expression') {
2529
+ const fldNode = rootNode.childForFieldName('field');
2530
+ if (!fldNode || ![
2531
+ 'field_identifier', 'integer_literal',
2532
+ ].includes(fldNode.type)) {
2533
+ fields.length = 0;
2534
+ break;
2535
+ }
2536
+ fields.unshift(fldNode.text);
2537
+ rootNode = rootNode.childForFieldName('value');
2538
+ }
2539
+ if (fields.length > 0 && rootNode &&
1478
2540
  (rootNode.type === 'identifier' || rootNode.type === 'self')) {
1479
2541
  receiverRoot = rootNode.text;
1480
- receiverField = fldNode.text;
2542
+ receiverFields = fields;
2543
+ receiverField = fields[fields.length - 1];
1481
2544
  receiverRootType = rootNode.type === 'self'
1482
2545
  ? findEnclosingImplType(node)
1483
2546
  : getReceiverType(rootNode.text, node);
2547
+ } else if (fields.length > 0 &&
2548
+ rootNode?.type === 'call_expression') {
2549
+ receiverFields = fields;
2550
+ receiverField = fields[fields.length - 1];
2551
+ receiverFieldCallRoot = rootNode;
1484
2552
  }
1485
2553
  } else if (obj && obj !== valueNode &&
1486
2554
  (obj.type === 'identifier' || obj.type === 'self')) {
@@ -1500,6 +2568,7 @@ function findCallsInCode(code, parser) {
1500
2568
  // rooted chains keep their synthetic receiver marker for
1501
2569
  // the bridge but get the link too.
1502
2570
  let receiverCall, receiverCallIsMethod, receiverCallLine;
2571
+ let receiverCallStart, receiverCallEnd;
1503
2572
  if ((!receiver || receiverIsChainRoot) && !receiverField &&
1504
2573
  valueNode?.type === 'call_expression') {
1505
2574
  let prodFunc = valueNode.childForFieldName('function');
@@ -1509,12 +2578,16 @@ function findCallsInCode(code, parser) {
1509
2578
  if (prodFunc?.type === 'identifier') {
1510
2579
  receiverCall = prodFunc.text;
1511
2580
  receiverCallLine = valueNode.startPosition.row + 1;
2581
+ receiverCallStart = valueNode.startIndex;
2582
+ receiverCallEnd = valueNode.endIndex;
1512
2583
  } else if (prodFunc?.type === 'field_expression') {
1513
2584
  const pf = prodFunc.childForFieldName('field');
1514
2585
  if (pf) {
1515
2586
  receiverCall = pf.text;
1516
2587
  receiverCallIsMethod = true;
1517
2588
  receiverCallLine = pf.startPosition.row + 1;
2589
+ receiverCallStart = valueNode.startIndex;
2590
+ receiverCallEnd = valueNode.endIndex;
1518
2591
  }
1519
2592
  } else if (prodFunc?.type === 'scoped_identifier') {
1520
2593
  // Path producer: Command::new(...).arg(...) — the
@@ -1527,8 +2600,41 @@ function findCallsInCode(code, parser) {
1527
2600
  receiverCall = prodName;
1528
2601
  receiverCallIsMethod = true;
1529
2602
  receiverCallLine = valueNode.startPosition.row + 1;
2603
+ receiverCallStart = valueNode.startIndex;
2604
+ receiverCallEnd = valueNode.endIndex;
1530
2605
  }
1531
2606
  }
2607
+ } else if ((!receiver || receiverIsChainRoot) && !receiverField &&
2608
+ valueNode?.type === 'macro_invocation') {
2609
+ const macro = rustMacroCallIdentity(
2610
+ valueNode.childForFieldName('macro'));
2611
+ if (macro) {
2612
+ receiverCall = macro.name;
2613
+ receiverCallLine = valueNode.startPosition.row + 1;
2614
+ receiverCallStart = valueNode.startIndex;
2615
+ receiverCallEnd = valueNode.endIndex;
2616
+ }
2617
+ }
2618
+ if (!receiverCall && receiverFieldCallRoot) {
2619
+ let prodFunc = receiverFieldCallRoot.childForFieldName('function');
2620
+ if (prodFunc?.type === 'generic_function') {
2621
+ prodFunc = prodFunc.childForFieldName('function') || prodFunc;
2622
+ }
2623
+ if (prodFunc?.type === 'identifier') {
2624
+ receiverCall = prodFunc.text;
2625
+ } else if (prodFunc?.type === 'field_expression') {
2626
+ receiverCall = prodFunc.childForFieldName('field')?.text;
2627
+ receiverCallIsMethod = !!receiverCall;
2628
+ } else if (prodFunc?.type === 'scoped_identifier') {
2629
+ const segments = prodFunc.text.split('::');
2630
+ receiverCall = segments[segments.length - 1];
2631
+ receiverCallIsMethod = !!receiverCall;
2632
+ }
2633
+ if (receiverCall) {
2634
+ receiverCallLine = receiverFieldCallRoot.startPosition.row + 1;
2635
+ receiverCallStart = receiverFieldCallRoot.startIndex;
2636
+ receiverCallEnd = receiverFieldCallRoot.endIndex;
2637
+ }
1532
2638
  }
1533
2639
  // Literal receivers carry their builtin type (fix #220,
1534
2640
  // ripgrep-measured): "match:fg:magenta".parse() is
@@ -1541,8 +2647,23 @@ function findCallsInCode(code, parser) {
1541
2647
  const receiverType = (receiver && receiver !== 'self' && !receiverIsChainRoot)
1542
2648
  ? getReceiverType(receiver, node)
1543
2649
  : literalReceiverType;
2650
+ const receiverTypeQualifier = receiver && receiverType
2651
+ ? getReceiverTypeQualifier(receiver, node)
2652
+ : undefined;
2653
+ const receiverIteratorItemType = receiver
2654
+ ? getReceiverIteratorItemType(receiver, node)
2655
+ : undefined;
1544
2656
  const receiverPatternShadow = !!(receiver && patternShadowsAt(node, receiver));
2657
+ const receiverPatternBinding = rustPatternBindingOf(node, receiver);
2658
+ if (receiverPatternBinding?.receiverPatternSourceVariable) {
2659
+ const sourceType = getReceiverAnnotationText(
2660
+ receiverPatternBinding.receiverPatternSourceVariable, node);
2661
+ if (sourceType) {
2662
+ receiverPatternBinding.receiverPatternSourceType = sourceType;
2663
+ }
2664
+ }
1545
2665
  const receiverFlowInvalidated = !!(receiver && flowInvalidatedAt(node, receiver));
2666
+ const iterationSource = rustIterationSourceOf(node, receiver);
1546
2667
  const firstArg = getFirstStringArg(node);
1547
2668
  // RUST-2: For chained calls like `a().b().parse::<T>().ok()`,
1548
2669
  // each method should report the line where its OWN identifier
@@ -1552,20 +2673,38 @@ function findCallsInCode(code, parser) {
1552
2673
  calls.push({
1553
2674
  name: fieldNode.text,
1554
2675
  line: fieldNode.startPosition.row + 1,
2676
+ callStart: node.startIndex,
2677
+ callEnd: node.endIndex,
1555
2678
  isMethod: true,
1556
2679
  receiver,
1557
2680
  ...(receiverType && { receiverType }),
2681
+ ...(receiverTypeQualifier && { receiverTypeQualifier }),
2682
+ ...(receiverIteratorItemType && { receiverIteratorItemType }),
1558
2683
  ...(receiverPatternShadow && { receiverPatternShadow: true }),
2684
+ ...(receiverPatternBinding || {}),
1559
2685
  ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
2686
+ ...(iterationSource || {}),
1560
2687
  ...(receiverIsChainRoot && { receiverIsChainRoot: true }),
1561
2688
  ...(receiverField && { receiverRoot, receiverField }),
2689
+ ...(receiverFields?.length > 1 && { receiverFields }),
1562
2690
  ...(receiverField && receiverRootType && { receiverRootType }),
1563
2691
  ...(receiverCall && { receiverCall }),
1564
2692
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
2693
+ ...(valueNode?.type === 'macro_invocation' && receiverCall && {
2694
+ receiverCallIsMacro: true,
2695
+ }),
1565
2696
  ...(receiverCallLine && { receiverCallLine }),
2697
+ ...(receiverCallStart != null && { receiverCallStart }),
2698
+ ...(receiverCallEnd != null && { receiverCallEnd }),
1566
2699
  argCount,
1567
2700
  ...(assigned && { assignedTo: assigned.assignedTo }),
1568
2701
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
2702
+ ...(assigned?.tuple && { assignedTuple: true }),
2703
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
2704
+ ...(collectResult && {
2705
+ explicitResultType: collectResult.type,
2706
+ explicitResultItemType: collectResult.itemType,
2707
+ }),
1569
2708
  enclosingFunction,
1570
2709
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1571
2710
  });
@@ -1585,12 +2724,16 @@ function findCallsInCode(code, parser) {
1585
2724
  calls.push({
1586
2725
  name: name,
1587
2726
  line: node.startPosition.row + 1,
2727
+ callStart: node.startIndex,
2728
+ callEnd: node.endIndex,
1588
2729
  isMethod: segments.length > 1,
1589
2730
  isPathCall: true, // Distinguishes Type::func()/module::func() from obj.method()
1590
2731
  receiver: recvSegments.length > 0 ? recvSegments.join('::') : undefined,
1591
2732
  argCount,
1592
2733
  ...(assigned && { assignedTo: assigned.assignedTo }),
1593
2734
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
2735
+ ...(assigned?.tuple && { assignedTuple: true }),
2736
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
1594
2737
  enclosingFunction,
1595
2738
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1596
2739
  });
@@ -1657,17 +2800,25 @@ function findCallsInCode(code, parser) {
1657
2800
  if (node.type === 'macro_invocation') {
1658
2801
  const macroNode = node.childForFieldName('macro');
1659
2802
  const enclosingFunction = getCurrentEnclosingFunction();
2803
+ let macro = null;
1660
2804
  if (macroNode) {
1661
- let macroName = macroNode.text;
1662
- // Remove the trailing ! if present in the name
1663
- if (macroName.endsWith('!')) {
1664
- macroName = macroName.slice(0, -1);
1665
- }
2805
+ macro = rustMacroCallIdentity(macroNode);
2806
+ const assigned = rustAssignmentTargetOf(node);
1666
2807
  calls.push({
1667
- name: macroName,
2808
+ name: macro?.name || macroNode.text.replace(/!$/, ''),
1668
2809
  line: node.startPosition.row + 1,
2810
+ callStart: node.startIndex,
2811
+ callEnd: node.endIndex,
1669
2812
  isMethod: false,
1670
2813
  isMacro: true,
2814
+ ...(macro?.receiver && {
2815
+ receiver: macro.receiver,
2816
+ isPathMacro: true,
2817
+ }),
2818
+ ...(assigned && { assignedTo: assigned.assignedTo }),
2819
+ ...(assigned?.unwrapped && { assignedUnwrap: true }),
2820
+ ...(assigned?.tuple && { assignedTuple: true }),
2821
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
1671
2822
  enclosingFunction
1672
2823
  });
1673
2824
  }
@@ -1680,12 +2831,38 @@ function findCallsInCode(code, parser) {
1680
2831
  if (child.type === 'token_tree') {
1681
2832
  extractCallsFromTokenTree(
1682
2833
  child, enclosingFunction, calls, getReceiverType,
1683
- patternShadowsAt, flowInvalidatedAt);
2834
+ patternShadowsAt, flowInvalidatedAt, {
2835
+ kind: 'invocation',
2836
+ containerMacro: macro?.name,
2837
+ });
1684
2838
  }
1685
2839
  }
1686
2840
  return true;
1687
2841
  }
1688
2842
 
2843
+ // Attribute arguments are token trees, but they contain ordinary
2844
+ // Rust paths and builder expressions that proc macros resolve at
2845
+ // compile time (`#[arg(value_parser = BoolishValueParser::new())]`).
2846
+ // Recover those calls with the same AST-token reconstruction used for
2847
+ // macro invocation arguments. The attribute name itself is metadata,
2848
+ // not a direct call site, so only its argument token tree is scanned.
2849
+ if (node.type === 'attribute_item') {
2850
+ const attribute = node.namedChildren.find(child => child.type === 'attribute');
2851
+ const attributeName = attribute?.namedChildren.find(child =>
2852
+ child.type === 'identifier' || child.type === 'scoped_identifier')?.text;
2853
+ const enclosingFunction = getCurrentEnclosingFunction();
2854
+ for (const child of attribute?.namedChildren || []) {
2855
+ if (child.type !== 'token_tree') continue;
2856
+ extractCallsFromTokenTree(
2857
+ child, enclosingFunction, calls, getReceiverType,
2858
+ patternShadowsAt, flowInvalidatedAt, {
2859
+ kind: 'attribute',
2860
+ containerMacro: attributeName,
2861
+ });
2862
+ }
2863
+ return true;
2864
+ }
2865
+
1689
2866
  // macro_rules! definitions: the transcriber token_tree holds concrete
1690
2867
  // call templates (write!(stderr, $($tt)*) in messages.rs) — real call
1691
2868
  // sites in every expansion. The matcher (token_tree_pattern) holds
@@ -1700,7 +2877,7 @@ function findCallsInCode(code, parser) {
1700
2877
  if (part.type === 'token_tree') {
1701
2878
  extractCallsFromTokenTree(
1702
2879
  part, enclosingFunction, calls, getReceiverType,
1703
- patternShadowsAt, flowInvalidatedAt);
2880
+ patternShadowsAt, flowInvalidatedAt, 'definition');
1704
2881
  }
1705
2882
  }
1706
2883
  }
@@ -1751,9 +2928,11 @@ function findCallsInCode(code, parser) {
1751
2928
  const typeMap = scopeTypes.get(scopeKey);
1752
2929
  if (typeMap) {
1753
2930
  let typeName = null;
2931
+ let typeQualifier = null;
1754
2932
  // Pattern 3: explicit type annotation — let s: Server = ...
1755
2933
  if (typeAnnotation) {
1756
2934
  typeName = extractTypeName(typeAnnotation);
2935
+ typeQualifier = extractTypeQualifier(typeAnnotation);
1757
2936
  }
1758
2937
  if (!typeName && valueNode) {
1759
2938
  // Pattern 1: struct expression — let s = Server { ... }
@@ -1763,6 +2942,7 @@ function findCallsInCode(code, parser) {
1763
2942
  // Strip path prefix: module::Server → Server
1764
2943
  if (typeName && typeName.includes('::')) {
1765
2944
  const parts = typeName.split('::');
2945
+ typeQualifier = parts.slice(0, -1).join('::');
1766
2946
  typeName = parts[parts.length - 1];
1767
2947
  }
1768
2948
  }
@@ -1774,6 +2954,7 @@ function findCallsInCode(code, parser) {
1774
2954
  typeName = nameNode?.text || null;
1775
2955
  if (typeName && typeName.includes('::')) {
1776
2956
  const parts = typeName.split('::');
2957
+ typeQualifier = parts.slice(0, -1).join('::');
1777
2958
  typeName = parts[parts.length - 1];
1778
2959
  }
1779
2960
  }
@@ -1788,13 +2969,17 @@ function findCallsInCode(code, parser) {
1788
2969
  const methodName = segments[segments.length - 1];
1789
2970
  if (/^(new|from|default|with_|create|build|open|connect|init)/.test(methodName)) {
1790
2971
  typeName = segments[segments.length - 2];
2972
+ typeQualifier = segments.slice(0, -2).join('::') || null;
1791
2973
  if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
1792
2974
  }
1793
2975
  }
1794
2976
  }
1795
2977
  }
1796
2978
  }
1797
- if (typeName) typeMap.set(varName, typeName);
2979
+ if (typeName) {
2980
+ typeMap.set(varName, typeName);
2981
+ if (typeQualifier) typeMap.qualifiers.set(varName, typeQualifier);
2982
+ }
1798
2983
  }
1799
2984
  }
1800
2985
  }
@@ -1812,7 +2997,29 @@ function findCallsInCode(code, parser) {
1812
2997
  }
1813
2998
  });
1814
2999
 
1815
- return calls;
3000
+ const declaration = declarationTrees(code, parser);
3001
+ if (!declaration.macroItemRecovery) return calls;
3002
+ const functions = findFunctions(code, parser);
3003
+ return calls
3004
+ .filter(call => !(call.inMacro &&
3005
+ declaration.macroDeclarationNameStarts.has(call.callStart)))
3006
+ .map(call => {
3007
+ if (!call.inMacro || call.enclosingFunction) return call;
3008
+ const owner = functions
3009
+ .filter(fn => fn.startLine <= call.line && fn.endLine >= call.line)
3010
+ .sort((left, right) =>
3011
+ (left.endLine - left.startLine) -
3012
+ (right.endLine - right.startLine))[0];
3013
+ if (!owner) return call;
3014
+ return {
3015
+ ...call,
3016
+ enclosingFunction: {
3017
+ name: owner.name,
3018
+ startLine: owner.startLine,
3019
+ endLine: owner.endLine,
3020
+ },
3021
+ };
3022
+ });
1816
3023
  }
1817
3024
 
1818
3025
  /**
@@ -1825,82 +3032,76 @@ function findImportsInCode(code, parser) {
1825
3032
  const tree = parseTree(parser, code);
1826
3033
  const imports = [];
1827
3034
 
3035
+ const joinUsePath = (prefix, suffix) => {
3036
+ const left = String(prefix || '').replace(/::$/, '');
3037
+ const right = String(suffix || '').replace(/^::/, '');
3038
+ return left && right ? `${left}::${right}` : left || right;
3039
+ };
3040
+ const addLeaf = (module, localName, type = 'use', dynamic = false, line) => {
3041
+ if (!module || !localName) return;
3042
+ imports.push({
3043
+ module,
3044
+ names: [localName],
3045
+ type,
3046
+ dynamic,
3047
+ line,
3048
+ });
3049
+ };
3050
+ const collectUseTree = (node, prefix, line) => {
3051
+ if (!node || node.type === 'visibility_modifier') return;
3052
+ if (node.type === 'scoped_use_list') {
3053
+ const pathNode = node.childForFieldName('path');
3054
+ const listNode = node.childForFieldName('list');
3055
+ const nextPrefix = joinUsePath(prefix, pathNode?.text);
3056
+ if (listNode) collectUseTree(listNode, nextPrefix, line);
3057
+ return;
3058
+ }
3059
+ if (node.type === 'use_list') {
3060
+ for (let i = 0; i < node.namedChildCount; i++) {
3061
+ collectUseTree(node.namedChild(i), prefix, line);
3062
+ }
3063
+ return;
3064
+ }
3065
+ if (node.type === 'use_as_clause') {
3066
+ const pathNode = node.namedChild(0);
3067
+ const aliasNode = node.childForFieldName('alias') || node.namedChild(1);
3068
+ if (pathNode && aliasNode) {
3069
+ addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
3070
+ 'use', false, line);
3071
+ }
3072
+ return;
3073
+ }
3074
+ if (node.type === 'use_wildcard') {
3075
+ const pathNode = node.namedChild(0);
3076
+ addLeaf(joinUsePath(prefix, pathNode?.text), '*',
3077
+ 'use-glob', true, line);
3078
+ return;
3079
+ }
3080
+ if (node.type === 'identifier' || node.type === 'scoped_identifier' ||
3081
+ node.type === 'crate' || node.type === 'self' || node.type === 'super') {
3082
+ if (node.text === 'self' && prefix) {
3083
+ addLeaf(prefix, prefix.split('::').pop(), 'use', false, line);
3084
+ return;
3085
+ }
3086
+ const module = joinUsePath(prefix, node.text);
3087
+ addLeaf(module, node.text.split('::').pop(), 'use', false, line);
3088
+ }
3089
+ };
3090
+
1828
3091
  traverseTreeCached(tree.rootNode, (node) => {
1829
3092
  // use declarations
1830
3093
  if (node.type === 'use_declaration') {
1831
3094
  const line = node.startPosition.row + 1;
1832
-
3095
+ // A use declaration has one semantic tree below optional
3096
+ // visibility. Recursively flatten every leaf while retaining its
3097
+ // full module path. In particular,
3098
+ // `use crate::{haystack::{Haystack, Builder}}` becomes the exact
3099
+ // bindings `crate::haystack::Haystack` and
3100
+ // `crate::haystack::Builder`, rather than the lossy old
3101
+ // `{ module: "crate", name: "haystack" }` approximation.
1833
3102
  for (let i = 0; i < node.namedChildCount; i++) {
1834
3103
  const child = node.namedChild(i);
1835
-
1836
- if (child.type === 'use_as_clause') {
1837
- // use foo::bar as baz
1838
- const pathNode = child.namedChild(0); // the original path
1839
- const aliasNode = child.childForFieldName('alias');
1840
- if (pathNode) {
1841
- const originalPath = pathNode.text;
1842
- const alias = aliasNode ? aliasNode.text : originalPath.split('::').pop();
1843
- imports.push({
1844
- module: originalPath,
1845
- names: [alias],
1846
- type: 'use',
1847
- dynamic: false,
1848
- line
1849
- });
1850
- }
1851
- } else if (child.type === 'scoped_identifier' || child.type === 'identifier') {
1852
- // use std::io or use foo
1853
- const path = child.text;
1854
- const segments = path.split('::');
1855
- imports.push({
1856
- module: path,
1857
- names: [segments[segments.length - 1]],
1858
- type: 'use',
1859
- dynamic: false,
1860
- line
1861
- });
1862
- } else if (child.type === 'use_wildcard') {
1863
- // use std::collections::*
1864
- const scopedId = child.namedChild(0);
1865
- if (scopedId) {
1866
- imports.push({
1867
- module: scopedId.text,
1868
- names: ['*'],
1869
- type: 'use-glob',
1870
- dynamic: true,
1871
- line
1872
- });
1873
- }
1874
- } else if (child.type === 'use_list' || child.type === 'scoped_use_list') {
1875
- // use std::{io, fs} or use foo::{bar, baz}
1876
- // Extract the base path and names
1877
- const pathNode = child.childForFieldName('path');
1878
- const listNode = child.childForFieldName('list');
1879
-
1880
- if (pathNode && listNode) {
1881
- const basePath = pathNode.text;
1882
- const names = [];
1883
- for (let j = 0; j < listNode.namedChildCount; j++) {
1884
- const item = listNode.namedChild(j);
1885
- if (item.type === 'identifier') {
1886
- names.push(item.text);
1887
- } else if (item.type === 'use_as_clause') {
1888
- const aliasNode = item.childForFieldName('alias');
1889
- const pathItem = item.namedChild(0);
1890
- names.push(aliasNode ? aliasNode.text : (pathItem ? pathItem.text : item.text));
1891
- } else if (item.type === 'scoped_identifier') {
1892
- names.push(item.text);
1893
- }
1894
- }
1895
- imports.push({
1896
- module: basePath,
1897
- names,
1898
- type: 'use',
1899
- dynamic: false,
1900
- line
1901
- });
1902
- }
1903
- }
3104
+ collectUseTree(child, '', line);
1904
3105
  }
1905
3106
  return true;
1906
3107
  }
@@ -1962,8 +3163,9 @@ function findImportsInCode(code, parser) {
1962
3163
  * @returns {Array<{name: string, type: string, line: number}>}
1963
3164
  */
1964
3165
  function findExportsInCode(code, parser) {
1965
- const tree = parseTree(parser, code);
3166
+ const { trees } = declarationTrees(code, parser);
1966
3167
  const exports = [];
3168
+ const seen = new Set();
1967
3169
 
1968
3170
  function hasVisibility(node) {
1969
3171
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -1975,7 +3177,15 @@ function findExportsInCode(code, parser) {
1975
3177
  return false;
1976
3178
  }
1977
3179
 
1978
- traverseTreeCached(tree.rootNode, (node) => {
3180
+ const append = entry => {
3181
+ const key = `${entry.name}\0${entry.type}\0${entry.line}\0${entry.alias || ''}`;
3182
+ if (!seen.has(key)) {
3183
+ seen.add(key);
3184
+ exports.push(entry);
3185
+ }
3186
+ };
3187
+
3188
+ const collect = tree => traverseTreeCached(tree.rootNode, (node) => {
1979
3189
  // Public renamed re-exports: `pub use foo::bar as baz;` (also nested in
1980
3190
  // use lists: `pub use m::{a as b}`). name keeps the source symbol; alias
1981
3191
  // carries the external name callers use. Plain (un-renamed) `pub use`
@@ -1998,7 +3208,7 @@ function findExportsInCode(code, parser) {
1998
3208
  }
1999
3209
  }
2000
3210
  if (local && aliasNode && aliasNode.text !== local) {
2001
- exports.push({
3211
+ append({
2002
3212
  name: local, type: 're-export', line,
2003
3213
  source: srcNode.text, alias: aliasNode.text,
2004
3214
  });
@@ -2015,7 +3225,7 @@ function findExportsInCode(code, parser) {
2015
3225
  if (node.type === 'function_item' && hasVisibility(node)) {
2016
3226
  const nameNode = node.childForFieldName('name');
2017
3227
  if (nameNode) {
2018
- exports.push({
3228
+ append({
2019
3229
  name: nameNode.text,
2020
3230
  type: 'function',
2021
3231
  line: node.startPosition.row + 1
@@ -2028,7 +3238,7 @@ function findExportsInCode(code, parser) {
2028
3238
  if (node.type === 'struct_item' && hasVisibility(node)) {
2029
3239
  const nameNode = node.childForFieldName('name');
2030
3240
  if (nameNode) {
2031
- exports.push({
3241
+ append({
2032
3242
  name: nameNode.text,
2033
3243
  type: 'struct',
2034
3244
  line: node.startPosition.row + 1
@@ -2041,7 +3251,7 @@ function findExportsInCode(code, parser) {
2041
3251
  if (node.type === 'enum_item' && hasVisibility(node)) {
2042
3252
  const nameNode = node.childForFieldName('name');
2043
3253
  if (nameNode) {
2044
- exports.push({
3254
+ append({
2045
3255
  name: nameNode.text,
2046
3256
  type: 'enum',
2047
3257
  line: node.startPosition.row + 1
@@ -2054,7 +3264,7 @@ function findExportsInCode(code, parser) {
2054
3264
  if (node.type === 'trait_item' && hasVisibility(node)) {
2055
3265
  const nameNode = node.childForFieldName('name');
2056
3266
  if (nameNode) {
2057
- exports.push({
3267
+ append({
2058
3268
  name: nameNode.text,
2059
3269
  type: 'trait',
2060
3270
  line: node.startPosition.row + 1
@@ -2067,7 +3277,7 @@ function findExportsInCode(code, parser) {
2067
3277
  if (node.type === 'mod_item' && hasVisibility(node)) {
2068
3278
  const nameNode = node.childForFieldName('name');
2069
3279
  if (nameNode) {
2070
- exports.push({
3280
+ append({
2071
3281
  name: nameNode.text,
2072
3282
  type: 'module',
2073
3283
  line: node.startPosition.row + 1
@@ -2080,7 +3290,7 @@ function findExportsInCode(code, parser) {
2080
3290
  if (node.type === 'type_item' && hasVisibility(node)) {
2081
3291
  const nameNode = node.childForFieldName('name');
2082
3292
  if (nameNode) {
2083
- exports.push({
3293
+ append({
2084
3294
  name: nameNode.text,
2085
3295
  type: 'type',
2086
3296
  line: node.startPosition.row + 1
@@ -2093,7 +3303,7 @@ function findExportsInCode(code, parser) {
2093
3303
  if (node.type === 'const_item' && hasVisibility(node)) {
2094
3304
  const nameNode = node.childForFieldName('name');
2095
3305
  if (nameNode) {
2096
- exports.push({
3306
+ append({
2097
3307
  name: nameNode.text,
2098
3308
  type: 'const',
2099
3309
  line: node.startPosition.row + 1
@@ -2106,7 +3316,7 @@ function findExportsInCode(code, parser) {
2106
3316
  if (node.type === 'static_item' && hasVisibility(node)) {
2107
3317
  const nameNode = node.childForFieldName('name');
2108
3318
  if (nameNode) {
2109
- exports.push({
3319
+ append({
2110
3320
  name: nameNode.text,
2111
3321
  type: 'static',
2112
3322
  line: node.startPosition.row + 1
@@ -2117,6 +3327,7 @@ function findExportsInCode(code, parser) {
2117
3327
 
2118
3328
  return true;
2119
3329
  });
3330
+ for (const tree of trees) collect(tree);
2120
3331
 
2121
3332
  return exports;
2122
3333
  }
@@ -2139,6 +3350,30 @@ function _indexInParent(node, parent) {
2139
3350
  function findUsagesInCode(code, name, parser, tree) {
2140
3351
  tree = tree || parseTree(parser, code);
2141
3352
  const usages = [];
3353
+ // Lazy same-file enum→variants map: built only when a paren-less
3354
+ // `Type::name` reference needs the enum-variant check.
3355
+ let _enumVariants = null;
3356
+ const sameFileEnumVariant = (enumName, variantName) => {
3357
+ if (_enumVariants === null) {
3358
+ _enumVariants = new Map();
3359
+ traverseTreeCached(tree.rootNode, (n) => {
3360
+ if (n.type !== 'enum_item') return;
3361
+ const enName = n.childForFieldName('name')?.text;
3362
+ const body = n.childForFieldName('body');
3363
+ if (!enName || !body) return;
3364
+ let set = _enumVariants.get(enName);
3365
+ if (!set) { set = new Set(); _enumVariants.set(enName, set); }
3366
+ for (let i = 0; i < body.namedChildCount; i++) {
3367
+ const child = body.namedChild(i);
3368
+ if (child.type === 'enum_variant') {
3369
+ const vn = child.childForFieldName('name')?.text;
3370
+ if (vn) set.add(vn);
3371
+ }
3372
+ }
3373
+ });
3374
+ }
3375
+ return _enumVariants.get(enumName)?.has(variantName) || false;
3376
+ };
2142
3377
 
2143
3378
  visitNameNodes(tree, code, name, (node) => {
2144
3379
  // Look for identifier, field_identifier (method names in obj.method() calls),
@@ -2201,6 +3436,34 @@ function findUsagesInCode(code, name, parser, tree) {
2201
3436
  return true;
2202
3437
  }
2203
3438
  }
3439
+ } else if (sameNode(parent.childForFieldName('name'), node)) {
3440
+ // Associated method value: `Cursive::quit` is a reference
3441
+ // to the method even though no call_expression wraps it.
3442
+ // Preserve its type receiver so the project-aware usage
3443
+ // layer can distinguish it from `Enum::Variant`.
3444
+ const pathNode = parent.childForFieldName('path');
3445
+ if (pathNode) {
3446
+ const segs = pathNode.text.split('::');
3447
+ const receiver = segs[segs.length - 1];
3448
+ // A same-file `enum Receiver { Name }` proves this is
3449
+ // the variant, not an associated item of the queried
3450
+ // symbol — provable without the index, so filtered
3451
+ // here; cross-file receivers stay for the project
3452
+ // layer's owner check.
3453
+ if (receiver && sameFileEnumVariant(receiver, name)) {
3454
+ return true;
3455
+ }
3456
+ if (receiver) {
3457
+ usages.push({
3458
+ line,
3459
+ column,
3460
+ usageType: 'reference',
3461
+ receiver,
3462
+ scopedReference: true,
3463
+ });
3464
+ return true;
3465
+ }
3466
+ }
2204
3467
  }
2205
3468
  }
2206
3469
  // Turbofish call on a bare name: f::<T>() — the identifier's parent