ucn 4.1.2 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/languages/rust.js CHANGED
@@ -907,7 +907,11 @@ function parse(code, parser) {
907
907
  classes.sort((a, b) => a.startLine - b.startLine);
908
908
  stateObjects.sort((a, b) => a.startLine - b.startLine);
909
909
 
910
- return { language: 'rust', totalLines: lines.length, functions, classes, stateObjects, imports: [], exports: [] };
910
+ return {
911
+ language: 'rust', totalLines: lines.length, functions, classes, stateObjects,
912
+ ...(tree.rootNode.hasError && { parseRecovery: true }),
913
+ imports: [], exports: [],
914
+ };
911
915
  }
912
916
 
913
917
  /**
@@ -976,16 +980,48 @@ function _findRustChainRootType(callNode) {
976
980
  * Emitted calls mirror the regular handlers' field contract and carry
977
981
  * inMacro: true.
978
982
  */
979
- function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType) {
983
+ function _tokenTreeCallArgsAfter(children, nameIndex) {
984
+ let nextIndex = nameIndex + 1;
985
+ // Rust turbofish: method::<T>(...) / collect::<Vec<_>>(). Token trees
986
+ // expose the generic tokens as flat siblings between the name and the
987
+ // argument token_tree, so a direct-next check misclassified the method as
988
+ // a reference and omitted it from the call graph. Count angle tokens by
989
+ // text because nested generic closers may arrive as one `>>` token.
990
+ if (children[nextIndex]?.type === '::' && children[nextIndex + 1]?.type === '<') {
991
+ let depth = 0;
992
+ nextIndex++;
993
+ for (; nextIndex < children.length; nextIndex++) {
994
+ const text = children[nextIndex]?.text || '';
995
+ for (const ch of text) {
996
+ if (ch === '<') depth++;
997
+ else if (ch === '>') depth--;
998
+ }
999
+ if (depth === 0) {
1000
+ nextIndex++;
1001
+ break;
1002
+ }
1003
+ }
1004
+ }
1005
+ const args = children[nextIndex];
1006
+ return args?.type === 'token_tree' && args.text.startsWith('(') ? args : null;
1007
+ }
1008
+
1009
+ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType,
1010
+ isPatternShadow, isFlowInvalidated) {
980
1011
  const children = [];
981
1012
  for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
982
1013
  for (let i = 0; i < children.length; i++) {
983
1014
  const tok = children[i];
984
1015
  if (tok.type === 'token_tree') {
985
- extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType);
1016
+ extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType,
1017
+ isPatternShadow, isFlowInvalidated);
986
1018
  continue;
987
1019
  }
988
- if (tok.type !== 'identifier') continue;
1020
+ // `default` is tokenized as the Rust keyword even in the valid
1021
+ // associated-call shape `Type::default()`. It is still an AST token,
1022
+ // so admitting it here preserves the AST-first rule while recovering
1023
+ // calls nested inside macro token trees (assert_eq!, matches!, ...).
1024
+ if (tok.type !== 'identifier' && tok.type !== 'default') continue;
989
1025
  const next = children[i + 1];
990
1026
  const prev = children[i - 1];
991
1027
  // $metavariable(...) — a macro fragment, not a named call
@@ -1003,7 +1039,8 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1003
1039
  });
1004
1040
  continue;
1005
1041
  }
1006
- if (!next || next.type !== 'token_tree' || !next.text.startsWith('(')) continue;
1042
+ const callArgs = _tokenTreeCallArgsAfter(children, i);
1043
+ if (!callArgs) continue;
1007
1044
  if (prev && prev.type === '::') {
1008
1045
  // Path call: Type::func(...) / module::sub::func(...) — segments
1009
1046
  // can be identifiers, primitives (char::from), or path keywords
@@ -1056,13 +1093,17 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1056
1093
  char_literal: 'char', boolean_literal: 'bool' })[recvTok.type]
1057
1094
  : undefined;
1058
1095
  const receiverType = (receiver && receiver !== 'self' && getReceiverType)
1059
- ? getReceiverType(receiver) : litType;
1096
+ ? getReceiverType(receiver, tok) : litType;
1097
+ const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
1098
+ const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
1060
1099
  calls.push({
1061
1100
  name: tok.text,
1062
1101
  line: tok.startPosition.row + 1,
1063
1102
  isMethod: true,
1064
1103
  receiver,
1065
1104
  ...(receiverType && { receiverType }),
1105
+ ...(receiverPatternShadow && { receiverPatternShadow: true }),
1106
+ ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
1066
1107
  inMacro: true,
1067
1108
  enclosingFunction
1068
1109
  });
@@ -1131,6 +1172,12 @@ function findCallsInCode(code, parser) {
1131
1172
  const functionStack = []; // Stack of { name, startLine, endLine }
1132
1173
  // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
1133
1174
  const scopeTypes = new Map();
1175
+ // Lexical rebindings that do not have a call producer (`let m = match m
1176
+ // { ... }`) invalidate query-time return flow. Without a tombstone, the
1177
+ // previous `m = make_result()` annotation survives and can positively
1178
+ // exclude true calls on the rebound value. Events are range-aware so a
1179
+ // nested-block shadow does not leak after the block.
1180
+ const scopeFlowEvents = new Map();
1134
1181
 
1135
1182
  // Helper: extract first string-arg literal from a call_expression node.
1136
1183
  // Used by route extraction to capture path arg of client.get("/users") and
@@ -1141,7 +1188,7 @@ function findCallsInCode(code, parser) {
1141
1188
  if (!argsNode) return null;
1142
1189
  for (let i = 0; i < argsNode.namedChildCount; i++) {
1143
1190
  const arg = argsNode.namedChild(i);
1144
- if (arg.type === 'comment') continue;
1191
+ if (arg.type.endsWith('comment')) continue;
1145
1192
  // format!() macro inside an arg: client.get(format!("/users/{}", id))
1146
1193
  if (arg.type === 'macro_invocation') {
1147
1194
  const macroNode = arg.childForFieldName('macro');
@@ -1224,8 +1271,62 @@ function findCallsInCode(code, parser) {
1224
1271
  : null;
1225
1272
  };
1226
1273
 
1274
+ const patternContainsName = (pattern, varName) => {
1275
+ if (!pattern) return false;
1276
+ const stack = [pattern];
1277
+ while (stack.length > 0) {
1278
+ const n = stack.pop();
1279
+ if (n.type === 'identifier' && n.text === varName) return true;
1280
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
1281
+ }
1282
+ return false;
1283
+ };
1284
+
1285
+ // Rust pattern bindings are block-scoped and may shadow a typed outer
1286
+ // variable (`if let Some(v) = v.downcast_mut::<T>() { v.m() }`). The
1287
+ // function-wide type map intentionally does not guess the pattern's type,
1288
+ // but it must also never smear the OUTER type onto the inner binding.
1289
+ const patternShadowsAt = (node, varName) => {
1290
+ for (let a = node?.parent; a && !isFunctionNode(a); a = a.parent) {
1291
+ if (a.type !== 'if_expression' && a.type !== 'while_expression') continue;
1292
+ const cond = a.namedChildren.find(c => c.type === 'let_condition');
1293
+ if (!cond) continue;
1294
+ const body = a.namedChildren.find(c =>
1295
+ c.type === 'block' && c.startIndex >= cond.endIndex);
1296
+ if (!body || node.startIndex < body.startIndex || node.endIndex > body.endIndex) continue;
1297
+ const pattern = cond.namedChild(0);
1298
+ if (patternContainsName(pattern, varName)) return true;
1299
+ }
1300
+ return false;
1301
+ };
1302
+
1303
+ const valueHasFlowProducer = (value) => {
1304
+ let n = value;
1305
+ while (n && ['try_expression', 'await_expression', 'parenthesized_expression'].includes(n.type)) {
1306
+ n = n.namedChildCount === 1 ? n.namedChild(0) : null;
1307
+ }
1308
+ return n?.type === 'call_expression';
1309
+ };
1310
+
1311
+ const flowInvalidatedAt = (node, varName) => {
1312
+ const pos = node?.startIndex ?? -1;
1313
+ for (let i = functionStack.length - 1; i >= 0; i--) {
1314
+ const byName = scopeFlowEvents.get(functionStack[i].startLine);
1315
+ const events = byName?.get(varName);
1316
+ if (!events) continue;
1317
+ let latest = null;
1318
+ for (const event of events) {
1319
+ if (event.at <= pos && pos <= event.until &&
1320
+ (!latest || event.at > latest.at)) latest = event;
1321
+ }
1322
+ if (latest) return latest.invalidated;
1323
+ }
1324
+ return false;
1325
+ };
1326
+
1227
1327
  // Look up variable type from scope chain
1228
- const getReceiverType = (varName) => {
1328
+ const getReceiverType = (varName, atNode) => {
1329
+ if (atNode && patternShadowsAt(atNode, varName)) return undefined;
1229
1330
  for (let i = functionStack.length - 1; i >= 0; i--) {
1230
1331
  const typeMap = scopeTypes.get(functionStack[i].startLine);
1231
1332
  if (typeMap?.has(varName)) return typeMap.get(varName);
@@ -1254,6 +1355,37 @@ function findCallsInCode(code, parser) {
1254
1355
  };
1255
1356
  functionStack.push(entry);
1256
1357
  scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
1358
+ scopeFlowEvents.set(entry.startLine, new Map());
1359
+ }
1360
+
1361
+ // Record binding state before visiting the initializer's children;
1362
+ // `at=node.endIndex` means the new binding takes effect only after
1363
+ // the RHS, matching Rust's shadowing semantics.
1364
+ if (functionStack.length > 0 &&
1365
+ (node.type === 'let_declaration' || node.type === 'assignment_expression')) {
1366
+ const pattern = node.type === 'let_declaration'
1367
+ ? node.childForFieldName('pattern') : node.childForFieldName('left');
1368
+ const value = node.type === 'let_declaration'
1369
+ ? node.childForFieldName('value') : node.childForFieldName('right');
1370
+ if (pattern?.type === 'identifier' && value) {
1371
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
1372
+ const byName = scopeFlowEvents.get(scopeKey);
1373
+ if (byName) {
1374
+ if (!byName.has(pattern.text)) byName.set(pattern.text, []);
1375
+ let until = functionStack[functionStack.length - 1].endIndex ?? Infinity;
1376
+ if (node.type === 'let_declaration') {
1377
+ for (let p = node.parent; p; p = p.parent) {
1378
+ if (p.type === 'block') { until = p.endIndex; break; }
1379
+ if (isFunctionNode(p)) break;
1380
+ }
1381
+ }
1382
+ byName.get(pattern.text).push({
1383
+ at: node.endIndex,
1384
+ until,
1385
+ invalidated: !valueHasFlowProducer(value),
1386
+ });
1387
+ }
1388
+ }
1257
1389
  }
1258
1390
 
1259
1391
  // Handle function calls: foo(), obj.method(), Type::func(), foo::<T>()
@@ -1280,7 +1412,7 @@ function findCallsInCode(code, parser) {
1280
1412
  let argCount = 0;
1281
1413
  if (argsNode) {
1282
1414
  for (let i = 0; i < argsNode.namedChildCount; i++) {
1283
- if (argsNode.namedChild(i).type === 'comment') continue;
1415
+ if (argsNode.namedChild(i).type.endsWith('comment')) continue;
1284
1416
  argCount++;
1285
1417
  }
1286
1418
  }
@@ -1348,7 +1480,7 @@ function findCallsInCode(code, parser) {
1348
1480
  receiverField = fldNode.text;
1349
1481
  receiverRootType = rootNode.type === 'self'
1350
1482
  ? findEnclosingImplType(node)
1351
- : getReceiverType(rootNode.text);
1483
+ : getReceiverType(rootNode.text, node);
1352
1484
  }
1353
1485
  } else if (obj && obj !== valueNode &&
1354
1486
  (obj.type === 'identifier' || obj.type === 'self')) {
@@ -1407,8 +1539,10 @@ function findCallsInCode(code, parser) {
1407
1539
  char_literal: 'char', boolean_literal: 'bool' })[valueNode.type]
1408
1540
  : undefined;
1409
1541
  const receiverType = (receiver && receiver !== 'self' && !receiverIsChainRoot)
1410
- ? getReceiverType(receiver)
1542
+ ? getReceiverType(receiver, node)
1411
1543
  : literalReceiverType;
1544
+ const receiverPatternShadow = !!(receiver && patternShadowsAt(node, receiver));
1545
+ const receiverFlowInvalidated = !!(receiver && flowInvalidatedAt(node, receiver));
1412
1546
  const firstArg = getFirstStringArg(node);
1413
1547
  // RUST-2: For chained calls like `a().b().parse::<T>().ok()`,
1414
1548
  // each method should report the line where its OWN identifier
@@ -1421,6 +1555,8 @@ function findCallsInCode(code, parser) {
1421
1555
  isMethod: true,
1422
1556
  receiver,
1423
1557
  ...(receiverType && { receiverType }),
1558
+ ...(receiverPatternShadow && { receiverPatternShadow: true }),
1559
+ ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
1424
1560
  ...(receiverIsChainRoot && { receiverIsChainRoot: true }),
1425
1561
  ...(receiverField && { receiverRoot, receiverField }),
1426
1562
  ...(receiverField && receiverRootType && { receiverRootType }),
@@ -1496,6 +1632,13 @@ function findCallsInCode(code, parser) {
1496
1632
  if (parts.length > 1) pathQualifier = parts[parts.length - 2] || null;
1497
1633
  }
1498
1634
  }
1635
+ // `Self { ... }` is a constructor for the enclosing impl
1636
+ // target, not a project type literally named Self. Preserve
1637
+ // the concrete identity in the call record so callers and
1638
+ // callees can reconcile it with the struct symbol.
1639
+ if (typeName === 'Self') {
1640
+ typeName = findEnclosingImplType(node) || typeName;
1641
+ }
1499
1642
  if (typeName) {
1500
1643
  const enclosingFunction = getCurrentEnclosingFunction();
1501
1644
  calls.push({
@@ -1535,7 +1678,9 @@ function findCallsInCode(code, parser) {
1535
1678
  for (let i = 0; i < node.childCount; i++) {
1536
1679
  const child = node.child(i);
1537
1680
  if (child.type === 'token_tree') {
1538
- extractCallsFromTokenTree(child, enclosingFunction, calls, getReceiverType);
1681
+ extractCallsFromTokenTree(
1682
+ child, enclosingFunction, calls, getReceiverType,
1683
+ patternShadowsAt, flowInvalidatedAt);
1539
1684
  }
1540
1685
  }
1541
1686
  return true;
@@ -1553,7 +1698,9 @@ function findCallsInCode(code, parser) {
1553
1698
  for (let j = 0; j < rule.childCount; j++) {
1554
1699
  const part = rule.child(j);
1555
1700
  if (part.type === 'token_tree') {
1556
- extractCallsFromTokenTree(part, enclosingFunction, calls, getReceiverType);
1701
+ extractCallsFromTokenTree(
1702
+ part, enclosingFunction, calls, getReceiverType,
1703
+ patternShadowsAt, flowInvalidatedAt);
1557
1704
  }
1558
1705
  }
1559
1706
  }
@@ -1569,7 +1716,9 @@ function findCallsInCode(code, parser) {
1569
1716
  const valueNode = node.childForFieldName('value');
1570
1717
  if (fieldNode) {
1571
1718
  const receiver = (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined;
1572
- const receiverType = (receiver && receiver !== 'self') ? getReceiverType(receiver) : undefined;
1719
+ const receiverType = (receiver && receiver !== 'self') ? getReceiverType(receiver, node) : undefined;
1720
+ const receiverPatternShadow = !!(receiver && patternShadowsAt(node, receiver));
1721
+ const receiverFlowInvalidated = !!(receiver && flowInvalidatedAt(node, receiver));
1573
1722
  const enclosingFunction = getCurrentEnclosingFunction();
1574
1723
  // RUST-2: use the field identifier's line, not the wrapping field_expression's
1575
1724
  calls.push({
@@ -1578,6 +1727,8 @@ function findCallsInCode(code, parser) {
1578
1727
  isMethod: true,
1579
1728
  receiver,
1580
1729
  ...(receiverType && { receiverType }),
1730
+ ...(receiverPatternShadow && { receiverPatternShadow: true }),
1731
+ ...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
1581
1732
  isFunctionReference: true,
1582
1733
  isPotentialCallback: true,
1583
1734
  enclosingFunction
@@ -1655,6 +1806,7 @@ function findCallsInCode(code, parser) {
1655
1806
  const leaving = functionStack.pop();
1656
1807
  if (leaving) {
1657
1808
  scopeTypes.delete(leaving.startLine);
1809
+ scopeFlowEvents.delete(leaving.startLine);
1658
1810
  }
1659
1811
  }
1660
1812
  }
@@ -2136,15 +2288,15 @@ function findUsagesInCode(code, name, parser, tree) {
2136
2288
  // field_expression. Detect the `obj.name(` pattern via siblings.
2137
2289
  else if (parent.type === 'token_tree') {
2138
2290
  const idx = _indexInParent(node, parent);
2291
+ const siblings = Array.from({ length: parent.childCount }, (_, i) => parent.child(i));
2292
+ const callArgs = _tokenTreeCallArgsAfter(siblings, idx);
2139
2293
  // Method call pattern: [obj] [.] [name] [()] inside macro
2140
2294
  if (idx >= 2) {
2141
2295
  const dot = parent.child(idx - 1);
2142
2296
  const obj = parent.child(idx - 2);
2143
- const next = parent.child(idx + 1);
2144
2297
  if (dot && dot.text === '.' && obj &&
2145
2298
  (obj.type === 'identifier' || obj.type === 'self')) {
2146
- if (next && next.type === 'token_tree' &&
2147
- next.childCount > 0 && next.child(0).text === '(') {
2299
+ if (callArgs) {
2148
2300
  usageType = 'call';
2149
2301
  }
2150
2302
  usages.push({ line, column, usageType, receiver: obj.text });
@@ -2153,12 +2305,9 @@ function findUsagesInCode(code, name, parser, tree) {
2153
2305
  }
2154
2306
  // Bare function call pattern: [name] [()] inside macro
2155
2307
  if (idx >= 0) {
2156
- const next = parent.child(idx + 1);
2157
2308
  // Check no preceding dot (would be method call handled above)
2158
2309
  const prev = idx > 0 ? parent.child(idx - 1) : null;
2159
- if ((!prev || prev.text !== '.') &&
2160
- next && next.type === 'token_tree' &&
2161
- next.childCount > 0 && next.child(0).text === '(') {
2310
+ if ((!prev || prev.text !== '.') && callArgs) {
2162
2311
  usageType = 'call';
2163
2312
  }
2164
2313
  }
@@ -2188,7 +2337,16 @@ function findUsagesInCode(code, name, parser, tree) {
2188
2337
  }
2189
2338
  }
2190
2339
 
2191
- usages.push({ line, column, usageType });
2340
+ let inAttribute = false;
2341
+ for (let a = parent; a; a = a.parent) {
2342
+ if (a.type === 'attribute' || a.type === 'attribute_item') {
2343
+ inAttribute = true;
2344
+ break;
2345
+ }
2346
+ if (a.type === 'function_item' || a.type === 'impl_item' ||
2347
+ a.type === 'struct_item') break;
2348
+ }
2349
+ usages.push({ line, column, usageType, ...(inAttribute && { inAttribute: true }) });
2192
2350
  return true;
2193
2351
  });
2194
2352
 
@@ -687,7 +687,6 @@ let _cachedSubtreeEnds = null;
687
687
  function _buildNodeList(rootNode) {
688
688
  const nodes = [];
689
689
  const subtreeEnds = [];
690
- const stack = [rootNode];
691
690
  // Iterative DFS with subtreeEnd tracking
692
691
  // We use a post-processing step to fill subtreeEnds
693
692
  function collect(node) {