ucn 4.1.1 → 4.1.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.
@@ -528,6 +528,18 @@ function _processFunction(node, functions, processedRanges, lines) {
528
528
  isArrow,
529
529
  isGenerator: isGen,
530
530
  modifiers: [],
531
+ // A property-assignment def (Reply.prototype.serialize
532
+ // = function, exports.h = () => ...) creates NO
533
+ // lexical name — a bare call in the file can never
534
+ // bind it (fix #269, fastify-measured: the prototype
535
+ // def stole the module-scope binding from the free
536
+ // `function serialize(...)` below it). Prototype
537
+ // assignments carry their class so typed-receiver
538
+ // method resolution reaches them.
539
+ ...(leftNode.type === 'member_expression' && { memberAssigned: true }),
540
+ ...(leftNode.type === 'member_expression' &&
541
+ /^([A-Za-z_$][\w$]*)\.prototype\.[A-Za-z_$][\w$]*$/.test(leftNode.text) &&
542
+ { className: leftNode.text.split('.')[0], isMethod: true }),
531
543
  ...typeAnno,
532
544
  ...(generics && { generics }),
533
545
  ...(docstring && { docstring })
@@ -752,7 +764,14 @@ function _processClass(node, classes, processedRanges, lines) {
752
764
  // TypeScript namespace/module declarations
753
765
  if (node.type === 'internal_module' || node.type === 'module') {
754
766
  const nameNode = node.childForFieldName('name');
755
- if (nameNode) {
767
+ // A STRING-named module declaration (`declare module '../vanilla'`)
768
+ // is a module AUGMENTATION/shape declaration, not a nameable symbol
769
+ // (fix #267, zustand-measured): it declares no identifier project
770
+ // code can reference, so indexing it as a namespace made deadcode
771
+ // claim every augmentation block dead (5 FALSE-DEADs on zustand's
772
+ // StoreMutators augmentations). The compiler merges it into the
773
+ // TARGET module — never claimable, never importable by this "name".
774
+ if (nameNode && nameNode.type !== 'string') {
756
775
  const { startLine, endLine } = nodeToLocation(node, lines);
757
776
  const docstring = extractJSDocstring(lines, startLine);
758
777
 
@@ -1280,6 +1299,21 @@ const JS_LITERAL_RECEIVER_TYPES = {
1280
1299
  number: 'Number',
1281
1300
  };
1282
1301
 
1302
+ // Literal ASSIGNMENTS type the variable (fix #262, the #218d Python rule):
1303
+ // `const lines = []` → lines is Array, so lines.push() is Array.push. Object
1304
+ // literals are deliberately absent — `const obj = {}` is the mutable
1305
+ // property-bag / namespace idiom (obj.render = fn happens later), so typing
1306
+ // it 'Object' would falsely externalize its assigned methods. A DIRECT
1307
+ // literal receiver (`{}.hasOwnProperty()`) has no such future, hence the
1308
+ // separate map above.
1309
+ const JS_LITERAL_ASSIGN_TYPES = {
1310
+ array: 'Array',
1311
+ string: 'String',
1312
+ template_string: 'String',
1313
+ regex: 'RegExp',
1314
+ number: 'Number',
1315
+ };
1316
+
1283
1317
  // Predefined TS types that pin a receiver; any/unknown/object say nothing.
1284
1318
  const TS_PREDEFINED_RECEIVER_TYPES = new Set(['string', 'number', 'boolean', 'bigint', 'symbol']);
1285
1319
 
@@ -1367,6 +1401,12 @@ function findCallsInCode(code, parser) {
1367
1401
  const aliases = new Map(); // Track local aliases: aliasName -> originalName (string or string[])
1368
1402
  const nonCallableNames = new Set(); // Track names assigned non-callable values
1369
1403
  const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
1404
+ // Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
1405
+ // params). The compiler enforces assignability for these, so reassignment
1406
+ // never stales them; inferred types (literal/new) DO stale and are
1407
+ // deleted on untyped reassignment (fix #262, #218d semantics).
1408
+ const declaredTypeVars = new Set();
1409
+ const declaredTypeVarsStack = [];
1370
1410
  const moduleAliases = new Set(); // Names bound to MODULES (import * as ns / const pkg = require(...))
1371
1411
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
1372
1412
 
@@ -1642,6 +1682,7 @@ function findCallsInCode(code, parser) {
1642
1682
  });
1643
1683
  // Save localVarTypes so inner declarations don't leak to sibling functions
1644
1684
  localVarTypesStack.push(new Map(localVarTypes));
1685
+ declaredTypeVarsStack.push(new Set(declaredTypeVars));
1645
1686
  }
1646
1687
 
1647
1688
  // Track local aliases: const myParse = parse, const { parse: csvParse } = ...
@@ -1706,7 +1747,13 @@ function findCallsInCode(code, parser) {
1706
1747
  const typeName = tsTypeName(typeId);
1707
1748
  if (typeName) {
1708
1749
  localVarTypes.set(nameNode.text, typeName);
1750
+ declaredTypeVars.add(nameNode.text);
1709
1751
  }
1752
+ } else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
1753
+ // Literal declaration types the variable (fix #262):
1754
+ // `const lines = []` → Array. Annotation, when present,
1755
+ // wins (the branch above).
1756
+ localVarTypes.set(nameNode.text, JS_LITERAL_ASSIGN_TYPES[initNode.type]);
1710
1757
  }
1711
1758
  }
1712
1759
  }
@@ -1720,6 +1767,7 @@ function findCallsInCode(code, parser) {
1720
1767
  const typeName = tsTypeName(inner);
1721
1768
  if (typeName) {
1722
1769
  localVarTypes.set(pat.text, typeName);
1770
+ declaredTypeVars.add(pat.text);
1723
1771
  }
1724
1772
  }
1725
1773
  }
@@ -1728,11 +1776,26 @@ function findCallsInCode(code, parser) {
1728
1776
  if (node.type === 'assignment_expression') {
1729
1777
  const left = node.childForFieldName('left');
1730
1778
  const right = node.childForFieldName('right');
1731
- if (left?.type === 'identifier' && right?.type === 'new_expression') {
1732
- nonCallableNames.add(left.text);
1733
- const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
1734
- if (ctorName) {
1735
- localVarTypes.set(left.text, ctorName);
1779
+ if (left?.type === 'identifier') {
1780
+ if (right?.type === 'new_expression') {
1781
+ nonCallableNames.add(left.text);
1782
+ const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
1783
+ if (ctorName) {
1784
+ localVarTypes.set(left.text, ctorName);
1785
+ } else if (!declaredTypeVars.has(left.text)) {
1786
+ localVarTypes.delete(left.text);
1787
+ }
1788
+ } else if (right && JS_LITERAL_ASSIGN_TYPES[right.type]) {
1789
+ // Literal reassignment re-types the variable (fix #262)
1790
+ if (!declaredTypeVars.has(left.text)) {
1791
+ localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type]);
1792
+ }
1793
+ } else if (localVarTypes.has(left.text) && !declaredTypeVars.has(left.text)) {
1794
+ // Rebinding without a known type makes any previously
1795
+ // INFERRED type stale — nearest-preceding-assignment
1796
+ // semantics (#218d). Annotation-declared types survive:
1797
+ // the TS compiler enforces assignability for those.
1798
+ localVarTypes.delete(left.text);
1736
1799
  }
1737
1800
  }
1738
1801
  // Handler-registration references (fix #252, the #221 family's
@@ -1878,7 +1941,7 @@ function findCallsInCode(code, parser) {
1878
1941
  // parseAsync(args).catch(...) — record the producer so
1879
1942
  // findCallers can type the receiver from its declared
1880
1943
  // return annotation (Promise<...> → Promise).
1881
- let receiverCall, receiverCallIsMethod, receiverCallAwaited;
1944
+ let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
1882
1945
  {
1883
1946
  let recvNode = objNode;
1884
1947
  if (recvNode && recvNode.type === 'parenthesized_expression') {
@@ -1892,11 +1955,17 @@ function findCallsInCode(code, parser) {
1892
1955
  const prodFunc = recvNode.childForFieldName('function');
1893
1956
  if (prodFunc?.type === 'identifier') {
1894
1957
  receiverCall = prodFunc.text;
1958
+ // Producer link (fix #258): plain-call
1959
+ // records carry the call node's start line
1960
+ receiverCallLine = recvNode.startPosition.row + 1;
1895
1961
  } else if (prodFunc?.type === 'member_expression') {
1896
1962
  const prodProp = prodFunc.childForFieldName('property');
1897
1963
  if (prodProp) {
1898
1964
  receiverCall = prodProp.text;
1899
1965
  receiverCallIsMethod = true;
1966
+ // Method records report the property
1967
+ // node's own line
1968
+ receiverCallLine = prodProp.startPosition.row + 1;
1900
1969
  }
1901
1970
  }
1902
1971
  }
@@ -1930,6 +1999,7 @@ function findCallsInCode(code, parser) {
1930
1999
  ...(receiverCall && { receiverCall }),
1931
2000
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1932
2001
  ...(receiverCallAwaited && { receiverCallAwaited: true }),
2002
+ ...(receiverCallLine && { receiverCallLine }),
1933
2003
  ...(assignedTo && { assignedTo }),
1934
2004
  enclosingFunction,
1935
2005
  uncertain,
@@ -2165,6 +2235,11 @@ function findCallsInCode(code, parser) {
2165
2235
  localVarTypes.clear();
2166
2236
  for (const [k, v] of saved) localVarTypes.set(k, v);
2167
2237
  }
2238
+ const savedDeclared = declaredTypeVarsStack.pop();
2239
+ if (savedDeclared) {
2240
+ declaredTypeVars.clear();
2241
+ for (const k of savedDeclared) declaredTypeVars.add(k);
2242
+ }
2168
2243
  }
2169
2244
  }
2170
2245
  });
@@ -2340,6 +2415,7 @@ function findImportsInCode(code, parser) {
2340
2415
  const line = node.startPosition.row + 1;
2341
2416
  let modulePath = null;
2342
2417
  const names = [];
2418
+ const esmRenames = [];
2343
2419
  let importType = 'named';
2344
2420
 
2345
2421
  // Find the module path (string node)
@@ -2385,6 +2461,7 @@ function findImportsInCode(code, parser) {
2385
2461
  if (nameNode && aliasNode && aliasNode.text !== nameNode.text) {
2386
2462
  if (!importAliases) importAliases = [];
2387
2463
  importAliases.push({ original: nameNode.text, local: aliasNode.text });
2464
+ esmRenames.push({ original: nameNode.text, local: aliasNode.text });
2388
2465
  }
2389
2466
  }
2390
2467
  }
@@ -2405,7 +2482,8 @@ function findImportsInCode(code, parser) {
2405
2482
  // Side-effect import: import 'x'
2406
2483
  importType = 'side-effect';
2407
2484
  }
2408
- imports.push({ module: modulePath, names, type: importType, line });
2485
+ imports.push({ module: modulePath, names, type: importType, line,
2486
+ ...(esmRenames.length > 0 && { renames: esmRenames }) });
2409
2487
  }
2410
2488
  return true;
2411
2489
  }
@@ -2451,6 +2529,7 @@ function findImportsInCode(code, parser) {
2451
2529
  const firstArg = argsNode.namedChild(0);
2452
2530
  const line = node.startPosition.row + 1;
2453
2531
  const names = [];
2532
+ const renames = [];
2454
2533
  let modulePath;
2455
2534
  let dynamic = false;
2456
2535
 
@@ -2482,6 +2561,7 @@ function findImportsInCode(code, parser) {
2482
2561
  if (key && val && val.text !== key.text) {
2483
2562
  if (!importAliases) importAliases = [];
2484
2563
  importAliases.push({ original: key.text, local: val.text });
2564
+ renames.push({ original: key.text, local: val.text });
2485
2565
  }
2486
2566
  }
2487
2567
  }
@@ -2490,7 +2570,13 @@ function findImportsInCode(code, parser) {
2490
2570
  }
2491
2571
 
2492
2572
  if (modulePath) {
2493
- imports.push({ module: modulePath, names, type: 'require', line, dynamic });
2573
+ imports.push({ module: modulePath, names, type: 'require', line, dynamic,
2574
+ // Per-import rename pairing (fix #269): the flat
2575
+ // importAliases list loses WHICH module a renamed
2576
+ // name came from — `{ validate: validateSchema }`
2577
+ // must pin to its own require, not any module
2578
+ // exporting the source name.
2579
+ ...(renames.length > 0 && { renames }) });
2494
2580
  }
2495
2581
  }
2496
2582
  }
@@ -138,6 +138,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
138
138
  ...(paramTypes && { paramTypes }),
139
139
  ...(docstring && { docstring }),
140
140
  ...(decorators.length > 0 && { decorators }),
141
+ ...(isOverloadDecorated(decorators) && { isSignature: true }),
141
142
  ...(nameLine !== decoratorStartLine && { nameLine })
142
143
  });
143
144
  }
@@ -285,6 +286,19 @@ function findFunctions(code, parser) {
285
286
  return functions;
286
287
  }
287
288
 
289
+ /**
290
+ * A typing @overload-decorated def is a SIGNATURE of the implementation that
291
+ * follows, not a callable of its own (fix #265 — TS overload parity): the
292
+ * runtime discards the stub bodies, so pin identity must close over the
293
+ * group and resolution must prefer the implementation.
294
+ */
295
+ function isOverloadDecorated(decorators) {
296
+ return decorators.some(d => {
297
+ const head = d.split('(')[0].trim();
298
+ return head === 'overload' || head.endsWith('.overload');
299
+ });
300
+ }
301
+
288
302
  /**
289
303
  * Extract decorators from a function/class node
290
304
  */
@@ -472,6 +486,7 @@ function extractClassMembers(classNode, code) {
472
486
  ...(paramTypes && { paramTypes }),
473
487
  ...(docstring && { docstring }),
474
488
  ...(memberDecorators.length > 0 && { decorators: memberDecorators }),
489
+ ...(isOverloadDecorated(memberDecorators) && { isSignature: true }),
475
490
  ...(nameLine !== startLine && { nameLine })
476
491
  });
477
492
  }
@@ -1073,7 +1088,7 @@ function findCallsInCode(code, parser) {
1073
1088
  // annotation. `(await f()).m()` unwraps to the call and
1074
1089
  // marks awaited (an un-awaited async producer's value is a
1075
1090
  // coroutine, not the annotation's type).
1076
- let receiverCall, receiverCallIsMethod, receiverCallAwaited;
1091
+ let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
1077
1092
 
1078
1093
  // Detect super().method() pattern
1079
1094
  if (objNode?.type === 'call') {
@@ -1093,11 +1108,17 @@ function findCallsInCode(code, parser) {
1093
1108
  const prodFunc = recvNode.childForFieldName('function');
1094
1109
  if (prodFunc?.type === 'identifier') {
1095
1110
  receiverCall = prodFunc.text;
1111
+ // Producer link (fix #258): plain-call records
1112
+ // carry the call node's start line
1113
+ receiverCallLine = recvNode.startPosition.row + 1;
1096
1114
  } else if (prodFunc?.type === 'attribute') {
1097
1115
  const prodAttr = prodFunc.childForFieldName('attribute');
1098
1116
  if (prodAttr) {
1099
1117
  receiverCall = prodAttr.text;
1100
1118
  receiverCallIsMethod = true;
1119
+ // Method records report the attribute
1120
+ // node's own line
1121
+ receiverCallLine = prodAttr.startPosition.row + 1;
1101
1122
  }
1102
1123
  }
1103
1124
  }
@@ -1140,6 +1161,7 @@ function findCallsInCode(code, parser) {
1140
1161
  ...(receiverCall && { receiverCall }),
1141
1162
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1142
1163
  ...(receiverCallAwaited && { receiverCallAwaited: true }),
1164
+ ...(receiverCallLine && { receiverCallLine }),
1143
1165
  ...(assignedTo && { assignedTo }),
1144
1166
  argCount,
1145
1167
  ...(argSpread && { argSpread: true }),
package/languages/rust.js CHANGED
@@ -1311,13 +1311,18 @@ function findCallsInCode(code, parser) {
1311
1311
  // chain to its root: if the chain originates at Router::new() or
1312
1312
  // any Router-typed call, set a synthetic receiver string so the
1313
1313
  // bridge layer can recognize this as a Router method invocation.
1314
+ let receiverIsChainRoot;
1314
1315
  if (!receiver && valueNode?.type === 'call_expression') {
1315
1316
  const rootType = _findRustChainRootType(valueNode);
1316
1317
  if (rootType) {
1317
1318
  // Synthetic marker — ROUTER_CHAIN:<RootTypeName>. The
1318
1319
  // <RootTypeName> portion lets the bridge match
1319
- // /^router/i case-insensitively.
1320
+ // /^router/i case-insensitively. receiverIsChainRoot
1321
+ // tells caller physics this is NOT an identifier in
1322
+ // the code (fix #258) — the chain fold types it from
1323
+ // the producer link instead.
1320
1324
  receiver = rootType;
1325
+ receiverIsChainRoot = true;
1321
1326
  }
1322
1327
  }
1323
1328
  // fix #202: one-hop declared-field receivers — self.dent.path(),
@@ -1354,16 +1359,43 @@ function findCallsInCode(code, parser) {
1354
1359
  // Chained receiver (fix #220): the receiver IS a call —
1355
1360
  // self.as_u8().as_color() — record the producer so
1356
1361
  // findCallers can type it from the declared return.
1357
- // Path producers (Config::load().x()) stay uncaptured
1358
- // until a measured family justifies the path branch.
1359
- let receiverCall, receiverCallIsMethod;
1360
- if (!receiver && !receiverField && valueNode?.type === 'call_expression') {
1361
- const prodFunc = valueNode.childForFieldName('function');
1362
+ // Fix #258 (clap-measured): receiverCallLine links to the
1363
+ // producer's OWN record (per-record line convention:
1364
+ // field identifier for method producers, call-node start
1365
+ // for plain/path producers) so the chain fold can walk
1366
+ // Command::new("x").a(...).b(...) hop by hop; path
1367
+ // producers (Config::load().x()) are captured now, and
1368
+ // rooted chains keep their synthetic receiver marker for
1369
+ // the bridge but get the link too.
1370
+ let receiverCall, receiverCallIsMethod, receiverCallLine;
1371
+ if ((!receiver || receiverIsChainRoot) && !receiverField &&
1372
+ valueNode?.type === 'call_expression') {
1373
+ let prodFunc = valueNode.childForFieldName('function');
1374
+ if (prodFunc?.type === 'generic_function') {
1375
+ prodFunc = prodFunc.childForFieldName('function') || prodFunc;
1376
+ }
1362
1377
  if (prodFunc?.type === 'identifier') {
1363
1378
  receiverCall = prodFunc.text;
1379
+ receiverCallLine = valueNode.startPosition.row + 1;
1364
1380
  } else if (prodFunc?.type === 'field_expression') {
1365
1381
  const pf = prodFunc.childForFieldName('field');
1366
- if (pf) { receiverCall = pf.text; receiverCallIsMethod = true; }
1382
+ if (pf) {
1383
+ receiverCall = pf.text;
1384
+ receiverCallIsMethod = true;
1385
+ receiverCallLine = pf.startPosition.row + 1;
1386
+ }
1387
+ } else if (prodFunc?.type === 'scoped_identifier') {
1388
+ // Path producer: Command::new(...).arg(...) — the
1389
+ // producer record's name is the last path segment
1390
+ // (turbofish segments dropped, matching the path
1391
+ // record's own derivation) at the call node's line.
1392
+ const segs = prodFunc.text.split('::');
1393
+ const prodName = segs[segs.length - 1];
1394
+ if (prodName && !prodName.startsWith('<')) {
1395
+ receiverCall = prodName;
1396
+ receiverCallIsMethod = true;
1397
+ receiverCallLine = valueNode.startPosition.row + 1;
1398
+ }
1367
1399
  }
1368
1400
  }
1369
1401
  // Literal receivers carry their builtin type (fix #220,
@@ -1374,7 +1406,7 @@ function findCallsInCode(code, parser) {
1374
1406
  ? ({ string_literal: 'str', raw_string_literal: 'str',
1375
1407
  char_literal: 'char', boolean_literal: 'bool' })[valueNode.type]
1376
1408
  : undefined;
1377
- const receiverType = (receiver && receiver !== 'self')
1409
+ const receiverType = (receiver && receiver !== 'self' && !receiverIsChainRoot)
1378
1410
  ? getReceiverType(receiver)
1379
1411
  : literalReceiverType;
1380
1412
  const firstArg = getFirstStringArg(node);
@@ -1389,10 +1421,12 @@ function findCallsInCode(code, parser) {
1389
1421
  isMethod: true,
1390
1422
  receiver,
1391
1423
  ...(receiverType && { receiverType }),
1424
+ ...(receiverIsChainRoot && { receiverIsChainRoot: true }),
1392
1425
  ...(receiverField && { receiverRoot, receiverField }),
1393
1426
  ...(receiverField && receiverRootType && { receiverRootType }),
1394
1427
  ...(receiverCall && { receiverCall }),
1395
1428
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1429
+ ...(receiverCallLine && { receiverCallLine }),
1396
1430
  argCount,
1397
1431
  ...(assigned && { assignedTo: assigned.assignedTo }),
1398
1432
  ...(assigned?.unwrapped && { assignedUnwrap: true }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",