ucn 5.0.6 → 5.2.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.
@@ -435,6 +435,34 @@ function extractReturnedConcreteType(node) {
435
435
  ? types[0] : null;
436
436
  }
437
437
 
438
+ /**
439
+ * Whether every explicit value-producing return yields the current receiver.
440
+ * A fallthrough/empty return cannot feed a subsequent chained call, so it does
441
+ * not compete with `this`; any other returned value makes the result unknown.
442
+ */
443
+ function returnsReceiverSelf(node) {
444
+ const body = node.childForFieldName('body');
445
+ if (!body) return false;
446
+ let sawSelf = false;
447
+ const stack = [body];
448
+ while (stack.length > 0) {
449
+ const current = stack.pop();
450
+ if (current !== body && FUNCTION_SCOPE_NODES.has(current.type)) continue;
451
+ if (current.type === 'class_declaration' || current.type === 'class') continue;
452
+ if (current.type === 'return_statement') {
453
+ const value = current.namedChild(0);
454
+ if (!value) continue;
455
+ if (value.type !== 'this') return false;
456
+ sawSelf = true;
457
+ continue;
458
+ }
459
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
460
+ stack.push(current.namedChild(i));
461
+ }
462
+ }
463
+ return sawSelf;
464
+ }
465
+
438
466
  /**
439
467
  * Process a node for function extraction (single-pass helper)
440
468
  * Returns true if node was matched, false otherwise
@@ -618,6 +646,7 @@ function _processFunction(node, functions, processedRanges, lines) {
618
646
  isGenerator: isGen,
619
647
  isAsync,
620
648
  modifiers,
649
+ ...lexicalOwnerRange(node),
621
650
  ...typeAnno,
622
651
  ...(generics && { generics }),
623
652
  ...(docstring && { docstring })
@@ -699,8 +728,14 @@ function _processFunction(node, functions, processedRanges, lines) {
699
728
  if (processedRanges.has(rangeKey)) return false;
700
729
 
701
730
  const leftNode = node.childForFieldName('left');
702
- const isPrototypeAssignment = leftNode && leftNode.type === 'member_expression' &&
703
- leftNode.text.includes('.prototype.');
731
+ const assignedObject = leftNode?.type === 'member_expression'
732
+ ? leftNode.childForFieldName('object') : null;
733
+ const prototypeBase = assignedObject?.type === 'member_expression' &&
734
+ assignedObject.childForFieldName('property')?.text === 'prototype'
735
+ ? assignedObject.childForFieldName('object') : null;
736
+ const prototypeOwner = prototypeBase?.type === 'identifier'
737
+ ? prototypeBase.text : null;
738
+ const isPrototypeAssignment = !!prototypeOwner;
704
739
 
705
740
  // For non-prototype assignments, check if nested
706
741
  if (!isPrototypeAssignment) {
@@ -748,7 +783,8 @@ function _processFunction(node, functions, processedRanges, lines) {
748
783
  processedRanges.add(rangeKey);
749
784
  const paramsNode = rightNode.childForFieldName('parameters');
750
785
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
751
- const returnType = extractReturnType(rightNode);
786
+ const returnType = extractReturnType(rightNode) ||
787
+ (prototypeOwner && returnsReceiverSelf(rightNode) ? 'this' : null);
752
788
  const generics = extractGenerics(rightNode);
753
789
  const docstring = extractJSDocstring(lines, startLine);
754
790
  const isGen = isGenerator(rightNode);
@@ -774,9 +810,14 @@ function _processFunction(node, functions, processedRanges, lines) {
774
810
  // assignments carry their class so typed-receiver
775
811
  // method resolution reaches them.
776
812
  ...(leftNode.type === 'member_expression' && { memberAssigned: true }),
813
+ // One-hop member assignments record the object they
814
+ // patch (fix #286a: `console.log = () => {}` — the
815
+ // builtin-global exclusion must see cross-file that
816
+ // the project rebinds this global's member).
777
817
  ...(leftNode.type === 'member_expression' &&
778
- /^([A-Za-z_$][\w$]*)\.prototype\.[A-Za-z_$][\w$]*$/.test(leftNode.text) &&
779
- { className: leftNode.text.split('.')[0], isMethod: true }),
818
+ leftNode.childForFieldName('object')?.type === 'identifier' &&
819
+ { assignedReceiver: leftNode.childForFieldName('object').text }),
820
+ ...(prototypeOwner && { className: prototypeOwner, isMethod: true }),
780
821
  ...typeAnno,
781
822
  ...(generics && { generics }),
782
823
  ...(docstring && { docstring })
@@ -1258,7 +1299,8 @@ function extractClassMembers(classNode, codeOrLines) {
1258
1299
  }
1259
1300
 
1260
1301
  const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
1261
- const returnType = extractReturnType(child);
1302
+ const returnType = extractReturnType(child) ||
1303
+ (returnsReceiverSelf(child) ? 'this' : null);
1262
1304
  const docstring = extractJSDocstring(code, startLine);
1263
1305
  const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
1264
1306
  const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
@@ -1372,6 +1414,8 @@ function extractClassMembers(classNode, codeOrLines) {
1372
1414
  const name = nameNode.text;
1373
1415
  const valueNode = child.childForFieldName('value');
1374
1416
  const isArrow = valueNode && valueNode.type === 'arrow_function';
1417
+ const isStatic = Array.from({ length: child.childCount }, (_, ci) => child.child(ci))
1418
+ .some(part => part.type === 'static');
1375
1419
 
1376
1420
  // Collect decorators — children of the field node (TS) or preceding siblings (JS)
1377
1421
  const fieldDecorators = [];
@@ -1410,6 +1454,7 @@ function extractClassMembers(classNode, codeOrLines) {
1410
1454
  startLine,
1411
1455
  endLine,
1412
1456
  memberType: name.startsWith('#') ? 'private' : 'field',
1457
+ ...(isStatic && { modifiers: ['static'] }),
1413
1458
  isArrow: true,
1414
1459
  isMethod: true, // Arrow fields are callable like methods
1415
1460
  ...typeAnno,
@@ -1427,6 +1472,7 @@ function extractClassMembers(classNode, codeOrLines) {
1427
1472
  startLine,
1428
1473
  endLine,
1429
1474
  memberType: name.startsWith('#') ? 'private field' : 'field',
1475
+ ...(isStatic && { modifiers: ['static'] }),
1430
1476
  ...(fieldType && { fieldType }),
1431
1477
  ...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
1432
1478
  // Not a method - regular field
@@ -1484,6 +1530,48 @@ function _processState(node, objects, lines) {
1484
1530
  return false;
1485
1531
  }
1486
1532
 
1533
+ /**
1534
+ * Record immutable module-scope aliases of a statically named class member.
1535
+ *
1536
+ * `const make = Widget.create` preserves the member's compiler-visible
1537
+ * callable signature. The normalized IR can therefore expose both the local
1538
+ * callable value and any `export { make as widget }` surface without guessing
1539
+ * from a later call spelling. Mutable/local/object aliases deliberately stay
1540
+ * out: they need data-flow evidence, not a declaration-shape shortcut.
1541
+ */
1542
+ function _processCallableAlias(node, aliases) {
1543
+ if (node.type !== 'lexical_declaration' || !isModuleScope(node)) return false;
1544
+ const declarationKind = node.child(0)?.text;
1545
+ if (declarationKind !== 'const') return false;
1546
+
1547
+ let matched = false;
1548
+ for (let i = 0; i < node.namedChildCount; i++) {
1549
+ const declarator = node.namedChild(i);
1550
+ if (declarator.type !== 'variable_declarator') continue;
1551
+ const nameNode = declarator.childForFieldName('name');
1552
+ let valueNode = declarator.childForFieldName('value');
1553
+ if (nameNode?.type !== 'identifier' || !valueNode) continue;
1554
+ while (valueNode && ['parenthesized_expression', 'as_expression',
1555
+ 'satisfies_expression', 'type_assertion'].includes(valueNode.type)) {
1556
+ valueNode = valueNode.namedChild(0);
1557
+ }
1558
+ if (valueNode?.type !== 'member_expression') continue;
1559
+ const owner = valueNode.childForFieldName('object');
1560
+ const member = valueNode.childForFieldName('property');
1561
+ if (owner?.type !== 'identifier' ||
1562
+ !['identifier', 'property_identifier'].includes(member?.type)) continue;
1563
+ aliases.push({
1564
+ name: nameNode.text,
1565
+ owner: owner.text,
1566
+ member: member.text,
1567
+ startLine: declarator.startPosition.row + 1,
1568
+ endLine: declarator.endPosition.row + 1,
1569
+ });
1570
+ matched = true;
1571
+ }
1572
+ return matched;
1573
+ }
1574
+
1487
1575
  /**
1488
1576
  * Find state objects (CONFIG, constants, etc.)
1489
1577
  */
@@ -1508,13 +1596,14 @@ function findStateObjects(code, parser) {
1508
1596
  function parse(code, parser) {
1509
1597
  const tree = parseTree(parser, code);
1510
1598
  const lines = code.split('\n');
1511
- const functions = [], classes = [], stateObjects = [];
1599
+ const functions = [], classes = [], stateObjects = [], callableAliases = [];
1512
1600
  const processedFn = new Set(), processedCls = new Set();
1513
1601
 
1514
1602
  traverseTreeCached(tree.rootNode, (node) => {
1515
1603
  _processFunction(node, functions, processedFn, lines);
1516
1604
  _processClass(node, classes, processedCls, lines);
1517
1605
  _processState(node, stateObjects, lines);
1606
+ _processCallableAlias(node, callableAliases);
1518
1607
  return true; // always continue, never skip subtrees
1519
1608
  });
1520
1609
 
@@ -1554,6 +1643,7 @@ function parse(code, parser) {
1554
1643
  declarationTokens.sort((a, b) => a.startIndex - b.startIndex);
1555
1644
 
1556
1645
  const recoveredFunctions = [], recoveredClasses = [], recoveredState = [];
1646
+ const recoveredCallableAliases = [];
1557
1647
  for (let i = 0; i < declarationTokens.length; i++) {
1558
1648
  const token = declarationTokens[i];
1559
1649
  const next = declarationTokens[i + 1];
@@ -1562,12 +1652,13 @@ function parse(code, parser) {
1562
1652
  if (!fragment.trim()) continue;
1563
1653
  const fragmentTree = parseTree(parser, fragment);
1564
1654
  const fragmentLines = fragment.split('\n');
1565
- const ff = [], fc = [], fs = [];
1655
+ const ff = [], fc = [], fs = [], fa = [];
1566
1656
  const pf = new Set(), pc = new Set();
1567
1657
  traverseTreeCached(fragmentTree.rootNode, (node) => {
1568
1658
  _processFunction(node, ff, pf, fragmentLines);
1569
1659
  _processClass(node, fc, pc, fragmentLines);
1570
1660
  _processState(node, fs, fragmentLines);
1661
+ _processCallableAlias(node, fa);
1571
1662
  return true;
1572
1663
  });
1573
1664
  const lineOffset = token.startPosition.row;
@@ -1585,6 +1676,7 @@ function parse(code, parser) {
1585
1676
  for (const item of ff) { shiftLines(item); recoveredFunctions.push(item); }
1586
1677
  for (const item of fc) { shiftLines(item); recoveredClasses.push(item); }
1587
1678
  for (const item of fs) { shiftLines(item); recoveredState.push(item); }
1679
+ for (const item of fa) { shiftLines(item); recoveredCallableAliases.push(item); }
1588
1680
  }
1589
1681
 
1590
1682
  const mergeUnique = (target, additions, kind) => {
@@ -1597,11 +1689,21 @@ function parse(code, parser) {
1597
1689
  mergeUnique(functions, recoveredFunctions, 'function');
1598
1690
  mergeUnique(classes, recoveredClasses, 'class');
1599
1691
  mergeUnique(stateObjects, recoveredState, 'state');
1692
+ const aliasKeys = new Set(callableAliases.map(alias =>
1693
+ `${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`));
1694
+ for (const alias of recoveredCallableAliases) {
1695
+ const key = `${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`;
1696
+ if (!aliasKeys.has(key)) {
1697
+ aliasKeys.add(key);
1698
+ callableAliases.push(alias);
1699
+ }
1700
+ }
1600
1701
  }
1601
1702
 
1602
1703
  functions.sort((a, b) => a.startLine - b.startLine);
1603
1704
  classes.sort((a, b) => a.startLine - b.startLine);
1604
1705
  stateObjects.sort((a, b) => a.startLine - b.startLine);
1706
+ callableAliases.sort((a, b) => a.startLine - b.startLine);
1605
1707
 
1606
1708
  return {
1607
1709
  language: 'javascript',
@@ -1609,6 +1711,7 @@ function parse(code, parser) {
1609
1711
  functions,
1610
1712
  classes,
1611
1713
  stateObjects,
1714
+ callableAliases,
1612
1715
  ...(tree.rootNode.hasError && { parseRecovery: true }),
1613
1716
  imports: [], // Handled by core/imports.js
1614
1717
  exports: [] // Handled by core/imports.js
@@ -1653,6 +1756,40 @@ const JS_LITERAL_ASSIGN_TYPES = {
1653
1756
  // Predefined TS types that pin a receiver; any/unknown/object say nothing.
1654
1757
  const TS_PREDEFINED_RECEIVER_TYPES = new Set(['string', 'number', 'boolean', 'bigint', 'symbol']);
1655
1758
 
1759
+ /**
1760
+ * Companion to tsTypeName: the namespace qualifier that owns the annotated
1761
+ * name (fix #286e — `app: ns.Flask` must carry 'ns' so two same-name classes
1762
+ * resolve by declaration origin, not directory proximity).
1763
+ */
1764
+ function tsTypeQualifier(node) {
1765
+ if (!node) return undefined;
1766
+ switch (node.type) {
1767
+ case 'nested_type_identifier': {
1768
+ const last = node.namedChild(node.namedChildCount - 1);
1769
+ const start = node.startIndex;
1770
+ return last && last.startIndex > start
1771
+ ? node.text.slice(0, last.startIndex - start).replace(/\.$/, '') || undefined
1772
+ : undefined;
1773
+ }
1774
+ case 'generic_type':
1775
+ return tsTypeQualifier(node.namedChild(0));
1776
+ case 'union_type': {
1777
+ for (let i = 0; i < node.namedChildCount; i++) {
1778
+ const c = node.namedChild(i);
1779
+ if (c.type === 'nested_type_identifier' || c.type === 'generic_type') {
1780
+ const q = tsTypeQualifier(c);
1781
+ if (q) return q;
1782
+ }
1783
+ }
1784
+ return undefined;
1785
+ }
1786
+ case 'parenthesized_type':
1787
+ return tsTypeQualifier(node.namedChild(0));
1788
+ default:
1789
+ return undefined;
1790
+ }
1791
+ }
1792
+
1656
1793
  /**
1657
1794
  * Extract a single concrete type name from a TS type node. Conservative by
1658
1795
  * design: a wrong type would exclude true callers downstream
@@ -1717,6 +1854,34 @@ function jsAssignmentTargetOf(callNode) {
1717
1854
  return undefined;
1718
1855
  }
1719
1856
 
1857
+ /**
1858
+ * For-of iteration target of a call used as the iterable:
1859
+ * `for (const x of getItems())`. The loop variable holds an ELEMENT of the
1860
+ * producer's result, never the return value — provenance only (fix #294).
1861
+ */
1862
+ function jsIterTargetOf(callNode) {
1863
+ let n = callNode;
1864
+ let p = n.parent;
1865
+ if (p && p.type === 'await_expression') { n = p; p = n.parent; }
1866
+ if (!p || p.type !== 'for_in_statement') return undefined;
1867
+ if (p.childForFieldName('operator')?.text !== 'of') return undefined;
1868
+ const right = p.childForFieldName('right');
1869
+ if (!right || right.id !== n.id) return undefined;
1870
+ const left = p.childForFieldName('left');
1871
+ if (!left) return undefined;
1872
+ const names = [];
1873
+ if (left.type === 'identifier') {
1874
+ names.push(left.text);
1875
+ } else if (left.type === 'array_pattern') {
1876
+ for (let i = 0; i < left.namedChildCount; i++) {
1877
+ const c = left.namedChild(i);
1878
+ if (c.type === 'identifier') names.push(c.text);
1879
+ }
1880
+ }
1881
+ if (names.length === 0) return undefined;
1882
+ return { first: names[0], rest: names.slice(1) };
1883
+ }
1884
+
1720
1885
  /**
1721
1886
  * Type name from a new-expression constructor node: new Foo() or new pkg.Foo().
1722
1887
  */
@@ -2180,6 +2345,12 @@ function findCallsInCode(code, parser) {
2180
2345
  if (typeName) {
2181
2346
  localVarTypes.set(nameNode.text, typeName);
2182
2347
  declaredTypeVars.add(nameNode.text);
2348
+ const annotationQualifier = tsTypeQualifier(typeId);
2349
+ if (annotationQualifier) {
2350
+ localVarTypeQualifiers.set(nameNode.text, annotationQualifier);
2351
+ } else {
2352
+ localVarTypeQualifiers.delete(nameNode.text);
2353
+ }
2183
2354
  }
2184
2355
  } else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
2185
2356
  // Literal declaration types the variable (fix #262):
@@ -2200,6 +2371,12 @@ function findCallsInCode(code, parser) {
2200
2371
  if (typeName) {
2201
2372
  localVarTypes.set(pat.text, typeName);
2202
2373
  declaredTypeVars.add(pat.text);
2374
+ const annotationQualifier = tsTypeQualifier(inner);
2375
+ if (annotationQualifier) {
2376
+ localVarTypeQualifiers.set(pat.text, annotationQualifier);
2377
+ } else {
2378
+ localVarTypeQualifiers.delete(pat.text);
2379
+ }
2203
2380
  }
2204
2381
  }
2205
2382
  }
@@ -2314,7 +2491,18 @@ function findCallsInCode(code, parser) {
2314
2491
  const resolvedName = typeof alias === 'string' ? alias : undefined;
2315
2492
  const resolvedNames = Array.isArray(alias) ? alias : undefined;
2316
2493
  const firstArg = getFirstStringArg(node);
2317
- const assignedTo = jsAssignmentTargetOf(node);
2494
+ let assignedTo = jsAssignmentTargetOf(node);
2495
+ let assignedIterFields = {};
2496
+ if (!assignedTo) {
2497
+ const iterTarget = jsIterTargetOf(node);
2498
+ if (iterTarget) {
2499
+ assignedTo = iterTarget.first;
2500
+ assignedIterFields = {
2501
+ assignedIter: true,
2502
+ ...(iterTarget.rest.length > 0 && { assignedTupleRest: iterTarget.rest }),
2503
+ };
2504
+ }
2505
+ }
2318
2506
  // MEDIUM-5: capture explicit method for fetch(url, { method }).
2319
2507
  const optionsMethod = funcNode.text === 'fetch'
2320
2508
  ? getOptionsMethod(node, 1)
@@ -2328,6 +2516,7 @@ function findCallsInCode(code, parser) {
2328
2516
  callEnd: node.endIndex,
2329
2517
  isMethod: false,
2330
2518
  ...(assignedTo && { assignedTo }),
2519
+ ...assignedIterFields,
2331
2520
  enclosingFunction,
2332
2521
  uncertain,
2333
2522
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
@@ -2509,7 +2698,19 @@ function findCallsInCode(code, parser) {
2509
2698
  !localVarTypes.has(receiver));
2510
2699
  const firstArg = getFirstStringArg(node);
2511
2700
  const argCount = getArgCount(node);
2512
- const assignedTo = jsAssignmentTargetOf(node);
2701
+ let assignedTo = jsAssignmentTargetOf(node);
2702
+ let assignedIterFields = {};
2703
+ if (!assignedTo) {
2704
+ const iterTarget = jsIterTargetOf(node);
2705
+ if (iterTarget) {
2706
+ assignedTo = iterTarget.first;
2707
+ assignedIterFields = {
2708
+ assignedIter: true,
2709
+ ...(iterTarget.rest.length > 0 &&
2710
+ { assignedTupleRest: iterTarget.rest }),
2711
+ };
2712
+ }
2713
+ }
2513
2714
  calls.push({
2514
2715
  name: propName,
2515
2716
  // Multi-line chains (builder.x()\n.y()) must report
@@ -2545,6 +2746,7 @@ function findCallsInCode(code, parser) {
2545
2746
  ...(receiverCallStart != null && { receiverCallStart }),
2546
2747
  ...(receiverCallEnd != null && { receiverCallEnd }),
2547
2748
  ...(assignedTo && { assignedTo }),
2749
+ ...assignedIterFields,
2548
2750
  enclosingFunction,
2549
2751
  uncertain,
2550
2752
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
@@ -2593,11 +2795,17 @@ function findCallsInCode(code, parser) {
2593
2795
  const propNode = arg.childForFieldName('property');
2594
2796
  const objNode = arg.childForFieldName('object');
2595
2797
  if (propNode && !SKIP_IDENTS.has(propNode.text)) {
2798
+ const hofRecv = objNode?.type === 'identifier' ? objNode.text : undefined;
2799
+ // Same typing evidence as the general-arg
2800
+ // member-value shape (fix #295) — tier must
2801
+ // not depend on argument position (#234).
2802
+ const hofRecvType = hofRecv ? localVarTypes.get(hofRecv) : undefined;
2596
2803
  calls.push({
2597
2804
  name: propNode.text,
2598
2805
  line: arg.startPosition.row + 1,
2599
2806
  isMethod: true,
2600
- receiver: objNode?.type === 'identifier' ? objNode.text : undefined,
2807
+ receiver: hofRecv,
2808
+ ...(hofRecvType && { receiverType: hofRecvType }),
2601
2809
  isFunctionReference: true,
2602
2810
  enclosingFunction
2603
2811
  });
@@ -2628,6 +2836,29 @@ function findCallsInCode(code, parser) {
2628
2836
  enclosingFunction
2629
2837
  });
2630
2838
  }
2839
+ // Method-value references (fix #295): `expectRaises(t.check, null)`
2840
+ // passes the method value t.check — one-hop identifier
2841
+ // receivers, typed from the same localVarTypes evidence
2842
+ // method calls use. No isPotentialCallback: the record
2843
+ // rides the MAIN path's full receiver physics (the HOF
2844
+ // member-value convention above).
2845
+ if (arg.type === 'member_expression') {
2846
+ const mvProp = arg.childForFieldName('property');
2847
+ const mvObj = arg.childForFieldName('object');
2848
+ if (mvProp && mvObj?.type === 'identifier' &&
2849
+ !SKIP_IDENTS.has(mvProp.text) && !SKIP_IDENTS.has(mvObj.text)) {
2850
+ const mvType = localVarTypes.get(mvObj.text);
2851
+ calls.push({
2852
+ name: mvProp.text,
2853
+ line: arg.startPosition.row + 1,
2854
+ isMethod: true,
2855
+ receiver: mvObj.text,
2856
+ ...(mvType && { receiverType: mvType }),
2857
+ isFunctionReference: true,
2858
+ enclosingFunction
2859
+ });
2860
+ }
2861
+ }
2631
2862
  // Scan object literal args for function refs in property values
2632
2863
  // e.g., doRequest({onSuccess: handleSuccess, onError: handleError})
2633
2864
  if (arg.type === 'object') {
@@ -3163,6 +3394,22 @@ function findImportsInCode(code, parser) {
3163
3394
  return imports;
3164
3395
  }
3165
3396
 
3397
+ /**
3398
+ * Return the local symbol created or referenced by a statically-owned
3399
+ * CommonJS property assignment. Dynamic expressions deliberately return
3400
+ * undefined: `exports.x = factory()` and `exports.x = other.y` may be
3401
+ * re-exports or arbitrary values, so they are not exclusion-grade identity.
3402
+ */
3403
+ function cjsAssignedLocalName(valueNode, exposedName) {
3404
+ if (!valueNode) return undefined;
3405
+ if (valueNode.type === 'identifier') return valueNode.text;
3406
+ if (['function_expression', 'generator_function', 'arrow_function', 'class']
3407
+ .includes(valueNode.type)) {
3408
+ return valueNode.childForFieldName('name')?.text || exposedName;
3409
+ }
3410
+ return undefined;
3411
+ }
3412
+
3166
3413
  /**
3167
3414
  * Find all exports in JavaScript/TypeScript code using tree-sitter AST
3168
3415
  * @param {string} code - Source code to analyze
@@ -3355,12 +3602,15 @@ function findExportsInCode(code, parser) {
3355
3602
  } else if (prop.type === 'pair') {
3356
3603
  const key = prop.childForFieldName('key');
3357
3604
  const value = prop.childForFieldName('value');
3358
- if (key) exports.push({
3359
- name: key.text,
3360
- ...(value?.type === 'identifier' && { localName: value.text }),
3361
- type: 'module.exports',
3362
- line,
3363
- });
3605
+ if (key) {
3606
+ const localName = cjsAssignedLocalName(value, key.text);
3607
+ exports.push({
3608
+ name: key.text,
3609
+ ...(localName && { localName }),
3610
+ type: 'module.exports',
3611
+ line,
3612
+ });
3613
+ }
3364
3614
  } else if (prop.type === 'method_definition') {
3365
3615
  // Shorthand methods are exports too
3366
3616
  // (fix #252 — `module.exports =
@@ -3402,9 +3652,21 @@ function findExportsInCode(code, parser) {
3402
3652
  }
3403
3653
  } else if (rightNode && rightNode.type === 'identifier') {
3404
3654
  // module.exports = something
3405
- exports.push({ name: rightNode.text, localName: rightNode.text, type: 'module.exports', line });
3655
+ exports.push({ name: rightNode.text, localName: rightNode.text,
3656
+ type: 'module.exports', defaultLike: true, line });
3406
3657
  } else {
3407
- exports.push({ name: 'default', type: 'module.exports', line });
3658
+ // A named function/class expression still exports
3659
+ // one callable default. Preserve its LOCAL symbol
3660
+ // identity so `require('./mod')()` resolves back
3661
+ // to the declaration; anonymous expressions use
3662
+ // the parser's synthetic `default` symbol.
3663
+ const localName = rightNode &&
3664
+ ['function_expression', 'generator_function', 'class'].includes(rightNode.type)
3665
+ ? rightNode.childForFieldName('name')?.text
3666
+ : undefined;
3667
+ exports.push({ name: 'default',
3668
+ ...(localName && { localName }),
3669
+ type: 'module.exports', defaultLike: true, line });
3408
3670
  }
3409
3671
  return true;
3410
3672
  }
@@ -3413,9 +3675,10 @@ function findExportsInCode(code, parser) {
3413
3675
  if (objNode.text === 'exports') {
3414
3676
  const line = node.startPosition.row + 1;
3415
3677
  const rightNode = node.childForFieldName('right');
3678
+ const localName = cjsAssignedLocalName(rightNode, propNode.text);
3416
3679
  exports.push({
3417
3680
  name: propNode.text,
3418
- ...(rightNode?.type === 'identifier' && { localName: rightNode.text }),
3681
+ ...(localName && { localName }),
3419
3682
  type: 'exports',
3420
3683
  line,
3421
3684
  });
@@ -3425,7 +3688,11 @@ function findExportsInCode(code, parser) {
3425
3688
  // module.exports.name = ...
3426
3689
  if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
3427
3690
  const line = node.startPosition.row + 1;
3428
- exports.push({ name: propNode.text, type: 'module.exports', line });
3691
+ const rightNode = node.childForFieldName('right');
3692
+ const localName = cjsAssignedLocalName(rightNode, propNode.text);
3693
+ exports.push({ name: propNode.text,
3694
+ ...(localName && { localName }),
3695
+ type: 'module.exports', line });
3429
3696
  return true;
3430
3697
  }
3431
3698
  }