ucn 5.2.1 → 5.3.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.
@@ -463,6 +463,55 @@ function returnsReceiverSelf(node) {
463
463
  return sawSelf;
464
464
  }
465
465
 
466
+ /**
467
+ * Exact call expression returned by an expression-bodied arrow. This is a
468
+ * syntax proof, not return-type inference: query-time flow resolves the call
469
+ * through its ordinary import/receiver ownership rails. Block bodies,
470
+ * conditionals, and other expressions deliberately stay unmarked.
471
+ */
472
+ function returnedArrowCallSpan(node) {
473
+ if (node.type !== 'arrow_function') return null;
474
+ let body = node.childForFieldName('body');
475
+ while (body?.type === 'parenthesized_expression' && body.namedChildCount === 1) {
476
+ body = body.namedChild(0);
477
+ }
478
+ if (body?.type !== 'call_expression') return null;
479
+ return { start: body.startIndex, end: body.endIndex };
480
+ }
481
+
482
+ /**
483
+ * Exact `this.field...` value returned by a one-statement method body. The
484
+ * deliberately narrow shape proves both that the member path is returned and
485
+ * that no fallthrough/alternate return widens the compiler-inferred type.
486
+ */
487
+ function returnedReceiverFieldPath(node) {
488
+ const body = node.childForFieldName('body');
489
+ if (!body || body.type !== 'statement_block' || body.namedChildCount !== 1) {
490
+ return null;
491
+ }
492
+ const statement = body.namedChild(0);
493
+ if (statement?.type !== 'return_statement' || statement.namedChildCount !== 1) {
494
+ return null;
495
+ }
496
+ let value = statement.namedChild(0);
497
+ while (value?.type === 'parenthesized_expression' && value.namedChildCount === 1) {
498
+ value = value.namedChild(0);
499
+ }
500
+ const fields = [];
501
+ while (value?.type === 'member_expression') {
502
+ const property = value.childForFieldName('property');
503
+ const object = value.childForFieldName('object');
504
+ if (!property || !object ||
505
+ !['property_identifier', 'private_property_identifier', 'identifier']
506
+ .includes(property.type)) {
507
+ return null;
508
+ }
509
+ fields.unshift(property.text);
510
+ value = object;
511
+ }
512
+ return value?.type === 'this' && fields.length > 0 ? fields : null;
513
+ }
514
+
466
515
  /**
467
516
  * Process a node for function extraction (single-pass helper)
468
517
  * Returns true if node was matched, false otherwise
@@ -634,6 +683,7 @@ function _processFunction(node, functions, processedRanges, lines) {
634
683
  // declaration text, so we double-check the value node directly.
635
684
  const valueIsAsync = valueNode.text.trimStart().startsWith('async ');
636
685
  const isAsync = valueIsAsync || modifiers.includes('async');
686
+ const returnedCall = returnedArrowCallSpan(valueNode);
637
687
 
638
688
  functions.push({
639
689
  name: nameNode.text,
@@ -648,6 +698,10 @@ function _processFunction(node, functions, processedRanges, lines) {
648
698
  modifiers,
649
699
  ...lexicalOwnerRange(node),
650
700
  ...typeAnno,
701
+ ...(returnedCall && {
702
+ returnedCallStart: returnedCall.start,
703
+ returnedCallEnd: returnedCall.end,
704
+ }),
651
705
  ...(generics && { generics }),
652
706
  ...(docstring && { docstring })
653
707
  });
@@ -1301,6 +1355,8 @@ function extractClassMembers(classNode, codeOrLines) {
1301
1355
  const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
1302
1356
  const returnType = extractReturnType(child) ||
1303
1357
  (returnsReceiverSelf(child) ? 'this' : null);
1358
+ const returnedReceiverPath = !returnType
1359
+ ? returnedReceiverFieldPath(child) : null;
1304
1360
  const docstring = extractJSDocstring(code, startLine);
1305
1361
  const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
1306
1362
  const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
@@ -1329,6 +1385,7 @@ function extractClassMembers(classNode, codeOrLines) {
1329
1385
  // (fix #230) — pickBestDefinition prefers the implementation.
1330
1386
  ...(child.type === 'method_signature' && { isSignature: true }),
1331
1387
  ...typeAnno,
1388
+ ...(returnedReceiverPath && { returnedReceiverPath }),
1332
1389
  ...(docstring && { docstring }),
1333
1390
  ...(decorators.length > 0 && { decorators }),
1334
1391
  ...(decoratorsWithArgs.length > 0 && { decoratorsWithArgs })
@@ -1467,6 +1524,14 @@ function extractClassMembers(classNode, codeOrLines) {
1467
1524
  const fieldTypeNode = child.childForFieldName('type');
1468
1525
  const fieldType = fieldTypeNode
1469
1526
  ? fieldTypeNode.text.replace(/^:\s*/, '').trim() : undefined;
1527
+ // A direct identifier initializer on a static field keeps
1528
+ // the lexical callable identity available to the IR:
1529
+ // `static create = createSchema`. Resolution remains
1530
+ // same-file and overload-disciplined in createFileIR;
1531
+ // expressions, member accesses, and instance fields do
1532
+ // not receive this proof marker.
1533
+ const callableTarget = isStatic && valueNode?.type === 'identifier'
1534
+ ? valueNode.text : undefined;
1470
1535
  members.push({
1471
1536
  name,
1472
1537
  startLine,
@@ -1474,6 +1539,7 @@ function extractClassMembers(classNode, codeOrLines) {
1474
1539
  memberType: name.startsWith('#') ? 'private field' : 'field',
1475
1540
  ...(isStatic && { modifiers: ['static'] }),
1476
1541
  ...(fieldType && { fieldType }),
1542
+ ...(callableTarget && { callableTarget }),
1477
1543
  ...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
1478
1544
  // Not a method - regular field
1479
1545
  });
@@ -1920,13 +1986,120 @@ function findCallsInCode(code, parser) {
1920
1986
  const tree = parseTree(parser, code);
1921
1987
  const calls = [];
1922
1988
  const assignedMembers = new Set();
1989
+ const mutatedObjectRoots = new Set();
1990
+ const moduleCompositions = new Map();
1991
+ const unsafeModuleCompositions = new Set();
1992
+ const namespaceAliases = new Set();
1993
+ const accessRoot = (node) => {
1994
+ let current = node;
1995
+ while (current && (current.type === 'member_expression' ||
1996
+ current.type === 'subscript_expression')) {
1997
+ current = current.childForFieldName('object');
1998
+ }
1999
+ return current?.type === 'identifier' ? current.text : undefined;
2000
+ };
1923
2001
  traverseTreeCached(tree.rootNode, node => {
1924
- if (node.type !== 'assignment_expression' &&
1925
- node.type !== 'augmented_assignment_expression') return true;
1926
- const left = node.childForFieldName('left');
1927
- if (left?.type === 'member_expression') assignedMembers.add(left.text);
2002
+ if (node.type === 'namespace_import') {
2003
+ const identifier = node.namedChild(0);
2004
+ if (identifier?.type === 'identifier') namespaceAliases.add(identifier.text);
2005
+ }
2006
+ if (node.type === 'variable_declarator') {
2007
+ const declaration = node.parent;
2008
+ const nameNode = node.childForFieldName('name');
2009
+ const valueNode = node.childForFieldName('value');
2010
+ if (nameNode?.type === 'identifier' && valueNode?.type === 'object' &&
2011
+ declaration?.type === 'lexical_declaration' &&
2012
+ declaration.child(0)?.text === 'const' && isModuleScope(declaration)) {
2013
+ const layers = [];
2014
+ let spreadCandidates = 0;
2015
+ for (let i = 0; i < valueNode.namedChildCount; i++) {
2016
+ const item = valueNode.namedChild(i);
2017
+ if (item.type === 'spread_element') {
2018
+ const value = item.namedChild(0);
2019
+ if (value?.type === 'identifier') {
2020
+ // Namespace imports may legally appear later in
2021
+ // the module. Resolve candidates after this pass
2022
+ // has seen the complete import surface.
2023
+ layers.push({ kind: 'spread-candidate', receiver: value.text });
2024
+ spreadCandidates++;
2025
+ } else {
2026
+ layers.push({ kind: 'unknown' });
2027
+ }
2028
+ continue;
2029
+ }
2030
+ if (item.type === 'pair') {
2031
+ const key = item.childForFieldName('key');
2032
+ const staticKey = key && ['property_identifier', 'identifier', 'string']
2033
+ .includes(key.type)
2034
+ ? key.text.replace(/^['"]|['"]$/g, '') : null;
2035
+ layers.push(staticKey
2036
+ ? { kind: 'property', name: staticKey }
2037
+ : { kind: 'unknown' });
2038
+ continue;
2039
+ }
2040
+ if (item.type === 'shorthand_property_identifier' ||
2041
+ item.type === 'method_definition') {
2042
+ const name = item.type === 'method_definition'
2043
+ ? item.childForFieldName('name')?.text : item.text;
2044
+ layers.push(name
2045
+ ? { kind: 'property', name }
2046
+ : { kind: 'unknown' });
2047
+ continue;
2048
+ }
2049
+ layers.push({ kind: 'unknown' });
2050
+ }
2051
+ if (spreadCandidates > 0) moduleCompositions.set(nameNode.text, layers);
2052
+ }
2053
+ }
2054
+ if (node.type === 'assignment_expression' ||
2055
+ node.type === 'augmented_assignment_expression') {
2056
+ const left = node.childForFieldName('left');
2057
+ if (left?.type === 'member_expression') assignedMembers.add(left.text);
2058
+ const root = accessRoot(left);
2059
+ if (root) mutatedObjectRoots.add(root);
2060
+ }
2061
+ // A namespace-spread composite is exact only while the ordinary
2062
+ // object remains private and unmodified. Any use of the object value
2063
+ // itself (export, alias, return, argument, spread, etc.) can expose a
2064
+ // mutation; property reads/calls are the sole accepted uses.
2065
+ if (node.type !== 'identifier' || !moduleCompositions.has(node.text)) return true;
2066
+ const parent = node.parent;
2067
+ if (parent?.type === 'variable_declarator' &&
2068
+ parent.childForFieldName('name')?.id === node.id) return true;
2069
+ if ((parent?.type === 'member_expression' ||
2070
+ parent?.type === 'subscript_expression') &&
2071
+ parent.childForFieldName('object')?.id === node.id) {
2072
+ let access = parent;
2073
+ while ((access.parent?.type === 'member_expression' ||
2074
+ access.parent?.type === 'subscript_expression') &&
2075
+ access.parent.childForFieldName('object')?.id === access.id) {
2076
+ access = access.parent;
2077
+ }
2078
+ const container = access.parent;
2079
+ const assigned = (container?.type === 'assignment_expression' ||
2080
+ container?.type === 'augmented_assignment_expression') &&
2081
+ container.childForFieldName('left')?.id === access.id;
2082
+ const updated = container?.type === 'update_expression';
2083
+ const deleted = container?.type === 'unary_expression' &&
2084
+ container.child(0)?.text === 'delete';
2085
+ if (!assigned && !updated && !deleted) return true;
2086
+ }
2087
+ unsafeModuleCompositions.add(node.text);
1928
2088
  return true;
1929
2089
  });
2090
+ for (const [name, layers] of moduleCompositions) {
2091
+ const normalized = layers.map(layer => layer.kind === 'spread-candidate'
2092
+ ? (namespaceAliases.has(layer.receiver)
2093
+ ? { kind: 'spread', receiver: layer.receiver }
2094
+ : { kind: 'unknown' })
2095
+ : layer);
2096
+ if (normalized.some(layer => layer.kind === 'spread')) {
2097
+ moduleCompositions.set(name, normalized);
2098
+ } else {
2099
+ moduleCompositions.delete(name);
2100
+ }
2101
+ }
2102
+ for (const name of mutatedObjectRoots) unsafeModuleCompositions.add(name);
1930
2103
  const functionStack = []; // Stack of { name, startLine, endLine }
1931
2104
  // Local aliases with lexical ownership. A flat aliasName→target map leaks
1932
2105
  // block locals into the rest of a module (`let effect = batchedEffect`
@@ -2162,6 +2335,16 @@ function findCallsInCode(code, parser) {
2162
2335
 
2163
2336
  const _patternDeclaresName = (pattern, name) => {
2164
2337
  if (!pattern) return false;
2338
+ // TypeScript parameter wrappers contain both the runtime binding
2339
+ // pattern and a type annotation. Only the pattern declares names:
2340
+ // `metadata: registries.GlobalMeta` must not make the namespace
2341
+ // identifier `registries` look like a shadowing parameter.
2342
+ if (pattern.type === 'required_parameter' ||
2343
+ pattern.type === 'optional_parameter') {
2344
+ return _patternDeclaresName(
2345
+ pattern.childForFieldName('pattern') ||
2346
+ pattern.childForFieldName('name'), name);
2347
+ }
2165
2348
  if ((pattern.type === 'identifier' ||
2166
2349
  pattern.type === 'shorthand_property_identifier_pattern') &&
2167
2350
  pattern.text === name) return true;
@@ -2188,6 +2371,94 @@ function findCallsInCode(code, parser) {
2188
2371
  }
2189
2372
  return false;
2190
2373
  };
2374
+ // fix #337: a declarator initialized from require()/import() is an
2375
+ // IMPORT binding — the module's own name reaching this scope — not a
2376
+ // local shadow. `function build() { const { Foo } = require('./lib');
2377
+ // new Foo() }` resolves through import ownership exactly like the
2378
+ // top-level require the walk already exempts as the module binding
2379
+ // itself. Unwraps `await import()`, parens, and `require('./x').Foo`.
2380
+ const _isImportBindingInitializer = (value) => {
2381
+ let v = value;
2382
+ for (;;) {
2383
+ if (!v) return false;
2384
+ if (v.type === 'await_expression' || v.type === 'parenthesized_expression') {
2385
+ v = v.namedChild(0);
2386
+ continue;
2387
+ }
2388
+ if (v.type === 'member_expression' || v.type === 'subscript_expression') {
2389
+ v = v.childForFieldName('object');
2390
+ continue;
2391
+ }
2392
+ break;
2393
+ }
2394
+ if (v.type !== 'call_expression') return false;
2395
+ const fn = v.childForFieldName('function');
2396
+ return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
2397
+ };
2398
+ const _declaresLocalShadow = (declNode, name) => {
2399
+ for (let i = 0; i < declNode.namedChildCount; i++) {
2400
+ const d = declNode.namedChild(i);
2401
+ if (d.type !== 'variable_declarator') continue;
2402
+ if (!_patternDeclaresName(d.childForFieldName('name'), name)) continue;
2403
+ if (_isImportBindingInitializer(d.childForFieldName('value'))) continue;
2404
+ return true;
2405
+ }
2406
+ return false;
2407
+ };
2408
+
2409
+ // Bare callback references need to distinguish a module-owned VALUE from
2410
+ // an unbound name. File-level import reachability cannot prove the value's
2411
+ // identity: `const app = express(); use(app)` may live in a file that also
2412
+ // imports the pinned target, but `app` is the factory result rather than a
2413
+ // direct lexical reference to that target. Keep the parser evidence exact
2414
+ // and cheap by collecting only declarations whose lexical owner is the
2415
+ // program/module root (export wrappers included).
2416
+ const moduleValueBindings = new Set();
2417
+ const collectPatternNames = (pattern) => {
2418
+ if (!pattern) return;
2419
+ if (pattern.type === 'identifier' ||
2420
+ pattern.type === 'shorthand_property_identifier_pattern') {
2421
+ moduleValueBindings.add(pattern.text);
2422
+ return;
2423
+ }
2424
+ if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
2425
+ collectPatternNames(pattern.childForFieldName('value'));
2426
+ return;
2427
+ }
2428
+ if (pattern.type === 'assignment_pattern') {
2429
+ collectPatternNames(
2430
+ pattern.childForFieldName('left') || pattern.childForFieldName('pattern'));
2431
+ return;
2432
+ }
2433
+ for (let i = 0; i < pattern.namedChildCount; i++) {
2434
+ collectPatternNames(pattern.namedChild(i));
2435
+ }
2436
+ };
2437
+ const collectModuleDeclaration = (statement) => {
2438
+ let declaration = statement;
2439
+ if (statement.type === 'export_statement') {
2440
+ declaration = null;
2441
+ for (let i = 0; i < statement.namedChildCount; i++) {
2442
+ const child = statement.namedChild(i);
2443
+ if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
2444
+ declaration = child;
2445
+ break;
2446
+ }
2447
+ }
2448
+ }
2449
+ if (!declaration ||
2450
+ (declaration.type !== 'lexical_declaration' &&
2451
+ declaration.type !== 'variable_declaration')) return;
2452
+ for (let i = 0; i < declaration.namedChildCount; i++) {
2453
+ const declarator = declaration.namedChild(i);
2454
+ if (declarator.type === 'variable_declarator') {
2455
+ collectPatternNames(declarator.childForFieldName('name'));
2456
+ }
2457
+ }
2458
+ };
2459
+ for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
2460
+ collectModuleDeclaration(tree.rootNode.namedChild(i));
2461
+ }
2191
2462
 
2192
2463
  // fix #203: is a bare-identifier function REFERENCE shadowed by a
2193
2464
  // let/const/var local, for/catch binding, or inner-arrow param in an
@@ -2210,7 +2481,7 @@ function findCallsInCode(code, parser) {
2210
2481
  stmt.childForFieldName('name')?.text === name) return true;
2211
2482
  if (stmt.startIndex >= refNode.startIndex) continue; // declaration-before-use
2212
2483
  if ((stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') &&
2213
- _declaresName(stmt, name)) return true;
2484
+ _declaresLocalShadow(stmt, name)) return true;
2214
2485
  }
2215
2486
  } else if (p.type === 'for_statement') {
2216
2487
  const init = p.childForFieldName('initializer');
@@ -2241,6 +2512,11 @@ function findCallsInCode(code, parser) {
2241
2512
  return false;
2242
2513
  };
2243
2514
 
2515
+ const bareReferenceBindingFields = (refNode) => ({
2516
+ ...(isShadowedByLocal(refNode, refNode.text) && { localShadow: true }),
2517
+ ...(moduleValueBindings.has(refNode.text) && { moduleLocalBinding: true }),
2518
+ });
2519
+
2244
2520
  const isConditionalReassignment = node => {
2245
2521
  for (let p = node.parent; p && !isFunctionNode(p); p = p.parent) {
2246
2522
  if (p.type === 'if_statement') {
@@ -2429,7 +2705,7 @@ function findCallsInCode(code, parser) {
2429
2705
  isMethod: false,
2430
2706
  isFunctionReference: true,
2431
2707
  isPotentialCallback: true,
2432
- ...(isShadowedByLocal(right, right.text) && { localShadow: true }),
2708
+ ...bareReferenceBindingFields(right),
2433
2709
  enclosingFunction: getCurrentEnclosingFunction(),
2434
2710
  });
2435
2711
  }
@@ -2696,6 +2972,9 @@ function findCallsInCode(code, parser) {
2696
2972
  const receiverIsModule = !!receiverModuleSpecifier ||
2697
2973
  (!!receiver && moduleAliases.has(receiver) &&
2698
2974
  !localVarTypes.has(receiver));
2975
+ const receiverModuleComposition = receiver &&
2976
+ !unsafeModuleCompositions.has(receiver)
2977
+ ? moduleCompositions.get(receiver) : undefined;
2699
2978
  const firstArg = getFirstStringArg(node);
2700
2979
  const argCount = getArgCount(node);
2701
2980
  let assignedTo = jsAssignmentTargetOf(node);
@@ -2730,6 +3009,9 @@ function findCallsInCode(code, parser) {
2730
3009
  }),
2731
3010
  ...(receiverIsModule && { receiverIsModule: true }),
2732
3011
  ...(receiverModuleSpecifier && { receiverModuleSpecifier }),
3012
+ ...(receiverModuleComposition && {
3013
+ receiverModuleComposition,
3014
+ }),
2733
3015
  ...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
2734
3016
  receiverMemberAssigned: true,
2735
3017
  }),
@@ -2787,7 +3069,7 @@ function findCallsInCode(code, parser) {
2787
3069
  line: arg.startPosition.row + 1,
2788
3070
  isMethod: false,
2789
3071
  isFunctionReference: true,
2790
- ...(isShadowedByLocal(arg, arg.text) && { localShadow: true }),
3072
+ ...bareReferenceBindingFields(arg),
2791
3073
  enclosingFunction
2792
3074
  });
2793
3075
  } else if (arg.type === 'member_expression') {
@@ -2832,7 +3114,7 @@ function findCallsInCode(code, parser) {
2832
3114
  isMethod: false,
2833
3115
  isFunctionReference: true,
2834
3116
  isPotentialCallback: true,
2835
- ...(isShadowedByLocal(arg, arg.text) && { localShadow: true }),
3117
+ ...bareReferenceBindingFields(arg),
2836
3118
  enclosingFunction
2837
3119
  });
2838
3120
  }
@@ -2873,7 +3155,7 @@ function findCallsInCode(code, parser) {
2873
3155
  isMethod: false,
2874
3156
  isFunctionReference: true,
2875
3157
  isPotentialCallback: true,
2876
- ...(isShadowedByLocal(val, val.text) && { localShadow: true }),
3158
+ ...bareReferenceBindingFields(val),
2877
3159
  enclosingFunction
2878
3160
  });
2879
3161
  }
@@ -2977,7 +3259,7 @@ function findCallsInCode(code, parser) {
2977
3259
  isMethod: false,
2978
3260
  isFunctionReference: true,
2979
3261
  isPotentialCallback: true,
2980
- ...(isShadowedByLocal(child, child.text) && { localShadow: true }),
3262
+ ...bareReferenceBindingFields(child),
2981
3263
  enclosingFunction
2982
3264
  });
2983
3265
  } else if (child.type === 'member_expression') {
@@ -3190,6 +3472,144 @@ function findImportsInCode(code, parser) {
3190
3472
  const imports = [];
3191
3473
  let importAliases = null; // {original, local}[] — tracks renamed imports
3192
3474
 
3475
+ // fix #338: classify edges that do not execute during module
3476
+ // initialization so dependency-cycle reporting can separate an eager
3477
+ // import-time loop from a deliberate lazy one. `require()`/`import()`
3478
+ // nested in any function body (incl. `() => require('./x')` thunks) runs
3479
+ // only when that function is called; TS `import type` / `export type`
3480
+ // re-exports and all-`type` specifier lists are erased at compile time.
3481
+ const FUNCTION_LIKE = new Set(['function_declaration', 'function_expression', 'arrow_function',
3482
+ 'method_definition', 'generator_function_declaration', 'generator_function', 'function']);
3483
+ const importDeferral = (node) => {
3484
+ for (let p = node.parent; p; p = p.parent) {
3485
+ if (FUNCTION_LIKE.has(p.type)) return 'function-local';
3486
+ }
3487
+ return null;
3488
+ };
3489
+ // Static path folding is positive identity evidence. A method merely
3490
+ // named join/resolve need not be Node's path utility. Keep an ambiguous
3491
+ // or shadowed binding dynamic rather than inventing a module edge.
3492
+ let pathBindings = null;
3493
+ const isPathModuleCall = node => {
3494
+ if (node?.type !== 'call_expression') return false;
3495
+ const fn = node.childForFieldName('function');
3496
+ const args = node.childForFieldName('arguments');
3497
+ const arg = args?.namedChild(0);
3498
+ return fn?.type === 'identifier' && fn.text === 'require' &&
3499
+ args.namedChildCount === 1 && arg?.type === 'string' &&
3500
+ ['path', 'node:path'].includes(arg.text.slice(1, -1));
3501
+ };
3502
+ const collectPathBindings = () => {
3503
+ if (pathBindings) return;
3504
+ pathBindings = new Map();
3505
+ const add = (pattern, value) => {
3506
+ if (!pattern) return;
3507
+ traverseTree(pattern, id => {
3508
+ if (id.type === 'identifier' || id.type === 'shorthand_property_identifier_pattern') {
3509
+ const entries = pathBindings.get(id.text) || [];
3510
+ entries.push({ pattern, value });
3511
+ pathBindings.set(id.text, entries);
3512
+ }
3513
+ return true;
3514
+ });
3515
+ };
3516
+ // File-wide ambiguity is deliberately conservative, including writes
3517
+ // and parameters in unrelated scopes. This rare syntax needs proof,
3518
+ // while ordinary literal require specifiers keep their existing path.
3519
+ traverseTree(tree.rootNode, n => {
3520
+ if (n.type === 'variable_declarator') add(n.childForFieldName('name'), n.childForFieldName('value'));
3521
+ else if (n.type === 'formal_parameters' || n.type === 'import_clause') add(n, null);
3522
+ else if (n.type === 'assignment_expression' || n.type === 'augmented_assignment_expression') add(n.childForFieldName('left'), null);
3523
+ else if (n.type === 'update_expression') add(n.childForFieldName('argument'), null);
3524
+ else if (n.type === 'class_declaration' || n.type === 'class') add(n.childForFieldName('name'), null);
3525
+ else if (n.type === 'catch_clause') add(n.childForFieldName('parameter'), null);
3526
+ else if (FUNCTION_LIKE.has(n.type)) {
3527
+ add(n.childForFieldName('name'), null);
3528
+ add(n.childForFieldName('parameter'), null); // unparenthesized arrow
3529
+ }
3530
+ return true;
3531
+ });
3532
+ };
3533
+ const isPathUtility = fn => {
3534
+ if (pathBindings.has('require')) return false;
3535
+ if (fn?.type !== 'member_expression' ||
3536
+ !['join', 'resolve'].includes(fn.childForFieldName('property')?.text)) return false;
3537
+ const object = fn.childForFieldName('object');
3538
+ if (isPathModuleCall(object)) return true;
3539
+ if (object?.type !== 'identifier') return false;
3540
+ const bindings = pathBindings.get(object.text) || [];
3541
+ return bindings.length === 1 && bindings[0].pattern.type === 'identifier' &&
3542
+ isPathModuleCall(bindings[0].value);
3543
+ };
3544
+ // Static composition of `__dirname`-rooted require paths (fix #337b).
3545
+ // Returns a relative specifier ('./x' / '../x') or null when any piece is
3546
+ // not a string literal.
3547
+ const unquote = (n) => (n.type === 'string' &&
3548
+ !n.namedChildren.some(child => child.type === 'escape_sequence') ? n.text.slice(1, -1) : null);
3549
+ const staticDirnamePath = (arg) => {
3550
+ collectPathBindings();
3551
+ if (pathBindings.has('__dirname') || pathBindings.has('require')) return null;
3552
+ let parts = null;
3553
+ if (arg.type === 'call_expression') {
3554
+ const fn = arg.childForFieldName('function');
3555
+ if (!isPathUtility(fn)) return null;
3556
+ const args = arg.childForFieldName('arguments');
3557
+ if (!args || args.namedChildCount < 2) return null;
3558
+ if (args.namedChild(0).type !== 'identifier' || args.namedChild(0).text !== '__dirname') return null;
3559
+ parts = [];
3560
+ for (let i = 1; i < args.namedChildCount; i++) {
3561
+ const piece = unquote(args.namedChild(i));
3562
+ if (piece == null || piece.startsWith('/')) return null;
3563
+ parts.push(piece);
3564
+ }
3565
+ } else if (arg.type === 'binary_expression') {
3566
+ const operands = [];
3567
+ const flatten = (n) => {
3568
+ if (n.type === 'binary_expression' && n.childForFieldName('operator')?.text === '+') {
3569
+ flatten(n.childForFieldName('left'));
3570
+ flatten(n.childForFieldName('right'));
3571
+ } else operands.push(n);
3572
+ };
3573
+ flatten(arg);
3574
+ if (operands.length < 2 || operands[0].type !== 'identifier' || operands[0].text !== '__dirname') return null;
3575
+ let tail = '';
3576
+ for (let i = 1; i < operands.length; i++) {
3577
+ const piece = unquote(operands[i]);
3578
+ if (piece == null) return null;
3579
+ tail += piece;
3580
+ }
3581
+ if (!tail.startsWith('/')) return null;
3582
+ parts = [tail.slice(1)];
3583
+ } else if (arg.type === 'template_string') {
3584
+ let tail = '';
3585
+ let sawDirname = false;
3586
+ for (let i = 0; i < arg.childCount; i++) {
3587
+ const c = arg.child(i);
3588
+ if (c.type === 'template_substitution') {
3589
+ if (sawDirname || c.namedChildCount !== 1 || c.namedChild(0).text !== '__dirname') return null;
3590
+ sawDirname = true;
3591
+ } else if (c.type === 'string_fragment') {
3592
+ if (!sawDirname) return null;
3593
+ tail += c.text;
3594
+ } else if (c.type !== '`') return null;
3595
+ }
3596
+ if (!sawDirname || !tail.startsWith('/')) return null;
3597
+ parts = [tail.slice(1)];
3598
+ }
3599
+ if (!parts || parts.length === 0) return null;
3600
+ const joined = parts.join('/').replace(/\\/g, '/');
3601
+ if (!joined || joined.includes('${')) return null;
3602
+ const normalized = require('path').posix.normalize(joined);
3603
+ if (normalized.startsWith('/') || normalized === '.') return null;
3604
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
3605
+ };
3606
+ const hasTypeKeyword = (node) => {
3607
+ for (let i = 0; i < node.childCount; i++) {
3608
+ if (node.child(i).type === 'type') return true;
3609
+ }
3610
+ return false;
3611
+ };
3612
+
3193
3613
  traverseTreeCached(tree.rootNode, (node) => {
3194
3614
  // ES6 import statements
3195
3615
  if (node.type === 'import_statement') {
@@ -3198,6 +3618,10 @@ function findImportsInCode(code, parser) {
3198
3618
  const names = [];
3199
3619
  const esmRenames = [];
3200
3620
  let importType = 'named';
3621
+ let typeOnly = hasTypeKeyword(node);
3622
+ let specifierCount = 0;
3623
+ let typeSpecifierCount = 0;
3624
+ let hasValueBinding = false;
3201
3625
 
3202
3626
  // Find the module path (string node)
3203
3627
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3218,7 +3642,8 @@ function findImportsInCode(code, parser) {
3218
3642
  if (c.type === 'string') src = c.text.slice(1, -1);
3219
3643
  }
3220
3644
  if (src) {
3221
- imports.push({ module: src, names: alias ? [alias] : [], type: 'require', line });
3645
+ imports.push({ module: src, names: alias ? [alias] : [], type: 'require', line,
3646
+ ...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
3222
3647
  }
3223
3648
  return true;
3224
3649
  }
@@ -3230,6 +3655,7 @@ function findImportsInCode(code, parser) {
3230
3655
  // Default import: import foo from 'x'
3231
3656
  names.push(clauseChild.text);
3232
3657
  importType = 'default';
3658
+ hasValueBinding = true;
3233
3659
  } else if (clauseChild.type === 'named_imports') {
3234
3660
  // Named imports: import { a, b } from 'x'
3235
3661
  for (let k = 0; k < clauseChild.namedChildCount; k++) {
@@ -3237,6 +3663,8 @@ function findImportsInCode(code, parser) {
3237
3663
  if (specifier.type === 'import_specifier') {
3238
3664
  const nameNode = specifier.namedChild(0);
3239
3665
  const aliasNode = specifier.namedChild(1);
3666
+ specifierCount++;
3667
+ if (hasTypeKeyword(specifier)) typeSpecifierCount++;
3240
3668
  if (nameNode) names.push(nameNode.text);
3241
3669
  // Track renamed imports: import { X as Y }
3242
3670
  if (nameNode && aliasNode && aliasNode.text !== nameNode.text) {
@@ -3253,6 +3681,7 @@ function findImportsInCode(code, parser) {
3253
3681
  clauseChild.namedChild(0);
3254
3682
  if (nsName) names.push(nsName.text);
3255
3683
  importType = 'namespace';
3684
+ hasValueBinding = true;
3256
3685
  }
3257
3686
  }
3258
3687
  }
@@ -3263,8 +3692,13 @@ function findImportsInCode(code, parser) {
3263
3692
  // Side-effect import: import 'x'
3264
3693
  importType = 'side-effect';
3265
3694
  }
3695
+ if (!typeOnly && specifierCount > 0 && typeSpecifierCount === specifierCount &&
3696
+ importType === 'named' && !hasValueBinding) {
3697
+ typeOnly = true;
3698
+ }
3266
3699
  imports.push({ module: modulePath, names, type: importType, line,
3267
- ...(esmRenames.length > 0 && { renames: esmRenames }) });
3700
+ ...(esmRenames.length > 0 && { renames: esmRenames }),
3701
+ ...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
3268
3702
  }
3269
3703
  return true;
3270
3704
  }
@@ -3274,6 +3708,8 @@ function findImportsInCode(code, parser) {
3274
3708
  if (node.type === 'export_statement') {
3275
3709
  let source = null;
3276
3710
  const names = [];
3711
+ let specifierCount = 0;
3712
+ let typeSpecifierCount = 0;
3277
3713
 
3278
3714
  // Find the source module (string node with 'from')
3279
3715
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3285,6 +3721,8 @@ function findImportsInCode(code, parser) {
3285
3721
  for (let j = 0; j < child.namedChildCount; j++) {
3286
3722
  const specifier = child.namedChild(j);
3287
3723
  if (specifier.type === 'export_specifier') {
3724
+ specifierCount++;
3725
+ if (hasTypeKeyword(specifier)) typeSpecifierCount++;
3288
3726
  const nameNode = specifier.namedChild(0);
3289
3727
  if (nameNode) names.push(nameNode.text);
3290
3728
  }
@@ -3296,7 +3734,9 @@ function findImportsInCode(code, parser) {
3296
3734
  const line = node.startPosition.row + 1;
3297
3735
  const isStarReExport = node.text.includes('export *');
3298
3736
  const importType = isStarReExport ? 'namespace' : 'named';
3299
- imports.push({ module: source, names, type: importType, line, isReExport: true });
3737
+ imports.push({ module: source, names, type: importType, line, isReExport: true,
3738
+ ...((hasTypeKeyword(node) || (specifierCount > 0 && specifierCount === typeSpecifierCount)) &&
3739
+ { deferred: true, deferredReason: 'type-only' }) });
3300
3740
  }
3301
3741
  return true;
3302
3742
  }
@@ -3314,8 +3754,17 @@ function findImportsInCode(code, parser) {
3314
3754
  let modulePath;
3315
3755
  let dynamic = false;
3316
3756
 
3757
+ const composedPath = firstArg && firstArg.type !== 'string' ? staticDirnamePath(firstArg) : null;
3317
3758
  if (firstArg && firstArg.type === 'string') {
3318
3759
  modulePath = firstArg.text.slice(1, -1);
3760
+ } else if (composedPath) {
3761
+ // fix #337b: `require(path.join(__dirname, '..', 'x'))`,
3762
+ // `require(__dirname + '/x')`, `require(\`${__dirname}/x\`)`
3763
+ // compose to an exact relative specifier — the CJS
3764
+ // test-suite idiom that used to be an unresolvable
3765
+ // dynamic module (excluding every constructor call it
3766
+ // bound as other-definition-import).
3767
+ modulePath = composedPath;
3319
3768
  } else {
3320
3769
  dynamic = true;
3321
3770
  modulePath = firstArg ? firstArg.text : null;
@@ -3357,7 +3806,9 @@ function findImportsInCode(code, parser) {
3357
3806
  }
3358
3807
 
3359
3808
  if (modulePath) {
3809
+ const deferral = importDeferral(node);
3360
3810
  imports.push({ module: modulePath, names, type: 'require', line, dynamic,
3811
+ ...(deferral && { deferred: true, deferredReason: deferral }),
3361
3812
  ...(defaultLike && { defaultLike: true }),
3362
3813
  // Per-import rename pairing (fix #269): the flat
3363
3814
  // importAliases list loses WHICH module a renamed
@@ -3375,11 +3826,15 @@ function findImportsInCode(code, parser) {
3375
3826
  if (argsNode && argsNode.namedChildCount > 0) {
3376
3827
  const firstArg = argsNode.namedChild(0);
3377
3828
  const line = node.startPosition.row + 1;
3829
+ const deferral = importDeferral(node);
3830
+ const deferredFields = deferral ? { deferred: true, deferredReason: deferral } : {};
3378
3831
  if (firstArg && firstArg.type === 'string') {
3379
3832
  const modulePath = firstArg.text.slice(1, -1);
3380
- imports.push({ module: modulePath, names: [], type: 'dynamic', line, dynamic: false });
3833
+ imports.push({ module: modulePath, names: [], type: 'dynamic', line, dynamic: false,
3834
+ ...deferredFields });
3381
3835
  } else if (firstArg) {
3382
- imports.push({ module: firstArg.text, names: [], type: 'dynamic', line, dynamic: true });
3836
+ imports.push({ module: firstArg.text, names: [], type: 'dynamic', line, dynamic: true,
3837
+ ...deferredFields });
3383
3838
  }
3384
3839
  }
3385
3840
  }