ucn 3.8.25 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +44 -18
- package/README.md +95 -28
- package/cli/index.js +28 -5
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/bridge.js +0 -16
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3434 -158
- package/core/confidence.js +82 -19
- package/core/deadcode.js +114 -21
- package/core/execute.js +4 -0
- package/core/graph-build.js +44 -2
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +8 -2
- package/core/output/shared.js +33 -2
- package/core/output/tracing.js +208 -10
- package/core/project.js +19 -2
- package/core/registry.js +15 -3
- package/core/search.js +0 -42
- package/core/tracing.js +534 -190
- package/languages/go.js +317 -6
- package/languages/index.js +79 -0
- package/languages/java.js +243 -16
- package/languages/javascript.js +357 -24
- package/languages/python.js +423 -28
- package/languages/rust.js +377 -8
- package/languages/utils.js +72 -18
- package/mcp/server.js +3 -3
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/languages/rust.js
CHANGED
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
traverseTreeCached,
|
|
11
11
|
nodeToLocation,
|
|
12
12
|
parseStructuredParams,
|
|
13
|
-
extractRustDocstring
|
|
13
|
+
extractRustDocstring,
|
|
14
|
+
visitNameNodes,
|
|
14
15
|
} = require('./utils');
|
|
15
16
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
16
17
|
|
|
@@ -44,6 +45,30 @@ function extractRustParams(paramsNode) {
|
|
|
44
45
|
return params;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Base type name from a type-alias target (fix #208): SpannedString<Style>
|
|
50
|
+
* → SpannedString, module::Type → Type, &T → T. dyn/impl/tuple/fn shapes
|
|
51
|
+
* return null — not nominal method receivers.
|
|
52
|
+
*/
|
|
53
|
+
function aliasBaseTypeName(typeNode) {
|
|
54
|
+
if (!typeNode) return null;
|
|
55
|
+
if (typeNode.type === 'type_identifier') return typeNode.text;
|
|
56
|
+
if (typeNode.type === 'reference_type') {
|
|
57
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
58
|
+
const r = aliasBaseTypeName(typeNode.namedChild(i));
|
|
59
|
+
if (r) return r;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (typeNode.type === 'generic_type') {
|
|
64
|
+
return aliasBaseTypeName(typeNode.namedChild(0));
|
|
65
|
+
}
|
|
66
|
+
if (typeNode.type === 'scoped_type_identifier') {
|
|
67
|
+
return typeNode.childForFieldName('name')?.text || null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
/**
|
|
48
73
|
* Extract visibility modifier
|
|
49
74
|
*/
|
|
@@ -492,6 +517,11 @@ function _processClass(node, types, processedRanges, lines, code) {
|
|
|
492
517
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
493
518
|
const docstring = extractRustDocstring(lines, startLine);
|
|
494
519
|
const visibility = extractVisibility(node.text);
|
|
520
|
+
// `pub type StyledString = SpannedString<Style>;` — the alias IS
|
|
521
|
+
// the aliased type (compiler identity). Record the base name so
|
|
522
|
+
// callers can treat alias-qualified receivers as the base type
|
|
523
|
+
// (fix #208 — cursive StyledString::plain).
|
|
524
|
+
const aliasOf = aliasBaseTypeName(node.childForFieldName('type'));
|
|
495
525
|
|
|
496
526
|
types.push({
|
|
497
527
|
name: nameNode.text,
|
|
@@ -500,6 +530,7 @@ function _processClass(node, types, processedRanges, lines, code) {
|
|
|
500
530
|
type: 'type',
|
|
501
531
|
members: [],
|
|
502
532
|
modifiers: visibility ? [visibility] : [],
|
|
533
|
+
...(aliasOf && { aliasOf }),
|
|
503
534
|
...(docstring && { docstring })
|
|
504
535
|
});
|
|
505
536
|
}
|
|
@@ -912,6 +943,164 @@ function _findRustChainRootType(callNode) {
|
|
|
912
943
|
* @param {object} parser - Tree-sitter parser instance
|
|
913
944
|
* @returns {Array<{name: string, line: number, isMethod: boolean, receiver?: string, isMacro?: boolean}>}
|
|
914
945
|
*/
|
|
946
|
+
/**
|
|
947
|
+
* Extract call-shaped token sequences from a macro body. Macro arguments are
|
|
948
|
+
* almost always ordinary expressions (assert_eq!, format!, vec!, write!), but
|
|
949
|
+
* tree-sitter parses them as a flat token_tree, so the regular call_expression
|
|
950
|
+
* handler never sees them. Recognized shapes (still AST token nodes, no text
|
|
951
|
+
* regex): ident (…) · recv . ident (…) · Path :: ident (…)
|
|
952
|
+
* Emitted calls mirror the regular handlers' field contract and carry
|
|
953
|
+
* inMacro: true.
|
|
954
|
+
*/
|
|
955
|
+
function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType) {
|
|
956
|
+
const children = [];
|
|
957
|
+
for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
|
|
958
|
+
for (let i = 0; i < children.length; i++) {
|
|
959
|
+
const tok = children[i];
|
|
960
|
+
if (tok.type === 'token_tree') {
|
|
961
|
+
extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (tok.type !== 'identifier') continue;
|
|
965
|
+
const next = children[i + 1];
|
|
966
|
+
const prev = children[i - 1];
|
|
967
|
+
// $metavariable(...) — a macro fragment, not a named call
|
|
968
|
+
if (prev && prev.type === '$') continue;
|
|
969
|
+
// Nested macro invocation: name!(...)
|
|
970
|
+
if (next && next.type === '!' &&
|
|
971
|
+
children[i + 2] && children[i + 2].type === 'token_tree') {
|
|
972
|
+
calls.push({
|
|
973
|
+
name: tok.text,
|
|
974
|
+
line: tok.startPosition.row + 1,
|
|
975
|
+
isMethod: false,
|
|
976
|
+
isMacro: true,
|
|
977
|
+
inMacro: true,
|
|
978
|
+
enclosingFunction
|
|
979
|
+
});
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (!next || next.type !== 'token_tree' || !next.text.startsWith('(')) continue;
|
|
983
|
+
if (prev && prev.type === '::') {
|
|
984
|
+
// Path call: Type::func(...) / module::sub::func(...) — segments
|
|
985
|
+
// can be identifiers, primitives (char::from), or path keywords
|
|
986
|
+
const isSegment = (n) => n && ['identifier', 'primitive_type', 'self', 'super', 'crate'].includes(n.type);
|
|
987
|
+
const segments = [];
|
|
988
|
+
let j = i - 1;
|
|
989
|
+
while (j >= 1 && children[j].type === '::') {
|
|
990
|
+
let k = j - 1;
|
|
991
|
+
if (children[k] && children[k].type === '>') {
|
|
992
|
+
// Turbofish: `Vec::<PatternSource>::new(...)` — angle
|
|
993
|
+
// brackets do NOT group into token_trees, so skip the
|
|
994
|
+
// <...> token run (nesting-aware) back to the matching
|
|
995
|
+
// `<`, which the turbofish form introduces with `::`.
|
|
996
|
+
// Without this the walk stopped at `>`, emitted a
|
|
997
|
+
// receiver-less path call, and `Vec::<T>::new()` inside
|
|
998
|
+
// assert_eq! scope-confirmed against every project `new`
|
|
999
|
+
// (fix #222, ripgrep-seed-C-measured).
|
|
1000
|
+
let depth = 1;
|
|
1001
|
+
k--;
|
|
1002
|
+
while (k >= 0 && depth > 0) {
|
|
1003
|
+
if (children[k].type === '>') depth++;
|
|
1004
|
+
else if (children[k].type === '<') depth--;
|
|
1005
|
+
if (depth > 0) k--;
|
|
1006
|
+
}
|
|
1007
|
+
if (k < 1 || children[k - 1].type !== '::') break;
|
|
1008
|
+
k -= 2;
|
|
1009
|
+
}
|
|
1010
|
+
if (!isSegment(children[k])) break;
|
|
1011
|
+
segments.unshift(children[k].text);
|
|
1012
|
+
j = k - 1;
|
|
1013
|
+
}
|
|
1014
|
+
calls.push({
|
|
1015
|
+
name: tok.text,
|
|
1016
|
+
line: tok.startPosition.row + 1,
|
|
1017
|
+
isMethod: segments.length > 0,
|
|
1018
|
+
isPathCall: true,
|
|
1019
|
+
receiver: segments.length > 0 ? segments.join('::') : undefined,
|
|
1020
|
+
inMacro: true,
|
|
1021
|
+
enclosingFunction
|
|
1022
|
+
});
|
|
1023
|
+
} else if (prev && prev.type === '.') {
|
|
1024
|
+
// Method call: recv.method(...)
|
|
1025
|
+
const recvTok = children[i - 2];
|
|
1026
|
+
const receiver = recvTok && (recvTok.type === 'identifier' || recvTok.type === 'self')
|
|
1027
|
+
? recvTok.text : undefined;
|
|
1028
|
+
// Literal receivers type as builtins inside macros too (fix #220,
|
|
1029
|
+
// ripgrep-measured: assert_eq!(.., vec!["match:fg".parse()...]))
|
|
1030
|
+
const litType = recvTok
|
|
1031
|
+
? ({ string_literal: 'str', raw_string_literal: 'str',
|
|
1032
|
+
char_literal: 'char', boolean_literal: 'bool' })[recvTok.type]
|
|
1033
|
+
: undefined;
|
|
1034
|
+
const receiverType = (receiver && receiver !== 'self' && getReceiverType)
|
|
1035
|
+
? getReceiverType(receiver) : litType;
|
|
1036
|
+
calls.push({
|
|
1037
|
+
name: tok.text,
|
|
1038
|
+
line: tok.startPosition.row + 1,
|
|
1039
|
+
isMethod: true,
|
|
1040
|
+
receiver,
|
|
1041
|
+
...(receiverType && { receiverType }),
|
|
1042
|
+
inMacro: true,
|
|
1043
|
+
enclosingFunction
|
|
1044
|
+
});
|
|
1045
|
+
} else {
|
|
1046
|
+
// Plain call: func(...) — includes enum-variant constructors
|
|
1047
|
+
calls.push({
|
|
1048
|
+
name: tok.text,
|
|
1049
|
+
line: tok.startPosition.row + 1,
|
|
1050
|
+
isMethod: false,
|
|
1051
|
+
inMacro: true,
|
|
1052
|
+
enclosingFunction
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Variable receiving this call's result (fix #207 return-type flow):
|
|
1060
|
+
* let x = f(...); → { assignedTo: 'x' }
|
|
1061
|
+
* let x = f(...)?; → { assignedTo: 'x', unwrapped: true }
|
|
1062
|
+
* let x = f(...).unwrap(); → { assignedTo: 'x', unwrapped: true } (also .expect(...))
|
|
1063
|
+
* x = f(...); → { assignedTo: 'x' }
|
|
1064
|
+
* Value-transparent wrappers (`?`, .unwrap(), .expect(), .await) are walked
|
|
1065
|
+
* through so the INNER call carries the target; the flow map then unwraps
|
|
1066
|
+
* Result<T, _>/Option<T> from the producer's return annotation. `let mut x`
|
|
1067
|
+
* works too — the pattern field is the plain identifier.
|
|
1068
|
+
*/
|
|
1069
|
+
function rustAssignmentTargetOf(callNode) {
|
|
1070
|
+
let n = callNode;
|
|
1071
|
+
let p = n.parent;
|
|
1072
|
+
let unwrapped = false;
|
|
1073
|
+
for (;;) {
|
|
1074
|
+
if (!p) return undefined;
|
|
1075
|
+
if (p.type === 'try_expression') { unwrapped = true; n = p; p = n.parent; continue; }
|
|
1076
|
+
if (p.type === 'await_expression') { n = p; p = n.parent; continue; }
|
|
1077
|
+
if (p.type === 'field_expression' &&
|
|
1078
|
+
p.childForFieldName('value')?.id === n.id &&
|
|
1079
|
+
['unwrap', 'expect'].includes(p.childForFieldName('field')?.text) &&
|
|
1080
|
+
p.parent?.type === 'call_expression' &&
|
|
1081
|
+
p.parent.childForFieldName('function')?.id === p.id) {
|
|
1082
|
+
unwrapped = true; n = p.parent; p = n.parent; continue;
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
if (p.type === 'let_declaration') {
|
|
1087
|
+
const value = p.childForFieldName('value');
|
|
1088
|
+
const pattern = p.childForFieldName('pattern');
|
|
1089
|
+
if (value && value.id === n.id && pattern?.type === 'identifier') {
|
|
1090
|
+
return { assignedTo: pattern.text, ...(unwrapped && { unwrapped: true }) };
|
|
1091
|
+
}
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
if (p.type === 'assignment_expression') {
|
|
1095
|
+
const right = p.childForFieldName('right');
|
|
1096
|
+
const left = p.childForFieldName('left');
|
|
1097
|
+
if (right && right.id === n.id && left?.type === 'identifier') {
|
|
1098
|
+
return { assignedTo: left.text, ...(unwrapped && { unwrapped: true }) };
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
915
1104
|
function findCallsInCode(code, parser) {
|
|
916
1105
|
const tree = parseTree(parser, code);
|
|
917
1106
|
const calls = [];
|
|
@@ -1020,6 +1209,17 @@ function findCallsInCode(code, parser) {
|
|
|
1020
1209
|
return undefined;
|
|
1021
1210
|
};
|
|
1022
1211
|
|
|
1212
|
+
// Walk up to the enclosing impl block's target type (impl<T> Foo<T> → Foo).
|
|
1213
|
+
const findEnclosingImplType = (n) => {
|
|
1214
|
+
for (let p = n.parent; p; p = p.parent) {
|
|
1215
|
+
if (p.type === 'impl_item') {
|
|
1216
|
+
const t = p.childForFieldName('type');
|
|
1217
|
+
return (t && extractTypeName(t)) || undefined;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return undefined;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1023
1223
|
traverseTree(tree.rootNode, (node) => {
|
|
1024
1224
|
// Track function entry
|
|
1025
1225
|
if (isFunctionNode(node)) {
|
|
@@ -1044,6 +1244,23 @@ function findCallsInCode(code, parser) {
|
|
|
1044
1244
|
|
|
1045
1245
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1046
1246
|
|
|
1247
|
+
// Assignment target for return-type flow (fix #207): let args =
|
|
1248
|
+
// parse_low_raw(...)? lets findCallers type args from the
|
|
1249
|
+
// producer's declared return type at query time.
|
|
1250
|
+
const assigned = rustAssignmentTargetOf(node);
|
|
1251
|
+
|
|
1252
|
+
// Call-site arg count for arity pruning (no spread syntax in Rust;
|
|
1253
|
+
// UFCS `Type::method(&x, ...)` counts the explicit self — the
|
|
1254
|
+
// pruning range accounts for the shift).
|
|
1255
|
+
const argsNode = node.childForFieldName('arguments');
|
|
1256
|
+
let argCount = 0;
|
|
1257
|
+
if (argsNode) {
|
|
1258
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1259
|
+
if (argsNode.namedChild(i).type === 'comment') continue;
|
|
1260
|
+
argCount++;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1047
1264
|
if (funcNode.type === 'identifier') {
|
|
1048
1265
|
// Direct call: foo()
|
|
1049
1266
|
const firstArg = getFirstStringArg(node);
|
|
@@ -1051,6 +1268,9 @@ function findCallsInCode(code, parser) {
|
|
|
1051
1268
|
name: funcNode.text,
|
|
1052
1269
|
line: node.startPosition.row + 1,
|
|
1053
1270
|
isMethod: false,
|
|
1271
|
+
argCount,
|
|
1272
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1273
|
+
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
1054
1274
|
enclosingFunction,
|
|
1055
1275
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1056
1276
|
});
|
|
@@ -1076,7 +1296,63 @@ function findCallsInCode(code, parser) {
|
|
|
1076
1296
|
receiver = rootType;
|
|
1077
1297
|
}
|
|
1078
1298
|
}
|
|
1079
|
-
|
|
1299
|
+
// fix #202: one-hop declared-field receivers — self.dent.path(),
|
|
1300
|
+
// low.sep.into_bytes() — with .clone() transparency (clone()
|
|
1301
|
+
// returns Self by stdlib convention). receiverRoot/Field/RootType
|
|
1302
|
+
// let findCallers hop to the field's declared type cross-file.
|
|
1303
|
+
let receiverRoot, receiverField, receiverRootType;
|
|
1304
|
+
if (!receiver) {
|
|
1305
|
+
let obj = valueNode;
|
|
1306
|
+
while (obj?.type === 'call_expression') {
|
|
1307
|
+
const innerFn = obj.childForFieldName('function');
|
|
1308
|
+
if (innerFn?.type === 'field_expression' &&
|
|
1309
|
+
innerFn.childForFieldName('field')?.text === 'clone') {
|
|
1310
|
+
obj = innerFn.childForFieldName('value');
|
|
1311
|
+
} else break;
|
|
1312
|
+
}
|
|
1313
|
+
if (obj?.type === 'field_expression') {
|
|
1314
|
+
const rootNode = obj.childForFieldName('value');
|
|
1315
|
+
const fldNode = obj.childForFieldName('field');
|
|
1316
|
+
if (fldNode?.type === 'field_identifier' && rootNode &&
|
|
1317
|
+
(rootNode.type === 'identifier' || rootNode.type === 'self')) {
|
|
1318
|
+
receiverRoot = rootNode.text;
|
|
1319
|
+
receiverField = fldNode.text;
|
|
1320
|
+
receiverRootType = rootNode.type === 'self'
|
|
1321
|
+
? findEnclosingImplType(node)
|
|
1322
|
+
: getReceiverType(rootNode.text);
|
|
1323
|
+
}
|
|
1324
|
+
} else if (obj && obj !== valueNode &&
|
|
1325
|
+
(obj.type === 'identifier' || obj.type === 'self')) {
|
|
1326
|
+
// x.clone().m() — the receiver is effectively x
|
|
1327
|
+
receiver = obj.text;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Chained receiver (fix #220): the receiver IS a call —
|
|
1331
|
+
// self.as_u8().as_color() — record the producer so
|
|
1332
|
+
// findCallers can type it from the declared return.
|
|
1333
|
+
// Path producers (Config::load().x()) stay uncaptured
|
|
1334
|
+
// until a measured family justifies the path branch.
|
|
1335
|
+
let receiverCall, receiverCallIsMethod;
|
|
1336
|
+
if (!receiver && !receiverField && valueNode?.type === 'call_expression') {
|
|
1337
|
+
const prodFunc = valueNode.childForFieldName('function');
|
|
1338
|
+
if (prodFunc?.type === 'identifier') {
|
|
1339
|
+
receiverCall = prodFunc.text;
|
|
1340
|
+
} else if (prodFunc?.type === 'field_expression') {
|
|
1341
|
+
const pf = prodFunc.childForFieldName('field');
|
|
1342
|
+
if (pf) { receiverCall = pf.text; receiverCallIsMethod = true; }
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
// Literal receivers carry their builtin type (fix #220,
|
|
1346
|
+
// ripgrep-measured): "match:fg:magenta".parse() is
|
|
1347
|
+
// str::parse, never a project method. Numeric literals
|
|
1348
|
+
// stay untyped (i32/u64/f64 ambiguity).
|
|
1349
|
+
const literalReceiverType = (!receiver && valueNode)
|
|
1350
|
+
? ({ string_literal: 'str', raw_string_literal: 'str',
|
|
1351
|
+
char_literal: 'char', boolean_literal: 'bool' })[valueNode.type]
|
|
1352
|
+
: undefined;
|
|
1353
|
+
const receiverType = (receiver && receiver !== 'self')
|
|
1354
|
+
? getReceiverType(receiver)
|
|
1355
|
+
: literalReceiverType;
|
|
1080
1356
|
const firstArg = getFirstStringArg(node);
|
|
1081
1357
|
// RUST-2: For chained calls like `a().b().parse::<T>().ok()`,
|
|
1082
1358
|
// each method should report the line where its OWN identifier
|
|
@@ -1089,6 +1365,13 @@ function findCallsInCode(code, parser) {
|
|
|
1089
1365
|
isMethod: true,
|
|
1090
1366
|
receiver,
|
|
1091
1367
|
...(receiverType && { receiverType }),
|
|
1368
|
+
...(receiverField && { receiverRoot, receiverField }),
|
|
1369
|
+
...(receiverField && receiverRootType && { receiverRootType }),
|
|
1370
|
+
...(receiverCall && { receiverCall }),
|
|
1371
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1372
|
+
argCount,
|
|
1373
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1374
|
+
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
1092
1375
|
enclosingFunction,
|
|
1093
1376
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1094
1377
|
});
|
|
@@ -1100,12 +1383,20 @@ function findCallsInCode(code, parser) {
|
|
|
1100
1383
|
const segments = pathText.split('::');
|
|
1101
1384
|
const name = segments[segments.length - 1];
|
|
1102
1385
|
const firstArg = getFirstStringArg(node);
|
|
1386
|
+
// Turbofish receivers (`Vec::<String>::new`) carry the type
|
|
1387
|
+
// arguments as their own `::`-split segments — drop them so
|
|
1388
|
+
// the receiver is the plain type/module path (fix #222).
|
|
1389
|
+
const recvSegments = segments.slice(0, -1)
|
|
1390
|
+
.filter(s => !s.startsWith('<') && !s.endsWith('>'));
|
|
1103
1391
|
calls.push({
|
|
1104
1392
|
name: name,
|
|
1105
1393
|
line: node.startPosition.row + 1,
|
|
1106
1394
|
isMethod: segments.length > 1,
|
|
1107
1395
|
isPathCall: true, // Distinguishes Type::func()/module::func() from obj.method()
|
|
1108
|
-
receiver:
|
|
1396
|
+
receiver: recvSegments.length > 0 ? recvSegments.join('::') : undefined,
|
|
1397
|
+
argCount,
|
|
1398
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1399
|
+
...(assigned?.unwrapped && { assignedUnwrap: true }),
|
|
1109
1400
|
enclosingFunction,
|
|
1110
1401
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1111
1402
|
});
|
|
@@ -1124,17 +1415,27 @@ function findCallsInCode(code, parser) {
|
|
|
1124
1415
|
const nameNode = node.childForFieldName('name');
|
|
1125
1416
|
if (nameNode) {
|
|
1126
1417
|
let typeName = null;
|
|
1418
|
+
let pathQualifier = null;
|
|
1127
1419
|
if (nameNode.type === 'type_identifier') {
|
|
1128
1420
|
typeName = nameNode.text;
|
|
1129
1421
|
} else if (nameNode.type === 'scoped_type_identifier') {
|
|
1130
|
-
// path::Foo or Enum::Variant — emit as the rightmost name
|
|
1422
|
+
// path::Foo or Enum::Variant — emit as the rightmost name,
|
|
1423
|
+
// keeping the qualifier as receiver (fix #206): a
|
|
1424
|
+
// path-qualified type must not resolve to a same-file
|
|
1425
|
+
// binding of an unrelated same-name symbol.
|
|
1131
1426
|
const innerNameNode = nameNode.childForFieldName('name');
|
|
1132
1427
|
if (innerNameNode) {
|
|
1133
1428
|
typeName = innerNameNode.text;
|
|
1429
|
+
const pathNode = nameNode.childForFieldName('path');
|
|
1430
|
+
if (pathNode) {
|
|
1431
|
+
const segs = pathNode.text.split('::');
|
|
1432
|
+
pathQualifier = segs[segs.length - 1] || null;
|
|
1433
|
+
}
|
|
1134
1434
|
} else {
|
|
1135
1435
|
// Fallback: split by ::
|
|
1136
1436
|
const parts = nameNode.text.split('::');
|
|
1137
1437
|
typeName = parts[parts.length - 1];
|
|
1438
|
+
if (parts.length > 1) pathQualifier = parts[parts.length - 2] || null;
|
|
1138
1439
|
}
|
|
1139
1440
|
}
|
|
1140
1441
|
if (typeName) {
|
|
@@ -1144,6 +1445,7 @@ function findCallsInCode(code, parser) {
|
|
|
1144
1445
|
line: node.startPosition.row + 1,
|
|
1145
1446
|
isMethod: false,
|
|
1146
1447
|
isConstructor: true,
|
|
1448
|
+
...(pathQualifier && { receiver: pathQualifier }),
|
|
1147
1449
|
enclosingFunction
|
|
1148
1450
|
});
|
|
1149
1451
|
}
|
|
@@ -1153,13 +1455,13 @@ function findCallsInCode(code, parser) {
|
|
|
1153
1455
|
// Handle macro invocations: println!(), vec![]
|
|
1154
1456
|
if (node.type === 'macro_invocation') {
|
|
1155
1457
|
const macroNode = node.childForFieldName('macro');
|
|
1458
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1156
1459
|
if (macroNode) {
|
|
1157
1460
|
let macroName = macroNode.text;
|
|
1158
1461
|
// Remove the trailing ! if present in the name
|
|
1159
1462
|
if (macroName.endsWith('!')) {
|
|
1160
1463
|
macroName = macroName.slice(0, -1);
|
|
1161
1464
|
}
|
|
1162
|
-
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1163
1465
|
calls.push({
|
|
1164
1466
|
name: macroName,
|
|
1165
1467
|
line: node.startPosition.row + 1,
|
|
@@ -1168,6 +1470,35 @@ function findCallsInCode(code, parser) {
|
|
|
1168
1470
|
enclosingFunction
|
|
1169
1471
|
});
|
|
1170
1472
|
}
|
|
1473
|
+
// Calls INSIDE the macro body: tree-sitter parses macro arguments
|
|
1474
|
+
// as an unstructured token_tree, which hid every call written
|
|
1475
|
+
// inside assert_eq!/format!/vec!/write! (measured: 175 unclaimed
|
|
1476
|
+
// call lines on ripgrep — test assertions live in macros).
|
|
1477
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1478
|
+
const child = node.child(i);
|
|
1479
|
+
if (child.type === 'token_tree') {
|
|
1480
|
+
extractCallsFromTokenTree(child, enclosingFunction, calls, getReceiverType);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// macro_rules! definitions: the transcriber token_tree holds concrete
|
|
1487
|
+
// call templates (write!(stderr, $($tt)*) in messages.rs) — real call
|
|
1488
|
+
// sites in every expansion. The matcher (token_tree_pattern) holds
|
|
1489
|
+
// fragment specifiers, never calls — skipped.
|
|
1490
|
+
if (node.type === 'macro_definition') {
|
|
1491
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1492
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1493
|
+
const rule = node.namedChild(i);
|
|
1494
|
+
if (rule.type !== 'macro_rule') continue;
|
|
1495
|
+
for (let j = 0; j < rule.childCount; j++) {
|
|
1496
|
+
const part = rule.child(j);
|
|
1497
|
+
if (part.type === 'token_tree') {
|
|
1498
|
+
extractCallsFromTokenTree(part, enclosingFunction, calls, getReceiverType);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1171
1502
|
return true;
|
|
1172
1503
|
}
|
|
1173
1504
|
|
|
@@ -1435,6 +1766,41 @@ function findExportsInCode(code, parser) {
|
|
|
1435
1766
|
}
|
|
1436
1767
|
|
|
1437
1768
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
1769
|
+
// Public renamed re-exports: `pub use foo::bar as baz;` (also nested in
|
|
1770
|
+
// use lists: `pub use m::{a as b}`). name keeps the source symbol; alias
|
|
1771
|
+
// carries the external name callers use. Plain (un-renamed) `pub use`
|
|
1772
|
+
// re-exports are intentionally not emitted here — only renames feed the
|
|
1773
|
+
// export-alias caller resolution.
|
|
1774
|
+
if (node.type === 'use_declaration' && hasVisibility(node)) {
|
|
1775
|
+
const line = node.startPosition.row + 1;
|
|
1776
|
+
const collectAsClauses = (n) => {
|
|
1777
|
+
if (n.type === 'use_as_clause') {
|
|
1778
|
+
const srcNode = n.namedChild(0);
|
|
1779
|
+
const aliasNode = n.namedChild(1);
|
|
1780
|
+
// Last path segment is the source symbol name (foo::bar -> bar)
|
|
1781
|
+
let local = null;
|
|
1782
|
+
if (srcNode) {
|
|
1783
|
+
if (srcNode.type === 'identifier' || srcNode.type === 'type_identifier') {
|
|
1784
|
+
local = srcNode.text;
|
|
1785
|
+
} else if (srcNode.type === 'scoped_identifier') {
|
|
1786
|
+
const nameField = srcNode.childForFieldName('name');
|
|
1787
|
+
local = nameField ? nameField.text : null;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (local && aliasNode && aliasNode.text !== local) {
|
|
1791
|
+
exports.push({
|
|
1792
|
+
name: local, type: 're-export', line,
|
|
1793
|
+
source: srcNode.text, alias: aliasNode.text,
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
for (let i = 0; i < n.namedChildCount; i++) collectAsClauses(n.namedChild(i));
|
|
1799
|
+
};
|
|
1800
|
+
collectAsClauses(node);
|
|
1801
|
+
return true;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1438
1804
|
// Public functions
|
|
1439
1805
|
if (node.type === 'function_item' && hasVisibility(node)) {
|
|
1440
1806
|
const nameNode = node.childForFieldName('name');
|
|
@@ -1563,7 +1929,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1563
1929
|
const tree = parseTree(parser, code);
|
|
1564
1930
|
const usages = [];
|
|
1565
1931
|
|
|
1566
|
-
|
|
1932
|
+
visitNameNodes(tree, code, name, (node) => {
|
|
1567
1933
|
// Look for identifier, field_identifier (method names in obj.method() calls),
|
|
1568
1934
|
// and type_identifier (type references in params, return types, struct expressions, etc.)
|
|
1569
1935
|
const isIdentifier = node.type === 'identifier' || node.type === 'field_identifier' || node.type === 'type_identifier';
|
|
@@ -1598,11 +1964,14 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1598
1964
|
parent.childForFieldName('function') === node) {
|
|
1599
1965
|
usageType = 'call';
|
|
1600
1966
|
}
|
|
1601
|
-
// Scoped call: Type::method() —
|
|
1967
|
+
// Scoped call: Type::method() — only the LAST segment is the callee;
|
|
1968
|
+
// the path qualifier (Type in Type::method()) is a type reference,
|
|
1969
|
+
// not a call of Type
|
|
1602
1970
|
else if (parent.type === 'scoped_identifier') {
|
|
1603
1971
|
const grandparent = parent.parent;
|
|
1604
1972
|
if (grandparent && grandparent.type === 'call_expression' &&
|
|
1605
|
-
grandparent.childForFieldName('function') === parent
|
|
1973
|
+
grandparent.childForFieldName('function') === parent &&
|
|
1974
|
+
parent.childForFieldName('name') === node) {
|
|
1606
1975
|
usageType = 'call';
|
|
1607
1976
|
}
|
|
1608
1977
|
}
|
package/languages/utils.js
CHANGED
|
@@ -460,36 +460,56 @@ function parseJSDocTags(codeOrLines, startLine) {
|
|
|
460
460
|
}
|
|
461
461
|
const block = blockLines.join('\n');
|
|
462
462
|
|
|
463
|
-
// @param {Type} name
|
|
463
|
+
// @param {Type} name — balanced-brace scan so nested types survive
|
|
464
|
+
// (e.g. `{{ ok: boolean, error?: string }}` or `{Object<string, {x: number}>}`)
|
|
464
465
|
const paramTypes = {};
|
|
465
466
|
let any = false;
|
|
466
|
-
const
|
|
467
|
+
const paramTagRegex = /@param\s+/g;
|
|
467
468
|
let m;
|
|
468
|
-
while ((m =
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const paramOptRegex = /@param\s+\{([^}]+)\}\s+\[([A-Za-z_$][\w$]*)(?:\s*=[^\]]*)?\]/g;
|
|
478
|
-
while ((m = paramOptRegex.exec(block)) !== null) {
|
|
479
|
-
const type = m[1].trim();
|
|
480
|
-
const name = m[2];
|
|
481
|
-
if (!paramTypes[name]) paramTypes[name] = type;
|
|
469
|
+
while ((m = paramTagRegex.exec(block)) !== null) {
|
|
470
|
+
const braced = extractBracedType(block, m.index + m[0].length);
|
|
471
|
+
if (!braced) continue;
|
|
472
|
+
// Name follows the closing brace: plain `name` or optional `[name]` / `[name=default]`
|
|
473
|
+
const rest = block.slice(braced.endIdx);
|
|
474
|
+
const nameMatch = rest.match(/^\s+(?:\[([A-Za-z_$][\w$]*)(?:\s*=[^\]]*)?\]|([A-Za-z_$][\w$]*))/);
|
|
475
|
+
if (!nameMatch) continue;
|
|
476
|
+
const name = nameMatch[1] || nameMatch[2];
|
|
477
|
+
paramTypes[name] = braced.type.trim().replace(/\s+/g, ' ');
|
|
482
478
|
any = true;
|
|
483
479
|
}
|
|
484
480
|
|
|
485
481
|
// @returns {Type} or @return {Type}
|
|
486
|
-
const
|
|
482
|
+
const retTag = block.match(/@returns?\s+/);
|
|
483
|
+
const retBraced = retTag ? extractBracedType(block, retTag.index + retTag[0].length) : null;
|
|
487
484
|
const result = {};
|
|
488
485
|
if (any) result.paramTypes = paramTypes;
|
|
489
|
-
if (
|
|
486
|
+
if (retBraced) result.returnType = retBraced.type.trim().replace(/\s+/g, ' ');
|
|
490
487
|
return result;
|
|
491
488
|
}
|
|
492
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Extract a balanced-brace type expression starting at an opening `{`.
|
|
492
|
+
* Returns the inner text (without the outer braces) and the index just past
|
|
493
|
+
* the matching closing brace, or null when text[openIdx] is not `{` or the
|
|
494
|
+
* braces never balance.
|
|
495
|
+
* @param {string} text
|
|
496
|
+
* @param {number} openIdx - index expected to hold the opening `{`
|
|
497
|
+
* @returns {{ type: string, endIdx: number }|null}
|
|
498
|
+
*/
|
|
499
|
+
function extractBracedType(text, openIdx) {
|
|
500
|
+
if (text[openIdx] !== '{') return null;
|
|
501
|
+
let depth = 0;
|
|
502
|
+
for (let k = openIdx; k < text.length; k++) {
|
|
503
|
+
const ch = text[k];
|
|
504
|
+
if (ch === '{') depth++;
|
|
505
|
+
else if (ch === '}') {
|
|
506
|
+
depth--;
|
|
507
|
+
if (depth === 0) return { type: text.slice(openIdx + 1, k), endIdx: k + 1 };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
493
513
|
/**
|
|
494
514
|
* Compute the final paramTypes/returnType for a function symbol.
|
|
495
515
|
* Native AST types take precedence; JSDoc fills gaps.
|
|
@@ -708,6 +728,39 @@ function traverseTreeCached(rootNode, callback) {
|
|
|
708
728
|
}
|
|
709
729
|
}
|
|
710
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Visit the AST node covering each whole-word text occurrence of `name`.
|
|
733
|
+
*
|
|
734
|
+
* Equivalent to a full-tree walk whose callback filters on
|
|
735
|
+
* `node.text === name` token types — but O(occurrences) instead of
|
|
736
|
+
* O(nodes): the source string locates candidate offsets (indexOf + ASCII
|
|
737
|
+
* word-boundary pre-check), and descendantForIndex jumps straight to the
|
|
738
|
+
* deepest node spanning each. The callback receives the identifier-style
|
|
739
|
+
* token when the occurrence is code, or a string/comment/longer-identifier
|
|
740
|
+
* token otherwise — callers' existing type/text guards skip those, so
|
|
741
|
+
* false-positive candidates are safe and false negatives are impossible
|
|
742
|
+
* (a token equal to `name` cannot have identifier characters adjacent,
|
|
743
|
+
* or the token would extend past the name).
|
|
744
|
+
*
|
|
745
|
+
* node-tree-sitter indexes are UTF-16 code units, same as JS string
|
|
746
|
+
* offsets — unicode content needs no special handling.
|
|
747
|
+
*/
|
|
748
|
+
function visitNameNodes(tree, code, name, callback) {
|
|
749
|
+
const isWordCode = (c) =>
|
|
750
|
+
(c >= 48 && c <= 57) || (c >= 65 && c <= 90) ||
|
|
751
|
+
(c >= 97 && c <= 122) || c === 95 || c === 36; // [0-9A-Za-z_$]
|
|
752
|
+
const len = name.length;
|
|
753
|
+
let idx = code.indexOf(name);
|
|
754
|
+
while (idx !== -1) {
|
|
755
|
+
if ((idx === 0 || !isWordCode(code.charCodeAt(idx - 1))) &&
|
|
756
|
+
(idx + len >= code.length || !isWordCode(code.charCodeAt(idx + len)))) {
|
|
757
|
+
const node = tree.rootNode.descendantForIndex(idx);
|
|
758
|
+
if (node) callback(node);
|
|
759
|
+
}
|
|
760
|
+
idx = code.indexOf(name, idx + len);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
711
764
|
/**
|
|
712
765
|
* Clear the cached node list (call when the tree changes).
|
|
713
766
|
*/
|
|
@@ -900,6 +953,7 @@ function extractSprintfPrefix(callNode) {
|
|
|
900
953
|
module.exports = {
|
|
901
954
|
traverseTree,
|
|
902
955
|
traverseTreeCached,
|
|
956
|
+
visitNameNodes,
|
|
903
957
|
getCachedNodeList,
|
|
904
958
|
clearNodeListCache,
|
|
905
959
|
nodeToLocation,
|