ucn 4.2.3 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +438 -305
- 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 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- 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 +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- 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 +212 -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 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- 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 +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- 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
|
|
@@ -487,6 +862,7 @@ function _processClass(node, types, processedRanges, lines, code) {
|
|
|
487
862
|
if (nameNode) {
|
|
488
863
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
489
864
|
const docstring = extractRustDocstring(lines, startLine);
|
|
865
|
+
const inferred = _inferMacroReturn(node, nameNode.text);
|
|
490
866
|
|
|
491
867
|
types.push({
|
|
492
868
|
name: nameNode.text,
|
|
@@ -495,6 +871,7 @@ function _processClass(node, types, processedRanges, lines, code) {
|
|
|
495
871
|
type: 'macro',
|
|
496
872
|
members: [],
|
|
497
873
|
modifiers: [],
|
|
874
|
+
...inferred,
|
|
498
875
|
...(docstring && { docstring })
|
|
499
876
|
});
|
|
500
877
|
}
|
|
@@ -627,14 +1004,16 @@ function _processState(node, objects, lines) {
|
|
|
627
1004
|
* Find all functions in Rust code using tree-sitter
|
|
628
1005
|
*/
|
|
629
1006
|
function findFunctions(code, parser) {
|
|
630
|
-
const
|
|
1007
|
+
const { trees } = declarationTrees(code, parser);
|
|
631
1008
|
const lines = code.split('\n');
|
|
632
1009
|
const functions = [];
|
|
633
1010
|
const processedRanges = new Set();
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1011
|
+
for (const tree of trees) {
|
|
1012
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
1013
|
+
_processFunction(node, functions, processedRanges, lines, code);
|
|
1014
|
+
return true;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
638
1017
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
639
1018
|
return functions;
|
|
640
1019
|
}
|
|
@@ -654,16 +1033,18 @@ function extractGenerics(node) {
|
|
|
654
1033
|
* Find all types (structs, enums, traits, impls) in Rust code
|
|
655
1034
|
*/
|
|
656
1035
|
function findClasses(code, parser) {
|
|
657
|
-
const
|
|
1036
|
+
const { trees } = declarationTrees(code, parser);
|
|
658
1037
|
const lines = code.split('\n');
|
|
659
1038
|
const types = [];
|
|
660
1039
|
const processedRanges = new Set();
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1040
|
+
for (const tree of trees) {
|
|
1041
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
1042
|
+
const matched = _processClass(node, types, processedRanges, lines, code);
|
|
1043
|
+
// For impl_item, don't traverse into impl body (original behavior)
|
|
1044
|
+
if (matched && node.type === 'impl_item') return false;
|
|
1045
|
+
return true;
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
667
1048
|
_postProcessTraitImpls(types);
|
|
668
1049
|
types.sort((a, b) => a.startLine - b.startLine);
|
|
669
1050
|
return types;
|
|
@@ -748,11 +1129,15 @@ function extractImplInfo(implNode) {
|
|
|
748
1129
|
typeName = typeNode.text;
|
|
749
1130
|
}
|
|
750
1131
|
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1132
|
+
// Resolve the AST head instead of stripping generic text with a regex.
|
|
1133
|
+
// Nested type arguments (`Deserializer<read::StrRead<'a>>`) defeated the
|
|
1134
|
+
// old `<[^>]*>` expression and left the impossible owner
|
|
1135
|
+
// `Deserializer>`. Reference impls (`impl Trait for &'a mut Writer<T>`)
|
|
1136
|
+
// likewise need the referent owner so declared-field receiver evidence
|
|
1137
|
+
// and method definitions use the same identity.
|
|
1138
|
+
const stripGenerics = (s) => s ? s.replace(/<.*$/s, '').trim() : s;
|
|
1139
|
+
const bareTypeName = extractImplTypeHead(typeNode) || stripGenerics(typeName);
|
|
1140
|
+
const bareTraitName = extractImplTypeHead(traitNode) || stripGenerics(traitName);
|
|
756
1141
|
|
|
757
1142
|
let name;
|
|
758
1143
|
if (bareTraitName && bareTypeName) {
|
|
@@ -769,6 +1154,37 @@ function extractImplInfo(implNode) {
|
|
|
769
1154
|
return { name, traitName, typeName: bareTypeName, generics: typeParams || undefined };
|
|
770
1155
|
}
|
|
771
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Concrete lookup head of a Rust impl type.
|
|
1159
|
+
*
|
|
1160
|
+
* Generic wrappers remain their outer owner (`Box<Foo>` → `Box`), while
|
|
1161
|
+
* transparent references unwrap (`&mut Foo` → `Foo`). Scoped types use their
|
|
1162
|
+
* terminal name. Returning null for shapes without a named owner preserves
|
|
1163
|
+
* the conservative text fallback in extractImplInfo.
|
|
1164
|
+
*/
|
|
1165
|
+
function extractImplTypeHead(typeNode) {
|
|
1166
|
+
if (!typeNode) return null;
|
|
1167
|
+
if (typeNode.type === 'type_identifier' ||
|
|
1168
|
+
typeNode.type === 'primitive_type') {
|
|
1169
|
+
return typeNode.text;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeNode.type === 'scoped_type_identifier') {
|
|
1172
|
+
return typeNode.childForFieldName('name')?.text || null;
|
|
1173
|
+
}
|
|
1174
|
+
if (typeNode.type === 'reference_type' ||
|
|
1175
|
+
typeNode.type === 'parenthesized_type') {
|
|
1176
|
+
const inner = typeNode.childForFieldName('type') ||
|
|
1177
|
+
typeNode.namedChildren?.find(child =>
|
|
1178
|
+
!['lifetime', 'mutable_specifier'].includes(child.type));
|
|
1179
|
+
return extractImplTypeHead(inner);
|
|
1180
|
+
}
|
|
1181
|
+
if (typeNode.type === 'generic_type') {
|
|
1182
|
+
return extractImplTypeHead(typeNode.childForFieldName('type') ||
|
|
1183
|
+
typeNode.namedChild(0));
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
772
1188
|
/**
|
|
773
1189
|
* Extract enum variants
|
|
774
1190
|
*/
|
|
@@ -822,7 +1238,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
|
|
|
822
1238
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
823
1239
|
const paramsNode = child.childForFieldName('parameters');
|
|
824
1240
|
const returnType = extractReturnType(child);
|
|
1241
|
+
const iteratorItemType = extractRustIteratorItemType(child);
|
|
825
1242
|
const hasSelf = paramsNode && paramsNode.text.includes('self');
|
|
1243
|
+
const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
|
|
826
1244
|
|
|
827
1245
|
// Rust vocabulary (fix #248): trait members carry the trait's
|
|
828
1246
|
// OWN visibility — a method of a private trait is not `pub`,
|
|
@@ -837,7 +1255,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
|
|
|
837
1255
|
modifiers: traitVisibility ? [traitVisibility] : [],
|
|
838
1256
|
...(paramsNode && { params: extractRustParams(paramsNode) }),
|
|
839
1257
|
...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
|
|
1258
|
+
...(callbackParamTypes && { callbackParamTypes }),
|
|
840
1259
|
...(returnType && { returnType }),
|
|
1260
|
+
...(iteratorItemType && { iteratorItemType }),
|
|
841
1261
|
...(hasSelf && { receiver: 'self' })
|
|
842
1262
|
});
|
|
843
1263
|
}
|
|
@@ -857,6 +1277,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
|
|
|
857
1277
|
const members = [];
|
|
858
1278
|
const bodyNode = implNode.childForFieldName('body');
|
|
859
1279
|
if (!bodyNode) return members;
|
|
1280
|
+
const implAttributes = extractAttributes(implNode, codeOrLines);
|
|
860
1281
|
|
|
861
1282
|
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
862
1283
|
const child = bodyNode.namedChild(i);
|
|
@@ -870,6 +1291,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
|
|
|
870
1291
|
const text = child.text;
|
|
871
1292
|
const firstLine = text.split('\n')[0];
|
|
872
1293
|
const returnType = extractReturnType(child);
|
|
1294
|
+
const iteratorItemType = extractRustIteratorItemType(child);
|
|
873
1295
|
const docstring = extractRustDocstring(code, startLine);
|
|
874
1296
|
const visibility = extractVisibility(text);
|
|
875
1297
|
|
|
@@ -889,13 +1311,18 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
|
|
|
889
1311
|
if (firstLine.includes('const fn')) modifiers.push('const');
|
|
890
1312
|
if (firstLine.includes('extern ')) modifiers.push('extern');
|
|
891
1313
|
for (const attr of attributes) modifiers.push(attr);
|
|
1314
|
+
for (const attr of implAttributes) {
|
|
1315
|
+
if (!modifiers.includes(attr)) modifiers.push(attr);
|
|
1316
|
+
}
|
|
892
1317
|
if (inCfgTest) modifiers.push('cfg_test_module');
|
|
893
1318
|
|
|
894
1319
|
const memberGenerics = extractGenerics(child);
|
|
1320
|
+
const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
|
|
895
1321
|
members.push({
|
|
896
1322
|
name: nameNode.text,
|
|
897
1323
|
params: extractRustParams(paramsNode),
|
|
898
1324
|
paramsStructured: parseStructuredParams(paramsNode, 'rust'),
|
|
1325
|
+
...(callbackParamTypes && { callbackParamTypes }),
|
|
899
1326
|
startLine,
|
|
900
1327
|
endLine,
|
|
901
1328
|
memberType: 'method',
|
|
@@ -904,6 +1331,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
|
|
|
904
1331
|
modifiers,
|
|
905
1332
|
...(typeName && { receiver: typeName }), // All impl members get receiver for findMethodsForType
|
|
906
1333
|
...(returnType && { returnType }),
|
|
1334
|
+
...(iteratorItemType && { iteratorItemType }),
|
|
907
1335
|
...(docstring && { docstring }),
|
|
908
1336
|
// Method-level type params (fix #229): generic-param receiver
|
|
909
1337
|
// types inside the method resolve against this declaration.
|
|
@@ -920,13 +1348,15 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
|
|
|
920
1348
|
* Find state objects (const/static) in Rust code
|
|
921
1349
|
*/
|
|
922
1350
|
function findStateObjects(code, parser) {
|
|
923
|
-
const
|
|
1351
|
+
const { trees } = declarationTrees(code, parser);
|
|
924
1352
|
const lines = code.split('\n');
|
|
925
1353
|
const objects = [];
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1354
|
+
for (const tree of trees) {
|
|
1355
|
+
traverseTreeCached(tree.rootNode, (node) => {
|
|
1356
|
+
_processState(node, objects, lines);
|
|
1357
|
+
return true;
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
930
1360
|
objects.sort((a, b) => a.startLine - b.startLine);
|
|
931
1361
|
return objects;
|
|
932
1362
|
}
|
|
@@ -935,17 +1365,20 @@ function findStateObjects(code, parser) {
|
|
|
935
1365
|
* Parse a Rust file completely
|
|
936
1366
|
*/
|
|
937
1367
|
function parse(code, parser) {
|
|
938
|
-
const
|
|
1368
|
+
const declaration = declarationTrees(code, parser);
|
|
1369
|
+
const tree = declaration.primary;
|
|
939
1370
|
const lines = code.split('\n');
|
|
940
1371
|
const functions = [], classes = [], stateObjects = [];
|
|
941
1372
|
const processedFn = new Set(), processedCls = new Set();
|
|
942
1373
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1374
|
+
for (const declarationTree of declaration.trees) {
|
|
1375
|
+
traverseTreeCached(declarationTree.rootNode, (node) => {
|
|
1376
|
+
_processFunction(node, functions, processedFn, lines, code);
|
|
1377
|
+
_processClass(node, classes, processedCls, lines, code);
|
|
1378
|
+
_processState(node, stateObjects, lines);
|
|
1379
|
+
return true; // always continue, never skip subtrees
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
949
1382
|
|
|
950
1383
|
_postProcessTraitImpls(classes);
|
|
951
1384
|
|
|
@@ -955,7 +1388,9 @@ function parse(code, parser) {
|
|
|
955
1388
|
|
|
956
1389
|
return {
|
|
957
1390
|
language: 'rust', totalLines: lines.length, functions, classes, stateObjects,
|
|
958
|
-
...(tree.rootNode.hasError && {
|
|
1391
|
+
...((tree.rootNode.hasError || declaration.macroItemRecovery) && {
|
|
1392
|
+
parseRecovery: true,
|
|
1393
|
+
}),
|
|
959
1394
|
imports: [], exports: [],
|
|
960
1395
|
};
|
|
961
1396
|
}
|
|
@@ -1053,14 +1488,22 @@ function _tokenTreeCallArgsAfter(children, nameIndex) {
|
|
|
1053
1488
|
}
|
|
1054
1489
|
|
|
1055
1490
|
function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType,
|
|
1056
|
-
isPatternShadow, isFlowInvalidated) {
|
|
1491
|
+
isPatternShadow, isFlowInvalidated, context = 'invocation') {
|
|
1492
|
+
const contextKind = typeof context === 'string' ? context : context.kind;
|
|
1493
|
+
const containerMacro = typeof context === 'object' ? context.containerMacro : undefined;
|
|
1057
1494
|
const children = [];
|
|
1058
1495
|
for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
|
|
1496
|
+
let lastProducer = null;
|
|
1497
|
+
const macroFields = {
|
|
1498
|
+
inMacro: true,
|
|
1499
|
+
...(contextKind === 'definition' && { inMacroDefinition: true }),
|
|
1500
|
+
...(containerMacro && { macroContainer: containerMacro }),
|
|
1501
|
+
};
|
|
1059
1502
|
for (let i = 0; i < children.length; i++) {
|
|
1060
1503
|
const tok = children[i];
|
|
1061
1504
|
if (tok.type === 'token_tree') {
|
|
1062
1505
|
extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType,
|
|
1063
|
-
isPatternShadow, isFlowInvalidated);
|
|
1506
|
+
isPatternShadow, isFlowInvalidated, context);
|
|
1064
1507
|
continue;
|
|
1065
1508
|
}
|
|
1066
1509
|
// `default` is tokenized as the Rust keyword even in the valid
|
|
@@ -1075,14 +1518,34 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1075
1518
|
// Nested macro invocation: name!(...)
|
|
1076
1519
|
if (next && next.type === '!' &&
|
|
1077
1520
|
children[i + 2] && children[i + 2].type === 'token_tree') {
|
|
1078
|
-
|
|
1521
|
+
const segments = [];
|
|
1522
|
+
let startNode = tok;
|
|
1523
|
+
let j = i - 1;
|
|
1524
|
+
while (j >= 1 && children[j].type === '::') {
|
|
1525
|
+
const segment = children[j - 1];
|
|
1526
|
+
if (!segment || ![
|
|
1527
|
+
'identifier', 'metavariable', 'crate', 'self', 'super',
|
|
1528
|
+
].includes(segment.type)) break;
|
|
1529
|
+
segments.unshift(segment.text === '$crate' ? 'crate' : segment.text);
|
|
1530
|
+
startNode = segment;
|
|
1531
|
+
j -= 2;
|
|
1532
|
+
}
|
|
1533
|
+
const record = {
|
|
1079
1534
|
name: tok.text,
|
|
1080
1535
|
line: tok.startPosition.row + 1,
|
|
1536
|
+
callStart: startNode.startIndex,
|
|
1537
|
+
callEnd: children[i + 2].endIndex,
|
|
1081
1538
|
isMethod: false,
|
|
1082
1539
|
isMacro: true,
|
|
1083
|
-
|
|
1540
|
+
...(segments.length > 0 && {
|
|
1541
|
+
receiver: segments.join('::'),
|
|
1542
|
+
isPathMacro: true,
|
|
1543
|
+
}),
|
|
1544
|
+
...macroFields,
|
|
1084
1545
|
enclosingFunction
|
|
1085
|
-
}
|
|
1546
|
+
};
|
|
1547
|
+
calls.push(record);
|
|
1548
|
+
lastProducer = record;
|
|
1086
1549
|
continue;
|
|
1087
1550
|
}
|
|
1088
1551
|
const callArgs = _tokenTreeCallArgsAfter(children, i);
|
|
@@ -1090,8 +1553,11 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1090
1553
|
if (prev && prev.type === '::') {
|
|
1091
1554
|
// Path call: Type::func(...) / module::sub::func(...) — segments
|
|
1092
1555
|
// can be identifiers, primitives (char::from), or path keywords
|
|
1093
|
-
const isSegment = (n) => n && [
|
|
1556
|
+
const isSegment = (n) => n && [
|
|
1557
|
+
'identifier', 'primitive_type', 'metavariable', 'self', 'super', 'crate',
|
|
1558
|
+
].includes(n.type);
|
|
1094
1559
|
const segments = [];
|
|
1560
|
+
let startNode = tok;
|
|
1095
1561
|
let j = i - 1;
|
|
1096
1562
|
while (j >= 1 && children[j].type === '::') {
|
|
1097
1563
|
let k = j - 1;
|
|
@@ -1115,23 +1581,39 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1115
1581
|
k -= 2;
|
|
1116
1582
|
}
|
|
1117
1583
|
if (!isSegment(children[k])) break;
|
|
1118
|
-
segments.unshift(children[k].text);
|
|
1584
|
+
segments.unshift(children[k].text === '$crate' ? 'crate' : children[k].text);
|
|
1585
|
+
startNode = children[k];
|
|
1119
1586
|
j = k - 1;
|
|
1120
1587
|
}
|
|
1121
|
-
|
|
1588
|
+
const record = {
|
|
1122
1589
|
name: tok.text,
|
|
1123
1590
|
line: tok.startPosition.row + 1,
|
|
1591
|
+
callStart: startNode.startIndex,
|
|
1592
|
+
callEnd: callArgs.endIndex,
|
|
1124
1593
|
isMethod: segments.length > 0,
|
|
1125
1594
|
isPathCall: true,
|
|
1126
1595
|
receiver: segments.length > 0 ? segments.join('::') : undefined,
|
|
1127
|
-
|
|
1596
|
+
...macroFields,
|
|
1128
1597
|
enclosingFunction
|
|
1129
|
-
}
|
|
1598
|
+
};
|
|
1599
|
+
calls.push(record);
|
|
1600
|
+
lastProducer = record;
|
|
1130
1601
|
} else if (prev && prev.type === '.') {
|
|
1131
1602
|
// Method call: recv.method(...)
|
|
1132
1603
|
const recvTok = children[i - 2];
|
|
1133
1604
|
const receiver = recvTok && (recvTok.type === 'identifier' || recvTok.type === 'self')
|
|
1134
1605
|
? recvTok.text : undefined;
|
|
1606
|
+
// Token trees flatten `root.field.method(...)`. Retain the same
|
|
1607
|
+
// one-hop field contract as the regular AST path so query-time
|
|
1608
|
+
// analysis can type `args.separator.into_bytes()` from the
|
|
1609
|
+
// declared type of `LowArgs.separator`.
|
|
1610
|
+
let receiverRoot, receiverField;
|
|
1611
|
+
if (receiver && children[i - 3]?.type === '.' &&
|
|
1612
|
+
(children[i - 4]?.type === 'identifier' ||
|
|
1613
|
+
children[i - 4]?.type === 'self')) {
|
|
1614
|
+
receiverRoot = children[i - 4].text;
|
|
1615
|
+
receiverField = receiver;
|
|
1616
|
+
}
|
|
1135
1617
|
// Literal receivers type as builtins inside macros too (fix #220,
|
|
1136
1618
|
// ripgrep-measured: assert_eq!(.., vec!["match:fg".parse()...]))
|
|
1137
1619
|
const litType = recvTok
|
|
@@ -1142,26 +1624,47 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1142
1624
|
? getReceiverType(receiver, tok) : litType;
|
|
1143
1625
|
const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
|
|
1144
1626
|
const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
|
|
1145
|
-
|
|
1627
|
+
const iterationSource = rustIterationSourceOf(tok, receiver);
|
|
1628
|
+
const producer = !receiver && lastProducer &&
|
|
1629
|
+
lastProducer.callEnd === recvTok?.endIndex ? lastProducer : null;
|
|
1630
|
+
const record = {
|
|
1146
1631
|
name: tok.text,
|
|
1147
1632
|
line: tok.startPosition.row + 1,
|
|
1633
|
+
callStart: producer?.callStart ?? recvTok?.startIndex ?? tok.startIndex,
|
|
1634
|
+
callEnd: callArgs.endIndex,
|
|
1148
1635
|
isMethod: true,
|
|
1149
|
-
receiver,
|
|
1636
|
+
receiver: receiverField ? undefined : receiver,
|
|
1637
|
+
...(receiverField && { receiverRoot, receiverField }),
|
|
1150
1638
|
...(receiverType && { receiverType }),
|
|
1151
1639
|
...(receiverPatternShadow && { receiverPatternShadow: true }),
|
|
1152
1640
|
...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
|
|
1153
|
-
|
|
1641
|
+
...(iterationSource || {}),
|
|
1642
|
+
...(producer && {
|
|
1643
|
+
receiverCall: producer.name,
|
|
1644
|
+
...(producer.isMethod && { receiverCallIsMethod: true }),
|
|
1645
|
+
...(producer.isMacro && { receiverCallIsMacro: true }),
|
|
1646
|
+
receiverCallLine: producer.line,
|
|
1647
|
+
receiverCallStart: producer.callStart,
|
|
1648
|
+
receiverCallEnd: producer.callEnd,
|
|
1649
|
+
}),
|
|
1650
|
+
...macroFields,
|
|
1154
1651
|
enclosingFunction
|
|
1155
|
-
}
|
|
1652
|
+
};
|
|
1653
|
+
calls.push(record);
|
|
1654
|
+
lastProducer = record;
|
|
1156
1655
|
} else {
|
|
1157
1656
|
// Plain call: func(...) — includes enum-variant constructors
|
|
1158
|
-
|
|
1657
|
+
const record = {
|
|
1159
1658
|
name: tok.text,
|
|
1160
1659
|
line: tok.startPosition.row + 1,
|
|
1660
|
+
callStart: tok.startIndex,
|
|
1661
|
+
callEnd: callArgs.endIndex,
|
|
1161
1662
|
isMethod: false,
|
|
1162
|
-
|
|
1663
|
+
...macroFields,
|
|
1163
1664
|
enclosingFunction
|
|
1164
|
-
}
|
|
1665
|
+
};
|
|
1666
|
+
calls.push(record);
|
|
1667
|
+
lastProducer = record;
|
|
1165
1668
|
}
|
|
1166
1669
|
}
|
|
1167
1670
|
}
|
|
@@ -1177,6 +1680,38 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1177
1680
|
* Result<T, _>/Option<T> from the producer's return annotation. `let mut x`
|
|
1178
1681
|
* works too — the pattern field is the plain identifier.
|
|
1179
1682
|
*/
|
|
1683
|
+
function rustDivergingExpression(node) {
|
|
1684
|
+
if (!node) return false;
|
|
1685
|
+
if (['return_expression', 'break_expression', 'continue_expression']
|
|
1686
|
+
.includes(node.type)) return true;
|
|
1687
|
+
if (node.type === 'expression_statement' && node.namedChildCount === 1) {
|
|
1688
|
+
return rustDivergingExpression(node.namedChild(0));
|
|
1689
|
+
}
|
|
1690
|
+
if (node.type !== 'block') return false;
|
|
1691
|
+
const last = node.namedChildCount > 0
|
|
1692
|
+
? node.namedChild(node.namedChildCount - 1) : null;
|
|
1693
|
+
return rustDivergingExpression(last);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function rustMatchCallProducer(matchExpression) {
|
|
1697
|
+
if (matchExpression?.type !== 'match_expression') return null;
|
|
1698
|
+
const body = matchExpression.childForFieldName('body');
|
|
1699
|
+
if (!body) return null;
|
|
1700
|
+
const calls = [];
|
|
1701
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1702
|
+
const arm = body.namedChild(i);
|
|
1703
|
+
if (arm.type !== 'match_arm') continue;
|
|
1704
|
+
const value = arm.childForFieldName('value');
|
|
1705
|
+
if (rustDivergingExpression(value)) continue;
|
|
1706
|
+
if (value?.type !== 'call_expression') return null;
|
|
1707
|
+
calls.push(value);
|
|
1708
|
+
}
|
|
1709
|
+
if (calls.length === 0) return null;
|
|
1710
|
+
const identities = new Set(calls.map(call =>
|
|
1711
|
+
call.childForFieldName('function')?.text || ''));
|
|
1712
|
+
return identities.size === 1 ? calls : null;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1180
1715
|
function rustAssignmentTargetOf(callNode) {
|
|
1181
1716
|
let n = callNode;
|
|
1182
1717
|
let p = n.parent;
|
|
@@ -1194,12 +1729,43 @@ function rustAssignmentTargetOf(callNode) {
|
|
|
1194
1729
|
}
|
|
1195
1730
|
break;
|
|
1196
1731
|
}
|
|
1732
|
+
if (p?.type === 'match_arm' &&
|
|
1733
|
+
p.childForFieldName('value')?.id === n.id) {
|
|
1734
|
+
let matchExpression = p.parent;
|
|
1735
|
+
while (matchExpression && matchExpression.type !== 'match_expression') {
|
|
1736
|
+
matchExpression = matchExpression.parent;
|
|
1737
|
+
}
|
|
1738
|
+
const producers = rustMatchCallProducer(matchExpression);
|
|
1739
|
+
if (producers?.some(producer => producer.id === callNode.id)) {
|
|
1740
|
+
const declaration = matchExpression.parent;
|
|
1741
|
+
if (declaration?.type === 'let_declaration' &&
|
|
1742
|
+
declaration.childForFieldName('value')?.id === matchExpression.id) {
|
|
1743
|
+
const pattern = declaration.childForFieldName('pattern');
|
|
1744
|
+
if (pattern?.type === 'identifier') {
|
|
1745
|
+
return { assignedTo: pattern.text };
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1197
1750
|
if (p.type === 'let_declaration') {
|
|
1198
1751
|
const value = p.childForFieldName('value');
|
|
1199
1752
|
const pattern = p.childForFieldName('pattern');
|
|
1200
1753
|
if (value && value.id === n.id && pattern?.type === 'identifier') {
|
|
1201
1754
|
return { assignedTo: pattern.text, ...(unwrapped && { unwrapped: true }) };
|
|
1202
1755
|
}
|
|
1756
|
+
if (value && value.id === n.id && pattern?.type === 'tuple_pattern') {
|
|
1757
|
+
const bindings = pattern.namedChildren
|
|
1758
|
+
.filter(child => child.type === 'identifier')
|
|
1759
|
+
.map(child => child.text);
|
|
1760
|
+
if (bindings.length === pattern.namedChildCount && bindings.length > 0) {
|
|
1761
|
+
return {
|
|
1762
|
+
assignedTo: bindings[0],
|
|
1763
|
+
tuple: true,
|
|
1764
|
+
...(bindings.length > 1 && { tupleRest: bindings.slice(1) }),
|
|
1765
|
+
...(unwrapped && { unwrapped: true }),
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1203
1769
|
return undefined;
|
|
1204
1770
|
}
|
|
1205
1771
|
if (p.type === 'assignment_expression') {
|
|
@@ -1212,6 +1778,170 @@ function rustAssignmentTargetOf(callNode) {
|
|
|
1212
1778
|
return undefined;
|
|
1213
1779
|
}
|
|
1214
1780
|
|
|
1781
|
+
function rustCallIdentity(callNode) {
|
|
1782
|
+
if (!callNode || callNode.type !== 'call_expression') return null;
|
|
1783
|
+
const fn = callNode.childForFieldName('function');
|
|
1784
|
+
if (!fn) return null;
|
|
1785
|
+
if (fn.type === 'identifier') {
|
|
1786
|
+
return { name: fn.text, isMethod: false };
|
|
1787
|
+
}
|
|
1788
|
+
if (fn.type === 'scoped_identifier' || fn.type === 'generic_function') {
|
|
1789
|
+
const parts = fn.text.split('::').filter(Boolean);
|
|
1790
|
+
const name = parts.pop()?.replace(/::<.*$/, '');
|
|
1791
|
+
return name ? { name, isMethod: false } : null;
|
|
1792
|
+
}
|
|
1793
|
+
if (fn.type === 'field_expression') {
|
|
1794
|
+
const field = fn.childForFieldName('field');
|
|
1795
|
+
return field ? { name: field.text, isMethod: true } : null;
|
|
1796
|
+
}
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* If this receiver is a for-loop binding, retain the exact iterator-source
|
|
1802
|
+
* call so query-time analysis can apply its declared Item contract.
|
|
1803
|
+
*/
|
|
1804
|
+
function rustIterationSourceOf(node, receiver) {
|
|
1805
|
+
if (!receiver) return null;
|
|
1806
|
+
let current = node.parent;
|
|
1807
|
+
while (current) {
|
|
1808
|
+
if (current.type === 'for_expression') {
|
|
1809
|
+
const pattern = current.childForFieldName('pattern');
|
|
1810
|
+
const value = current.childForFieldName('value');
|
|
1811
|
+
if (pattern?.type === 'identifier' && pattern.text === receiver) {
|
|
1812
|
+
if (value?.type === 'identifier') {
|
|
1813
|
+
return { receiverIterationVariable: value.text };
|
|
1814
|
+
}
|
|
1815
|
+
if (value?.type === 'call_expression') {
|
|
1816
|
+
const identity = rustCallIdentity(value);
|
|
1817
|
+
if (!identity) return null;
|
|
1818
|
+
return {
|
|
1819
|
+
receiverIterationCall: identity.name,
|
|
1820
|
+
...(identity.isMethod && { receiverIterationCallIsMethod: true }),
|
|
1821
|
+
receiverIterationCallLine: value.startPosition.row + 1,
|
|
1822
|
+
receiverIterationCallStart: value.startIndex,
|
|
1823
|
+
receiverIterationCallEnd: value.endIndex,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
current = current.parent;
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Retain the enum-variant contract that binds a match-arm receiver:
|
|
1835
|
+
* `DirEntryInner::Raw(ref entry) => entry.path()`. The variant's indexed
|
|
1836
|
+
* payload type is resolved query-time, where cross-file identity is known.
|
|
1837
|
+
* Destructured/nested payloads abstain unless the receiver is the whole
|
|
1838
|
+
* positional field.
|
|
1839
|
+
*/
|
|
1840
|
+
function rustPatternBindingOf(node, receiver) {
|
|
1841
|
+
if (!receiver) return null;
|
|
1842
|
+
const directBindingName = pattern => {
|
|
1843
|
+
if (!pattern) return null;
|
|
1844
|
+
if (pattern.type === 'identifier') return pattern.text;
|
|
1845
|
+
if (!['ref_pattern', 'mut_pattern', 'reference_pattern']
|
|
1846
|
+
.includes(pattern.type)) return null;
|
|
1847
|
+
const identifiers = [];
|
|
1848
|
+
const pending = [pattern];
|
|
1849
|
+
while (pending.length > 0) {
|
|
1850
|
+
const current = pending.pop();
|
|
1851
|
+
if (current.type === 'identifier') {
|
|
1852
|
+
identifiers.push(current.text);
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
if (current !== pattern &&
|
|
1856
|
+
['tuple_pattern', 'tuple_struct_pattern', 'struct_pattern']
|
|
1857
|
+
.includes(current.type)) {
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
1861
|
+
pending.push(current.namedChild(i));
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return identifiers.length === 1 ? identifiers[0] : null;
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
1868
|
+
if (ancestor.type === 'function_item') break;
|
|
1869
|
+
if (ancestor.type === 'closure_expression') {
|
|
1870
|
+
const params = ancestor.childForFieldName('parameters');
|
|
1871
|
+
if (params && patternContainsIdentifier(params, receiver)) break;
|
|
1872
|
+
continue; // captured binding from an outer match arm
|
|
1873
|
+
}
|
|
1874
|
+
if (ancestor.type !== 'match_arm') continue;
|
|
1875
|
+
let matchExpression = ancestor.parent;
|
|
1876
|
+
while (matchExpression && matchExpression.type !== 'match_expression' &&
|
|
1877
|
+
matchExpression.type !== 'function_item') {
|
|
1878
|
+
matchExpression = matchExpression.parent;
|
|
1879
|
+
}
|
|
1880
|
+
const matchValue = matchExpression?.type === 'match_expression'
|
|
1881
|
+
? matchExpression.childForFieldName('value')
|
|
1882
|
+
: null;
|
|
1883
|
+
const source = matchValue?.type === 'identifier'
|
|
1884
|
+
? { receiverPatternSourceVariable: matchValue.text }
|
|
1885
|
+
: {};
|
|
1886
|
+
const root = ancestor.childForFieldName('pattern');
|
|
1887
|
+
const pending = root ? [root] : [];
|
|
1888
|
+
while (pending.length > 0) {
|
|
1889
|
+
const pattern = pending.pop();
|
|
1890
|
+
if (pattern.type === 'tuple_struct_pattern') {
|
|
1891
|
+
const typeNode = pattern.childForFieldName('type');
|
|
1892
|
+
const positional = pattern.namedChildren
|
|
1893
|
+
.filter(child => !typeNode || child.id !== typeNode.id);
|
|
1894
|
+
for (let i = 0; i < positional.length; i++) {
|
|
1895
|
+
if (directBindingName(positional[i]) !== receiver) continue;
|
|
1896
|
+
const pathText = typeNode?.text;
|
|
1897
|
+
if (!pathText) return null;
|
|
1898
|
+
const segments = pathText.split('::').filter(Boolean);
|
|
1899
|
+
const variant = segments.pop();
|
|
1900
|
+
if (!variant) return null;
|
|
1901
|
+
return {
|
|
1902
|
+
receiverPatternVariant: variant,
|
|
1903
|
+
receiverPatternIndex: i,
|
|
1904
|
+
...source,
|
|
1905
|
+
...(segments.length > 0 && {
|
|
1906
|
+
receiverPatternOwner: segments.join('::'),
|
|
1907
|
+
}),
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
1912
|
+
pending.push(pattern.namedChild(i));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function patternContainsIdentifier(pattern, name) {
|
|
1921
|
+
const pending = pattern ? [pattern] : [];
|
|
1922
|
+
while (pending.length > 0) {
|
|
1923
|
+
const current = pending.pop();
|
|
1924
|
+
if (current.type === 'identifier' && current.text === name) return true;
|
|
1925
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
1926
|
+
pending.push(current.namedChild(i));
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function rustMacroCallIdentity(macroNode) {
|
|
1933
|
+
if (!macroNode) return null;
|
|
1934
|
+
const parts = macroNode.text.replace(/!$/, '').split('::').filter(Boolean);
|
|
1935
|
+
const name = parts.pop();
|
|
1936
|
+
if (!name) return null;
|
|
1937
|
+
return {
|
|
1938
|
+
name,
|
|
1939
|
+
...(parts.length > 0 && {
|
|
1940
|
+
receiver: parts.map(part => part === '$crate' ? 'crate' : part).join('::'),
|
|
1941
|
+
}),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1215
1945
|
function findCallsInCode(code, parser) {
|
|
1216
1946
|
const tree = parseTree(parser, code);
|
|
1217
1947
|
const calls = [];
|
|
@@ -1256,7 +1986,10 @@ function findCallsInCode(code, parser) {
|
|
|
1256
1986
|
// Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
|
|
1257
1987
|
const extractTypeName = (typeNode) => {
|
|
1258
1988
|
if (!typeNode) return null;
|
|
1259
|
-
if (typeNode.type === 'type_identifier'
|
|
1989
|
+
if (typeNode.type === 'type_identifier' ||
|
|
1990
|
+
typeNode.type === 'primitive_type') {
|
|
1991
|
+
return typeNode.text;
|
|
1992
|
+
}
|
|
1260
1993
|
if (typeNode.type === 'reference_type') {
|
|
1261
1994
|
// &Filter or &mut Filter -> Filter
|
|
1262
1995
|
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
@@ -1264,6 +1997,12 @@ function findCallsInCode(code, parser) {
|
|
|
1264
1997
|
if (r) return r;
|
|
1265
1998
|
}
|
|
1266
1999
|
}
|
|
2000
|
+
if (typeNode.type === 'abstract_type' || typeNode.type === 'dynamic_type') {
|
|
2001
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
2002
|
+
const r = extractTypeName(typeNode.namedChild(i));
|
|
2003
|
+
if (r) return r;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1267
2006
|
if (typeNode.type === 'generic_type') {
|
|
1268
2007
|
// Box<Filter> -> Filter (or get the outer type)
|
|
1269
2008
|
return extractTypeName(typeNode.namedChild(0));
|
|
@@ -1276,22 +2015,74 @@ function findCallsInCode(code, parser) {
|
|
|
1276
2015
|
return null;
|
|
1277
2016
|
};
|
|
1278
2017
|
|
|
2018
|
+
const extractTypeQualifier = (typeNode) => {
|
|
2019
|
+
if (!typeNode) return null;
|
|
2020
|
+
if (typeNode.type === 'reference_type') {
|
|
2021
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
2022
|
+
const qualifier = extractTypeQualifier(typeNode.namedChild(i));
|
|
2023
|
+
if (qualifier) return qualifier;
|
|
2024
|
+
}
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
if (typeNode.type === 'scoped_type_identifier') {
|
|
2028
|
+
const nameNode = typeNode.childForFieldName('name');
|
|
2029
|
+
const text = typeNode.text;
|
|
2030
|
+
const suffix = nameNode ? `::${nameNode.text}` : '';
|
|
2031
|
+
return suffix && text.endsWith(suffix)
|
|
2032
|
+
? text.slice(0, -suffix.length) : null;
|
|
2033
|
+
}
|
|
2034
|
+
return null;
|
|
2035
|
+
};
|
|
2036
|
+
|
|
1279
2037
|
// Build type map from function parameters (including self receiver for impl methods)
|
|
1280
2038
|
const buildScopeTypeMap = (node) => {
|
|
1281
2039
|
const typeMap = new Map();
|
|
2040
|
+
typeMap.qualifiers = new Map();
|
|
2041
|
+
typeMap.iteratorItems = new Map();
|
|
2042
|
+
typeMap.annotationTexts = new Map();
|
|
2043
|
+
typeMap.boundNames = new Set();
|
|
2044
|
+
const retainBoundNames = (pattern) => {
|
|
2045
|
+
if (!pattern) return;
|
|
2046
|
+
const pending = [pattern];
|
|
2047
|
+
while (pending.length > 0) {
|
|
2048
|
+
const current = pending.pop();
|
|
2049
|
+
if (current.type === 'identifier') {
|
|
2050
|
+
typeMap.boundNames.add(current.text);
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
for (let i = 0; i < current.namedChildCount; i++) {
|
|
2054
|
+
pending.push(current.namedChild(i));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
1282
2058
|
const paramsNode = node.childForFieldName('parameters');
|
|
1283
2059
|
if (paramsNode) {
|
|
1284
2060
|
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
1285
2061
|
const param = paramsNode.namedChild(i);
|
|
1286
2062
|
if (param.type === 'parameter') {
|
|
1287
2063
|
const patternNode = param.childForFieldName('pattern');
|
|
2064
|
+
retainBoundNames(patternNode);
|
|
1288
2065
|
const typeNode = param.childForFieldName('type');
|
|
1289
2066
|
const typeName = extractTypeName(typeNode);
|
|
2067
|
+
const qualifier = extractTypeQualifier(typeNode);
|
|
2068
|
+
const iteratorItem = extractRustIteratorItemTypeFromTypeNode(typeNode);
|
|
1290
2069
|
if (patternNode && typeName) {
|
|
1291
2070
|
// Pattern can be identifier or _
|
|
1292
2071
|
const name = patternNode.type === 'identifier' ? patternNode.text : null;
|
|
1293
|
-
if (name)
|
|
2072
|
+
if (name) {
|
|
2073
|
+
typeMap.set(name, typeName);
|
|
2074
|
+
typeMap.annotationTexts.set(name, typeNode.text);
|
|
2075
|
+
if (qualifier) typeMap.qualifiers.set(name, qualifier);
|
|
2076
|
+
if (iteratorItem) typeMap.iteratorItems.set(name, iteratorItem);
|
|
2077
|
+
}
|
|
1294
2078
|
}
|
|
2079
|
+
} else {
|
|
2080
|
+
// Closure parameters normally have no annotation. They
|
|
2081
|
+
// still bind the name and must stop lookup before an
|
|
2082
|
+
// identically-named outer parameter (`arg: &str`;
|
|
2083
|
+
// `.any(|arg| arg.method())`) leaks its unrelated type
|
|
2084
|
+
// into the closure call record.
|
|
2085
|
+
retainBoundNames(param);
|
|
1295
2086
|
}
|
|
1296
2087
|
}
|
|
1297
2088
|
}
|
|
@@ -1313,7 +2104,10 @@ function findCallsInCode(code, parser) {
|
|
|
1313
2104
|
// Helper to get current enclosing function
|
|
1314
2105
|
const getCurrentEnclosingFunction = () => {
|
|
1315
2106
|
return functionStack.length > 0
|
|
1316
|
-
? {
|
|
2107
|
+
? {
|
|
2108
|
+
...functionStack[functionStack.length - 1],
|
|
2109
|
+
scopeChain: functionStack.map(scope => scope.startLine),
|
|
2110
|
+
}
|
|
1317
2111
|
: null;
|
|
1318
2112
|
};
|
|
1319
2113
|
|
|
@@ -1351,10 +2145,11 @@ function findCallsInCode(code, parser) {
|
|
|
1351
2145
|
while (n && ['try_expression', 'await_expression', 'parenthesized_expression'].includes(n.type)) {
|
|
1352
2146
|
n = n.namedChildCount === 1 ? n.namedChild(0) : null;
|
|
1353
2147
|
}
|
|
1354
|
-
return n?.type === 'call_expression'
|
|
2148
|
+
return n?.type === 'call_expression' || n?.type === 'macro_invocation' ||
|
|
2149
|
+
!!rustMatchCallProducer(n);
|
|
1355
2150
|
};
|
|
1356
2151
|
|
|
1357
|
-
const
|
|
2152
|
+
const flowEventAt = (node, varName) => {
|
|
1358
2153
|
const pos = node?.startIndex ?? -1;
|
|
1359
2154
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
1360
2155
|
const byName = scopeFlowEvents.get(functionStack[i].startLine);
|
|
@@ -1365,21 +2160,125 @@ function findCallsInCode(code, parser) {
|
|
|
1365
2160
|
if (event.at <= pos && pos <= event.until &&
|
|
1366
2161
|
(!latest || event.at > latest.at)) latest = event;
|
|
1367
2162
|
}
|
|
1368
|
-
if (latest) return latest
|
|
2163
|
+
if (latest) return latest;
|
|
1369
2164
|
}
|
|
1370
|
-
return
|
|
2165
|
+
return null;
|
|
1371
2166
|
};
|
|
1372
2167
|
|
|
2168
|
+
const flowInvalidatedAt = (node, varName) =>
|
|
2169
|
+
!!flowEventAt(node, varName)?.invalidated;
|
|
2170
|
+
|
|
1373
2171
|
// Look up variable type from scope chain
|
|
1374
2172
|
const getReceiverType = (varName, atNode) => {
|
|
1375
2173
|
if (atNode && patternShadowsAt(atNode, varName)) return undefined;
|
|
2174
|
+
const flow = flowEventAt(atNode, varName);
|
|
2175
|
+
if (flow?.type) return flow.type;
|
|
1376
2176
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
1377
2177
|
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
1378
2178
|
if (typeMap?.has(varName)) return typeMap.get(varName);
|
|
2179
|
+
if (typeMap?.boundNames?.has(varName)) return undefined;
|
|
1379
2180
|
}
|
|
1380
2181
|
return undefined;
|
|
1381
2182
|
};
|
|
1382
2183
|
|
|
2184
|
+
const getReceiverTypeQualifier = (varName, atNode) => {
|
|
2185
|
+
if (atNode && patternShadowsAt(atNode, varName)) return undefined;
|
|
2186
|
+
const flow = flowEventAt(atNode, varName);
|
|
2187
|
+
if (flow?.qualifier) return flow.qualifier;
|
|
2188
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
2189
|
+
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
2190
|
+
if (typeMap?.qualifiers?.has(varName)) {
|
|
2191
|
+
return typeMap.qualifiers.get(varName);
|
|
2192
|
+
}
|
|
2193
|
+
if (typeMap?.boundNames?.has(varName)) return undefined;
|
|
2194
|
+
}
|
|
2195
|
+
return undefined;
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
const getReceiverIteratorItemType = (varName, atNode) => {
|
|
2199
|
+
if (atNode && patternShadowsAt(atNode, varName)) return undefined;
|
|
2200
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
2201
|
+
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
2202
|
+
if (typeMap?.iteratorItems?.has(varName)) {
|
|
2203
|
+
return typeMap.iteratorItems.get(varName);
|
|
2204
|
+
}
|
|
2205
|
+
if (typeMap?.boundNames?.has(varName)) return undefined;
|
|
2206
|
+
}
|
|
2207
|
+
return undefined;
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2210
|
+
const getReceiverAnnotationText = (varName, atNode) => {
|
|
2211
|
+
if (atNode && patternShadowsAt(atNode, varName)) return undefined;
|
|
2212
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
2213
|
+
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
2214
|
+
if (typeMap?.annotationTexts?.has(varName)) {
|
|
2215
|
+
return typeMap.annotationTexts.get(varName);
|
|
2216
|
+
}
|
|
2217
|
+
if (typeMap?.boundNames?.has(varName)) return undefined;
|
|
2218
|
+
}
|
|
2219
|
+
return undefined;
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
const matchBindingType = (value, atNode) => {
|
|
2223
|
+
if (value?.type !== 'match_expression') return null;
|
|
2224
|
+
const source = value.childForFieldName('value');
|
|
2225
|
+
if (source?.type !== 'identifier') return null;
|
|
2226
|
+
const sourceType = getReceiverAnnotationText(source.text, atNode);
|
|
2227
|
+
if (!sourceType) return null;
|
|
2228
|
+
const body = value.childForFieldName('body');
|
|
2229
|
+
if (!body) return null;
|
|
2230
|
+
let variant = null;
|
|
2231
|
+
let binding = null;
|
|
2232
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2233
|
+
const arm = body.namedChild(i);
|
|
2234
|
+
if (arm.type !== 'match_arm') continue;
|
|
2235
|
+
const armValue = arm.childForFieldName('value');
|
|
2236
|
+
if (['return_expression', 'break_expression', 'continue_expression']
|
|
2237
|
+
.includes(armValue?.type)) {
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (armValue?.type !== 'identifier') return null;
|
|
2241
|
+
const pattern = arm.childForFieldName('pattern');
|
|
2242
|
+
const tuples = [];
|
|
2243
|
+
const pending = pattern ? [pattern] : [];
|
|
2244
|
+
while (pending.length > 0) {
|
|
2245
|
+
const current = pending.pop();
|
|
2246
|
+
if (current.type === 'tuple_struct_pattern') tuples.push(current);
|
|
2247
|
+
for (let j = 0; j < current.namedChildCount; j++) {
|
|
2248
|
+
pending.push(current.namedChild(j));
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const tuple = tuples.find(candidate =>
|
|
2252
|
+
candidate.namedChildren.some(child =>
|
|
2253
|
+
child.type === 'identifier' && child.text === armValue.text));
|
|
2254
|
+
const typeNode = tuple?.childForFieldName('type');
|
|
2255
|
+
if (!tuple || !typeNode) return null;
|
|
2256
|
+
const parts = typeNode.text.split('::').filter(Boolean);
|
|
2257
|
+
const currentVariant = parts.pop();
|
|
2258
|
+
if (!currentVariant || (variant && variant !== currentVariant) ||
|
|
2259
|
+
(binding && binding !== armValue.text)) {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
variant = currentVariant;
|
|
2263
|
+
binding = armValue.text;
|
|
2264
|
+
}
|
|
2265
|
+
if (!variant || !binding) return null;
|
|
2266
|
+
const wrapper = sourceType.trim().match(
|
|
2267
|
+
/^(?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*)*(Option|Result)\s*<(.*)>$/s);
|
|
2268
|
+
if (!wrapper) return null;
|
|
2269
|
+
const args = wrapper[2].split(',');
|
|
2270
|
+
const raw = args[variant === 'Err' ? 1 : 0]?.trim();
|
|
2271
|
+
if (!raw || [...'<>()[]'].some(character => raw.includes(character))) {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
const match = raw.match(/^(?:(.*)::)?([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
2275
|
+
if (!match) return null;
|
|
2276
|
+
return {
|
|
2277
|
+
type: match[2],
|
|
2278
|
+
...(match[1] && { qualifier: match[1] }),
|
|
2279
|
+
};
|
|
2280
|
+
};
|
|
2281
|
+
|
|
1383
2282
|
// Walk up to the enclosing impl block's target type (impl<T> Foo<T> → Foo).
|
|
1384
2283
|
const findEnclosingImplType = (n) => {
|
|
1385
2284
|
for (let p = n.parent; p; p = p.parent) {
|
|
@@ -1391,13 +2290,96 @@ function findCallsInCode(code, parser) {
|
|
|
1391
2290
|
return undefined;
|
|
1392
2291
|
};
|
|
1393
2292
|
|
|
2293
|
+
const closureContractSource = (node) => {
|
|
2294
|
+
if (node.type !== 'closure_expression') return null;
|
|
2295
|
+
const argumentsNode = node.parent;
|
|
2296
|
+
const outerCall = argumentsNode?.type === 'arguments'
|
|
2297
|
+
? argumentsNode.parent : null;
|
|
2298
|
+
if (outerCall?.type !== 'call_expression') return null;
|
|
2299
|
+
let argumentIndex = 0;
|
|
2300
|
+
let found = false;
|
|
2301
|
+
for (let i = 0; i < argumentsNode.namedChildCount; i++) {
|
|
2302
|
+
const argument = argumentsNode.namedChild(i);
|
|
2303
|
+
if (argument.type.endsWith('comment')) continue;
|
|
2304
|
+
if (argument.id === node.id) {
|
|
2305
|
+
found = true;
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
argumentIndex++;
|
|
2309
|
+
}
|
|
2310
|
+
if (!found) return null;
|
|
2311
|
+
let functionNode = outerCall.childForFieldName('function');
|
|
2312
|
+
if (functionNode?.type === 'generic_function') {
|
|
2313
|
+
functionNode = functionNode.childForFieldName('function') || functionNode;
|
|
2314
|
+
}
|
|
2315
|
+
let callName;
|
|
2316
|
+
let callIsMethod = false;
|
|
2317
|
+
if (functionNode?.type === 'field_expression') {
|
|
2318
|
+
callName = functionNode.childForFieldName('field')?.text;
|
|
2319
|
+
callIsMethod = true;
|
|
2320
|
+
} else if (functionNode?.type === 'identifier') {
|
|
2321
|
+
callName = functionNode.text;
|
|
2322
|
+
} else if (functionNode?.type === 'scoped_identifier') {
|
|
2323
|
+
callName = functionNode.childForFieldName('name')?.text;
|
|
2324
|
+
callIsMethod = true;
|
|
2325
|
+
}
|
|
2326
|
+
if (!callName) return null;
|
|
2327
|
+
const parameters = node.childForFieldName('parameters');
|
|
2328
|
+
const parameterNames = [];
|
|
2329
|
+
let parametersComplete = true;
|
|
2330
|
+
if (parameters) {
|
|
2331
|
+
for (let i = 0; i < parameters.namedChildCount; i++) {
|
|
2332
|
+
const parameter = parameters.namedChild(i);
|
|
2333
|
+
if (parameter.type === 'identifier') {
|
|
2334
|
+
parameterNames.push(parameter.text);
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
// `|ref a, ref b|` and `|mut value|` bind the same callback
|
|
2338
|
+
// parameter as their identifier child. Destructuring patterns
|
|
2339
|
+
// intentionally abstain: a tuple member is not the callback's
|
|
2340
|
+
// whole declared type.
|
|
2341
|
+
if (['ref_pattern', 'mut_pattern', 'reference_pattern']
|
|
2342
|
+
.includes(parameter.type)) {
|
|
2343
|
+
const identifiers = [];
|
|
2344
|
+
const pending = [parameter];
|
|
2345
|
+
while (pending.length > 0) {
|
|
2346
|
+
const current = pending.pop();
|
|
2347
|
+
if (current.type === 'identifier') {
|
|
2348
|
+
identifiers.push(current.text);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
for (let j = 0; j < current.namedChildCount; j++) {
|
|
2352
|
+
pending.push(current.namedChild(j));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (identifiers.length === 1) {
|
|
2356
|
+
parameterNames.push(identifiers[0]);
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
parametersComplete = false;
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (!parametersComplete || parameterNames.length === 0) return null;
|
|
2365
|
+
return {
|
|
2366
|
+
closureSourceCall: callName,
|
|
2367
|
+
closureSourceCallStart: outerCall.startIndex,
|
|
2368
|
+
closureSourceCallEnd: outerCall.endIndex,
|
|
2369
|
+
closureSourceCallIsMethod: callIsMethod,
|
|
2370
|
+
closureArgumentIndex: argumentIndex,
|
|
2371
|
+
closureParameterNames: parameterNames,
|
|
2372
|
+
};
|
|
2373
|
+
};
|
|
2374
|
+
|
|
1394
2375
|
traverseTree(tree.rootNode, (node) => {
|
|
1395
2376
|
// Track function entry
|
|
1396
2377
|
if (isFunctionNode(node)) {
|
|
1397
2378
|
const entry = {
|
|
1398
2379
|
name: extractFunctionName(node),
|
|
1399
2380
|
startLine: node.startPosition.row + 1,
|
|
1400
|
-
endLine: node.endPosition.row + 1
|
|
2381
|
+
endLine: node.endPosition.row + 1,
|
|
2382
|
+
...closureContractSource(node),
|
|
1401
2383
|
};
|
|
1402
2384
|
functionStack.push(entry);
|
|
1403
2385
|
scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
|
|
@@ -1428,7 +2410,12 @@ function findCallsInCode(code, parser) {
|
|
|
1428
2410
|
byName.get(pattern.text).push({
|
|
1429
2411
|
at: node.endIndex,
|
|
1430
2412
|
until,
|
|
1431
|
-
|
|
2413
|
+
...(() => {
|
|
2414
|
+
const inferred = matchBindingType(value, node);
|
|
2415
|
+
return inferred
|
|
2416
|
+
? { invalidated: false, ...inferred }
|
|
2417
|
+
: { invalidated: !valueHasFlowProducer(value) };
|
|
2418
|
+
})(),
|
|
1432
2419
|
});
|
|
1433
2420
|
}
|
|
1434
2421
|
}
|
|
@@ -1440,6 +2427,7 @@ function findCallsInCode(code, parser) {
|
|
|
1440
2427
|
if (!funcNode) return true;
|
|
1441
2428
|
|
|
1442
2429
|
// Unwrap turbofish: parse::<i32>() has generic_function wrapping the actual function
|
|
2430
|
+
const collectResult = extractCollectResultContract(funcNode);
|
|
1443
2431
|
if (funcNode.type === 'generic_function') {
|
|
1444
2432
|
funcNode = funcNode.childForFieldName('function') || funcNode;
|
|
1445
2433
|
}
|
|
@@ -1469,10 +2457,14 @@ function findCallsInCode(code, parser) {
|
|
|
1469
2457
|
calls.push({
|
|
1470
2458
|
name: funcNode.text,
|
|
1471
2459
|
line: node.startPosition.row + 1,
|
|
2460
|
+
callStart: node.startIndex,
|
|
2461
|
+
callEnd: node.endIndex,
|
|
1472
2462
|
isMethod: false,
|
|
1473
2463
|
argCount,
|
|
1474
2464
|
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1475
2465
|
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
2466
|
+
...(assigned?.tuple && { assignedTuple: true }),
|
|
2467
|
+
...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
|
|
1476
2468
|
enclosingFunction,
|
|
1477
2469
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1478
2470
|
});
|
|
@@ -1483,6 +2475,18 @@ function findCallsInCode(code, parser) {
|
|
|
1483
2475
|
|
|
1484
2476
|
if (fieldNode) {
|
|
1485
2477
|
let receiver = (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined;
|
|
2478
|
+
// A range index preserves the receiver's collection/slice
|
|
2479
|
+
// type (`doc[start..].find(...)` still dispatches on
|
|
2480
|
+
// `str`). A single-element index does NOT: `items[i]`
|
|
2481
|
+
// dispatches on the element type, so only the
|
|
2482
|
+
// range-expression shape may reuse the root binding.
|
|
2483
|
+
if (!receiver && valueNode?.type === 'index_expression' &&
|
|
2484
|
+
valueNode.namedChild(1)?.type === 'range_expression') {
|
|
2485
|
+
const indexed = valueNode.namedChild(0);
|
|
2486
|
+
if (indexed?.type === 'identifier' || indexed?.type === 'self') {
|
|
2487
|
+
receiver = indexed.text;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
1486
2490
|
// Detect chained Router::new()-rooted method calls. axum's canonical
|
|
1487
2491
|
// idiom is `Router::new().route("/p", get(h)).route(...)` where the
|
|
1488
2492
|
// receiver of `.route(...)` is itself a call_expression. Walk the
|
|
@@ -1507,7 +2511,8 @@ function findCallsInCode(code, parser) {
|
|
|
1507
2511
|
// low.sep.into_bytes() — with .clone() transparency (clone()
|
|
1508
2512
|
// returns Self by stdlib convention). receiverRoot/Field/RootType
|
|
1509
2513
|
// let findCallers hop to the field's declared type cross-file.
|
|
1510
|
-
let receiverRoot, receiverField, receiverRootType;
|
|
2514
|
+
let receiverRoot, receiverField, receiverFields, receiverRootType;
|
|
2515
|
+
let receiverFieldCallRoot;
|
|
1511
2516
|
if (!receiver) {
|
|
1512
2517
|
let obj = valueNode;
|
|
1513
2518
|
while (obj?.type === 'call_expression') {
|
|
@@ -1518,15 +2523,32 @@ function findCallsInCode(code, parser) {
|
|
|
1518
2523
|
} else break;
|
|
1519
2524
|
}
|
|
1520
2525
|
if (obj?.type === 'field_expression') {
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
2526
|
+
const fields = [];
|
|
2527
|
+
let rootNode = obj;
|
|
2528
|
+
while (rootNode?.type === 'field_expression') {
|
|
2529
|
+
const fldNode = rootNode.childForFieldName('field');
|
|
2530
|
+
if (!fldNode || ![
|
|
2531
|
+
'field_identifier', 'integer_literal',
|
|
2532
|
+
].includes(fldNode.type)) {
|
|
2533
|
+
fields.length = 0;
|
|
2534
|
+
break;
|
|
2535
|
+
}
|
|
2536
|
+
fields.unshift(fldNode.text);
|
|
2537
|
+
rootNode = rootNode.childForFieldName('value');
|
|
2538
|
+
}
|
|
2539
|
+
if (fields.length > 0 && rootNode &&
|
|
1524
2540
|
(rootNode.type === 'identifier' || rootNode.type === 'self')) {
|
|
1525
2541
|
receiverRoot = rootNode.text;
|
|
1526
|
-
|
|
2542
|
+
receiverFields = fields;
|
|
2543
|
+
receiverField = fields[fields.length - 1];
|
|
1527
2544
|
receiverRootType = rootNode.type === 'self'
|
|
1528
2545
|
? findEnclosingImplType(node)
|
|
1529
2546
|
: getReceiverType(rootNode.text, node);
|
|
2547
|
+
} else if (fields.length > 0 &&
|
|
2548
|
+
rootNode?.type === 'call_expression') {
|
|
2549
|
+
receiverFields = fields;
|
|
2550
|
+
receiverField = fields[fields.length - 1];
|
|
2551
|
+
receiverFieldCallRoot = rootNode;
|
|
1530
2552
|
}
|
|
1531
2553
|
} else if (obj && obj !== valueNode &&
|
|
1532
2554
|
(obj.type === 'identifier' || obj.type === 'self')) {
|
|
@@ -1546,6 +2568,7 @@ function findCallsInCode(code, parser) {
|
|
|
1546
2568
|
// rooted chains keep their synthetic receiver marker for
|
|
1547
2569
|
// the bridge but get the link too.
|
|
1548
2570
|
let receiverCall, receiverCallIsMethod, receiverCallLine;
|
|
2571
|
+
let receiverCallStart, receiverCallEnd;
|
|
1549
2572
|
if ((!receiver || receiverIsChainRoot) && !receiverField &&
|
|
1550
2573
|
valueNode?.type === 'call_expression') {
|
|
1551
2574
|
let prodFunc = valueNode.childForFieldName('function');
|
|
@@ -1555,12 +2578,16 @@ function findCallsInCode(code, parser) {
|
|
|
1555
2578
|
if (prodFunc?.type === 'identifier') {
|
|
1556
2579
|
receiverCall = prodFunc.text;
|
|
1557
2580
|
receiverCallLine = valueNode.startPosition.row + 1;
|
|
2581
|
+
receiverCallStart = valueNode.startIndex;
|
|
2582
|
+
receiverCallEnd = valueNode.endIndex;
|
|
1558
2583
|
} else if (prodFunc?.type === 'field_expression') {
|
|
1559
2584
|
const pf = prodFunc.childForFieldName('field');
|
|
1560
2585
|
if (pf) {
|
|
1561
2586
|
receiverCall = pf.text;
|
|
1562
2587
|
receiverCallIsMethod = true;
|
|
1563
2588
|
receiverCallLine = pf.startPosition.row + 1;
|
|
2589
|
+
receiverCallStart = valueNode.startIndex;
|
|
2590
|
+
receiverCallEnd = valueNode.endIndex;
|
|
1564
2591
|
}
|
|
1565
2592
|
} else if (prodFunc?.type === 'scoped_identifier') {
|
|
1566
2593
|
// Path producer: Command::new(...).arg(...) — the
|
|
@@ -1573,8 +2600,41 @@ function findCallsInCode(code, parser) {
|
|
|
1573
2600
|
receiverCall = prodName;
|
|
1574
2601
|
receiverCallIsMethod = true;
|
|
1575
2602
|
receiverCallLine = valueNode.startPosition.row + 1;
|
|
2603
|
+
receiverCallStart = valueNode.startIndex;
|
|
2604
|
+
receiverCallEnd = valueNode.endIndex;
|
|
1576
2605
|
}
|
|
1577
2606
|
}
|
|
2607
|
+
} else if ((!receiver || receiverIsChainRoot) && !receiverField &&
|
|
2608
|
+
valueNode?.type === 'macro_invocation') {
|
|
2609
|
+
const macro = rustMacroCallIdentity(
|
|
2610
|
+
valueNode.childForFieldName('macro'));
|
|
2611
|
+
if (macro) {
|
|
2612
|
+
receiverCall = macro.name;
|
|
2613
|
+
receiverCallLine = valueNode.startPosition.row + 1;
|
|
2614
|
+
receiverCallStart = valueNode.startIndex;
|
|
2615
|
+
receiverCallEnd = valueNode.endIndex;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (!receiverCall && receiverFieldCallRoot) {
|
|
2619
|
+
let prodFunc = receiverFieldCallRoot.childForFieldName('function');
|
|
2620
|
+
if (prodFunc?.type === 'generic_function') {
|
|
2621
|
+
prodFunc = prodFunc.childForFieldName('function') || prodFunc;
|
|
2622
|
+
}
|
|
2623
|
+
if (prodFunc?.type === 'identifier') {
|
|
2624
|
+
receiverCall = prodFunc.text;
|
|
2625
|
+
} else if (prodFunc?.type === 'field_expression') {
|
|
2626
|
+
receiverCall = prodFunc.childForFieldName('field')?.text;
|
|
2627
|
+
receiverCallIsMethod = !!receiverCall;
|
|
2628
|
+
} else if (prodFunc?.type === 'scoped_identifier') {
|
|
2629
|
+
const segments = prodFunc.text.split('::');
|
|
2630
|
+
receiverCall = segments[segments.length - 1];
|
|
2631
|
+
receiverCallIsMethod = !!receiverCall;
|
|
2632
|
+
}
|
|
2633
|
+
if (receiverCall) {
|
|
2634
|
+
receiverCallLine = receiverFieldCallRoot.startPosition.row + 1;
|
|
2635
|
+
receiverCallStart = receiverFieldCallRoot.startIndex;
|
|
2636
|
+
receiverCallEnd = receiverFieldCallRoot.endIndex;
|
|
2637
|
+
}
|
|
1578
2638
|
}
|
|
1579
2639
|
// Literal receivers carry their builtin type (fix #220,
|
|
1580
2640
|
// ripgrep-measured): "match:fg:magenta".parse() is
|
|
@@ -1587,8 +2647,23 @@ function findCallsInCode(code, parser) {
|
|
|
1587
2647
|
const receiverType = (receiver && receiver !== 'self' && !receiverIsChainRoot)
|
|
1588
2648
|
? getReceiverType(receiver, node)
|
|
1589
2649
|
: literalReceiverType;
|
|
2650
|
+
const receiverTypeQualifier = receiver && receiverType
|
|
2651
|
+
? getReceiverTypeQualifier(receiver, node)
|
|
2652
|
+
: undefined;
|
|
2653
|
+
const receiverIteratorItemType = receiver
|
|
2654
|
+
? getReceiverIteratorItemType(receiver, node)
|
|
2655
|
+
: undefined;
|
|
1590
2656
|
const receiverPatternShadow = !!(receiver && patternShadowsAt(node, receiver));
|
|
2657
|
+
const receiverPatternBinding = rustPatternBindingOf(node, receiver);
|
|
2658
|
+
if (receiverPatternBinding?.receiverPatternSourceVariable) {
|
|
2659
|
+
const sourceType = getReceiverAnnotationText(
|
|
2660
|
+
receiverPatternBinding.receiverPatternSourceVariable, node);
|
|
2661
|
+
if (sourceType) {
|
|
2662
|
+
receiverPatternBinding.receiverPatternSourceType = sourceType;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
1591
2665
|
const receiverFlowInvalidated = !!(receiver && flowInvalidatedAt(node, receiver));
|
|
2666
|
+
const iterationSource = rustIterationSourceOf(node, receiver);
|
|
1592
2667
|
const firstArg = getFirstStringArg(node);
|
|
1593
2668
|
// RUST-2: For chained calls like `a().b().parse::<T>().ok()`,
|
|
1594
2669
|
// each method should report the line where its OWN identifier
|
|
@@ -1598,20 +2673,38 @@ function findCallsInCode(code, parser) {
|
|
|
1598
2673
|
calls.push({
|
|
1599
2674
|
name: fieldNode.text,
|
|
1600
2675
|
line: fieldNode.startPosition.row + 1,
|
|
2676
|
+
callStart: node.startIndex,
|
|
2677
|
+
callEnd: node.endIndex,
|
|
1601
2678
|
isMethod: true,
|
|
1602
2679
|
receiver,
|
|
1603
2680
|
...(receiverType && { receiverType }),
|
|
2681
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
2682
|
+
...(receiverIteratorItemType && { receiverIteratorItemType }),
|
|
1604
2683
|
...(receiverPatternShadow && { receiverPatternShadow: true }),
|
|
2684
|
+
...(receiverPatternBinding || {}),
|
|
1605
2685
|
...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
|
|
2686
|
+
...(iterationSource || {}),
|
|
1606
2687
|
...(receiverIsChainRoot && { receiverIsChainRoot: true }),
|
|
1607
2688
|
...(receiverField && { receiverRoot, receiverField }),
|
|
2689
|
+
...(receiverFields?.length > 1 && { receiverFields }),
|
|
1608
2690
|
...(receiverField && receiverRootType && { receiverRootType }),
|
|
1609
2691
|
...(receiverCall && { receiverCall }),
|
|
1610
2692
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
2693
|
+
...(valueNode?.type === 'macro_invocation' && receiverCall && {
|
|
2694
|
+
receiverCallIsMacro: true,
|
|
2695
|
+
}),
|
|
1611
2696
|
...(receiverCallLine && { receiverCallLine }),
|
|
2697
|
+
...(receiverCallStart != null && { receiverCallStart }),
|
|
2698
|
+
...(receiverCallEnd != null && { receiverCallEnd }),
|
|
1612
2699
|
argCount,
|
|
1613
2700
|
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1614
2701
|
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
2702
|
+
...(assigned?.tuple && { assignedTuple: true }),
|
|
2703
|
+
...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
|
|
2704
|
+
...(collectResult && {
|
|
2705
|
+
explicitResultType: collectResult.type,
|
|
2706
|
+
explicitResultItemType: collectResult.itemType,
|
|
2707
|
+
}),
|
|
1615
2708
|
enclosingFunction,
|
|
1616
2709
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1617
2710
|
});
|
|
@@ -1631,12 +2724,16 @@ function findCallsInCode(code, parser) {
|
|
|
1631
2724
|
calls.push({
|
|
1632
2725
|
name: name,
|
|
1633
2726
|
line: node.startPosition.row + 1,
|
|
2727
|
+
callStart: node.startIndex,
|
|
2728
|
+
callEnd: node.endIndex,
|
|
1634
2729
|
isMethod: segments.length > 1,
|
|
1635
2730
|
isPathCall: true, // Distinguishes Type::func()/module::func() from obj.method()
|
|
1636
2731
|
receiver: recvSegments.length > 0 ? recvSegments.join('::') : undefined,
|
|
1637
2732
|
argCount,
|
|
1638
2733
|
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1639
2734
|
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
2735
|
+
...(assigned?.tuple && { assignedTuple: true }),
|
|
2736
|
+
...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
|
|
1640
2737
|
enclosingFunction,
|
|
1641
2738
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1642
2739
|
});
|
|
@@ -1703,17 +2800,25 @@ function findCallsInCode(code, parser) {
|
|
|
1703
2800
|
if (node.type === 'macro_invocation') {
|
|
1704
2801
|
const macroNode = node.childForFieldName('macro');
|
|
1705
2802
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
2803
|
+
let macro = null;
|
|
1706
2804
|
if (macroNode) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
if (macroName.endsWith('!')) {
|
|
1710
|
-
macroName = macroName.slice(0, -1);
|
|
1711
|
-
}
|
|
2805
|
+
macro = rustMacroCallIdentity(macroNode);
|
|
2806
|
+
const assigned = rustAssignmentTargetOf(node);
|
|
1712
2807
|
calls.push({
|
|
1713
|
-
name:
|
|
2808
|
+
name: macro?.name || macroNode.text.replace(/!$/, ''),
|
|
1714
2809
|
line: node.startPosition.row + 1,
|
|
2810
|
+
callStart: node.startIndex,
|
|
2811
|
+
callEnd: node.endIndex,
|
|
1715
2812
|
isMethod: false,
|
|
1716
2813
|
isMacro: true,
|
|
2814
|
+
...(macro?.receiver && {
|
|
2815
|
+
receiver: macro.receiver,
|
|
2816
|
+
isPathMacro: true,
|
|
2817
|
+
}),
|
|
2818
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
2819
|
+
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
2820
|
+
...(assigned?.tuple && { assignedTuple: true }),
|
|
2821
|
+
...(assigned?.tupleRest && { assignedTupleRest: assigned.tupleRest }),
|
|
1717
2822
|
enclosingFunction
|
|
1718
2823
|
});
|
|
1719
2824
|
}
|
|
@@ -1726,12 +2831,38 @@ function findCallsInCode(code, parser) {
|
|
|
1726
2831
|
if (child.type === 'token_tree') {
|
|
1727
2832
|
extractCallsFromTokenTree(
|
|
1728
2833
|
child, enclosingFunction, calls, getReceiverType,
|
|
1729
|
-
patternShadowsAt, flowInvalidatedAt
|
|
2834
|
+
patternShadowsAt, flowInvalidatedAt, {
|
|
2835
|
+
kind: 'invocation',
|
|
2836
|
+
containerMacro: macro?.name,
|
|
2837
|
+
});
|
|
1730
2838
|
}
|
|
1731
2839
|
}
|
|
1732
2840
|
return true;
|
|
1733
2841
|
}
|
|
1734
2842
|
|
|
2843
|
+
// Attribute arguments are token trees, but they contain ordinary
|
|
2844
|
+
// Rust paths and builder expressions that proc macros resolve at
|
|
2845
|
+
// compile time (`#[arg(value_parser = BoolishValueParser::new())]`).
|
|
2846
|
+
// Recover those calls with the same AST-token reconstruction used for
|
|
2847
|
+
// macro invocation arguments. The attribute name itself is metadata,
|
|
2848
|
+
// not a direct call site, so only its argument token tree is scanned.
|
|
2849
|
+
if (node.type === 'attribute_item') {
|
|
2850
|
+
const attribute = node.namedChildren.find(child => child.type === 'attribute');
|
|
2851
|
+
const attributeName = attribute?.namedChildren.find(child =>
|
|
2852
|
+
child.type === 'identifier' || child.type === 'scoped_identifier')?.text;
|
|
2853
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
2854
|
+
for (const child of attribute?.namedChildren || []) {
|
|
2855
|
+
if (child.type !== 'token_tree') continue;
|
|
2856
|
+
extractCallsFromTokenTree(
|
|
2857
|
+
child, enclosingFunction, calls, getReceiverType,
|
|
2858
|
+
patternShadowsAt, flowInvalidatedAt, {
|
|
2859
|
+
kind: 'attribute',
|
|
2860
|
+
containerMacro: attributeName,
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
return true;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
1735
2866
|
// macro_rules! definitions: the transcriber token_tree holds concrete
|
|
1736
2867
|
// call templates (write!(stderr, $($tt)*) in messages.rs) — real call
|
|
1737
2868
|
// sites in every expansion. The matcher (token_tree_pattern) holds
|
|
@@ -1746,7 +2877,7 @@ function findCallsInCode(code, parser) {
|
|
|
1746
2877
|
if (part.type === 'token_tree') {
|
|
1747
2878
|
extractCallsFromTokenTree(
|
|
1748
2879
|
part, enclosingFunction, calls, getReceiverType,
|
|
1749
|
-
patternShadowsAt, flowInvalidatedAt);
|
|
2880
|
+
patternShadowsAt, flowInvalidatedAt, 'definition');
|
|
1750
2881
|
}
|
|
1751
2882
|
}
|
|
1752
2883
|
}
|
|
@@ -1797,9 +2928,11 @@ function findCallsInCode(code, parser) {
|
|
|
1797
2928
|
const typeMap = scopeTypes.get(scopeKey);
|
|
1798
2929
|
if (typeMap) {
|
|
1799
2930
|
let typeName = null;
|
|
2931
|
+
let typeQualifier = null;
|
|
1800
2932
|
// Pattern 3: explicit type annotation — let s: Server = ...
|
|
1801
2933
|
if (typeAnnotation) {
|
|
1802
2934
|
typeName = extractTypeName(typeAnnotation);
|
|
2935
|
+
typeQualifier = extractTypeQualifier(typeAnnotation);
|
|
1803
2936
|
}
|
|
1804
2937
|
if (!typeName && valueNode) {
|
|
1805
2938
|
// Pattern 1: struct expression — let s = Server { ... }
|
|
@@ -1809,6 +2942,7 @@ function findCallsInCode(code, parser) {
|
|
|
1809
2942
|
// Strip path prefix: module::Server → Server
|
|
1810
2943
|
if (typeName && typeName.includes('::')) {
|
|
1811
2944
|
const parts = typeName.split('::');
|
|
2945
|
+
typeQualifier = parts.slice(0, -1).join('::');
|
|
1812
2946
|
typeName = parts[parts.length - 1];
|
|
1813
2947
|
}
|
|
1814
2948
|
}
|
|
@@ -1820,6 +2954,7 @@ function findCallsInCode(code, parser) {
|
|
|
1820
2954
|
typeName = nameNode?.text || null;
|
|
1821
2955
|
if (typeName && typeName.includes('::')) {
|
|
1822
2956
|
const parts = typeName.split('::');
|
|
2957
|
+
typeQualifier = parts.slice(0, -1).join('::');
|
|
1823
2958
|
typeName = parts[parts.length - 1];
|
|
1824
2959
|
}
|
|
1825
2960
|
}
|
|
@@ -1834,13 +2969,17 @@ function findCallsInCode(code, parser) {
|
|
|
1834
2969
|
const methodName = segments[segments.length - 1];
|
|
1835
2970
|
if (/^(new|from|default|with_|create|build|open|connect|init)/.test(methodName)) {
|
|
1836
2971
|
typeName = segments[segments.length - 2];
|
|
2972
|
+
typeQualifier = segments.slice(0, -2).join('::') || null;
|
|
1837
2973
|
if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
|
|
1838
2974
|
}
|
|
1839
2975
|
}
|
|
1840
2976
|
}
|
|
1841
2977
|
}
|
|
1842
2978
|
}
|
|
1843
|
-
if (typeName)
|
|
2979
|
+
if (typeName) {
|
|
2980
|
+
typeMap.set(varName, typeName);
|
|
2981
|
+
if (typeQualifier) typeMap.qualifiers.set(varName, typeQualifier);
|
|
2982
|
+
}
|
|
1844
2983
|
}
|
|
1845
2984
|
}
|
|
1846
2985
|
}
|
|
@@ -1858,7 +2997,29 @@ function findCallsInCode(code, parser) {
|
|
|
1858
2997
|
}
|
|
1859
2998
|
});
|
|
1860
2999
|
|
|
1861
|
-
|
|
3000
|
+
const declaration = declarationTrees(code, parser);
|
|
3001
|
+
if (!declaration.macroItemRecovery) return calls;
|
|
3002
|
+
const functions = findFunctions(code, parser);
|
|
3003
|
+
return calls
|
|
3004
|
+
.filter(call => !(call.inMacro &&
|
|
3005
|
+
declaration.macroDeclarationNameStarts.has(call.callStart)))
|
|
3006
|
+
.map(call => {
|
|
3007
|
+
if (!call.inMacro || call.enclosingFunction) return call;
|
|
3008
|
+
const owner = functions
|
|
3009
|
+
.filter(fn => fn.startLine <= call.line && fn.endLine >= call.line)
|
|
3010
|
+
.sort((left, right) =>
|
|
3011
|
+
(left.endLine - left.startLine) -
|
|
3012
|
+
(right.endLine - right.startLine))[0];
|
|
3013
|
+
if (!owner) return call;
|
|
3014
|
+
return {
|
|
3015
|
+
...call,
|
|
3016
|
+
enclosingFunction: {
|
|
3017
|
+
name: owner.name,
|
|
3018
|
+
startLine: owner.startLine,
|
|
3019
|
+
endLine: owner.endLine,
|
|
3020
|
+
},
|
|
3021
|
+
};
|
|
3022
|
+
});
|
|
1862
3023
|
}
|
|
1863
3024
|
|
|
1864
3025
|
/**
|
|
@@ -1871,82 +3032,76 @@ function findImportsInCode(code, parser) {
|
|
|
1871
3032
|
const tree = parseTree(parser, code);
|
|
1872
3033
|
const imports = [];
|
|
1873
3034
|
|
|
3035
|
+
const joinUsePath = (prefix, suffix) => {
|
|
3036
|
+
const left = String(prefix || '').replace(/::$/, '');
|
|
3037
|
+
const right = String(suffix || '').replace(/^::/, '');
|
|
3038
|
+
return left && right ? `${left}::${right}` : left || right;
|
|
3039
|
+
};
|
|
3040
|
+
const addLeaf = (module, localName, type = 'use', dynamic = false, line) => {
|
|
3041
|
+
if (!module || !localName) return;
|
|
3042
|
+
imports.push({
|
|
3043
|
+
module,
|
|
3044
|
+
names: [localName],
|
|
3045
|
+
type,
|
|
3046
|
+
dynamic,
|
|
3047
|
+
line,
|
|
3048
|
+
});
|
|
3049
|
+
};
|
|
3050
|
+
const collectUseTree = (node, prefix, line) => {
|
|
3051
|
+
if (!node || node.type === 'visibility_modifier') return;
|
|
3052
|
+
if (node.type === 'scoped_use_list') {
|
|
3053
|
+
const pathNode = node.childForFieldName('path');
|
|
3054
|
+
const listNode = node.childForFieldName('list');
|
|
3055
|
+
const nextPrefix = joinUsePath(prefix, pathNode?.text);
|
|
3056
|
+
if (listNode) collectUseTree(listNode, nextPrefix, line);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (node.type === 'use_list') {
|
|
3060
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3061
|
+
collectUseTree(node.namedChild(i), prefix, line);
|
|
3062
|
+
}
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
if (node.type === 'use_as_clause') {
|
|
3066
|
+
const pathNode = node.namedChild(0);
|
|
3067
|
+
const aliasNode = node.childForFieldName('alias') || node.namedChild(1);
|
|
3068
|
+
if (pathNode && aliasNode) {
|
|
3069
|
+
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3070
|
+
'use', false, line);
|
|
3071
|
+
}
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (node.type === 'use_wildcard') {
|
|
3075
|
+
const pathNode = node.namedChild(0);
|
|
3076
|
+
addLeaf(joinUsePath(prefix, pathNode?.text), '*',
|
|
3077
|
+
'use-glob', true, line);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
if (node.type === 'identifier' || node.type === 'scoped_identifier' ||
|
|
3081
|
+
node.type === 'crate' || node.type === 'self' || node.type === 'super') {
|
|
3082
|
+
if (node.text === 'self' && prefix) {
|
|
3083
|
+
addLeaf(prefix, prefix.split('::').pop(), 'use', false, line);
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
const module = joinUsePath(prefix, node.text);
|
|
3087
|
+
addLeaf(module, node.text.split('::').pop(), 'use', false, line);
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
|
|
1874
3091
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
1875
3092
|
// use declarations
|
|
1876
3093
|
if (node.type === 'use_declaration') {
|
|
1877
3094
|
const line = node.startPosition.row + 1;
|
|
1878
|
-
|
|
3095
|
+
// A use declaration has one semantic tree below optional
|
|
3096
|
+
// visibility. Recursively flatten every leaf while retaining its
|
|
3097
|
+
// full module path. In particular,
|
|
3098
|
+
// `use crate::{haystack::{Haystack, Builder}}` becomes the exact
|
|
3099
|
+
// bindings `crate::haystack::Haystack` and
|
|
3100
|
+
// `crate::haystack::Builder`, rather than the lossy old
|
|
3101
|
+
// `{ module: "crate", name: "haystack" }` approximation.
|
|
1879
3102
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1880
3103
|
const child = node.namedChild(i);
|
|
1881
|
-
|
|
1882
|
-
if (child.type === 'use_as_clause') {
|
|
1883
|
-
// use foo::bar as baz
|
|
1884
|
-
const pathNode = child.namedChild(0); // the original path
|
|
1885
|
-
const aliasNode = child.childForFieldName('alias');
|
|
1886
|
-
if (pathNode) {
|
|
1887
|
-
const originalPath = pathNode.text;
|
|
1888
|
-
const alias = aliasNode ? aliasNode.text : originalPath.split('::').pop();
|
|
1889
|
-
imports.push({
|
|
1890
|
-
module: originalPath,
|
|
1891
|
-
names: [alias],
|
|
1892
|
-
type: 'use',
|
|
1893
|
-
dynamic: false,
|
|
1894
|
-
line
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
} else if (child.type === 'scoped_identifier' || child.type === 'identifier') {
|
|
1898
|
-
// use std::io or use foo
|
|
1899
|
-
const path = child.text;
|
|
1900
|
-
const segments = path.split('::');
|
|
1901
|
-
imports.push({
|
|
1902
|
-
module: path,
|
|
1903
|
-
names: [segments[segments.length - 1]],
|
|
1904
|
-
type: 'use',
|
|
1905
|
-
dynamic: false,
|
|
1906
|
-
line
|
|
1907
|
-
});
|
|
1908
|
-
} else if (child.type === 'use_wildcard') {
|
|
1909
|
-
// use std::collections::*
|
|
1910
|
-
const scopedId = child.namedChild(0);
|
|
1911
|
-
if (scopedId) {
|
|
1912
|
-
imports.push({
|
|
1913
|
-
module: scopedId.text,
|
|
1914
|
-
names: ['*'],
|
|
1915
|
-
type: 'use-glob',
|
|
1916
|
-
dynamic: true,
|
|
1917
|
-
line
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
} else if (child.type === 'use_list' || child.type === 'scoped_use_list') {
|
|
1921
|
-
// use std::{io, fs} or use foo::{bar, baz}
|
|
1922
|
-
// Extract the base path and names
|
|
1923
|
-
const pathNode = child.childForFieldName('path');
|
|
1924
|
-
const listNode = child.childForFieldName('list');
|
|
1925
|
-
|
|
1926
|
-
if (pathNode && listNode) {
|
|
1927
|
-
const basePath = pathNode.text;
|
|
1928
|
-
const names = [];
|
|
1929
|
-
for (let j = 0; j < listNode.namedChildCount; j++) {
|
|
1930
|
-
const item = listNode.namedChild(j);
|
|
1931
|
-
if (item.type === 'identifier') {
|
|
1932
|
-
names.push(item.text);
|
|
1933
|
-
} else if (item.type === 'use_as_clause') {
|
|
1934
|
-
const aliasNode = item.childForFieldName('alias');
|
|
1935
|
-
const pathItem = item.namedChild(0);
|
|
1936
|
-
names.push(aliasNode ? aliasNode.text : (pathItem ? pathItem.text : item.text));
|
|
1937
|
-
} else if (item.type === 'scoped_identifier') {
|
|
1938
|
-
names.push(item.text);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
imports.push({
|
|
1942
|
-
module: basePath,
|
|
1943
|
-
names,
|
|
1944
|
-
type: 'use',
|
|
1945
|
-
dynamic: false,
|
|
1946
|
-
line
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
3104
|
+
collectUseTree(child, '', line);
|
|
1950
3105
|
}
|
|
1951
3106
|
return true;
|
|
1952
3107
|
}
|
|
@@ -2008,8 +3163,9 @@ function findImportsInCode(code, parser) {
|
|
|
2008
3163
|
* @returns {Array<{name: string, type: string, line: number}>}
|
|
2009
3164
|
*/
|
|
2010
3165
|
function findExportsInCode(code, parser) {
|
|
2011
|
-
const
|
|
3166
|
+
const { trees } = declarationTrees(code, parser);
|
|
2012
3167
|
const exports = [];
|
|
3168
|
+
const seen = new Set();
|
|
2013
3169
|
|
|
2014
3170
|
function hasVisibility(node) {
|
|
2015
3171
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -2021,7 +3177,15 @@ function findExportsInCode(code, parser) {
|
|
|
2021
3177
|
return false;
|
|
2022
3178
|
}
|
|
2023
3179
|
|
|
2024
|
-
|
|
3180
|
+
const append = entry => {
|
|
3181
|
+
const key = `${entry.name}\0${entry.type}\0${entry.line}\0${entry.alias || ''}`;
|
|
3182
|
+
if (!seen.has(key)) {
|
|
3183
|
+
seen.add(key);
|
|
3184
|
+
exports.push(entry);
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
|
|
3188
|
+
const collect = tree => traverseTreeCached(tree.rootNode, (node) => {
|
|
2025
3189
|
// Public renamed re-exports: `pub use foo::bar as baz;` (also nested in
|
|
2026
3190
|
// use lists: `pub use m::{a as b}`). name keeps the source symbol; alias
|
|
2027
3191
|
// carries the external name callers use. Plain (un-renamed) `pub use`
|
|
@@ -2044,7 +3208,7 @@ function findExportsInCode(code, parser) {
|
|
|
2044
3208
|
}
|
|
2045
3209
|
}
|
|
2046
3210
|
if (local && aliasNode && aliasNode.text !== local) {
|
|
2047
|
-
|
|
3211
|
+
append({
|
|
2048
3212
|
name: local, type: 're-export', line,
|
|
2049
3213
|
source: srcNode.text, alias: aliasNode.text,
|
|
2050
3214
|
});
|
|
@@ -2061,7 +3225,7 @@ function findExportsInCode(code, parser) {
|
|
|
2061
3225
|
if (node.type === 'function_item' && hasVisibility(node)) {
|
|
2062
3226
|
const nameNode = node.childForFieldName('name');
|
|
2063
3227
|
if (nameNode) {
|
|
2064
|
-
|
|
3228
|
+
append({
|
|
2065
3229
|
name: nameNode.text,
|
|
2066
3230
|
type: 'function',
|
|
2067
3231
|
line: node.startPosition.row + 1
|
|
@@ -2074,7 +3238,7 @@ function findExportsInCode(code, parser) {
|
|
|
2074
3238
|
if (node.type === 'struct_item' && hasVisibility(node)) {
|
|
2075
3239
|
const nameNode = node.childForFieldName('name');
|
|
2076
3240
|
if (nameNode) {
|
|
2077
|
-
|
|
3241
|
+
append({
|
|
2078
3242
|
name: nameNode.text,
|
|
2079
3243
|
type: 'struct',
|
|
2080
3244
|
line: node.startPosition.row + 1
|
|
@@ -2087,7 +3251,7 @@ function findExportsInCode(code, parser) {
|
|
|
2087
3251
|
if (node.type === 'enum_item' && hasVisibility(node)) {
|
|
2088
3252
|
const nameNode = node.childForFieldName('name');
|
|
2089
3253
|
if (nameNode) {
|
|
2090
|
-
|
|
3254
|
+
append({
|
|
2091
3255
|
name: nameNode.text,
|
|
2092
3256
|
type: 'enum',
|
|
2093
3257
|
line: node.startPosition.row + 1
|
|
@@ -2100,7 +3264,7 @@ function findExportsInCode(code, parser) {
|
|
|
2100
3264
|
if (node.type === 'trait_item' && hasVisibility(node)) {
|
|
2101
3265
|
const nameNode = node.childForFieldName('name');
|
|
2102
3266
|
if (nameNode) {
|
|
2103
|
-
|
|
3267
|
+
append({
|
|
2104
3268
|
name: nameNode.text,
|
|
2105
3269
|
type: 'trait',
|
|
2106
3270
|
line: node.startPosition.row + 1
|
|
@@ -2113,7 +3277,7 @@ function findExportsInCode(code, parser) {
|
|
|
2113
3277
|
if (node.type === 'mod_item' && hasVisibility(node)) {
|
|
2114
3278
|
const nameNode = node.childForFieldName('name');
|
|
2115
3279
|
if (nameNode) {
|
|
2116
|
-
|
|
3280
|
+
append({
|
|
2117
3281
|
name: nameNode.text,
|
|
2118
3282
|
type: 'module',
|
|
2119
3283
|
line: node.startPosition.row + 1
|
|
@@ -2126,7 +3290,7 @@ function findExportsInCode(code, parser) {
|
|
|
2126
3290
|
if (node.type === 'type_item' && hasVisibility(node)) {
|
|
2127
3291
|
const nameNode = node.childForFieldName('name');
|
|
2128
3292
|
if (nameNode) {
|
|
2129
|
-
|
|
3293
|
+
append({
|
|
2130
3294
|
name: nameNode.text,
|
|
2131
3295
|
type: 'type',
|
|
2132
3296
|
line: node.startPosition.row + 1
|
|
@@ -2139,7 +3303,7 @@ function findExportsInCode(code, parser) {
|
|
|
2139
3303
|
if (node.type === 'const_item' && hasVisibility(node)) {
|
|
2140
3304
|
const nameNode = node.childForFieldName('name');
|
|
2141
3305
|
if (nameNode) {
|
|
2142
|
-
|
|
3306
|
+
append({
|
|
2143
3307
|
name: nameNode.text,
|
|
2144
3308
|
type: 'const',
|
|
2145
3309
|
line: node.startPosition.row + 1
|
|
@@ -2152,7 +3316,7 @@ function findExportsInCode(code, parser) {
|
|
|
2152
3316
|
if (node.type === 'static_item' && hasVisibility(node)) {
|
|
2153
3317
|
const nameNode = node.childForFieldName('name');
|
|
2154
3318
|
if (nameNode) {
|
|
2155
|
-
|
|
3319
|
+
append({
|
|
2156
3320
|
name: nameNode.text,
|
|
2157
3321
|
type: 'static',
|
|
2158
3322
|
line: node.startPosition.row + 1
|
|
@@ -2163,6 +3327,7 @@ function findExportsInCode(code, parser) {
|
|
|
2163
3327
|
|
|
2164
3328
|
return true;
|
|
2165
3329
|
});
|
|
3330
|
+
for (const tree of trees) collect(tree);
|
|
2166
3331
|
|
|
2167
3332
|
return exports;
|
|
2168
3333
|
}
|