ucn 4.2.3 → 5.0.2

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 +438 -305
  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 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  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 +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  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 +212 -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 -187
  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 +317 -185
  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 +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  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
@@ -487,6 +862,7 @@ function _processClass(node, types, processedRanges, lines, code) {
487
862
  if (nameNode) {
488
863
  const { startLine, endLine } = nodeToLocation(node, lines);
489
864
  const docstring = extractRustDocstring(lines, startLine);
865
+ const inferred = _inferMacroReturn(node, nameNode.text);
490
866
 
491
867
  types.push({
492
868
  name: nameNode.text,
@@ -495,6 +871,7 @@ function _processClass(node, types, processedRanges, lines, code) {
495
871
  type: 'macro',
496
872
  members: [],
497
873
  modifiers: [],
874
+ ...inferred,
498
875
  ...(docstring && { docstring })
499
876
  });
500
877
  }
@@ -627,14 +1004,16 @@ function _processState(node, objects, lines) {
627
1004
  * Find all functions in Rust code using tree-sitter
628
1005
  */
629
1006
  function findFunctions(code, parser) {
630
- const tree = parseTree(parser, code);
1007
+ const { trees } = declarationTrees(code, parser);
631
1008
  const lines = code.split('\n');
632
1009
  const functions = [];
633
1010
  const processedRanges = new Set();
634
- traverseTreeCached(tree.rootNode, (node) => {
635
- _processFunction(node, functions, processedRanges, lines, code);
636
- return true;
637
- });
1011
+ for (const tree of trees) {
1012
+ traverseTreeCached(tree.rootNode, (node) => {
1013
+ _processFunction(node, functions, processedRanges, lines, code);
1014
+ return true;
1015
+ });
1016
+ }
638
1017
  functions.sort((a, b) => a.startLine - b.startLine);
639
1018
  return functions;
640
1019
  }
@@ -654,16 +1033,18 @@ function extractGenerics(node) {
654
1033
  * Find all types (structs, enums, traits, impls) in Rust code
655
1034
  */
656
1035
  function findClasses(code, parser) {
657
- const tree = parseTree(parser, code);
1036
+ const { trees } = declarationTrees(code, parser);
658
1037
  const lines = code.split('\n');
659
1038
  const types = [];
660
1039
  const processedRanges = new Set();
661
- traverseTreeCached(tree.rootNode, (node) => {
662
- const matched = _processClass(node, types, processedRanges, lines, code);
663
- // For impl_item, don't traverse into impl body (original behavior)
664
- if (matched && node.type === 'impl_item') return false;
665
- return true;
666
- });
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
+ }
667
1048
  _postProcessTraitImpls(types);
668
1049
  types.sort((a, b) => a.startLine - b.startLine);
669
1050
  return types;
@@ -748,11 +1129,15 @@ function extractImplInfo(implNode) {
748
1129
  typeName = typeNode.text;
749
1130
  }
750
1131
 
751
- // Strip generic type arguments from typeName and traitName for lookup
752
- // e.g., "CacheService<T>" → "CacheService", "Entity" stays "Entity"
753
- const stripGenerics = (s) => s ? s.replace(/<[^>]*>/g, '').trim() : s;
754
- const bareTypeName = stripGenerics(typeName);
755
- 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);
756
1141
 
757
1142
  let name;
758
1143
  if (bareTraitName && bareTypeName) {
@@ -769,6 +1154,37 @@ function extractImplInfo(implNode) {
769
1154
  return { name, traitName, typeName: bareTypeName, generics: typeParams || undefined };
770
1155
  }
771
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
+
772
1188
  /**
773
1189
  * Extract enum variants
774
1190
  */
@@ -822,7 +1238,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
822
1238
  const { startLine, endLine } = nodeToLocation(child, code);
823
1239
  const paramsNode = child.childForFieldName('parameters');
824
1240
  const returnType = extractReturnType(child);
1241
+ const iteratorItemType = extractRustIteratorItemType(child);
825
1242
  const hasSelf = paramsNode && paramsNode.text.includes('self');
1243
+ const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
826
1244
 
827
1245
  // Rust vocabulary (fix #248): trait members carry the trait's
828
1246
  // OWN visibility — a method of a private trait is not `pub`,
@@ -837,7 +1255,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
837
1255
  modifiers: traitVisibility ? [traitVisibility] : [],
838
1256
  ...(paramsNode && { params: extractRustParams(paramsNode) }),
839
1257
  ...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
1258
+ ...(callbackParamTypes && { callbackParamTypes }),
840
1259
  ...(returnType && { returnType }),
1260
+ ...(iteratorItemType && { iteratorItemType }),
841
1261
  ...(hasSelf && { receiver: 'self' })
842
1262
  });
843
1263
  }
@@ -857,6 +1277,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
857
1277
  const members = [];
858
1278
  const bodyNode = implNode.childForFieldName('body');
859
1279
  if (!bodyNode) return members;
1280
+ const implAttributes = extractAttributes(implNode, codeOrLines);
860
1281
 
861
1282
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
862
1283
  const child = bodyNode.namedChild(i);
@@ -870,6 +1291,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
870
1291
  const text = child.text;
871
1292
  const firstLine = text.split('\n')[0];
872
1293
  const returnType = extractReturnType(child);
1294
+ const iteratorItemType = extractRustIteratorItemType(child);
873
1295
  const docstring = extractRustDocstring(code, startLine);
874
1296
  const visibility = extractVisibility(text);
875
1297
 
@@ -889,13 +1311,18 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
889
1311
  if (firstLine.includes('const fn')) modifiers.push('const');
890
1312
  if (firstLine.includes('extern ')) modifiers.push('extern');
891
1313
  for (const attr of attributes) modifiers.push(attr);
1314
+ for (const attr of implAttributes) {
1315
+ if (!modifiers.includes(attr)) modifiers.push(attr);
1316
+ }
892
1317
  if (inCfgTest) modifiers.push('cfg_test_module');
893
1318
 
894
1319
  const memberGenerics = extractGenerics(child);
1320
+ const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
895
1321
  members.push({
896
1322
  name: nameNode.text,
897
1323
  params: extractRustParams(paramsNode),
898
1324
  paramsStructured: parseStructuredParams(paramsNode, 'rust'),
1325
+ ...(callbackParamTypes && { callbackParamTypes }),
899
1326
  startLine,
900
1327
  endLine,
901
1328
  memberType: 'method',
@@ -904,6 +1331,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
904
1331
  modifiers,
905
1332
  ...(typeName && { receiver: typeName }), // All impl members get receiver for findMethodsForType
906
1333
  ...(returnType && { returnType }),
1334
+ ...(iteratorItemType && { iteratorItemType }),
907
1335
  ...(docstring && { docstring }),
908
1336
  // Method-level type params (fix #229): generic-param receiver
909
1337
  // types inside the method resolve against this declaration.
@@ -920,13 +1348,15 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
920
1348
  * Find state objects (const/static) in Rust code
921
1349
  */
922
1350
  function findStateObjects(code, parser) {
923
- const tree = parseTree(parser, code);
1351
+ const { trees } = declarationTrees(code, parser);
924
1352
  const lines = code.split('\n');
925
1353
  const objects = [];
926
- traverseTreeCached(tree.rootNode, (node) => {
927
- _processState(node, objects, lines);
928
- return true;
929
- });
1354
+ for (const tree of trees) {
1355
+ traverseTreeCached(tree.rootNode, (node) => {
1356
+ _processState(node, objects, lines);
1357
+ return true;
1358
+ });
1359
+ }
930
1360
  objects.sort((a, b) => a.startLine - b.startLine);
931
1361
  return objects;
932
1362
  }
@@ -935,17 +1365,20 @@ function findStateObjects(code, parser) {
935
1365
  * Parse a Rust file completely
936
1366
  */
937
1367
  function parse(code, parser) {
938
- const tree = parseTree(parser, code);
1368
+ const declaration = declarationTrees(code, parser);
1369
+ const tree = declaration.primary;
939
1370
  const lines = code.split('\n');
940
1371
  const functions = [], classes = [], stateObjects = [];
941
1372
  const processedFn = new Set(), processedCls = new Set();
942
1373
 
943
- traverseTreeCached(tree.rootNode, (node) => {
944
- _processFunction(node, functions, processedFn, lines, code);
945
- _processClass(node, classes, processedCls, lines, code);
946
- _processState(node, stateObjects, lines);
947
- return true; // always continue, never skip subtrees
948
- });
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
+ }
949
1382
 
950
1383
  _postProcessTraitImpls(classes);
951
1384
 
@@ -955,7 +1388,9 @@ function parse(code, parser) {
955
1388
 
956
1389
  return {
957
1390
  language: 'rust', totalLines: lines.length, functions, classes, stateObjects,
958
- ...(tree.rootNode.hasError && { parseRecovery: true }),
1391
+ ...((tree.rootNode.hasError || declaration.macroItemRecovery) && {
1392
+ parseRecovery: true,
1393
+ }),
959
1394
  imports: [], exports: [],
960
1395
  };
961
1396
  }
@@ -1053,14 +1488,22 @@ function _tokenTreeCallArgsAfter(children, nameIndex) {
1053
1488
  }
1054
1489
 
1055
1490
  function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType,
1056
- 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;
1057
1494
  const children = [];
1058
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
+ };
1059
1502
  for (let i = 0; i < children.length; i++) {
1060
1503
  const tok = children[i];
1061
1504
  if (tok.type === 'token_tree') {
1062
1505
  extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType,
1063
- isPatternShadow, isFlowInvalidated);
1506
+ isPatternShadow, isFlowInvalidated, context);
1064
1507
  continue;
1065
1508
  }
1066
1509
  // `default` is tokenized as the Rust keyword even in the valid
@@ -1075,14 +1518,34 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1075
1518
  // Nested macro invocation: name!(...)
1076
1519
  if (next && next.type === '!' &&
1077
1520
  children[i + 2] && children[i + 2].type === 'token_tree') {
1078
- 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 = {
1079
1534
  name: tok.text,
1080
1535
  line: tok.startPosition.row + 1,
1536
+ callStart: startNode.startIndex,
1537
+ callEnd: children[i + 2].endIndex,
1081
1538
  isMethod: false,
1082
1539
  isMacro: true,
1083
- inMacro: true,
1540
+ ...(segments.length > 0 && {
1541
+ receiver: segments.join('::'),
1542
+ isPathMacro: true,
1543
+ }),
1544
+ ...macroFields,
1084
1545
  enclosingFunction
1085
- });
1546
+ };
1547
+ calls.push(record);
1548
+ lastProducer = record;
1086
1549
  continue;
1087
1550
  }
1088
1551
  const callArgs = _tokenTreeCallArgsAfter(children, i);
@@ -1090,8 +1553,11 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1090
1553
  if (prev && prev.type === '::') {
1091
1554
  // Path call: Type::func(...) / module::sub::func(...) — segments
1092
1555
  // can be identifiers, primitives (char::from), or path keywords
1093
- 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);
1094
1559
  const segments = [];
1560
+ let startNode = tok;
1095
1561
  let j = i - 1;
1096
1562
  while (j >= 1 && children[j].type === '::') {
1097
1563
  let k = j - 1;
@@ -1115,23 +1581,39 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1115
1581
  k -= 2;
1116
1582
  }
1117
1583
  if (!isSegment(children[k])) break;
1118
- segments.unshift(children[k].text);
1584
+ segments.unshift(children[k].text === '$crate' ? 'crate' : children[k].text);
1585
+ startNode = children[k];
1119
1586
  j = k - 1;
1120
1587
  }
1121
- calls.push({
1588
+ const record = {
1122
1589
  name: tok.text,
1123
1590
  line: tok.startPosition.row + 1,
1591
+ callStart: startNode.startIndex,
1592
+ callEnd: callArgs.endIndex,
1124
1593
  isMethod: segments.length > 0,
1125
1594
  isPathCall: true,
1126
1595
  receiver: segments.length > 0 ? segments.join('::') : undefined,
1127
- inMacro: true,
1596
+ ...macroFields,
1128
1597
  enclosingFunction
1129
- });
1598
+ };
1599
+ calls.push(record);
1600
+ lastProducer = record;
1130
1601
  } else if (prev && prev.type === '.') {
1131
1602
  // Method call: recv.method(...)
1132
1603
  const recvTok = children[i - 2];
1133
1604
  const receiver = recvTok && (recvTok.type === 'identifier' || recvTok.type === 'self')
1134
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
+ }
1135
1617
  // Literal receivers type as builtins inside macros too (fix #220,
1136
1618
  // ripgrep-measured: assert_eq!(.., vec!["match:fg".parse()...]))
1137
1619
  const litType = recvTok
@@ -1142,26 +1624,47 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1142
1624
  ? getReceiverType(receiver, tok) : litType;
1143
1625
  const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
1144
1626
  const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
1145
- calls.push({
1627
+ const iterationSource = rustIterationSourceOf(tok, receiver);
1628
+ const producer = !receiver && lastProducer &&
1629
+ lastProducer.callEnd === recvTok?.endIndex ? lastProducer : null;
1630
+ const record = {
1146
1631
  name: tok.text,
1147
1632
  line: tok.startPosition.row + 1,
1633
+ callStart: producer?.callStart ?? recvTok?.startIndex ?? tok.startIndex,
1634
+ callEnd: callArgs.endIndex,
1148
1635
  isMethod: true,
1149
- receiver,
1636
+ receiver: receiverField ? undefined : receiver,
1637
+ ...(receiverField && { receiverRoot, receiverField }),
1150
1638
  ...(receiverType && { receiverType }),
1151
1639
  ...(receiverPatternShadow && { receiverPatternShadow: true }),
1152
1640
  ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
1153
- 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,
1154
1651
  enclosingFunction
1155
- });
1652
+ };
1653
+ calls.push(record);
1654
+ lastProducer = record;
1156
1655
  } else {
1157
1656
  // Plain call: func(...) — includes enum-variant constructors
1158
- calls.push({
1657
+ const record = {
1159
1658
  name: tok.text,
1160
1659
  line: tok.startPosition.row + 1,
1660
+ callStart: tok.startIndex,
1661
+ callEnd: callArgs.endIndex,
1161
1662
  isMethod: false,
1162
- inMacro: true,
1663
+ ...macroFields,
1163
1664
  enclosingFunction
1164
- });
1665
+ };
1666
+ calls.push(record);
1667
+ lastProducer = record;
1165
1668
  }
1166
1669
  }
1167
1670
  }
@@ -1177,6 +1680,38 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1177
1680
  * Result<T, _>/Option<T> from the producer's return annotation. `let mut x`
1178
1681
  * works too — the pattern field is the plain identifier.
1179
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
+
1180
1715
  function rustAssignmentTargetOf(callNode) {
1181
1716
  let n = callNode;
1182
1717
  let p = n.parent;
@@ -1194,12 +1729,43 @@ function rustAssignmentTargetOf(callNode) {
1194
1729
  }
1195
1730
  break;
1196
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
+ }
1197
1750
  if (p.type === 'let_declaration') {
1198
1751
  const value = p.childForFieldName('value');
1199
1752
  const pattern = p.childForFieldName('pattern');
1200
1753
  if (value && value.id === n.id && pattern?.type === 'identifier') {
1201
1754
  return { assignedTo: pattern.text, ...(unwrapped && { unwrapped: true }) };
1202
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
+ }
1203
1769
  return undefined;
1204
1770
  }
1205
1771
  if (p.type === 'assignment_expression') {
@@ -1212,6 +1778,170 @@ function rustAssignmentTargetOf(callNode) {
1212
1778
  return undefined;
1213
1779
  }
1214
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
+
1215
1945
  function findCallsInCode(code, parser) {
1216
1946
  const tree = parseTree(parser, code);
1217
1947
  const calls = [];
@@ -1256,7 +1986,10 @@ function findCallsInCode(code, parser) {
1256
1986
  // Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
1257
1987
  const extractTypeName = (typeNode) => {
1258
1988
  if (!typeNode) return null;
1259
- if (typeNode.type === 'type_identifier') return typeNode.text;
1989
+ if (typeNode.type === 'type_identifier' ||
1990
+ typeNode.type === 'primitive_type') {
1991
+ return typeNode.text;
1992
+ }
1260
1993
  if (typeNode.type === 'reference_type') {
1261
1994
  // &Filter or &mut Filter -> Filter
1262
1995
  for (let i = 0; i < typeNode.namedChildCount; i++) {
@@ -1264,6 +1997,12 @@ function findCallsInCode(code, parser) {
1264
1997
  if (r) return r;
1265
1998
  }
1266
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
+ }
1267
2006
  if (typeNode.type === 'generic_type') {
1268
2007
  // Box<Filter> -> Filter (or get the outer type)
1269
2008
  return extractTypeName(typeNode.namedChild(0));
@@ -1276,22 +2015,74 @@ function findCallsInCode(code, parser) {
1276
2015
  return null;
1277
2016
  };
1278
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
+
1279
2037
  // Build type map from function parameters (including self receiver for impl methods)
1280
2038
  const buildScopeTypeMap = (node) => {
1281
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
+ };
1282
2058
  const paramsNode = node.childForFieldName('parameters');
1283
2059
  if (paramsNode) {
1284
2060
  for (let i = 0; i < paramsNode.namedChildCount; i++) {
1285
2061
  const param = paramsNode.namedChild(i);
1286
2062
  if (param.type === 'parameter') {
1287
2063
  const patternNode = param.childForFieldName('pattern');
2064
+ retainBoundNames(patternNode);
1288
2065
  const typeNode = param.childForFieldName('type');
1289
2066
  const typeName = extractTypeName(typeNode);
2067
+ const qualifier = extractTypeQualifier(typeNode);
2068
+ const iteratorItem = extractRustIteratorItemTypeFromTypeNode(typeNode);
1290
2069
  if (patternNode && typeName) {
1291
2070
  // Pattern can be identifier or _
1292
2071
  const name = patternNode.type === 'identifier' ? patternNode.text : null;
1293
- 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
+ }
1294
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);
1295
2086
  }
1296
2087
  }
1297
2088
  }
@@ -1313,7 +2104,10 @@ function findCallsInCode(code, parser) {
1313
2104
  // Helper to get current enclosing function
1314
2105
  const getCurrentEnclosingFunction = () => {
1315
2106
  return functionStack.length > 0
1316
- ? { ...functionStack[functionStack.length - 1] }
2107
+ ? {
2108
+ ...functionStack[functionStack.length - 1],
2109
+ scopeChain: functionStack.map(scope => scope.startLine),
2110
+ }
1317
2111
  : null;
1318
2112
  };
1319
2113
 
@@ -1351,10 +2145,11 @@ function findCallsInCode(code, parser) {
1351
2145
  while (n && ['try_expression', 'await_expression', 'parenthesized_expression'].includes(n.type)) {
1352
2146
  n = n.namedChildCount === 1 ? n.namedChild(0) : null;
1353
2147
  }
1354
- return n?.type === 'call_expression';
2148
+ return n?.type === 'call_expression' || n?.type === 'macro_invocation' ||
2149
+ !!rustMatchCallProducer(n);
1355
2150
  };
1356
2151
 
1357
- const flowInvalidatedAt = (node, varName) => {
2152
+ const flowEventAt = (node, varName) => {
1358
2153
  const pos = node?.startIndex ?? -1;
1359
2154
  for (let i = functionStack.length - 1; i >= 0; i--) {
1360
2155
  const byName = scopeFlowEvents.get(functionStack[i].startLine);
@@ -1365,21 +2160,125 @@ function findCallsInCode(code, parser) {
1365
2160
  if (event.at <= pos && pos <= event.until &&
1366
2161
  (!latest || event.at > latest.at)) latest = event;
1367
2162
  }
1368
- if (latest) return latest.invalidated;
2163
+ if (latest) return latest;
1369
2164
  }
1370
- return false;
2165
+ return null;
1371
2166
  };
1372
2167
 
2168
+ const flowInvalidatedAt = (node, varName) =>
2169
+ !!flowEventAt(node, varName)?.invalidated;
2170
+
1373
2171
  // Look up variable type from scope chain
1374
2172
  const getReceiverType = (varName, atNode) => {
1375
2173
  if (atNode && patternShadowsAt(atNode, varName)) return undefined;
2174
+ const flow = flowEventAt(atNode, varName);
2175
+ if (flow?.type) return flow.type;
1376
2176
  for (let i = functionStack.length - 1; i >= 0; i--) {
1377
2177
  const typeMap = scopeTypes.get(functionStack[i].startLine);
1378
2178
  if (typeMap?.has(varName)) return typeMap.get(varName);
2179
+ if (typeMap?.boundNames?.has(varName)) return undefined;
1379
2180
  }
1380
2181
  return undefined;
1381
2182
  };
1382
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;
2206
+ }
2207
+ return undefined;
2208
+ };
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
+
1383
2282
  // Walk up to the enclosing impl block's target type (impl<T> Foo<T> → Foo).
1384
2283
  const findEnclosingImplType = (n) => {
1385
2284
  for (let p = n.parent; p; p = p.parent) {
@@ -1391,13 +2290,96 @@ function findCallsInCode(code, parser) {
1391
2290
  return undefined;
1392
2291
  };
1393
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
+
1394
2375
  traverseTree(tree.rootNode, (node) => {
1395
2376
  // Track function entry
1396
2377
  if (isFunctionNode(node)) {
1397
2378
  const entry = {
1398
2379
  name: extractFunctionName(node),
1399
2380
  startLine: node.startPosition.row + 1,
1400
- endLine: node.endPosition.row + 1
2381
+ endLine: node.endPosition.row + 1,
2382
+ ...closureContractSource(node),
1401
2383
  };
1402
2384
  functionStack.push(entry);
1403
2385
  scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
@@ -1428,7 +2410,12 @@ function findCallsInCode(code, parser) {
1428
2410
  byName.get(pattern.text).push({
1429
2411
  at: node.endIndex,
1430
2412
  until,
1431
- invalidated: !valueHasFlowProducer(value),
2413
+ ...(() => {
2414
+ const inferred = matchBindingType(value, node);
2415
+ return inferred
2416
+ ? { invalidated: false, ...inferred }
2417
+ : { invalidated: !valueHasFlowProducer(value) };
2418
+ })(),
1432
2419
  });
1433
2420
  }
1434
2421
  }
@@ -1440,6 +2427,7 @@ function findCallsInCode(code, parser) {
1440
2427
  if (!funcNode) return true;
1441
2428
 
1442
2429
  // Unwrap turbofish: parse::<i32>() has generic_function wrapping the actual function
2430
+ const collectResult = extractCollectResultContract(funcNode);
1443
2431
  if (funcNode.type === 'generic_function') {
1444
2432
  funcNode = funcNode.childForFieldName('function') || funcNode;
1445
2433
  }
@@ -1469,10 +2457,14 @@ function findCallsInCode(code, parser) {
1469
2457
  calls.push({
1470
2458
  name: funcNode.text,
1471
2459
  line: node.startPosition.row + 1,
2460
+ callStart: node.startIndex,
2461
+ callEnd: node.endIndex,
1472
2462
  isMethod: false,
1473
2463
  argCount,
1474
2464
  ...(assigned && { assignedTo: assigned.assignedTo }),
1475
2465
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
2466
+ ...(assigned?.tuple && { assignedTuple: true }),
2467
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
1476
2468
  enclosingFunction,
1477
2469
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1478
2470
  });
@@ -1483,6 +2475,18 @@ function findCallsInCode(code, parser) {
1483
2475
 
1484
2476
  if (fieldNode) {
1485
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
+ }
1486
2490
  // Detect chained Router::new()-rooted method calls. axum's canonical
1487
2491
  // idiom is `Router::new().route("/p", get(h)).route(...)` where the
1488
2492
  // receiver of `.route(...)` is itself a call_expression. Walk the
@@ -1507,7 +2511,8 @@ function findCallsInCode(code, parser) {
1507
2511
  // low.sep.into_bytes() — with .clone() transparency (clone()
1508
2512
  // returns Self by stdlib convention). receiverRoot/Field/RootType
1509
2513
  // let findCallers hop to the field's declared type cross-file.
1510
- let receiverRoot, receiverField, receiverRootType;
2514
+ let receiverRoot, receiverField, receiverFields, receiverRootType;
2515
+ let receiverFieldCallRoot;
1511
2516
  if (!receiver) {
1512
2517
  let obj = valueNode;
1513
2518
  while (obj?.type === 'call_expression') {
@@ -1518,15 +2523,32 @@ function findCallsInCode(code, parser) {
1518
2523
  } else break;
1519
2524
  }
1520
2525
  if (obj?.type === 'field_expression') {
1521
- const rootNode = obj.childForFieldName('value');
1522
- const fldNode = obj.childForFieldName('field');
1523
- if ((fldNode?.type === 'field_identifier' || fldNode?.type === 'integer_literal') && 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 &&
1524
2540
  (rootNode.type === 'identifier' || rootNode.type === 'self')) {
1525
2541
  receiverRoot = rootNode.text;
1526
- receiverField = fldNode.text;
2542
+ receiverFields = fields;
2543
+ receiverField = fields[fields.length - 1];
1527
2544
  receiverRootType = rootNode.type === 'self'
1528
2545
  ? findEnclosingImplType(node)
1529
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;
1530
2552
  }
1531
2553
  } else if (obj && obj !== valueNode &&
1532
2554
  (obj.type === 'identifier' || obj.type === 'self')) {
@@ -1546,6 +2568,7 @@ function findCallsInCode(code, parser) {
1546
2568
  // rooted chains keep their synthetic receiver marker for
1547
2569
  // the bridge but get the link too.
1548
2570
  let receiverCall, receiverCallIsMethod, receiverCallLine;
2571
+ let receiverCallStart, receiverCallEnd;
1549
2572
  if ((!receiver || receiverIsChainRoot) && !receiverField &&
1550
2573
  valueNode?.type === 'call_expression') {
1551
2574
  let prodFunc = valueNode.childForFieldName('function');
@@ -1555,12 +2578,16 @@ function findCallsInCode(code, parser) {
1555
2578
  if (prodFunc?.type === 'identifier') {
1556
2579
  receiverCall = prodFunc.text;
1557
2580
  receiverCallLine = valueNode.startPosition.row + 1;
2581
+ receiverCallStart = valueNode.startIndex;
2582
+ receiverCallEnd = valueNode.endIndex;
1558
2583
  } else if (prodFunc?.type === 'field_expression') {
1559
2584
  const pf = prodFunc.childForFieldName('field');
1560
2585
  if (pf) {
1561
2586
  receiverCall = pf.text;
1562
2587
  receiverCallIsMethod = true;
1563
2588
  receiverCallLine = pf.startPosition.row + 1;
2589
+ receiverCallStart = valueNode.startIndex;
2590
+ receiverCallEnd = valueNode.endIndex;
1564
2591
  }
1565
2592
  } else if (prodFunc?.type === 'scoped_identifier') {
1566
2593
  // Path producer: Command::new(...).arg(...) — the
@@ -1573,8 +2600,41 @@ function findCallsInCode(code, parser) {
1573
2600
  receiverCall = prodName;
1574
2601
  receiverCallIsMethod = true;
1575
2602
  receiverCallLine = valueNode.startPosition.row + 1;
2603
+ receiverCallStart = valueNode.startIndex;
2604
+ receiverCallEnd = valueNode.endIndex;
1576
2605
  }
1577
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
+ }
1578
2638
  }
1579
2639
  // Literal receivers carry their builtin type (fix #220,
1580
2640
  // ripgrep-measured): "match:fg:magenta".parse() is
@@ -1587,8 +2647,23 @@ function findCallsInCode(code, parser) {
1587
2647
  const receiverType = (receiver && receiver !== 'self' && !receiverIsChainRoot)
1588
2648
  ? getReceiverType(receiver, node)
1589
2649
  : literalReceiverType;
2650
+ const receiverTypeQualifier = receiver && receiverType
2651
+ ? getReceiverTypeQualifier(receiver, node)
2652
+ : undefined;
2653
+ const receiverIteratorItemType = receiver
2654
+ ? getReceiverIteratorItemType(receiver, node)
2655
+ : undefined;
1590
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
+ }
1591
2665
  const receiverFlowInvalidated = !!(receiver && flowInvalidatedAt(node, receiver));
2666
+ const iterationSource = rustIterationSourceOf(node, receiver);
1592
2667
  const firstArg = getFirstStringArg(node);
1593
2668
  // RUST-2: For chained calls like `a().b().parse::<T>().ok()`,
1594
2669
  // each method should report the line where its OWN identifier
@@ -1598,20 +2673,38 @@ function findCallsInCode(code, parser) {
1598
2673
  calls.push({
1599
2674
  name: fieldNode.text,
1600
2675
  line: fieldNode.startPosition.row + 1,
2676
+ callStart: node.startIndex,
2677
+ callEnd: node.endIndex,
1601
2678
  isMethod: true,
1602
2679
  receiver,
1603
2680
  ...(receiverType && { receiverType }),
2681
+ ...(receiverTypeQualifier && { receiverTypeQualifier }),
2682
+ ...(receiverIteratorItemType && { receiverIteratorItemType }),
1604
2683
  ...(receiverPatternShadow && { receiverPatternShadow: true }),
2684
+ ...(receiverPatternBinding || {}),
1605
2685
  ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
2686
+ ...(iterationSource || {}),
1606
2687
  ...(receiverIsChainRoot && { receiverIsChainRoot: true }),
1607
2688
  ...(receiverField && { receiverRoot, receiverField }),
2689
+ ...(receiverFields?.length > 1 && { receiverFields }),
1608
2690
  ...(receiverField && receiverRootType && { receiverRootType }),
1609
2691
  ...(receiverCall && { receiverCall }),
1610
2692
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
2693
+ ...(valueNode?.type === 'macro_invocation' && receiverCall && {
2694
+ receiverCallIsMacro: true,
2695
+ }),
1611
2696
  ...(receiverCallLine && { receiverCallLine }),
2697
+ ...(receiverCallStart != null && { receiverCallStart }),
2698
+ ...(receiverCallEnd != null && { receiverCallEnd }),
1612
2699
  argCount,
1613
2700
  ...(assigned && { assignedTo: assigned.assignedTo }),
1614
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
+ }),
1615
2708
  enclosingFunction,
1616
2709
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1617
2710
  });
@@ -1631,12 +2724,16 @@ function findCallsInCode(code, parser) {
1631
2724
  calls.push({
1632
2725
  name: name,
1633
2726
  line: node.startPosition.row + 1,
2727
+ callStart: node.startIndex,
2728
+ callEnd: node.endIndex,
1634
2729
  isMethod: segments.length > 1,
1635
2730
  isPathCall: true, // Distinguishes Type::func()/module::func() from obj.method()
1636
2731
  receiver: recvSegments.length > 0 ? recvSegments.join('::') : undefined,
1637
2732
  argCount,
1638
2733
  ...(assigned && { assignedTo: assigned.assignedTo }),
1639
2734
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
2735
+ ...(assigned?.tuple && { assignedTuple: true }),
2736
+ ...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
1640
2737
  enclosingFunction,
1641
2738
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1642
2739
  });
@@ -1703,17 +2800,25 @@ function findCallsInCode(code, parser) {
1703
2800
  if (node.type === 'macro_invocation') {
1704
2801
  const macroNode = node.childForFieldName('macro');
1705
2802
  const enclosingFunction = getCurrentEnclosingFunction();
2803
+ let macro = null;
1706
2804
  if (macroNode) {
1707
- let macroName = macroNode.text;
1708
- // Remove the trailing ! if present in the name
1709
- if (macroName.endsWith('!')) {
1710
- macroName = macroName.slice(0, -1);
1711
- }
2805
+ macro = rustMacroCallIdentity(macroNode);
2806
+ const assigned = rustAssignmentTargetOf(node);
1712
2807
  calls.push({
1713
- name: macroName,
2808
+ name: macro?.name || macroNode.text.replace(/!$/, ''),
1714
2809
  line: node.startPosition.row + 1,
2810
+ callStart: node.startIndex,
2811
+ callEnd: node.endIndex,
1715
2812
  isMethod: false,
1716
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 }),
1717
2822
  enclosingFunction
1718
2823
  });
1719
2824
  }
@@ -1726,12 +2831,38 @@ function findCallsInCode(code, parser) {
1726
2831
  if (child.type === 'token_tree') {
1727
2832
  extractCallsFromTokenTree(
1728
2833
  child, enclosingFunction, calls, getReceiverType,
1729
- patternShadowsAt, flowInvalidatedAt);
2834
+ patternShadowsAt, flowInvalidatedAt, {
2835
+ kind: 'invocation',
2836
+ containerMacro: macro?.name,
2837
+ });
1730
2838
  }
1731
2839
  }
1732
2840
  return true;
1733
2841
  }
1734
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
+
1735
2866
  // macro_rules! definitions: the transcriber token_tree holds concrete
1736
2867
  // call templates (write!(stderr, $($tt)*) in messages.rs) — real call
1737
2868
  // sites in every expansion. The matcher (token_tree_pattern) holds
@@ -1746,7 +2877,7 @@ function findCallsInCode(code, parser) {
1746
2877
  if (part.type === 'token_tree') {
1747
2878
  extractCallsFromTokenTree(
1748
2879
  part, enclosingFunction, calls, getReceiverType,
1749
- patternShadowsAt, flowInvalidatedAt);
2880
+ patternShadowsAt, flowInvalidatedAt, 'definition');
1750
2881
  }
1751
2882
  }
1752
2883
  }
@@ -1797,9 +2928,11 @@ function findCallsInCode(code, parser) {
1797
2928
  const typeMap = scopeTypes.get(scopeKey);
1798
2929
  if (typeMap) {
1799
2930
  let typeName = null;
2931
+ let typeQualifier = null;
1800
2932
  // Pattern 3: explicit type annotation — let s: Server = ...
1801
2933
  if (typeAnnotation) {
1802
2934
  typeName = extractTypeName(typeAnnotation);
2935
+ typeQualifier = extractTypeQualifier(typeAnnotation);
1803
2936
  }
1804
2937
  if (!typeName && valueNode) {
1805
2938
  // Pattern 1: struct expression — let s = Server { ... }
@@ -1809,6 +2942,7 @@ function findCallsInCode(code, parser) {
1809
2942
  // Strip path prefix: module::Server → Server
1810
2943
  if (typeName && typeName.includes('::')) {
1811
2944
  const parts = typeName.split('::');
2945
+ typeQualifier = parts.slice(0, -1).join('::');
1812
2946
  typeName = parts[parts.length - 1];
1813
2947
  }
1814
2948
  }
@@ -1820,6 +2954,7 @@ function findCallsInCode(code, parser) {
1820
2954
  typeName = nameNode?.text || null;
1821
2955
  if (typeName && typeName.includes('::')) {
1822
2956
  const parts = typeName.split('::');
2957
+ typeQualifier = parts.slice(0, -1).join('::');
1823
2958
  typeName = parts[parts.length - 1];
1824
2959
  }
1825
2960
  }
@@ -1834,13 +2969,17 @@ function findCallsInCode(code, parser) {
1834
2969
  const methodName = segments[segments.length - 1];
1835
2970
  if (/^(new|from|default|with_|create|build|open|connect|init)/.test(methodName)) {
1836
2971
  typeName = segments[segments.length - 2];
2972
+ typeQualifier = segments.slice(0, -2).join('::') || null;
1837
2973
  if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
1838
2974
  }
1839
2975
  }
1840
2976
  }
1841
2977
  }
1842
2978
  }
1843
- if (typeName) typeMap.set(varName, typeName);
2979
+ if (typeName) {
2980
+ typeMap.set(varName, typeName);
2981
+ if (typeQualifier) typeMap.qualifiers.set(varName, typeQualifier);
2982
+ }
1844
2983
  }
1845
2984
  }
1846
2985
  }
@@ -1858,7 +2997,29 @@ function findCallsInCode(code, parser) {
1858
2997
  }
1859
2998
  });
1860
2999
 
1861
- 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
+ });
1862
3023
  }
1863
3024
 
1864
3025
  /**
@@ -1871,82 +3032,76 @@ function findImportsInCode(code, parser) {
1871
3032
  const tree = parseTree(parser, code);
1872
3033
  const imports = [];
1873
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
+
1874
3091
  traverseTreeCached(tree.rootNode, (node) => {
1875
3092
  // use declarations
1876
3093
  if (node.type === 'use_declaration') {
1877
3094
  const line = node.startPosition.row + 1;
1878
-
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.
1879
3102
  for (let i = 0; i < node.namedChildCount; i++) {
1880
3103
  const child = node.namedChild(i);
1881
-
1882
- if (child.type === 'use_as_clause') {
1883
- // use foo::bar as baz
1884
- const pathNode = child.namedChild(0); // the original path
1885
- const aliasNode = child.childForFieldName('alias');
1886
- if (pathNode) {
1887
- const originalPath = pathNode.text;
1888
- const alias = aliasNode ? aliasNode.text : originalPath.split('::').pop();
1889
- imports.push({
1890
- module: originalPath,
1891
- names: [alias],
1892
- type: 'use',
1893
- dynamic: false,
1894
- line
1895
- });
1896
- }
1897
- } else if (child.type === 'scoped_identifier' || child.type === 'identifier') {
1898
- // use std::io or use foo
1899
- const path = child.text;
1900
- const segments = path.split('::');
1901
- imports.push({
1902
- module: path,
1903
- names: [segments[segments.length - 1]],
1904
- type: 'use',
1905
- dynamic: false,
1906
- line
1907
- });
1908
- } else if (child.type === 'use_wildcard') {
1909
- // use std::collections::*
1910
- const scopedId = child.namedChild(0);
1911
- if (scopedId) {
1912
- imports.push({
1913
- module: scopedId.text,
1914
- names: ['*'],
1915
- type: 'use-glob',
1916
- dynamic: true,
1917
- line
1918
- });
1919
- }
1920
- } else if (child.type === 'use_list' || child.type === 'scoped_use_list') {
1921
- // use std::{io, fs} or use foo::{bar, baz}
1922
- // Extract the base path and names
1923
- const pathNode = child.childForFieldName('path');
1924
- const listNode = child.childForFieldName('list');
1925
-
1926
- if (pathNode && listNode) {
1927
- const basePath = pathNode.text;
1928
- const names = [];
1929
- for (let j = 0; j < listNode.namedChildCount; j++) {
1930
- const item = listNode.namedChild(j);
1931
- if (item.type === 'identifier') {
1932
- names.push(item.text);
1933
- } else if (item.type === 'use_as_clause') {
1934
- const aliasNode = item.childForFieldName('alias');
1935
- const pathItem = item.namedChild(0);
1936
- names.push(aliasNode ? aliasNode.text : (pathItem ? pathItem.text : item.text));
1937
- } else if (item.type === 'scoped_identifier') {
1938
- names.push(item.text);
1939
- }
1940
- }
1941
- imports.push({
1942
- module: basePath,
1943
- names,
1944
- type: 'use',
1945
- dynamic: false,
1946
- line
1947
- });
1948
- }
1949
- }
3104
+ collectUseTree(child, '', line);
1950
3105
  }
1951
3106
  return true;
1952
3107
  }
@@ -2008,8 +3163,9 @@ function findImportsInCode(code, parser) {
2008
3163
  * @returns {Array<{name: string, type: string, line: number}>}
2009
3164
  */
2010
3165
  function findExportsInCode(code, parser) {
2011
- const tree = parseTree(parser, code);
3166
+ const { trees } = declarationTrees(code, parser);
2012
3167
  const exports = [];
3168
+ const seen = new Set();
2013
3169
 
2014
3170
  function hasVisibility(node) {
2015
3171
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -2021,7 +3177,15 @@ function findExportsInCode(code, parser) {
2021
3177
  return false;
2022
3178
  }
2023
3179
 
2024
- 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) => {
2025
3189
  // Public renamed re-exports: `pub use foo::bar as baz;` (also nested in
2026
3190
  // use lists: `pub use m::{a as b}`). name keeps the source symbol; alias
2027
3191
  // carries the external name callers use. Plain (un-renamed) `pub use`
@@ -2044,7 +3208,7 @@ function findExportsInCode(code, parser) {
2044
3208
  }
2045
3209
  }
2046
3210
  if (local && aliasNode && aliasNode.text !== local) {
2047
- exports.push({
3211
+ append({
2048
3212
  name: local, type: 're-export', line,
2049
3213
  source: srcNode.text, alias: aliasNode.text,
2050
3214
  });
@@ -2061,7 +3225,7 @@ function findExportsInCode(code, parser) {
2061
3225
  if (node.type === 'function_item' && hasVisibility(node)) {
2062
3226
  const nameNode = node.childForFieldName('name');
2063
3227
  if (nameNode) {
2064
- exports.push({
3228
+ append({
2065
3229
  name: nameNode.text,
2066
3230
  type: 'function',
2067
3231
  line: node.startPosition.row + 1
@@ -2074,7 +3238,7 @@ function findExportsInCode(code, parser) {
2074
3238
  if (node.type === 'struct_item' && hasVisibility(node)) {
2075
3239
  const nameNode = node.childForFieldName('name');
2076
3240
  if (nameNode) {
2077
- exports.push({
3241
+ append({
2078
3242
  name: nameNode.text,
2079
3243
  type: 'struct',
2080
3244
  line: node.startPosition.row + 1
@@ -2087,7 +3251,7 @@ function findExportsInCode(code, parser) {
2087
3251
  if (node.type === 'enum_item' && hasVisibility(node)) {
2088
3252
  const nameNode = node.childForFieldName('name');
2089
3253
  if (nameNode) {
2090
- exports.push({
3254
+ append({
2091
3255
  name: nameNode.text,
2092
3256
  type: 'enum',
2093
3257
  line: node.startPosition.row + 1
@@ -2100,7 +3264,7 @@ function findExportsInCode(code, parser) {
2100
3264
  if (node.type === 'trait_item' && hasVisibility(node)) {
2101
3265
  const nameNode = node.childForFieldName('name');
2102
3266
  if (nameNode) {
2103
- exports.push({
3267
+ append({
2104
3268
  name: nameNode.text,
2105
3269
  type: 'trait',
2106
3270
  line: node.startPosition.row + 1
@@ -2113,7 +3277,7 @@ function findExportsInCode(code, parser) {
2113
3277
  if (node.type === 'mod_item' && hasVisibility(node)) {
2114
3278
  const nameNode = node.childForFieldName('name');
2115
3279
  if (nameNode) {
2116
- exports.push({
3280
+ append({
2117
3281
  name: nameNode.text,
2118
3282
  type: 'module',
2119
3283
  line: node.startPosition.row + 1
@@ -2126,7 +3290,7 @@ function findExportsInCode(code, parser) {
2126
3290
  if (node.type === 'type_item' && hasVisibility(node)) {
2127
3291
  const nameNode = node.childForFieldName('name');
2128
3292
  if (nameNode) {
2129
- exports.push({
3293
+ append({
2130
3294
  name: nameNode.text,
2131
3295
  type: 'type',
2132
3296
  line: node.startPosition.row + 1
@@ -2139,7 +3303,7 @@ function findExportsInCode(code, parser) {
2139
3303
  if (node.type === 'const_item' && hasVisibility(node)) {
2140
3304
  const nameNode = node.childForFieldName('name');
2141
3305
  if (nameNode) {
2142
- exports.push({
3306
+ append({
2143
3307
  name: nameNode.text,
2144
3308
  type: 'const',
2145
3309
  line: node.startPosition.row + 1
@@ -2152,7 +3316,7 @@ function findExportsInCode(code, parser) {
2152
3316
  if (node.type === 'static_item' && hasVisibility(node)) {
2153
3317
  const nameNode = node.childForFieldName('name');
2154
3318
  if (nameNode) {
2155
- exports.push({
3319
+ append({
2156
3320
  name: nameNode.text,
2157
3321
  type: 'static',
2158
3322
  line: node.startPosition.row + 1
@@ -2163,6 +3327,7 @@ function findExportsInCode(code, parser) {
2163
3327
 
2164
3328
  return true;
2165
3329
  });
3330
+ for (const tree of trees) collect(tree);
2166
3331
 
2167
3332
  return exports;
2168
3333
  }