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