ucn 5.3.4 → 5.3.6

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.
@@ -5,6 +5,9 @@
5
5
  * interfaces, type aliases, enums, and state objects.
6
6
  */
7
7
 
8
+ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
9
+
10
+
8
11
  const {
9
12
  traverseTree,
10
13
  traverseTreeCached,
@@ -1898,6 +1901,23 @@ function tsTypeName(node) {
1898
1901
  }
1899
1902
  }
1900
1903
 
1904
+ function tsArrayElement(node) {
1905
+ if (!node) return null;
1906
+ if (['type_annotation', 'parenthesized_type', 'readonly_type'].includes(node.type)) {
1907
+ return tsArrayElement(node.namedChild(0));
1908
+ }
1909
+ if (node.type === 'union_type') {
1910
+ const present = node.namedChildren.filter(child =>
1911
+ !(child.type === 'literal_type' && ['null', 'undefined'].includes(child.text)));
1912
+ return present.length === 1 ? tsArrayElement(present[0]) : null;
1913
+ }
1914
+ if (node.type !== 'array_type') return null;
1915
+ const item = node.namedChild(0);
1916
+ if (!['type_identifier', 'nested_type_identifier', 'generic_type', 'predefined_type'].includes(item?.type)) return null;
1917
+ const type = tsTypeName(item);
1918
+ return type ? { type, qualifier: tsTypeQualifier(item), node: item } : null;
1919
+ }
1920
+
1901
1921
  /**
1902
1922
  * Variable receiving this call's result: `const x = foo()` / `x = await foo()`
1903
1923
  * → 'x'. Identifier targets only. Compared by node id — tree-sitter wrapper
@@ -1990,6 +2010,7 @@ function findCallsInCode(code, parser) {
1990
2010
  const moduleCompositions = new Map();
1991
2011
  const unsafeModuleCompositions = new Set();
1992
2012
  const namespaceAliases = new Set();
2013
+ const arrayAnnotationNames = new Set();
1993
2014
  const accessRoot = (node) => {
1994
2015
  let current = node;
1995
2016
  while (current && (current.type === 'member_expression' ||
@@ -1999,6 +2020,12 @@ function findCallsInCode(code, parser) {
1999
2020
  return current?.type === 'identifier' ? current.text : undefined;
2000
2021
  };
2001
2022
  traverseTreeCached(tree.rootNode, node => {
2023
+ if (['variable_declarator', 'required_parameter', 'optional_parameter'].includes(node.type)) {
2024
+ const name = node.childForFieldName('name') || node.childForFieldName('pattern');
2025
+ if (name?.type === 'identifier' && tsArrayElement(node.childForFieldName('type'))) {
2026
+ arrayAnnotationNames.add(name.text);
2027
+ }
2028
+ }
2002
2029
  if (node.type === 'namespace_import') {
2003
2030
  const identifier = node.namedChild(0);
2004
2031
  if (identifier?.type === 'identifier') namespaceAliases.add(identifier.text);
@@ -2107,7 +2134,7 @@ function findCallsInCode(code, parser) {
2107
2134
  // false external edges and caller/callee disagreement.
2108
2135
  const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
2109
2136
  const nonCallableNames = new Set(); // Track names assigned non-callable values
2110
- const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
2137
+ const localVarTypes = new ReceiverTypeMap(); // Track local variable types: varName -> typeName (for receiverType inference)
2111
2138
  const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
2112
2139
  // Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
2113
2140
  // params). The compiler enforces assignability for these, so reassignment
@@ -2361,6 +2388,79 @@ function findCallsInCode(code, parser) {
2361
2388
  return false;
2362
2389
  };
2363
2390
 
2391
+ // Resolve the actual lexical declaration for an indexed receiver. Keep
2392
+ // untyped bindings too: a shadow must stop an outer annotation from
2393
+ // leaking into a nested function or block. This lookup is cached per
2394
+ // spelling and used only for bracket receivers, not every ordinary call.
2395
+ const indexedBindings = new Map();
2396
+ const indexedBinding = (name, site) => {
2397
+ if (!indexedBindings.has(name)) {
2398
+ const records = [];
2399
+ const add = (pattern, declaration, scope) => {
2400
+ if (!scope || !_patternDeclaresName(pattern, name)) return;
2401
+ records.push({ declaration, scope, type: pattern?.type === 'identifier'
2402
+ ? declaration.childForFieldName('type') : null });
2403
+ };
2404
+ traverseTree(tree.rootNode, current => {
2405
+ if (current.type === 'variable_declarator') {
2406
+ add(current.childForFieldName('name'), current, aliasScope(current));
2407
+ } else if (isFunctionNode(current)) {
2408
+ const parameters = current.childForFieldName('parameters');
2409
+ for (const parameter of parameters?.namedChildren || []) {
2410
+ const pattern = parameter.childForFieldName('pattern') ||
2411
+ parameter.childForFieldName('name') || parameter;
2412
+ add(pattern, parameter, current);
2413
+ }
2414
+ const lone = current.childForFieldName('parameter');
2415
+ if (lone) add(lone, lone, current);
2416
+ const fnName = current.childForFieldName('name');
2417
+ if (fnName?.text === name) add(fnName, current,
2418
+ current.type === 'function_declaration' ? current.parent : current);
2419
+ } else if (current.type === 'catch_clause') {
2420
+ add(current.childForFieldName('parameter'), current, current);
2421
+ }
2422
+ return true;
2423
+ });
2424
+ indexedBindings.set(name, records);
2425
+ }
2426
+ const matches = indexedBindings.get(name).filter(record =>
2427
+ record.scope.startIndex <= site.startIndex && record.scope.endIndex >= site.endIndex)
2428
+ .sort((a, b) => (a.scope.endIndex - a.scope.startIndex) - (b.scope.endIndex - b.scope.startIndex));
2429
+ const nearest = matches[0];
2430
+ if (!nearest || nearest.declaration.startIndex > site.startIndex ||
2431
+ (matches[1] && matches[1].scope.id === nearest.scope.id)) return null;
2432
+ return nearest;
2433
+ };
2434
+ const indexedArrayReceiver = object => {
2435
+ if (object?.type !== 'subscript_expression') return null;
2436
+ const root = object.childForFieldName('object');
2437
+ const offset = object.childForFieldName('index');
2438
+ if (root?.type !== 'identifier' || !arrayAnnotationNames.has(root.text) || !offset) return null;
2439
+ let numeric = offset.type === 'number';
2440
+ if (offset.type === 'identifier') {
2441
+ const indexBinding = indexedBinding(offset.text, object);
2442
+ const annotation = indexBinding?.type?.namedChild(0);
2443
+ numeric = annotation?.type === 'predefined_type' && annotation.text === 'number';
2444
+ if (!annotation && indexBinding?.declaration.childForFieldName('value')?.type === 'number') {
2445
+ numeric = localVarTypes.get(offset.text) === 'Number';
2446
+ }
2447
+ }
2448
+ if (!numeric) return null;
2449
+ const binding = indexedBinding(root.text, object);
2450
+ const element = tsArrayElement(binding?.type);
2451
+ if (element && !element.qualifier) {
2452
+ for (let scope = binding.declaration.parent; scope; scope = scope.parent) {
2453
+ const parameters = scope.childForFieldName('type_parameters');
2454
+ if (parameters?.namedChildren.some(parameter =>
2455
+ (parameter.childForFieldName('name')?.text || parameter.text) === element.type)) return null;
2456
+ }
2457
+ }
2458
+ return element && { ...element,
2459
+ evidence: { ...typeOrigin('annotation', binding.type), projection: 'array-element',
2460
+ container: root.text, elementType: element.node.text,
2461
+ index: { start: offset.startIndex, end: offset.endIndex, nodeType: offset.type } } };
2462
+ };
2463
+
2364
2464
  // fix #203: does a declaration node declare `name` (including nested destructuring)?
2365
2465
  const _declaresName = (declNode, name) => {
2366
2466
  for (let i = 0; i < declNode.namedChildCount; i++) {
@@ -2549,7 +2649,7 @@ function findCallsInCode(code, parser) {
2549
2649
  endLine: node.endPosition.row + 1
2550
2650
  });
2551
2651
  // Save localVarTypes so inner declarations don't leak to sibling functions
2552
- localVarTypesStack.push(new Map(localVarTypes));
2652
+ localVarTypesStack.push(new ReceiverTypeMap(localVarTypes));
2553
2653
  localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
2554
2654
  declaredTypeVarsStack.push(new Set(declaredTypeVars));
2555
2655
  }
@@ -2603,7 +2703,7 @@ function findCallsInCode(code, parser) {
2603
2703
  // Infer type: const x = new Foo() / new pkg.Foo() → x is Foo
2604
2704
  const ctorName = jsConstructorTypeName(initNode.childForFieldName('constructor'));
2605
2705
  if (ctorName) {
2606
- localVarTypes.set(nameNode.text, ctorName);
2706
+ localVarTypes.set(nameNode.text, ctorName, 'constructor', initNode);
2607
2707
  const qualifier = jsConstructorTypeQualifier(
2608
2708
  initNode.childForFieldName('constructor'));
2609
2709
  if (qualifier) localVarTypeQualifiers.set(nameNode.text, qualifier);
@@ -2619,7 +2719,7 @@ function findCallsInCode(code, parser) {
2619
2719
  ? typeNode.namedChild(0) : typeNode;
2620
2720
  const typeName = tsTypeName(typeId);
2621
2721
  if (typeName) {
2622
- localVarTypes.set(nameNode.text, typeName);
2722
+ localVarTypes.set(nameNode.text, typeName, 'annotation', typeNode);
2623
2723
  declaredTypeVars.add(nameNode.text);
2624
2724
  const annotationQualifier = tsTypeQualifier(typeId);
2625
2725
  if (annotationQualifier) {
@@ -2632,7 +2732,7 @@ function findCallsInCode(code, parser) {
2632
2732
  // Literal declaration types the variable (fix #262):
2633
2733
  // `const lines = []` → Array. Annotation, when present,
2634
2734
  // wins (the branch above).
2635
- localVarTypes.set(nameNode.text, JS_LITERAL_ASSIGN_TYPES[initNode.type]);
2735
+ localVarTypes.set(nameNode.text, JS_LITERAL_ASSIGN_TYPES[initNode.type], 'literal', initNode);
2636
2736
  }
2637
2737
  }
2638
2738
  }
@@ -2645,7 +2745,7 @@ function findCallsInCode(code, parser) {
2645
2745
  const inner = typeNode.type === 'type_annotation' ? typeNode.namedChild(0) : typeNode;
2646
2746
  const typeName = tsTypeName(inner);
2647
2747
  if (typeName) {
2648
- localVarTypes.set(pat.text, typeName);
2748
+ localVarTypes.set(pat.text, typeName, 'annotation', node);
2649
2749
  declaredTypeVars.add(pat.text);
2650
2750
  const annotationQualifier = tsTypeQualifier(inner);
2651
2751
  if (annotationQualifier) {
@@ -2666,7 +2766,7 @@ function findCallsInCode(code, parser) {
2666
2766
  nonCallableNames.add(left.text);
2667
2767
  const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
2668
2768
  if (ctorName && !isConditionalReassignment(node)) {
2669
- localVarTypes.set(left.text, ctorName);
2769
+ localVarTypes.set(left.text, ctorName, 'constructor', right);
2670
2770
  const qualifier = jsConstructorTypeQualifier(
2671
2771
  right.childForFieldName('constructor'));
2672
2772
  if (qualifier) localVarTypeQualifiers.set(left.text, qualifier);
@@ -2678,7 +2778,7 @@ function findCallsInCode(code, parser) {
2678
2778
  } else if (right && JS_LITERAL_ASSIGN_TYPES[right.type]) {
2679
2779
  // Literal reassignment re-types the variable (fix #262)
2680
2780
  if (!declaredTypeVars.has(left.text)) {
2681
- localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type]);
2781
+ localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type], 'literal', right);
2682
2782
  localVarTypeQualifiers.delete(left.text);
2683
2783
  }
2684
2784
  } else if (localVarTypes.has(left.text) && !declaredTypeVars.has(left.text)) {
@@ -2861,7 +2961,11 @@ function findCallsInCode(code, parser) {
2861
2961
  isMethod: true,
2862
2962
  boundCall: true,
2863
2963
  receiver: boundReceiver,
2864
- ...(boundReceiverType && { receiverType: boundReceiverType }),
2964
+ ...(boundReceiverType && { receiverType: boundReceiverType,
2965
+ ...(prototypeOwner ? {
2966
+ receiverTypeSource: 'type-qualified',
2967
+ receiverTypeEvidence: typeOrigin('type-qualified', innerObj),
2968
+ } : localVarTypes.fields(innerObj?.text, boundReceiverType)) }),
2865
2969
  ...(innerObj?.type === 'identifier' &&
2866
2970
  localVarTypeQualifiers.has(innerObj.text) && {
2867
2971
  receiverTypeQualifier: localVarTypeQualifiers.get(innerObj.text),
@@ -2962,10 +3066,11 @@ function findCallsInCode(code, parser) {
2962
3066
  const constructedReceiverQualifier = objNode?.type === 'new_expression'
2963
3067
  ? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
2964
3068
  : undefined;
2965
- const receiverType = receiver
3069
+ const indexedReceiver = indexedArrayReceiver(objNode);
3070
+ const receiverType = indexedReceiver?.type || (receiver
2966
3071
  ? localVarTypes.get(receiver)
2967
3072
  : (constructedReceiverType ||
2968
- (objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
3073
+ (objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
2969
3074
  // Module receiver (ns.helper()) — unless locally shadowed
2970
3075
  // by a typed instance binding
2971
3076
  const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
@@ -3001,10 +3106,17 @@ function findCallsInCode(code, parser) {
3001
3106
  callEnd: node.endIndex,
3002
3107
  isMethod: true,
3003
3108
  receiver,
3004
- ...(receiverType && { receiverType }),
3005
- ...((constructedReceiverQualifier ||
3109
+ ...(receiverType && { receiverType,
3110
+ ...(indexedReceiver ? { receiverTypeSource: 'annotation',
3111
+ receiverTypeEvidence: indexedReceiver.evidence }
3112
+ : receiver ? localVarTypes.fields(receiver, receiverType) : {
3113
+ receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
3114
+ receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
3115
+ }),
3116
+ }),
3117
+ ...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
3006
3118
  (receiver && localVarTypeQualifiers.get(receiver))) && {
3007
- receiverTypeQualifier: constructedReceiverQualifier ||
3119
+ receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
3008
3120
  localVarTypeQualifiers.get(receiver),
3009
3121
  }),
3010
3122
  ...(receiverIsModule && { receiverIsModule: true }),
@@ -3087,7 +3199,7 @@ function findCallsInCode(code, parser) {
3087
3199
  line: arg.startPosition.row + 1,
3088
3200
  isMethod: true,
3089
3201
  receiver: hofRecv,
3090
- ...(hofRecvType && { receiverType: hofRecvType }),
3202
+ ...(hofRecvType && { receiverType: hofRecvType, ...localVarTypes.fields(hofRecv, hofRecvType) }),
3091
3203
  isFunctionReference: true,
3092
3204
  enclosingFunction
3093
3205
  });
@@ -3135,7 +3247,7 @@ function findCallsInCode(code, parser) {
3135
3247
  line: arg.startPosition.row + 1,
3136
3248
  isMethod: true,
3137
3249
  receiver: mvObj.text,
3138
- ...(mvType && { receiverType: mvType }),
3250
+ ...(mvType && { receiverType: mvType, ...localVarTypes.fields(mvObj.text, mvType) }),
3139
3251
  isFunctionReference: true,
3140
3252
  enclosingFunction
3141
3253
  });
@@ -3290,8 +3402,7 @@ function findCallsInCode(code, parser) {
3290
3402
  // Restore localVarTypes to pre-function state
3291
3403
  const saved = localVarTypesStack.pop();
3292
3404
  if (saved) {
3293
- localVarTypes.clear();
3294
- for (const [k, v] of saved) localVarTypes.set(k, v);
3405
+ localVarTypes.restore(saved);
3295
3406
  }
3296
3407
  const savedQualifiers = localVarTypeQualifiersStack.pop();
3297
3408
  if (savedQualifiers) {