ucn 4.2.3 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -38,6 +38,68 @@ function extractReturnType(node) {
38
38
  return null;
39
39
  }
40
40
 
41
+ const PY_FUNCTION_SCOPE_NODES = new Set([
42
+ 'function_definition', 'async_function_definition', 'lambda',
43
+ ]);
44
+
45
+ /**
46
+ * Concrete constructor calls returned by one Python function.
47
+ *
48
+ * This is deliberately data-only parser evidence. Query-time analysis still
49
+ * resolves each qualifier against the producer file before deciding whether
50
+ * the runtime class is project-owned or external. Recording the full
51
+ * qualifier is what makes aliases such as
52
+ * `Event = Union[asyncio.Event, trio.Event]` usable without treating the
53
+ * annotation's unresolved terminal name as type identity.
54
+ */
55
+ function extractReturnedConstructors(node) {
56
+ const body = node.childForFieldName('body');
57
+ if (!body) return null;
58
+ const constructors = [];
59
+ let incomplete = false;
60
+ const unwrapValue = value => {
61
+ let current = value;
62
+ while (current && ['parenthesized_expression', 'await'].includes(current.type) &&
63
+ current.namedChildCount === 1) {
64
+ current = current.namedChild(0);
65
+ }
66
+ return current;
67
+ };
68
+ const stack = [body];
69
+ while (stack.length > 0) {
70
+ const current = stack.pop();
71
+ if (current !== body && PY_FUNCTION_SCOPE_NODES.has(current.type)) continue;
72
+ if (current.type === 'class_definition') continue;
73
+ if (current.type === 'return_statement') {
74
+ const value = unwrapValue(current.namedChild(0));
75
+ if (value?.type !== 'call') {
76
+ incomplete = true;
77
+ continue;
78
+ }
79
+ const callable = value.childForFieldName('function');
80
+ let type, qualifier;
81
+ if (callable?.type === 'identifier') {
82
+ type = callable.text;
83
+ } else if (callable?.type === 'attribute') {
84
+ type = callable.childForFieldName('attribute')?.text;
85
+ qualifier = callable.childForFieldName('object')?.text;
86
+ }
87
+ if (!type || !/^[A-Z]/.test(type) || (qualifier && !/^[\w.]+$/.test(qualifier))) {
88
+ incomplete = true;
89
+ continue;
90
+ }
91
+ constructors.push({ type, ...(qualifier && { qualifier }) });
92
+ continue;
93
+ }
94
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
95
+ stack.push(current.namedChild(i));
96
+ }
97
+ }
98
+ if (incomplete || constructors.length === 0 ||
99
+ new Set(constructors.map(item => item.type)).size !== 1) return null;
100
+ return constructors;
101
+ }
102
+
41
103
  /**
42
104
  * Find the actual def line (not decorator) for docstring extraction
43
105
  */
@@ -110,6 +172,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
110
172
  const endLine = node.endPosition.row + 1;
111
173
  const indent = getIndent(node, code);
112
174
  const returnType = extractReturnType(node);
175
+ const returnedConstructors = extractReturnedConstructors(node);
113
176
  const defLine = getDefLine(node);
114
177
  const docstring = extractPythonDocstring(lines, defLine);
115
178
 
@@ -135,6 +198,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
135
198
  isAsync,
136
199
  modifiers: isAsync ? ['async'] : [],
137
200
  ...(returnType && { returnType }),
201
+ ...(returnedConstructors && { returnedConstructors }),
138
202
  ...(paramTypes && { paramTypes }),
139
203
  ...(docstring && { docstring }),
140
204
  ...(decorators.length > 0 && { decorators }),
@@ -317,30 +381,64 @@ function extractDecorators(node) {
317
381
 
318
382
 
319
383
  /**
320
- * Type aliases (fix #251 — rule 7: TS/Rust/Go aliases are indexed, Python's
321
- * were invisible to typedef/find): PEP 695 `type X = int` and annotated
322
- * `X: TypeAlias = ...` assignments become 'type' symbols with aliasOf.
384
+ * Return the runtime/type-identity head of a Python alias expression.
385
+ * `typing.Dict[str, int]` and `dict[str, int]` both alias `dict`; a project
386
+ * generic such as `Page[T]` aliases `Page`. Unions deliberately have no
387
+ * single identity and therefore return null.
388
+ */
389
+ const PY_ALIAS_RUNTIME_TYPES = new Map([
390
+ ['Dict', 'dict'], ['List', 'list'], ['Set', 'set'], ['Tuple', 'tuple'],
391
+ ['FrozenSet', 'frozenset'], ['Text', 'str'],
392
+ ]);
393
+
394
+ function pythonAliasBase(node) {
395
+ let current = unwrapTypeNode(node);
396
+ while (current?.type === 'parenthesized_expression' &&
397
+ current.namedChildCount === 1) {
398
+ current = unwrapTypeNode(current.namedChild(0));
399
+ }
400
+ if (!current) return null;
401
+ if (current.type === 'binary_operator') return null;
402
+ if (current.type === 'subscript' || current.type === 'generic_type') {
403
+ const parts = genericTypeParts(current);
404
+ if (!parts?.base) return null;
405
+ return PY_ALIAS_RUNTIME_TYPES.get(parts.base) || parts.base;
406
+ }
407
+ const base = typeNameFromExpr(current);
408
+ return base ? (PY_ALIAS_RUNTIME_TYPES.get(base) || base) : null;
409
+ }
410
+
411
+ /**
412
+ * Python type aliases: PEP 695 `type X = int`, annotated
413
+ * `X: TypeAlias = ...`, and module-scope static aliases such as
414
+ * `Scope = typing.Dict[str, Any]` become `type` symbols. The last form is
415
+ * accepted only for conventional type-style names and generic type
416
+ * expressions, avoiding ordinary runtime assignments.
323
417
  */
324
418
  function _processTypeAlias(node, classes, processedRanges, lines) {
325
419
  let name;
326
- let valueText;
420
+ let valueNode;
327
421
  if (node.type === 'type_alias_statement') {
328
422
  // grammar shape: type <left> = <right>
329
423
  const left = node.namedChild(0);
330
424
  const right = node.namedChild(1);
331
425
  if (!left) return false;
332
426
  name = left.text.replace(/\[.*\]$/, ''); // strip PEP 695 type params
333
- valueText = right ? right.text : null;
427
+ valueNode = right || null;
334
428
  } else if (node.type === 'expression_statement') {
335
429
  const child = node.namedChild(0);
336
430
  if (!child || child.type !== 'assignment') return false;
337
431
  const typeNode = child.childForFieldName('type');
338
- if (!typeNode || !/\bTypeAlias\b/.test(typeNode.text)) return false;
339
432
  const leftNode = child.childForFieldName('left');
340
433
  const rightNode = child.childForFieldName('right');
341
434
  if (!leftNode || leftNode.type !== 'identifier') return false;
435
+ const explicit = !!typeNode && /\bTypeAlias\b/.test(typeNode.text);
436
+ const implicit = !typeNode && node.parent?.type === 'module' &&
437
+ /^[A-Z][A-Za-z0-9_]*$/.test(leftNode.text) &&
438
+ ['subscript', 'generic_type'].includes(unwrapTypeNode(rightNode)?.type);
439
+ if (!explicit && !implicit) return false;
342
440
  name = leftNode.text;
343
- valueText = rightNode ? rightNode.text : null;
441
+ valueNode = rightNode || null;
344
442
  } else {
345
443
  return false;
346
444
  }
@@ -349,6 +447,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
349
447
  if (processedRanges.has(rangeKey)) return true;
350
448
  processedRanges.add(rangeKey);
351
449
  const { startLine, endLine } = nodeToLocation(node, lines);
450
+ const aliasOf = pythonAliasBase(valueNode);
352
451
  classes.push({
353
452
  name,
354
453
  type: 'type',
@@ -356,7 +455,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
356
455
  endLine,
357
456
  methods: [],
358
457
  members: [],
359
- ...(valueText && { aliasOf: valueText.trim() }),
458
+ ...(aliasOf && { aliasOf }),
360
459
  });
361
460
  return true;
362
461
  }
@@ -464,6 +563,7 @@ function extractClassMembers(classNode, code) {
464
563
 
465
564
  const isAsync = funcNode.text.trimStart().startsWith('async ');
466
565
  const returnType = extractReturnType(funcNode);
566
+ const returnedConstructors = extractReturnedConstructors(funcNode);
467
567
  const defLine = getDefLine(funcNode);
468
568
  const docstring = extractPythonDocstring(code, defLine);
469
569
  // nameLine: where the name identifier lives (differs from startLine when decorated)
@@ -483,6 +583,7 @@ function extractClassMembers(classNode, code) {
483
583
  // Match top-level Python functions: `async def` → ['async'] modifiers.
484
584
  modifiers: isAsync ? ['async'] : [],
485
585
  ...(returnType && { returnType }),
586
+ ...(returnedConstructors && { returnedConstructors }),
486
587
  ...(paramTypes && { paramTypes }),
487
588
  ...(docstring && { docstring }),
488
589
  ...(memberDecorators.length > 0 && { decorators: memberDecorators }),
@@ -585,6 +686,263 @@ function typeNameFromAnnotation(typeNode) {
585
686
  return typeNameFromExpr(inner);
586
687
  }
587
688
 
689
+ function typeNamesFromAnnotation(typeNode) {
690
+ if (!typeNode) return [];
691
+ const inner = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
692
+ const collect = node => {
693
+ if (!node) return [];
694
+ if (node.type === 'binary_operator' && node.text.includes('|')) {
695
+ return [...collect(node.namedChild(0)), ...collect(node.namedChild(1))];
696
+ }
697
+ if (node.type === 'identifier') return node.text === 'None' ? [] : [node.text];
698
+ if (node.type === 'none') return [];
699
+ if (node.type === 'attribute') {
700
+ const attr = node.childForFieldName('attribute');
701
+ return attr ? [attr.text] : [];
702
+ }
703
+ return [];
704
+ };
705
+ return [...new Set(collect(inner))];
706
+ }
707
+
708
+ function unwrapTypeNode(node) {
709
+ let current = node;
710
+ while (current?.type === 'type' && current.namedChildCount === 1) {
711
+ current = current.namedChild(0);
712
+ }
713
+ return current;
714
+ }
715
+
716
+ function genericTypeParts(node) {
717
+ const current = unwrapTypeNode(node);
718
+ if (!current || !['generic_type', 'subscript'].includes(current.type)) return null;
719
+ const baseNode = current.type === 'subscript'
720
+ ? (current.childForFieldName('value') || current.namedChild(0))
721
+ : current.namedChild(0);
722
+ const base = typeNameFromExpr(baseNode);
723
+ if (!base) return null;
724
+ let args;
725
+ if (current.type === 'subscript') {
726
+ args = [];
727
+ for (let i = 0; i < current.namedChildCount; i++) {
728
+ const child = current.namedChild(i);
729
+ if (child.id !== baseNode.id) args.push(unwrapTypeNode(child));
730
+ }
731
+ } else {
732
+ const params = current.namedChild(1);
733
+ args = [];
734
+ if (params) {
735
+ for (let i = 0; i < params.namedChildCount; i++) {
736
+ args.push(unwrapTypeNode(params.namedChild(i)));
737
+ }
738
+ }
739
+ }
740
+ return { base, args };
741
+ }
742
+
743
+ const PY_ITERABLE_ANNOTATIONS = new Set([
744
+ 'list', 'set', 'tuple', 'Iterable', 'Iterator', 'Sequence',
745
+ 'Collection', 'Generator', 'List', 'Set', 'Tuple',
746
+ ]);
747
+
748
+ function iterableBindingTypes(typeNode) {
749
+ const outer = genericTypeParts(typeNode);
750
+ if (!outer || !PY_ITERABLE_ANNOTATIONS.has(outer.base) ||
751
+ outer.args.length === 0) return [];
752
+ const item = outer.args[0];
753
+ const tuple = genericTypeParts(item);
754
+ if (['tuple', 'Tuple'].includes(tuple?.base) && tuple.args.length > 0) {
755
+ return tuple.args.map(arg => typeNameFromExpr(unwrapTypeNode(arg)));
756
+ }
757
+ const typeName = typeNameFromExpr(unwrapTypeNode(item));
758
+ return typeName ? [typeName] : [];
759
+ }
760
+
761
+ function patternIdentifiers(node, out = []) {
762
+ if (!node) return out;
763
+ if (node.type === 'identifier') {
764
+ out.push(node.text);
765
+ return out;
766
+ }
767
+ for (let i = 0; i < node.namedChildCount; i++) {
768
+ patternIdentifiers(node.namedChild(i), out);
769
+ }
770
+ return out;
771
+ }
772
+
773
+ function enclosingPythonClassName(node) {
774
+ for (let current = node; current; current = current.parent) {
775
+ if (current.type === 'class_definition') {
776
+ return current.childForFieldName('name')?.text;
777
+ }
778
+ }
779
+ return null;
780
+ }
781
+
782
+ function explicitInstanceFieldContracts(tree, parser) {
783
+ const result = new Map();
784
+ const parseCommentType = comment => {
785
+ const text = comment?.text?.trim();
786
+ const prefix = '# type:';
787
+ if (!text?.startsWith(prefix)) return null;
788
+ const annotation = text.slice(prefix.length).trim();
789
+ if (!annotation) return null;
790
+ const annotationTree = parseTree(parser, `_value: ${annotation}`);
791
+ const statement = annotationTree.rootNode.namedChild(0);
792
+ const assignment = statement?.namedChild(0);
793
+ return assignment?.childForFieldName('type') || null;
794
+ };
795
+ traverseTreeCached(tree.rootNode, node => {
796
+ if (node.type !== 'expression_statement') return true;
797
+ const assignment = node.namedChild(0);
798
+ if (assignment?.type !== 'assignment') return true;
799
+ const left = assignment.childForFieldName('left');
800
+ if (left?.type !== 'attribute' ||
801
+ left.childForFieldName('object')?.text !== 'self') return true;
802
+ const field = left.childForFieldName('attribute')?.text;
803
+ const className = enclosingPythonClassName(node);
804
+ if (!field || !className) return true;
805
+ let typeNode = assignment.childForFieldName('type');
806
+ if (!typeNode) {
807
+ const parent = node.parent;
808
+ for (let i = 0; parent && i < parent.namedChildCount; i++) {
809
+ const sibling = parent.namedChild(i);
810
+ if (sibling.type === 'comment' &&
811
+ sibling.startPosition.row === node.endPosition.row) {
812
+ typeNode = parseCommentType(sibling);
813
+ if (typeNode) break;
814
+ }
815
+ }
816
+ }
817
+ if (!typeNode) return true;
818
+ const type = typeNameFromAnnotation(typeNode);
819
+ const itemTypes = iterableBindingTypes(typeNode);
820
+ if (!type && itemTypes.length === 0) return true;
821
+ if (!result.has(className)) result.set(className, new Map());
822
+ result.get(className).set(field, {
823
+ ...(type && { type }),
824
+ ...(itemTypes.length > 0 && { itemTypes }),
825
+ });
826
+ return true;
827
+ });
828
+ return result;
829
+ }
830
+
831
+ function comprehensionReceiverType(
832
+ refNode, receiverName, iterableTypes, callableIterableTypes = new Map(),
833
+ instanceFieldContracts = new Map()) {
834
+ for (let current = refNode?.parent; current; current = current.parent) {
835
+ if (['generator_expression', 'list_comprehension',
836
+ 'set_comprehension', 'dictionary_comprehension'].includes(current.type)) {
837
+ for (let i = 0; i < current.namedChildCount; i++) {
838
+ const clause = current.namedChild(i);
839
+ if (clause.type !== 'for_in_clause') continue;
840
+ const names = patternIdentifiers(clause.childForFieldName('left'));
841
+ const index = names.indexOf(receiverName);
842
+ const iterable = clause.childForFieldName('right');
843
+ const types = iterableTypesFromExpr(
844
+ iterable, iterableTypes, callableIterableTypes,
845
+ instanceFieldContracts, clause);
846
+ if (index >= 0 && types?.[index]) return types[index];
847
+ }
848
+ }
849
+ if (current.type === 'function_definition' ||
850
+ current.type === 'async_function_definition' ||
851
+ current.type === 'lambda') break;
852
+ }
853
+ return undefined;
854
+ }
855
+
856
+ function iterableTypesFromExpr(
857
+ node, iterableTypes, callableIterableTypes,
858
+ instanceFieldContracts = new Map(), contextNode = node) {
859
+ const current = unwrapTypeNode(node);
860
+ if (!current) return null;
861
+ if (current.type === 'identifier') return iterableTypes.get(current.text) || null;
862
+ if (current.type === 'call') {
863
+ const fn = current.childForFieldName('function');
864
+ if (fn?.type === 'identifier') return callableIterableTypes.get(fn.text) || null;
865
+ }
866
+ if (current.type === 'attribute' &&
867
+ current.childForFieldName('object')?.text === 'self') {
868
+ const field = current.childForFieldName('attribute')?.text;
869
+ const className = enclosingPythonClassName(contextNode);
870
+ return instanceFieldContracts.get(className)?.get(field)?.itemTypes || null;
871
+ }
872
+ return null;
873
+ }
874
+
875
+ function nodeContains(ancestor, node) {
876
+ return !!ancestor && !!node &&
877
+ node.startIndex >= ancestor.startIndex && node.endIndex <= ancestor.endIndex;
878
+ }
879
+
880
+ function isinstanceTypes(condition, receiverName) {
881
+ if (!condition || condition.type !== 'call') return [];
882
+ const fn = condition.childForFieldName('function');
883
+ const args = condition.childForFieldName('arguments');
884
+ if (fn?.type !== 'identifier' || fn.text !== 'isinstance' ||
885
+ !args || args.namedChildCount < 2) return [];
886
+ const value = args.namedChild(0);
887
+ const typeExpr = args.namedChild(1);
888
+ if (value?.type !== 'identifier' || value.text !== receiverName) return [];
889
+ const extract = node => {
890
+ if (!node) return [];
891
+ if (node.type === 'identifier') return [node.text];
892
+ if (node.type === 'attribute') {
893
+ const attr = node.childForFieldName('attribute');
894
+ return attr ? [attr.text] : [];
895
+ }
896
+ if (node.type === 'tuple') {
897
+ const out = [];
898
+ for (let i = 0; i < node.namedChildCount; i++) {
899
+ out.push(...extract(node.namedChild(i)));
900
+ }
901
+ return out;
902
+ }
903
+ return [];
904
+ };
905
+ return [...new Set(extract(typeExpr))];
906
+ }
907
+
908
+ function narrowedReceiverType(refNode, receiverName, declaredUnion) {
909
+ for (let current = refNode; current?.parent; current = current.parent) {
910
+ const parent = current.parent;
911
+ if (parent.type === 'if_statement') {
912
+ const condition = parent.childForFieldName('condition');
913
+ const positive = isinstanceTypes(condition, receiverName);
914
+ if (positive.length === 0) continue;
915
+ const consequence = parent.childForFieldName('consequence');
916
+ const alternative = parent.childForFieldName('alternative');
917
+ if (nodeContains(consequence, refNode) && positive.length === 1) {
918
+ return positive[0];
919
+ }
920
+ if (nodeContains(alternative, refNode) && declaredUnion?.length) {
921
+ const remaining = declaredUnion.filter(type => !positive.includes(type));
922
+ if (remaining.length === 1) return remaining[0];
923
+ }
924
+ }
925
+ if (parent.type === 'conditional_expression' &&
926
+ parent.namedChildCount >= 3) {
927
+ const consequence = parent.namedChild(0);
928
+ const condition = parent.namedChild(1);
929
+ const alternative = parent.namedChild(2);
930
+ const positive = isinstanceTypes(condition, receiverName);
931
+ if (nodeContains(consequence, refNode) && positive.length === 1) {
932
+ return positive[0];
933
+ }
934
+ if (nodeContains(alternative, refNode) && declaredUnion?.length) {
935
+ const remaining = declaredUnion.filter(type => !positive.includes(type));
936
+ if (remaining.length === 1) return remaining[0];
937
+ }
938
+ }
939
+ if (parent.type === 'function_definition' ||
940
+ parent.type === 'async_function_definition' ||
941
+ parent.type === 'lambda') break;
942
+ }
943
+ return undefined;
944
+ }
945
+
588
946
  function typeNameFromExpr(node) {
589
947
  if (!node) return undefined;
590
948
  switch (node.type) {
@@ -595,6 +953,9 @@ function typeNameFromExpr(node) {
595
953
  const attr = node.childForFieldName('attribute');
596
954
  return attr?.text;
597
955
  }
956
+ case 'parenthesized_expression':
957
+ return node.namedChildCount === 1
958
+ ? typeNameFromExpr(node.namedChild(0)) : undefined;
598
959
  case 'binary_operator': {
599
960
  // PEP 604 union: X | None → X; unions of two real types are ambiguous
600
961
  const left = node.namedChild(0);
@@ -654,6 +1015,18 @@ function assignmentTargetOf(callNode) {
654
1015
  return undefined;
655
1016
  }
656
1017
 
1018
+ function contextTargetOf(callNode) {
1019
+ const pattern = callNode?.parent;
1020
+ if (pattern?.type !== 'as_pattern' ||
1021
+ pattern.namedChild(0)?.id !== callNode.id) return undefined;
1022
+ const target = pattern.namedChild(pattern.namedChildCount - 1);
1023
+ if (target?.type === 'as_pattern_target') {
1024
+ const identifier = target.namedChild(0);
1025
+ return identifier?.type === 'identifier' ? identifier.text : undefined;
1026
+ }
1027
+ return target?.type === 'identifier' ? target.text : undefined;
1028
+ }
1029
+
657
1030
  /**
658
1031
  * Type identity hint from a constructor-call callee: ClassName(...) or
659
1032
  * pkg.ClassName(...). Preserve the qualifier: dropping `threading` from
@@ -677,14 +1050,161 @@ function constructorTypeInfo(funcNode) {
677
1050
  return undefined;
678
1051
  }
679
1052
 
1053
+ function attributeReceiverPath(node) {
1054
+ if (!node || node.type !== 'attribute') return null;
1055
+ const fields = [];
1056
+ let current = node;
1057
+ while (current?.type === 'attribute') {
1058
+ const attribute = current.childForFieldName('attribute');
1059
+ if (!attribute) return null;
1060
+ fields.unshift(attribute.text);
1061
+ current = current.childForFieldName('object');
1062
+ }
1063
+ return current?.type === 'identifier' && fields.length > 0
1064
+ ? { root: current.text, fields }
1065
+ : null;
1066
+ }
1067
+
1068
+ function iterableAttributeSource(node, localVarTypes) {
1069
+ const current = unwrapTypeNode(node);
1070
+ const path = attributeReceiverPath(current);
1071
+ if (!path) return null;
1072
+ return {
1073
+ root: path.root,
1074
+ fields: path.fields,
1075
+ ...(localVarTypes.get(path.root) && {
1076
+ rootType: localVarTypes.get(path.root),
1077
+ }),
1078
+ };
1079
+ }
1080
+
1081
+ function literalStringValue(node) {
1082
+ if (node?.type !== 'string') return null;
1083
+ let value = '';
1084
+ for (let i = 0; i < node.namedChildCount; i++) {
1085
+ const child = node.namedChild(i);
1086
+ if (child.type === 'interpolation') return null;
1087
+ if (child.type === 'string_content') value += child.text;
1088
+ }
1089
+ return value;
1090
+ }
1091
+
1092
+ /**
1093
+ * Return the method name when a method call is dominated by a positive,
1094
+ * receiver-exact capability check:
1095
+ *
1096
+ * if hasattr(self._stream, "aread"):
1097
+ * await self._stream.aread(...)
1098
+ *
1099
+ * This is not type evidence—the runtime object may implement any matching
1100
+ * contract—but it is useful provenance for the visible dispatch tier. Keep
1101
+ * the recognizer deliberately narrow: a direct hasattr call or an AND term in
1102
+ * the if/elif condition. OR and NOT do not guarantee the capability on entry.
1103
+ */
1104
+ function receiverCapabilityGuard(callNode, receiverNode, methodName) {
1105
+ if (!callNode || !receiverNode || !methodName) return undefined;
1106
+ const receiverText = receiverNode.text;
1107
+
1108
+ const unwrap = node => {
1109
+ let current = node;
1110
+ while (current?.type === 'parenthesized_expression' &&
1111
+ current.namedChildCount === 1) {
1112
+ current = current.namedChild(0);
1113
+ }
1114
+ return current;
1115
+ };
1116
+
1117
+ const matchesHasattr = node => {
1118
+ const current = unwrap(node);
1119
+ if (current?.type !== 'call') return false;
1120
+ const fn = current.childForFieldName('function');
1121
+ if (fn?.type !== 'identifier' || fn.text !== 'hasattr') return false;
1122
+ const args = current.childForFieldName('arguments');
1123
+ if (!args || args.namedChildCount < 2) return false;
1124
+ return args.namedChild(0)?.text === receiverText &&
1125
+ literalStringValue(args.namedChild(1)) === methodName;
1126
+ };
1127
+
1128
+ const positivelyRequiresCapability = node => {
1129
+ const current = unwrap(node);
1130
+ if (matchesHasattr(current)) return true;
1131
+ if (current?.type !== 'boolean_operator') return false;
1132
+ let hasAnd = false;
1133
+ for (let i = 0; i < current.childCount; i++) {
1134
+ const child = current.child(i);
1135
+ if (!child.isNamed && child.type === 'and') hasAnd = true;
1136
+ if (!child.isNamed && child.type === 'or') return false;
1137
+ }
1138
+ return hasAnd && Array.from(
1139
+ { length: current.namedChildCount },
1140
+ (_, i) => current.namedChild(i)
1141
+ ).some(positivelyRequiresCapability);
1142
+ };
1143
+
1144
+ let child = callNode;
1145
+ for (let current = callNode.parent; current; child = current, current = current.parent) {
1146
+ if (current.type === 'function_definition' || current.type === 'lambda') break;
1147
+ if (current.type !== 'if_statement' && current.type !== 'elif_clause') continue;
1148
+ const consequence = current.childForFieldName('consequence');
1149
+ if (!consequence || child.id !== consequence.id) continue;
1150
+ const condition = current.childForFieldName('condition') || current.namedChild(0);
1151
+ if (positivelyRequiresCapability(condition)) return methodName;
1152
+ }
1153
+ return undefined;
1154
+ }
1155
+
1156
+ function pickleRoundTripSource(node) {
1157
+ if (node?.type !== 'call') return null;
1158
+ const loads = node.childForFieldName('function');
1159
+ if (loads?.type !== 'attribute' ||
1160
+ loads.childForFieldName('object')?.text !== 'pickle' ||
1161
+ loads.childForFieldName('attribute')?.text !== 'loads') return null;
1162
+ const loadsArgs = node.childForFieldName('arguments');
1163
+ const dumped = loadsArgs?.namedChild(0);
1164
+ if (dumped?.type !== 'call') return null;
1165
+ const dumps = dumped.childForFieldName('function');
1166
+ if (dumps?.type !== 'attribute' ||
1167
+ dumps.childForFieldName('object')?.text !== 'pickle' ||
1168
+ dumps.childForFieldName('attribute')?.text !== 'dumps') return null;
1169
+ const value = dumped.childForFieldName('arguments')?.namedChild(0);
1170
+ return value?.type === 'identifier' ? value.text : null;
1171
+ }
1172
+
680
1173
  function findCallsInCode(code, parser) {
681
1174
  const tree = parseTree(parser, code);
682
1175
  const calls = [];
1176
+ const instanceFieldContracts = explicitInstanceFieldContracts(tree, parser);
1177
+ // Same-file callable return contracts are a closed type source for loop
1178
+ // bindings: `for item in parse_items()` can use
1179
+ // `parse_items() -> list[Item]` without guessing about external code.
1180
+ const callableIterableTypes = new Map();
1181
+ traverseTreeCached(tree.rootNode, node => {
1182
+ if (node.type !== 'function_definition') return true;
1183
+ const name = node.childForFieldName('name')?.text;
1184
+ const returnType = node.childForFieldName('return_type');
1185
+ const itemTypes = iterableBindingTypes(returnType);
1186
+ if (!name || itemTypes.length === 0) return true;
1187
+ const previous = callableIterableTypes.get(name);
1188
+ if (!previous) callableIterableTypes.set(name, itemTypes);
1189
+ else if (previous.join('\0') !== itemTypes.join('\0')) {
1190
+ // Overloads/redefinitions must agree before their result can type
1191
+ // a receiver.
1192
+ callableIterableTypes.delete(name);
1193
+ }
1194
+ return true;
1195
+ });
683
1196
  const functionStack = []; // Stack of { name, startLine, endLine }
684
1197
  const aliases = new Map(); // Track local aliases: aliasName -> originalName
685
1198
  const nonCallableNames = new Set(); // Track names assigned non-callable values
686
1199
  const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
1200
+ const declaredVarTypes = new Map(); // Compiler-checked annotations survive later assignments
687
1201
  const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
1202
+ const localVarUnionTypes = new Map(); // varName -> concrete PEP 604 alternatives
1203
+ const localIterableTypes = new Map(); // iterable binding -> loop-variable types
1204
+ const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
1205
+ const localDictValueTypes = new Map(); // local dict -> exact string-key value types
1206
+ const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
1207
+ const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment receiver type
688
1208
  const constructedReceiverVars = new Set(); // exact constructor-result bindings
689
1209
  const withBindingVars = new Set(); // names produced by a context-manager as-target
690
1210
  // Member-access aliases (fix #218): `append = output.append` makes a later
@@ -698,7 +1218,13 @@ function findCallsInCode(code, parser) {
698
1218
  const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
699
1219
  const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
700
1220
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
1221
+ const declaredVarTypesStack = [];
701
1222
  const localVarTypeQualifiersStack = [];
1223
+ const localVarUnionTypesStack = [];
1224
+ const localIterableTypesStack = [];
1225
+ const localIterationSourcesStack = [];
1226
+ const localDictValueTypesStack = [];
1227
+ const localVarStdlibContractsStack = [];
702
1228
  const constructedReceiverVarsStack = [];
703
1229
  const withBindingVarsStack = [];
704
1230
 
@@ -779,9 +1305,11 @@ function findCallsInCode(code, parser) {
779
1305
 
780
1306
  // Helper to get current enclosing function
781
1307
  const getCurrentEnclosingFunction = () => {
782
- return functionStack.length > 0
783
- ? { ...functionStack[functionStack.length - 1] }
784
- : null;
1308
+ if (functionStack.length === 0) return null;
1309
+ return {
1310
+ ...functionStack[functionStack.length - 1],
1311
+ scopeChain: functionStack.map(scope => scope.startLine),
1312
+ };
785
1313
  };
786
1314
 
787
1315
  // fix #203: is a bare-identifier function REFERENCE shadowed by a local of
@@ -894,14 +1422,29 @@ function findCallsInCode(code, parser) {
894
1422
  if (node.parent && node.parent.type === 'decorated_definition') {
895
1423
  startLine = node.parent.startPosition.row + 1;
896
1424
  }
1425
+ const returnParts = genericTypeParts(
1426
+ node.childForFieldName('return_type'));
1427
+ const generatorSendType =
1428
+ ['Generator', 'AsyncGenerator'].includes(returnParts?.base) &&
1429
+ returnParts.args.length > 1
1430
+ ? typeNameFromExpr(returnParts.args[1]) : undefined;
897
1431
  functionStack.push({
898
1432
  name: extractFunctionName(node),
899
1433
  startLine,
900
- endLine: node.endPosition.row + 1
1434
+ endLine: node.endPosition.row + 1,
1435
+ ...(generatorSendType && { generatorSendType }),
901
1436
  });
902
1437
  // Save localVarTypes so inner declarations don't leak to sibling functions
903
1438
  localVarTypesStack.push(new Map(localVarTypes));
1439
+ declaredVarTypesStack.push(new Map(declaredVarTypes));
904
1440
  localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
1441
+ localVarUnionTypesStack.push(new Map(localVarUnionTypes));
1442
+ localIterableTypesStack.push(new Map(localIterableTypes));
1443
+ localIterationSourcesStack.push(new Map(localIterationSources));
1444
+ localDictValueTypesStack.push(new Map(
1445
+ [...localDictValueTypes].map(([name, values]) =>
1446
+ [name, new Map(values)])));
1447
+ localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
905
1448
  constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
906
1449
  withBindingVarsStack.push(new Set(withBindingVars));
907
1450
  memberAliasesStack.push(new Map(memberAliases));
@@ -910,12 +1453,59 @@ function findCallsInCode(code, parser) {
910
1453
  // Track parameter type annotations: def foo(x: Foo) → x is Foo
911
1454
  if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
912
1455
  // typed_default_parameter has 'name' field; typed_parameter does not — use namedChild(0)
913
- const nameNode = node.childForFieldName('name') || node.namedChild(0);
1456
+ let nameNode = node.childForFieldName('name') || node.namedChild(0);
1457
+ const parameterPattern = nameNode;
1458
+ if (nameNode && ['dictionary_splat_pattern', 'list_splat_pattern']
1459
+ .includes(nameNode.type)) {
1460
+ nameNode = nameNode.namedChild(0);
1461
+ }
914
1462
  const typeNode = node.childForFieldName('type');
915
1463
  if (nameNode?.type === 'identifier' && typeNode) {
916
1464
  const typeName = typeNameFromAnnotation(typeNode);
917
- if (typeName && !['self', 'cls'].includes(nameNode.text)) {
918
- localVarTypes.set(nameNode.text, typeName);
1465
+ const unionTypes = typeNamesFromAnnotation(typeNode);
1466
+ const itemTypes = iterableBindingTypes(typeNode);
1467
+ const receiverType = parameterPattern?.type === 'dictionary_splat_pattern'
1468
+ ? 'dict'
1469
+ : (parameterPattern?.type === 'list_splat_pattern' ? 'tuple' : typeName);
1470
+ if (receiverType && !['self', 'cls'].includes(nameNode.text)) {
1471
+ localVarTypes.set(nameNode.text, receiverType);
1472
+ declaredVarTypes.set(nameNode.text, receiverType);
1473
+ }
1474
+ if (unionTypes.length > 1) localVarUnionTypes.set(nameNode.text, unionTypes);
1475
+ else localVarUnionTypes.delete(nameNode.text);
1476
+ if (itemTypes.length > 0) localIterableTypes.set(nameNode.text, itemTypes);
1477
+ else localIterableTypes.delete(nameNode.text);
1478
+ }
1479
+ }
1480
+
1481
+ // Compiler-declared iterable element flow for ordinary for-loops.
1482
+ // Tuple destructuring is positional: `for name, value in
1483
+ // headers:`, where headers is list[tuple[bytes, bytes]], types both
1484
+ // receivers as bytes. Comprehensions are handled at the call site
1485
+ // because their result expression precedes the for-clause in the AST.
1486
+ if (node.type === 'for_statement') {
1487
+ const left = node.childForFieldName('left');
1488
+ const right = node.childForFieldName('right');
1489
+ const itemTypes = iterableTypesFromExpr(
1490
+ right, localIterableTypes, callableIterableTypes,
1491
+ instanceFieldContracts, node);
1492
+ if (itemTypes?.length) {
1493
+ const names = patternIdentifiers(left);
1494
+ if (names.length === itemTypes.length) {
1495
+ for (let i = 0; i < names.length; i++) {
1496
+ if (itemTypes[i]) localVarTypes.set(names[i], itemTypes[i]);
1497
+ }
1498
+ } else if (names.length === 1 && itemTypes.length === 1) {
1499
+ localVarTypes.set(names[0], itemTypes[0]);
1500
+ }
1501
+ } else {
1502
+ const source = iterableAttributeSource(right, localVarTypes);
1503
+ const names = patternIdentifiers(left);
1504
+ for (let i = 0; source && i < names.length; i++) {
1505
+ localIterationSources.set(names[i], {
1506
+ ...source,
1507
+ index: i,
1508
+ });
919
1509
  }
920
1510
  }
921
1511
  }
@@ -944,6 +1534,13 @@ function findCallsInCode(code, parser) {
944
1534
  } else {
945
1535
  localVarTypeQualifiers.delete(targetId.text);
946
1536
  }
1537
+ } else if (ctx.childForFieldName('function')?.type === 'identifier' &&
1538
+ ctx.childForFieldName('function').text === 'open' &&
1539
+ !isShadowedByLocal(ctx.childForFieldName('function'), 'open')) {
1540
+ // Builtin open() is its own context value and always
1541
+ // yields an IO object. The exact text/binary subtype
1542
+ // is irrelevant for method-owner exclusion.
1543
+ localVarTypes.set(targetId.text, 'IO');
947
1544
  }
948
1545
  }
949
1546
  }
@@ -954,13 +1551,29 @@ function findCallsInCode(code, parser) {
954
1551
  const left = node.childForFieldName('left');
955
1552
  const right = node.childForFieldName('right');
956
1553
  if (left?.type === 'identifier') {
1554
+ const previousType = localVarTypes.get(left.text);
1555
+ if (previousType && right?.type === 'call') {
1556
+ const rightFunction = right.childForFieldName('function');
1557
+ if (rightFunction?.type === 'attribute' &&
1558
+ rightFunction.childForFieldName('object')?.type === 'identifier' &&
1559
+ rightFunction.childForFieldName('object').text === left.text) {
1560
+ assignmentRhsReceiverTypes.set(right.id, previousType);
1561
+ }
1562
+ }
957
1563
  // Track type annotation: x: Foo = ... → x is Foo
958
1564
  const typeNode = node.childForFieldName('type');
959
1565
  if (typeNode) {
960
1566
  const typeName = typeNameFromAnnotation(typeNode);
1567
+ const unionTypes = typeNamesFromAnnotation(typeNode);
1568
+ const itemTypes = iterableBindingTypes(typeNode);
961
1569
  if (typeName) {
962
1570
  localVarTypes.set(left.text, typeName);
1571
+ declaredVarTypes.set(left.text, typeName);
963
1572
  }
1573
+ if (unionTypes.length > 1) localVarUnionTypes.set(left.text, unionTypes);
1574
+ else localVarUnionTypes.delete(left.text);
1575
+ if (itemTypes.length > 0) localIterableTypes.set(left.text, itemTypes);
1576
+ else localIterableTypes.delete(left.text);
964
1577
  }
965
1578
  memberAliases.delete(left.text); // any assignment rebinds the name
966
1579
  constructedReceiverVars.delete(left.text);
@@ -972,11 +1585,59 @@ function findCallsInCode(code, parser) {
972
1585
  if (!typeNode) {
973
1586
  localVarTypes.delete(left.text);
974
1587
  localVarTypeQualifiers.delete(left.text);
1588
+ localVarUnionTypes.delete(left.text);
1589
+ localIterableTypes.delete(left.text);
1590
+ localIterationSources.delete(left.text);
1591
+ localDictValueTypes.delete(left.text);
1592
+ localVarStdlibContracts.delete(left.text);
1593
+ // Python assignments remain constrained by a variable or
1594
+ // parameter annotation. Constructor/literal inference is
1595
+ // nearest-assignment only, but a declared contract is
1596
+ // valid for every subsequent assignment in type-correct
1597
+ // code (`boundary: bytes | None; boundary = make()`).
1598
+ const declaredType = declaredVarTypes.get(left.text);
1599
+ if (declaredType) localVarTypes.set(left.text, declaredType);
975
1600
  } else {
976
1601
  // An annotation is the authoritative type source; a
977
1602
  // previous constructor qualifier must not survive it.
978
1603
  localVarTypeQualifiers.delete(left.text);
979
1604
  }
1605
+ // Preserve a declared collection contract through the common
1606
+ // normalization idiom `x = {} if x is None else x`. The
1607
+ // literal branch is a concrete implementation of the prior
1608
+ // protocol; retaining that protocol is compiler-safe and
1609
+ // keeps later method dispatch out of unrelated project
1610
+ // classes.
1611
+ if (!typeNode && previousType &&
1612
+ right?.type === 'conditional_expression') {
1613
+ const consequence = right.namedChild(0);
1614
+ const alternative = right.namedChild(2);
1615
+ const selfBranch = [consequence, alternative].find(
1616
+ branch => branch?.type === 'identifier' &&
1617
+ branch.text === left.text);
1618
+ const otherBranch = selfBranch === consequence
1619
+ ? alternative : consequence;
1620
+ const literalType = otherBranch
1621
+ ? PY_LITERAL_RECEIVER_TYPES[otherBranch.type] : null;
1622
+ const compatible = {
1623
+ Mapping: new Set(['dict']),
1624
+ MutableMapping: new Set(['dict']),
1625
+ Sequence: new Set(['list', 'tuple', 'str', 'bytes']),
1626
+ MutableSequence: new Set(['list']),
1627
+ Collection: new Set(['list', 'tuple', 'set', 'dict', 'str', 'bytes']),
1628
+ Iterable: new Set(['list', 'tuple', 'set', 'dict', 'str', 'bytes']),
1629
+ };
1630
+ if (selfBranch && literalType &&
1631
+ (previousType === literalType ||
1632
+ compatible[previousType]?.has(literalType))) {
1633
+ localVarTypes.set(left.text, previousType);
1634
+ }
1635
+ }
1636
+ if (!typeNode && right?.type === 'yield') {
1637
+ const sendType = functionStack[
1638
+ functionStack.length - 1]?.generatorSendType;
1639
+ if (sendType) localVarTypes.set(left.text, sendType);
1640
+ }
980
1641
  // Literal assignment types the variable (fix #218):
981
1642
  // ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
982
1643
  // same trust grade as literal receivers ({}.get() → dict).
@@ -985,6 +1646,29 @@ function findCallsInCode(code, parser) {
985
1646
  if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
986
1647
  localVarTypes.set(left.text, litType);
987
1648
  }
1649
+ if (right?.type === 'dictionary') {
1650
+ const valueTypes = new Map();
1651
+ for (let i = 0; i < right.namedChildCount; i++) {
1652
+ const pair = right.namedChild(i);
1653
+ if (pair.type !== 'pair') continue;
1654
+ const key = literalStringValue(pair.childForFieldName('key'));
1655
+ const value = pair.childForFieldName('value');
1656
+ const valueType = value?.type === 'identifier'
1657
+ ? localVarTypes.get(value.text)
1658
+ : PY_LITERAL_RECEIVER_TYPES[value?.type];
1659
+ if (key != null && valueType) valueTypes.set(key, valueType);
1660
+ }
1661
+ if (valueTypes.size > 0) {
1662
+ localDictValueTypes.set(left.text, valueTypes);
1663
+ }
1664
+ }
1665
+ const roundTripSource = pickleRoundTripSource(right);
1666
+ const roundTripType = roundTripSource
1667
+ ? localVarTypes.get(roundTripSource) : null;
1668
+ if (roundTripType && moduleAliases.has('pickle')) {
1669
+ localVarTypes.set(left.text, roundTripType);
1670
+ localVarStdlibContracts.set(left.text, 'pickle');
1671
+ }
988
1672
  if (right?.type === 'identifier') {
989
1673
  aliases.set(left.text, right.text);
990
1674
  }
@@ -1068,7 +1752,8 @@ function findCallsInCode(code, parser) {
1068
1752
 
1069
1753
  const enclosingFunction = getCurrentEnclosingFunction();
1070
1754
  let uncertain = false;
1071
- const assignedTo = assignmentTargetOf(node);
1755
+ const assignedContext = contextTargetOf(node);
1756
+ const assignedTo = assignmentTargetOf(node) || assignedContext;
1072
1757
 
1073
1758
  // Call-site arg count (positional + keyword) for arity pruning.
1074
1759
  // *args/**kwargs splats make the count open-ended — flag them so
@@ -1108,6 +1793,7 @@ function findCallsInCode(code, parser) {
1108
1793
  ...(recvType && { receiverType: recvType }),
1109
1794
  ...(recvIsModule && { receiverIsModule: true }),
1110
1795
  ...(assignedTo && { assignedTo }),
1796
+ ...(assignedContext && { assignedContext: true }),
1111
1797
  argCount,
1112
1798
  ...(argSpread && { argSpread: true }),
1113
1799
  enclosingFunction,
@@ -1123,6 +1809,7 @@ function findCallsInCode(code, parser) {
1123
1809
  line: node.startPosition.row + 1,
1124
1810
  isMethod: false,
1125
1811
  ...(assignedTo && { assignedTo }),
1812
+ ...(assignedContext && { assignedContext: true }),
1126
1813
  argCount,
1127
1814
  ...(argSpread && { argSpread: true }),
1128
1815
  enclosingFunction,
@@ -1139,6 +1826,7 @@ function findCallsInCode(code, parser) {
1139
1826
  if (attrNode) {
1140
1827
  let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
1141
1828
  let selfAttribute = undefined;
1829
+ const receiverPath = attributeReceiverPath(objNode);
1142
1830
  // Chained receiver (fix #219): the receiver IS a call —
1143
1831
  // fetch_data().json() — record the producer so findCallers
1144
1832
  // can type the receiver from its declared return
@@ -1156,10 +1844,17 @@ function findCallsInCode(code, parser) {
1156
1844
  }
1157
1845
  {
1158
1846
  let recvNode = objNode;
1159
- if (recvNode?.type === 'parenthesized_expression' &&
1160
- recvNode.namedChild(0)?.type === 'await') {
1847
+ while (recvNode?.type === 'parenthesized_expression' &&
1848
+ recvNode.namedChildCount === 1) {
1849
+ recvNode = recvNode.namedChild(0);
1850
+ }
1851
+ if (recvNode?.type === 'await') {
1161
1852
  receiverCallAwaited = true;
1162
- recvNode = recvNode.namedChild(0).namedChild(0);
1853
+ recvNode = recvNode.namedChild(0);
1854
+ while (recvNode?.type === 'parenthesized_expression' &&
1855
+ recvNode.namedChildCount === 1) {
1856
+ recvNode = recvNode.namedChild(0);
1857
+ }
1163
1858
  }
1164
1859
  if (recvNode?.type === 'call' && receiver !== 'super') {
1165
1860
  const prodFunc = recvNode.childForFieldName('function');
@@ -1196,17 +1891,63 @@ function findCallsInCode(code, parser) {
1196
1891
 
1197
1892
  // Literal receivers carry their builtin type: {}.get() can
1198
1893
  // never be a project class method
1894
+ let subscriptReceiverType;
1895
+ if (objNode?.type === 'subscript') {
1896
+ const base = objNode.childForFieldName('value');
1897
+ const key = literalStringValue(
1898
+ objNode.childForFieldName('subscript'));
1899
+ if (base?.type === 'identifier' && key != null) {
1900
+ subscriptReceiverType =
1901
+ localDictValueTypes.get(base.text)?.get(key);
1902
+ }
1903
+ }
1199
1904
  const receiverType = receiver
1200
- ? localVarTypes.get(receiver)
1201
- : (objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined);
1905
+ ? (narrowedReceiverType(
1906
+ objNode, receiver, localVarUnionTypes.get(receiver)) ||
1907
+ comprehensionReceiverType(
1908
+ objNode, receiver, localIterableTypes,
1909
+ callableIterableTypes, instanceFieldContracts) ||
1910
+ localVarTypes.get(receiver))
1911
+ || assignmentRhsReceiverTypes.get(node.id)
1912
+ : (subscriptReceiverType ||
1913
+ (objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
1914
+ let iterationSource = receiver
1915
+ ? localIterationSources.get(receiver) : null;
1916
+ if (receiver && !iterationSource) {
1917
+ for (let current = objNode?.parent; current; current = current.parent) {
1918
+ if (['generator_expression', 'list_comprehension',
1919
+ 'set_comprehension', 'dictionary_comprehension']
1920
+ .includes(current.type)) {
1921
+ for (let i = 0; i < current.namedChildCount; i++) {
1922
+ const clause = current.namedChild(i);
1923
+ if (clause.type !== 'for_in_clause') continue;
1924
+ const names = patternIdentifiers(
1925
+ clause.childForFieldName('left'));
1926
+ const index = names.indexOf(receiver);
1927
+ if (index < 0) continue;
1928
+ const source = iterableAttributeSource(
1929
+ clause.childForFieldName('right'),
1930
+ localVarTypes);
1931
+ if (source) iterationSource = {
1932
+ ...source, index,
1933
+ };
1934
+ }
1935
+ }
1936
+ if (isFunctionNode(current)) break;
1937
+ }
1938
+ }
1202
1939
  const receiverTypeQualifier = receiver
1203
1940
  ? localVarTypeQualifiers.get(receiver)
1204
1941
  : undefined;
1942
+ const receiverRootType = receiverPath
1943
+ ? localVarTypes.get(receiverPath.root) : undefined;
1205
1944
  // Module receiver (httpx.get()) — unless locally shadowed
1206
1945
  // by a typed instance binding
1207
1946
  const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
1208
1947
  !localVarTypes.has(receiver);
1209
1948
  const firstArg = getFirstStringArg(node);
1949
+ const capabilityGuard = receiverCapabilityGuard(
1950
+ node, objNode, attrNode.text);
1210
1951
  calls.push({
1211
1952
  name: attrNode.text,
1212
1953
  // Multi-line chains (obj.x()\n.y()) must report each
@@ -1216,18 +1957,40 @@ function findCallsInCode(code, parser) {
1216
1957
  isMethod: true,
1217
1958
  receiver,
1218
1959
  ...(receiverType && { receiverType }),
1960
+ ...(receiver && localVarStdlibContracts.has(receiver) && {
1961
+ receiverTypeStdlibModule:
1962
+ localVarStdlibContracts.get(receiver),
1963
+ }),
1219
1964
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1220
1965
  ...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
1221
1966
  ...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
1222
1967
  ...(receiverIsModule && { receiverIsModule: true }),
1223
1968
  ...(receiver && objNode?.type === 'identifier' &&
1224
1969
  isShadowedByLocal(objNode, receiver) && { receiverLocalBinding: true }),
1970
+ ...(receiverPath && {
1971
+ receiverRoot: receiverPath.root,
1972
+ receiverField: receiverPath.fields[receiverPath.fields.length - 1],
1973
+ receiverFields: receiverPath.fields,
1974
+ }),
1975
+ ...(receiverRootType && { receiverRootType }),
1976
+ ...(iterationSource && {
1977
+ receiverIterationRoot: iterationSource.root,
1978
+ receiverIterationFields: iterationSource.fields,
1979
+ receiverIterationIndex: iterationSource.index,
1980
+ ...(iterationSource.rootType && {
1981
+ receiverIterationRootType: iterationSource.rootType,
1982
+ }),
1983
+ }),
1225
1984
  ...(selfAttribute && { selfAttribute }),
1226
1985
  ...(receiverCall && { receiverCall }),
1227
1986
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1228
1987
  ...(receiverCallAwaited && { receiverCallAwaited: true }),
1229
1988
  ...(receiverCallLine && { receiverCallLine }),
1989
+ ...(capabilityGuard && {
1990
+ receiverCapabilityGuard: capabilityGuard,
1991
+ }),
1230
1992
  ...(assignedTo && { assignedTo }),
1993
+ ...(assignedContext && { assignedContext: true }),
1231
1994
  argCount,
1232
1995
  ...(argSpread && { argSpread: true }),
1233
1996
  enclosingFunction,
@@ -1299,11 +2062,47 @@ function findCallsInCode(code, parser) {
1299
2062
  localVarTypes.clear();
1300
2063
  for (const [k, v] of saved) localVarTypes.set(k, v);
1301
2064
  }
2065
+ const savedDeclared = declaredVarTypesStack.pop();
2066
+ if (savedDeclared) {
2067
+ declaredVarTypes.clear();
2068
+ for (const [k, v] of savedDeclared) declaredVarTypes.set(k, v);
2069
+ }
1302
2070
  const savedQualifiers = localVarTypeQualifiersStack.pop();
1303
2071
  if (savedQualifiers) {
1304
2072
  localVarTypeQualifiers.clear();
1305
2073
  for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
1306
2074
  }
2075
+ const savedUnions = localVarUnionTypesStack.pop();
2076
+ if (savedUnions) {
2077
+ localVarUnionTypes.clear();
2078
+ for (const [k, v] of savedUnions) localVarUnionTypes.set(k, v);
2079
+ }
2080
+ const savedIterables = localIterableTypesStack.pop();
2081
+ if (savedIterables) {
2082
+ localIterableTypes.clear();
2083
+ for (const [k, v] of savedIterables) localIterableTypes.set(k, v);
2084
+ }
2085
+ const savedIterationSources = localIterationSourcesStack.pop();
2086
+ if (savedIterationSources) {
2087
+ localIterationSources.clear();
2088
+ for (const [k, v] of savedIterationSources) {
2089
+ localIterationSources.set(k, v);
2090
+ }
2091
+ }
2092
+ const savedDictValueTypes = localDictValueTypesStack.pop();
2093
+ if (savedDictValueTypes) {
2094
+ localDictValueTypes.clear();
2095
+ for (const [name, values] of savedDictValueTypes) {
2096
+ localDictValueTypes.set(name, values);
2097
+ }
2098
+ }
2099
+ const savedStdlibContracts = localVarStdlibContractsStack.pop();
2100
+ if (savedStdlibContracts) {
2101
+ localVarStdlibContracts.clear();
2102
+ for (const [name, moduleName] of savedStdlibContracts) {
2103
+ localVarStdlibContracts.set(name, moduleName);
2104
+ }
2105
+ }
1307
2106
  const savedConstructed = constructedReceiverVarsStack.pop();
1308
2107
  constructedReceiverVars.clear();
1309
2108
  if (savedConstructed) for (const name of savedConstructed) constructedReceiverVars.add(name);
@@ -1471,6 +2270,33 @@ function findExportsInCode(code, parser) {
1471
2270
  const exports = [];
1472
2271
 
1473
2272
  traverseTreeCached(tree.rootNode, (node) => {
2273
+ // PEP 484 explicit re-export idiom:
2274
+ // `from .core import public_thing as public_thing`. The redundant
2275
+ // alias is deliberate compiler/tooling evidence that the imported
2276
+ // name belongs to this module's public surface even without __all__.
2277
+ if (node.type === 'import_from_statement') {
2278
+ let source = '';
2279
+ for (let index = 0; index < node.namedChildCount; index++) {
2280
+ const child = node.namedChild(index);
2281
+ if (!source && (child.type === 'dotted_name' ||
2282
+ child.type === 'relative_import')) {
2283
+ source = child.text;
2284
+ continue;
2285
+ }
2286
+ if (child.type !== 'aliased_import') continue;
2287
+ const imported = child.namedChild(0);
2288
+ const alias = child.namedChild(1);
2289
+ if (imported?.text && alias?.text === imported.text) {
2290
+ exports.push({
2291
+ name: alias.text,
2292
+ type: 're-export',
2293
+ source,
2294
+ line: node.startPosition.row + 1,
2295
+ });
2296
+ }
2297
+ }
2298
+ return true;
2299
+ }
1474
2300
  // Look for __all__ = [...]
1475
2301
  if (node.type === 'expression_statement') {
1476
2302
  const child = node.namedChild(0);
@@ -1634,9 +2460,10 @@ function findUsagesInCode(code, name, parser, tree) {
1634
2460
  * @param {object} parser - Tree-sitter parser instance
1635
2461
  * @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
1636
2462
  */
1637
- function findInstanceAttributeTypes(code, parser) {
2463
+ function findInstanceAttributeTypes(code, parser, options = {}) {
1638
2464
  const tree = parseTree(parser, code);
1639
2465
  const result = new Map(); // className -> Map(attrName -> typeName)
2466
+ const explicitContracts = explicitInstanceFieldContracts(tree, parser);
1640
2467
 
1641
2468
  const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
1642
2469
 
@@ -1651,6 +2478,23 @@ function findInstanceAttributeTypes(code, parser) {
1651
2478
  if (!body) return false;
1652
2479
 
1653
2480
  const attrTypes = new Map();
2481
+ for (const [field, contract] of explicitContracts.get(className) || []) {
2482
+ if (contract.type) attrTypes.set(field, contract.type);
2483
+ }
2484
+ const methodReturns = new Map();
2485
+ for (let i = 0; i < body.namedChildCount; i++) {
2486
+ let member = body.namedChild(i);
2487
+ if (member.type === 'decorated_definition') {
2488
+ member = Array.from({ length: member.namedChildCount },
2489
+ (_, index) => member.namedChild(index))
2490
+ .find(child => child.type === 'function_definition') || member;
2491
+ }
2492
+ if (member.type !== 'function_definition') continue;
2493
+ const methodName = member.childForFieldName('name')?.text;
2494
+ const returnType = typeNameFromAnnotation(
2495
+ member.childForFieldName('return_type'));
2496
+ if (methodName && returnType) methodReturns.set(methodName, returnType);
2497
+ }
1654
2498
 
1655
2499
  // Scan annotated class-level fields: name: Type [= ...]. Originally
1656
2500
  // @dataclass-only (fix #28); a BARE class-body annotation is the
@@ -1758,6 +2602,19 @@ function findInstanceAttributeTypes(code, parser) {
1758
2602
  const typeName = extractConstructorName(rhs);
1759
2603
  if (typeName) {
1760
2604
  attrTypes.set(attrName, typeName);
2605
+ } else if (rhs.type === 'call') {
2606
+ const fn = rhs.childForFieldName('function');
2607
+ if (fn?.type === 'attribute') {
2608
+ const moduleName = fn.childForFieldName('object')?.text;
2609
+ const functionName = fn.childForFieldName('attribute')?.text;
2610
+ const methodType = moduleName === 'self' && functionName
2611
+ ? methodReturns.get(functionName) : null;
2612
+ const builtinType = moduleName && functionName &&
2613
+ options.resolveBuiltinCallType?.(moduleName, functionName);
2614
+ if (methodType || builtinType) {
2615
+ attrTypes.set(attrName, methodType || builtinType);
2616
+ }
2617
+ }
1761
2618
  } else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
1762
2619
  // self.X = param where param has type annotation
1763
2620
  attrTypes.set(attrName, paramTypes.get(rhs.text));
@@ -1767,6 +2624,63 @@ function findInstanceAttributeTypes(code, parser) {
1767
2624
  });
1768
2625
  }
1769
2626
 
2627
+ // Stable external/runtime constructors assigned outside __init__ are
2628
+ // still exact field evidence when every observed contract agrees.
2629
+ // This covers lifecycle-owned fields such as
2630
+ // `self.ready = asyncio.Event()` in serve(), without generalizing the
2631
+ // old constructor-name heuristic to arbitrary methods. Unknown or
2632
+ // conflicting assignments leave the field untyped.
2633
+ const runtimeFieldTypes = new Map();
2634
+ const invalidRuntimeFields = new Set();
2635
+ for (let i = 0; i < body.namedChildCount; i++) {
2636
+ let member = body.namedChild(i);
2637
+ if (member.type === 'decorated_definition') {
2638
+ member = Array.from({ length: member.namedChildCount },
2639
+ (_, index) => member.namedChild(index))
2640
+ .find(child => child.type === 'function_definition') || member;
2641
+ }
2642
+ if (member.type !== 'function_definition') continue;
2643
+ const memberBody = member.childForFieldName('body');
2644
+ if (!memberBody) continue;
2645
+ traverseTree(memberBody, stmt => {
2646
+ if (stmt.type !== 'expression_statement') return true;
2647
+ const assignment = stmt.firstChild;
2648
+ if (assignment?.type !== 'assignment') return true;
2649
+ const left = assignment.childForFieldName('left');
2650
+ const field = left?.type === 'attribute' &&
2651
+ left.childForFieldName('object')?.text === 'self'
2652
+ ? left.childForFieldName('attribute')?.text : null;
2653
+ if (!field) return true;
2654
+ const right = assignment.childForFieldName('right');
2655
+ const callable = right?.type === 'call'
2656
+ ? right.childForFieldName('function') : null;
2657
+ const moduleName = callable?.type === 'attribute'
2658
+ ? callable.childForFieldName('object')?.text : null;
2659
+ const functionName = callable?.type === 'attribute'
2660
+ ? callable.childForFieldName('attribute')?.text : null;
2661
+ const runtimeType = moduleName && functionName
2662
+ ? options.resolveBuiltinCallType?.(moduleName, functionName)
2663
+ : null;
2664
+ if (!runtimeType) {
2665
+ // None is a harmless uninitialized state; any other
2666
+ // unknown write could replace the field with a project
2667
+ // value and therefore invalidates exclusion-grade flow.
2668
+ if (right?.type !== 'none') invalidRuntimeFields.add(field);
2669
+ return true;
2670
+ }
2671
+ if (!runtimeFieldTypes.has(field)) runtimeFieldTypes.set(field, new Set());
2672
+ runtimeFieldTypes.get(field).add(runtimeType);
2673
+ return true;
2674
+ });
2675
+ }
2676
+ for (const [field, types] of runtimeFieldTypes) {
2677
+ if (invalidRuntimeFields.has(field) || types.size !== 1) continue;
2678
+ const type = [...types][0];
2679
+ if (!attrTypes.has(field) || attrTypes.get(field) === type) {
2680
+ attrTypes.set(field, type);
2681
+ }
2682
+ }
2683
+
1770
2684
  if (attrTypes.size > 0) {
1771
2685
  result.set(className, attrTypes);
1772
2686
  }
@@ -1866,6 +2780,32 @@ function isEntryPoint(symbol) {
1866
2780
  return getEntryPointKind(symbol) !== null;
1867
2781
  }
1868
2782
 
2783
+ // Stable CPython/stdlib runtime contracts. These are intentionally small and
2784
+ // language-owned: callers must still prove that the import is external (not a
2785
+ // project module with the same name) before using them as exclusion evidence.
2786
+ const PY_BUILTIN_CALL_RETURNS = Object.freeze({
2787
+ 'asyncio.Event': 'AsyncEvent',
2788
+ 'base64.b64encode': 'bytes',
2789
+ 'base64.b64decode': 'bytes',
2790
+ 'json.dumps': 'str',
2791
+ 'os.urandom': 'bytes',
2792
+ 'urllib.request.getproxies': 'dict',
2793
+ 'zlib.compressobj': 'ZlibCompress',
2794
+ 'zlib.decompressobj': 'ZlibDecompress',
2795
+ });
2796
+
2797
+ const PY_BUILTIN_FIELD_TYPES = Object.freeze({
2798
+ 'os.environ': 'dict',
2799
+ });
2800
+
2801
+ function getBuiltinCallReturnType(moduleName, functionName) {
2802
+ return PY_BUILTIN_CALL_RETURNS[`${moduleName}.${functionName}`] || null;
2803
+ }
2804
+
2805
+ function getBuiltinFieldType(moduleName, fieldName) {
2806
+ return PY_BUILTIN_FIELD_TYPES[`${moduleName}.${fieldName}`] || null;
2807
+ }
2808
+
1869
2809
  module.exports = {
1870
2810
  findFunctions,
1871
2811
  findClasses,
@@ -1875,6 +2815,8 @@ module.exports = {
1875
2815
  findExportsInCode,
1876
2816
  findUsagesInCode,
1877
2817
  findInstanceAttributeTypes,
2818
+ getBuiltinCallReturnType,
2819
+ getBuiltinFieldType,
1878
2820
  isEntryPoint,
1879
2821
  getEntryPointKind,
1880
2822
  parse