ucn 5.0.6 → 5.1.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/core/index-ir.js CHANGED
@@ -16,6 +16,7 @@ function createImportBindings(imports) {
16
16
  return {
17
17
  name,
18
18
  module: item.module,
19
+ ...(item.type && { kind: item.type }),
19
20
  ...(item.line != null && { line: item.line }),
20
21
  ...(rename && { alias: rename.local }),
21
22
  ...(item.defaultLike && { defaultLike: true }),
@@ -69,13 +70,14 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
69
70
  'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
70
71
  'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
71
72
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
72
- 'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
73
+ 'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
73
74
  'registryMember', 'registryContainer', 'namespace',
74
75
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
75
76
  'lexicalScopeStartLine', 'lexicalScopeEndLine',
76
77
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
77
78
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
78
- 'linkage', 'functionLike',
79
+ 'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
80
+ 'aliasOwner', 'aliasMember',
79
81
  ]);
80
82
 
81
83
  function materializeSymbol(fileEntry, item) {
@@ -91,7 +93,9 @@ function materializeSymbol(fileEntry, item) {
91
93
  returnType: item.returnType,
92
94
  modifiers: item.modifiers,
93
95
  docstring: item.docstring,
94
- bindingId: `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
96
+ bindingId: item.id
97
+ ? `${fileEntry.relativePath}:${item.id}`
98
+ : `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
95
99
  ...(item.owner && { className: item.owner }),
96
100
  };
97
101
  for (const field of OPTIONAL_SYMBOL_FIELDS) {
@@ -109,7 +113,13 @@ function materializeSymbol(fileEntry, item) {
109
113
  function addIRSymbol(fileEntry, item, symbolTable = null) {
110
114
  const symbol = materializeSymbol(fileEntry, item);
111
115
  fileEntry.symbols.push(symbol);
112
- if (!item.memberAssigned && !item.bodyScopedName) {
116
+ // A Rust `impl X`/`impl Trait for X` block introduces NO name into any
117
+ // scope (fix #286b, cursive-measured: the impl symbol stole the bare-name
118
+ // binding of ColorPair from the cross-file struct, excluding a compiler-
119
+ // true composite-literal caller as other-definition). The struct/enum
120
+ // claim covers the impl — same discipline as deadcode's CLASS_AUDIT_KINDS.
121
+ if (!item.memberAssigned && !item.bodyScopedName && !item.exportedAlias &&
122
+ item.kind !== 'impl') {
113
123
  fileEntry.bindings.push({
114
124
  id: symbol.bindingId,
115
125
  name: symbol.name,
package/core/ir.js CHANGED
@@ -47,13 +47,14 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
47
47
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
48
48
  'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
49
49
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
50
- 'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
50
+ 'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
51
51
  'registryMember', 'registryContainer', 'isConstructor',
52
52
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
53
53
  'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
54
54
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
55
55
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
56
- 'linkage', 'functionLike',
56
+ 'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
57
+ 'aliasOwner', 'aliasMember',
57
58
  ];
58
59
  for (const field of passthrough) {
59
60
  if (symbol[field] !== undefined && symbol[field] !== null) {
@@ -114,6 +115,51 @@ function createFileIR({
114
115
  for (const symbol of stateObjects) append(symbol, 'state', 'state');
115
116
  for (const symbol of macros) append(symbol,
116
117
  symbol.functionLike ? 'callable' : 'state', 'macro');
118
+ // An immutable module-scope member alias has the callable signature of
119
+ // the class member it captures: `const make = Widget.create`. Materialize
120
+ // the local value and each explicit export alias as real function symbols
121
+ // only when its member is static and every declared return type agrees.
122
+ // This is compiler-visible identity; mutable aliases and ambiguous
123
+ // overload returns were rejected by the parser/agreement gate above.
124
+ for (const alias of (parsed.callableAliases || [])) {
125
+ const sources = normalizedSymbols.filter(symbol =>
126
+ symbol.name === alias.member && symbol.owner === alias.owner &&
127
+ (symbol.params !== undefined || symbol.paramsStructured) &&
128
+ (symbol.modifiers?.includes('static') ||
129
+ String(symbol.memberType || symbol.kind).startsWith('static')) &&
130
+ symbol.returnType);
131
+ if (sources.length === 0) continue;
132
+ const sourceReturns = new Set(sources.map(source => source.returnType));
133
+ if (sourceReturns.size !== 1) continue;
134
+ const source = sources[0];
135
+ const exported = (parsed.exports || []).filter(item =>
136
+ !item.source && item.name === alias.name);
137
+ const exposed = exported.map(item => ({
138
+ name: item.type === 'default' ? 'default' : (item.alias || item.name),
139
+ line: item.line || alias.startLine,
140
+ }));
141
+ const localIsExported = exposed.some(item => item.name === alias.name);
142
+ const makeAlias = (name, startLine, isExported, exportedAlias = false) => ({
143
+ bindingId: `callable-alias:${alias.owner}.${alias.member}:${name}:${startLine}`,
144
+ name,
145
+ startLine,
146
+ endLine: startLine,
147
+ params: source.params,
148
+ ...(source.paramsStructured && { paramsStructured: source.paramsStructured }),
149
+ returnType: source.returnType,
150
+ modifiers: isExported ? ['export'] : [],
151
+ callableAlias: true,
152
+ ...(exportedAlias && { exportedAlias: true }),
153
+ aliasOwner: alias.owner,
154
+ aliasMember: alias.member,
155
+ });
156
+ append(makeAlias(alias.name, alias.startLine, localIsExported),
157
+ 'callable', 'function');
158
+ for (const item of exposed) {
159
+ if (item.name === alias.name) continue;
160
+ append(makeAlias(item.name, item.line, true, true), 'callable', 'function');
161
+ }
162
+ }
117
163
  const imports = [...(parsed.imports || [])];
118
164
  return {
119
165
  schemaVersion: IR_SCHEMA_VERSION,
@@ -74,6 +74,23 @@ function isTestEntry(entry) {
74
74
  return isTestPath(entry.relativePath || entry.file || '');
75
75
  }
76
76
 
77
+ function formatAmbiguityCandidates(lines, ambiguity) {
78
+ if (!ambiguity?.items?.length) return;
79
+ const owners = ambiguity.dispatchOwners
80
+ ? `; ${ambiguity.dispatchOwners} dispatch owners` : '';
81
+ lines.push(` competing definitions (${ambiguity.totalDefinitions}${owners}):`);
82
+ for (const candidate of ambiguity.items) {
83
+ const owner = candidate.owner
84
+ ? ` on ${candidate.owner}${candidate.memberAssignment ? ' (member assignment)' : ''}` : '';
85
+ const selected = candidate.selected ? ' [selected target]' : '';
86
+ lines.push(` - ${candidate.handle} — ${candidate.type}${owner}${selected}`);
87
+ }
88
+ if (ambiguity.truncated) {
89
+ lines.push(` (+${ambiguity.totalDefinitions - ambiguity.items.length} more — ` +
90
+ `use find ${ambiguity.name})`);
91
+ }
92
+ }
93
+
77
94
  /**
78
95
  * Render the conservation contract lines: ACCOUNT and CONTRACT (always),
79
96
  * WARNING (unparsed files containing the symbol), FILTERED (display-filter
@@ -273,6 +290,9 @@ function formatContextJson(context) {
273
290
  ? 'runtime-dispatch' : 'actionable-ambiguity'),
274
291
  ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
275
292
  })),
293
+ ...(context.ambiguityCandidates && {
294
+ ambiguityCandidates: context.ambiguityCandidates,
295
+ }),
276
296
  ...(context.warnings && { warnings: context.warnings })
277
297
  }
278
298
  });
@@ -327,6 +347,9 @@ function formatContextJson(context) {
327
347
  ? 'runtime-dispatch' : 'actionable-ambiguity'),
328
348
  ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
329
349
  })),
350
+ ...(context.ambiguityCandidates && {
351
+ ambiguityCandidates: context.ambiguityCandidates,
352
+ }),
330
353
  callees: callees.map(c => ({
331
354
  name: c.name,
332
355
  type: c.type,
@@ -427,6 +450,7 @@ function formatContext(ctx, options = {}) {
427
450
  const typeUnverified = ctx.unverifiedCallers || [];
428
451
  if (typeUnverified.length > 0) {
429
452
  lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
453
+ formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
430
454
  const cap = 10;
431
455
  let shown = 0;
432
456
  for (const u of typeUnverified) {
@@ -653,6 +677,7 @@ function formatContext(ctx, options = {}) {
653
677
  // Always visible and capped at 10 one-liners unless --all.
654
678
  if (actionableUnverified.length > 0) {
655
679
  lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
680
+ formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
656
681
  const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
657
682
  let shown = 0;
658
683
  for (const u of actionableUnverified) {
@@ -338,7 +338,7 @@ function formatGitLine(git) {
338
338
  * Display label for an unverified-tier entry's reason. Dispatch-tiered
339
339
  * entries (nominal languages) carry attribution metadata: the declared
340
340
  * supertype the call dispatches through (dispatchVia) and how many
341
- * same-name definitions the dispatch could land on (dispatchCandidates).
341
+ * distinct same-name owners the dispatch could land on (dispatchCandidates).
342
342
  */
343
343
  function unverifiedReasonLabel(entry) {
344
344
  if (!entry || !entry.reason) return '';
@@ -356,7 +356,7 @@ function unverifiedReasonLabel(entry) {
356
356
  : `possible-dispatch via ${entry.dispatchVia}`;
357
357
  }
358
358
  if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
359
- return `method-ambiguous — ${entry.dispatchCandidates} same-name definitions`;
359
+ return `method-ambiguous — ${entry.dispatchCandidates} dispatch owners`;
360
360
  }
361
361
  if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
362
362
  return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
@@ -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
@@ -2180,6 +2317,12 @@ function findCallsInCode(code, parser) {
2180
2317
  if (typeName) {
2181
2318
  localVarTypes.set(nameNode.text, typeName);
2182
2319
  declaredTypeVars.add(nameNode.text);
2320
+ const annotationQualifier = tsTypeQualifier(typeId);
2321
+ if (annotationQualifier) {
2322
+ localVarTypeQualifiers.set(nameNode.text, annotationQualifier);
2323
+ } else {
2324
+ localVarTypeQualifiers.delete(nameNode.text);
2325
+ }
2183
2326
  }
2184
2327
  } else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
2185
2328
  // Literal declaration types the variable (fix #262):
@@ -2200,6 +2343,12 @@ function findCallsInCode(code, parser) {
2200
2343
  if (typeName) {
2201
2344
  localVarTypes.set(pat.text, typeName);
2202
2345
  declaredTypeVars.add(pat.text);
2346
+ const annotationQualifier = tsTypeQualifier(inner);
2347
+ if (annotationQualifier) {
2348
+ localVarTypeQualifiers.set(pat.text, annotationQualifier);
2349
+ } else {
2350
+ localVarTypeQualifiers.delete(pat.text);
2351
+ }
2203
2352
  }
2204
2353
  }
2205
2354
  }
@@ -3163,6 +3312,22 @@ function findImportsInCode(code, parser) {
3163
3312
  return imports;
3164
3313
  }
3165
3314
 
3315
+ /**
3316
+ * Return the local symbol created or referenced by a statically-owned
3317
+ * CommonJS property assignment. Dynamic expressions deliberately return
3318
+ * undefined: `exports.x = factory()` and `exports.x = other.y` may be
3319
+ * re-exports or arbitrary values, so they are not exclusion-grade identity.
3320
+ */
3321
+ function cjsAssignedLocalName(valueNode, exposedName) {
3322
+ if (!valueNode) return undefined;
3323
+ if (valueNode.type === 'identifier') return valueNode.text;
3324
+ if (['function_expression', 'generator_function', 'arrow_function', 'class']
3325
+ .includes(valueNode.type)) {
3326
+ return valueNode.childForFieldName('name')?.text || exposedName;
3327
+ }
3328
+ return undefined;
3329
+ }
3330
+
3166
3331
  /**
3167
3332
  * Find all exports in JavaScript/TypeScript code using tree-sitter AST
3168
3333
  * @param {string} code - Source code to analyze
@@ -3355,12 +3520,15 @@ function findExportsInCode(code, parser) {
3355
3520
  } else if (prop.type === 'pair') {
3356
3521
  const key = prop.childForFieldName('key');
3357
3522
  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
- });
3523
+ if (key) {
3524
+ const localName = cjsAssignedLocalName(value, key.text);
3525
+ exports.push({
3526
+ name: key.text,
3527
+ ...(localName && { localName }),
3528
+ type: 'module.exports',
3529
+ line,
3530
+ });
3531
+ }
3364
3532
  } else if (prop.type === 'method_definition') {
3365
3533
  // Shorthand methods are exports too
3366
3534
  // (fix #252 — `module.exports =
@@ -3402,9 +3570,21 @@ function findExportsInCode(code, parser) {
3402
3570
  }
3403
3571
  } else if (rightNode && rightNode.type === 'identifier') {
3404
3572
  // module.exports = something
3405
- exports.push({ name: rightNode.text, localName: rightNode.text, type: 'module.exports', line });
3573
+ exports.push({ name: rightNode.text, localName: rightNode.text,
3574
+ type: 'module.exports', defaultLike: true, line });
3406
3575
  } else {
3407
- exports.push({ name: 'default', type: 'module.exports', line });
3576
+ // A named function/class expression still exports
3577
+ // one callable default. Preserve its LOCAL symbol
3578
+ // identity so `require('./mod')()` resolves back
3579
+ // to the declaration; anonymous expressions use
3580
+ // the parser's synthetic `default` symbol.
3581
+ const localName = rightNode &&
3582
+ ['function_expression', 'generator_function', 'class'].includes(rightNode.type)
3583
+ ? rightNode.childForFieldName('name')?.text
3584
+ : undefined;
3585
+ exports.push({ name: 'default',
3586
+ ...(localName && { localName }),
3587
+ type: 'module.exports', defaultLike: true, line });
3408
3588
  }
3409
3589
  return true;
3410
3590
  }
@@ -3413,9 +3593,10 @@ function findExportsInCode(code, parser) {
3413
3593
  if (objNode.text === 'exports') {
3414
3594
  const line = node.startPosition.row + 1;
3415
3595
  const rightNode = node.childForFieldName('right');
3596
+ const localName = cjsAssignedLocalName(rightNode, propNode.text);
3416
3597
  exports.push({
3417
3598
  name: propNode.text,
3418
- ...(rightNode?.type === 'identifier' && { localName: rightNode.text }),
3599
+ ...(localName && { localName }),
3419
3600
  type: 'exports',
3420
3601
  line,
3421
3602
  });
@@ -3425,7 +3606,11 @@ function findExportsInCode(code, parser) {
3425
3606
  // module.exports.name = ...
3426
3607
  if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
3427
3608
  const line = node.startPosition.row + 1;
3428
- exports.push({ name: propNode.text, type: 'module.exports', line });
3609
+ const rightNode = node.childForFieldName('right');
3610
+ const localName = cjsAssignedLocalName(rightNode, propNode.text);
3611
+ exports.push({ name: propNode.text,
3612
+ ...(localName && { localName }),
3613
+ type: 'module.exports', line });
3429
3614
  return true;
3430
3615
  }
3431
3616
  }