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/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
- const receiverType = (receiver && receiver !== 'self') ? getReceiverType(receiver) : undefined;
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: segments.length > 1 ? segments.slice(0, -1).join('::') : undefined,
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
- traverseTreeCached(tree.rootNode, (node) => {
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() — identifier inside scoped_identifier inside call_expression
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
  }
@@ -460,36 +460,56 @@ function parseJSDocTags(codeOrLines, startLine) {
460
460
  }
461
461
  const block = blockLines.join('\n');
462
462
 
463
- // @param {Type} name capture balanced braces (no nested braces in JSDoc types in practice)
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 paramRegex = /@param\s+\{([^}]+)\}\s+([A-Za-z_$][\w$]*)/g;
467
+ const paramTagRegex = /@param\s+/g;
467
468
  let m;
468
- while ((m = paramRegex.exec(block)) !== null) {
469
- const type = m[1].trim();
470
- const name = m[2];
471
- // Strip optional brackets if author wrote @param {Type} [name]; here we already excluded that
472
- // but also handle @param {Type} [name=default] form by scanning a separate regex
473
- paramTypes[name] = type;
474
- any = true;
475
- }
476
- // Optional-bracket form: @param {Type} [name] or [name=default]
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 retMatch = block.match(/@returns?\s+\{([^}]+)\}/);
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 (retMatch) result.returnType = retMatch[1].trim();
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,