ucn 4.2.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +445 -300
  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 -131
  13. package/core/cache.js +533 -11
  14. package/core/callers.js +5533 -494
  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 +421 -20
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +204 -42
  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 +216 -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 -177
  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 +371 -116
  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 +428 -16
  65. package/languages/javascript.js +452 -49
  66. package/languages/python.js +1041 -32
  67. package/languages/rust.js +1415 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +41 -24
  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
@@ -671,20 +1044,169 @@ function constructorTypeInfo(funcNode) {
671
1044
  const attr = funcNode.childForFieldName('attribute');
672
1045
  const object = funcNode.childForFieldName('object');
673
1046
  return attr && /^[A-Z]/.test(attr.text)
674
- ? { type: attr.text, qualifier: object?.type === 'identifier' ? object.text : undefined }
1047
+ ? { type: attr.text, qualifier: object?.text || undefined }
675
1048
  : undefined;
676
1049
  }
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
1208
+ const constructedReceiverVars = new Set(); // exact constructor-result bindings
1209
+ const withBindingVars = new Set(); // names produced by a context-manager as-target
688
1210
  // Member-access aliases (fix #218): `append = output.append` makes a later
689
1211
  // bare `append(part)` a METHOD call on `output` — it must carry the
690
1212
  // receiver's evidence, never bind by bare name to a same-file def
@@ -696,7 +1218,15 @@ function findCallsInCode(code, parser) {
696
1218
  const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
697
1219
  const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
698
1220
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
1221
+ const declaredVarTypesStack = [];
699
1222
  const localVarTypeQualifiersStack = [];
1223
+ const localVarUnionTypesStack = [];
1224
+ const localIterableTypesStack = [];
1225
+ const localIterationSourcesStack = [];
1226
+ const localDictValueTypesStack = [];
1227
+ const localVarStdlibContractsStack = [];
1228
+ const constructedReceiverVarsStack = [];
1229
+ const withBindingVarsStack = [];
700
1230
 
701
1231
  // Helper: extract first string-arg literal from a call node.
702
1232
  // Used by route extraction to capture path arg of requests.get('/users'), httpx.get('/users') etc.
@@ -775,9 +1305,11 @@ function findCallsInCode(code, parser) {
775
1305
 
776
1306
  // Helper to get current enclosing function
777
1307
  const getCurrentEnclosingFunction = () => {
778
- return functionStack.length > 0
779
- ? { ...functionStack[functionStack.length - 1] }
780
- : null;
1308
+ if (functionStack.length === 0) return null;
1309
+ return {
1310
+ ...functionStack[functionStack.length - 1],
1311
+ scopeChain: functionStack.map(scope => scope.startLine),
1312
+ };
781
1313
  };
782
1314
 
783
1315
  // fix #203: is a bare-identifier function REFERENCE shadowed by a local of
@@ -848,6 +1380,16 @@ function findCallsInCode(code, parser) {
848
1380
  }
849
1381
  }
850
1382
  if (p.type === 'function_definition' || p.type === 'async_function_definition') {
1383
+ const params = p.childForFieldName('parameters');
1384
+ if (params) {
1385
+ for (let i = 0; i < params.namedChildCount; i++) {
1386
+ const prm = params.namedChild(i);
1387
+ const prmName = prm.type === 'identifier'
1388
+ ? prm
1389
+ : (prm.childForFieldName('name') || prm.namedChild(0));
1390
+ if (prmName?.type === 'identifier' && prmName.text === name) return true;
1391
+ }
1392
+ }
851
1393
  const body = p.childForFieldName('body');
852
1394
  return body ? _bindsNameInScope(body, name) : false;
853
1395
  }
@@ -880,26 +1422,90 @@ function findCallsInCode(code, parser) {
880
1422
  if (node.parent && node.parent.type === 'decorated_definition') {
881
1423
  startLine = node.parent.startPosition.row + 1;
882
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;
883
1431
  functionStack.push({
884
1432
  name: extractFunctionName(node),
885
1433
  startLine,
886
- endLine: node.endPosition.row + 1
1434
+ endLine: node.endPosition.row + 1,
1435
+ ...(generatorSendType && { generatorSendType }),
887
1436
  });
888
1437
  // Save localVarTypes so inner declarations don't leak to sibling functions
889
1438
  localVarTypesStack.push(new Map(localVarTypes));
1439
+ declaredVarTypesStack.push(new Map(declaredVarTypes));
890
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));
1448
+ constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
1449
+ withBindingVarsStack.push(new Set(withBindingVars));
891
1450
  memberAliasesStack.push(new Map(memberAliases));
892
1451
  }
893
1452
 
894
1453
  // Track parameter type annotations: def foo(x: Foo) → x is Foo
895
1454
  if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
896
1455
  // typed_default_parameter has 'name' field; typed_parameter does not — use namedChild(0)
897
- 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
+ }
898
1462
  const typeNode = node.childForFieldName('type');
899
1463
  if (nameNode?.type === 'identifier' && typeNode) {
900
1464
  const typeName = typeNameFromAnnotation(typeNode);
901
- if (typeName && !['self', 'cls'].includes(nameNode.text)) {
902
- 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
+ });
903
1509
  }
904
1510
  }
905
1511
  }
@@ -912,15 +1518,29 @@ function findCallsInCode(code, parser) {
912
1518
  const ctx = value.namedChild(0);
913
1519
  const target = value.namedChildCount > 1 ? value.namedChild(value.namedChildCount - 1) : null;
914
1520
  const targetId = target?.type === 'as_pattern_target' ? target.namedChild(0) : null;
1521
+ const recordWithTargets = n => {
1522
+ if (!n) return;
1523
+ if (n.type === 'identifier') withBindingVars.add(n.text);
1524
+ for (let i = 0; i < n.namedChildCount; i++) recordWithTargets(n.namedChild(i));
1525
+ };
1526
+ recordWithTargets(targetId || target);
915
1527
  if (ctx?.type === 'call' && targetId?.type === 'identifier') {
916
1528
  const ctor = constructorTypeInfo(ctx.childForFieldName('function'));
917
1529
  if (ctor) {
918
1530
  localVarTypes.set(targetId.text, ctor.type);
919
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
1531
+ constructedReceiverVars.add(targetId.text);
1532
+ if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
920
1533
  localVarTypeQualifiers.set(targetId.text, ctor.qualifier);
921
1534
  } else {
922
1535
  localVarTypeQualifiers.delete(targetId.text);
923
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');
924
1544
  }
925
1545
  }
926
1546
  }
@@ -931,15 +1551,33 @@ function findCallsInCode(code, parser) {
931
1551
  const left = node.childForFieldName('left');
932
1552
  const right = node.childForFieldName('right');
933
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
+ }
934
1563
  // Track type annotation: x: Foo = ... → x is Foo
935
1564
  const typeNode = node.childForFieldName('type');
936
1565
  if (typeNode) {
937
1566
  const typeName = typeNameFromAnnotation(typeNode);
1567
+ const unionTypes = typeNamesFromAnnotation(typeNode);
1568
+ const itemTypes = iterableBindingTypes(typeNode);
938
1569
  if (typeName) {
939
1570
  localVarTypes.set(left.text, typeName);
1571
+ declaredVarTypes.set(left.text, typeName);
940
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);
941
1577
  }
942
1578
  memberAliases.delete(left.text); // any assignment rebinds the name
1579
+ constructedReceiverVars.delete(left.text);
1580
+ withBindingVars.delete(left.text);
943
1581
  // Rebinding without a known type makes any previously inferred
944
1582
  // type stale — nearest-preceding-assignment semantics (#199's
945
1583
  // documented rule). Without this, `x = ""; x = render(); x.m()`
@@ -947,11 +1585,59 @@ function findCallsInCode(code, parser) {
947
1585
  if (!typeNode) {
948
1586
  localVarTypes.delete(left.text);
949
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);
950
1600
  } else {
951
1601
  // An annotation is the authoritative type source; a
952
1602
  // previous constructor qualifier must not survive it.
953
1603
  localVarTypeQualifiers.delete(left.text);
954
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
+ }
955
1641
  // Literal assignment types the variable (fix #218):
956
1642
  // ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
957
1643
  // same trust grade as literal receivers ({}.get() → dict).
@@ -960,6 +1646,29 @@ function findCallsInCode(code, parser) {
960
1646
  if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
961
1647
  localVarTypes.set(left.text, litType);
962
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
+ }
963
1672
  if (right?.type === 'identifier') {
964
1673
  aliases.set(left.text, right.text);
965
1674
  }
@@ -1017,7 +1726,8 @@ function findCallsInCode(code, parser) {
1017
1726
  const ctor = constructorTypeInfo(right.childForFieldName('function'));
1018
1727
  if (ctor) {
1019
1728
  localVarTypes.set(left.text, ctor.type);
1020
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
1729
+ constructedReceiverVars.add(left.text);
1730
+ if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
1021
1731
  localVarTypeQualifiers.set(left.text, ctor.qualifier);
1022
1732
  } else {
1023
1733
  localVarTypeQualifiers.delete(left.text);
@@ -1042,7 +1752,8 @@ function findCallsInCode(code, parser) {
1042
1752
 
1043
1753
  const enclosingFunction = getCurrentEnclosingFunction();
1044
1754
  let uncertain = false;
1045
- const assignedTo = assignmentTargetOf(node);
1755
+ const assignedContext = contextTargetOf(node);
1756
+ const assignedTo = assignmentTargetOf(node) || assignedContext;
1046
1757
 
1047
1758
  // Call-site arg count (positional + keyword) for arity pruning.
1048
1759
  // *args/**kwargs splats make the count open-ended — flag them so
@@ -1082,6 +1793,7 @@ function findCallsInCode(code, parser) {
1082
1793
  ...(recvType && { receiverType: recvType }),
1083
1794
  ...(recvIsModule && { receiverIsModule: true }),
1084
1795
  ...(assignedTo && { assignedTo }),
1796
+ ...(assignedContext && { assignedContext: true }),
1085
1797
  argCount,
1086
1798
  ...(argSpread && { argSpread: true }),
1087
1799
  enclosingFunction,
@@ -1097,10 +1809,12 @@ function findCallsInCode(code, parser) {
1097
1809
  line: node.startPosition.row + 1,
1098
1810
  isMethod: false,
1099
1811
  ...(assignedTo && { assignedTo }),
1812
+ ...(assignedContext && { assignedContext: true }),
1100
1813
  argCount,
1101
1814
  ...(argSpread && { argSpread: true }),
1102
1815
  enclosingFunction,
1103
1816
  uncertain,
1817
+ ...(isShadowedByLocal(funcNode, funcNode.text) && { localShadow: true }),
1104
1818
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1105
1819
  });
1106
1820
  }
@@ -1112,6 +1826,7 @@ function findCallsInCode(code, parser) {
1112
1826
  if (attrNode) {
1113
1827
  let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
1114
1828
  let selfAttribute = undefined;
1829
+ const receiverPath = attributeReceiverPath(objNode);
1115
1830
  // Chained receiver (fix #219): the receiver IS a call —
1116
1831
  // fetch_data().json() — record the producer so findCallers
1117
1832
  // can type the receiver from its declared return
@@ -1129,10 +1844,17 @@ function findCallsInCode(code, parser) {
1129
1844
  }
1130
1845
  {
1131
1846
  let recvNode = objNode;
1132
- if (recvNode?.type === 'parenthesized_expression' &&
1133
- 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') {
1134
1852
  receiverCallAwaited = true;
1135
- 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
+ }
1136
1858
  }
1137
1859
  if (recvNode?.type === 'call' && receiver !== 'super') {
1138
1860
  const prodFunc = recvNode.childForFieldName('function');
@@ -1169,17 +1891,63 @@ function findCallsInCode(code, parser) {
1169
1891
 
1170
1892
  // Literal receivers carry their builtin type: {}.get() can
1171
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
+ }
1172
1904
  const receiverType = receiver
1173
- ? localVarTypes.get(receiver)
1174
- : (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
+ }
1175
1939
  const receiverTypeQualifier = receiver
1176
1940
  ? localVarTypeQualifiers.get(receiver)
1177
1941
  : undefined;
1942
+ const receiverRootType = receiverPath
1943
+ ? localVarTypes.get(receiverPath.root) : undefined;
1178
1944
  // Module receiver (httpx.get()) — unless locally shadowed
1179
1945
  // by a typed instance binding
1180
1946
  const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
1181
1947
  !localVarTypes.has(receiver);
1182
1948
  const firstArg = getFirstStringArg(node);
1949
+ const capabilityGuard = receiverCapabilityGuard(
1950
+ node, objNode, attrNode.text);
1183
1951
  calls.push({
1184
1952
  name: attrNode.text,
1185
1953
  // Multi-line chains (obj.x()\n.y()) must report each
@@ -1189,14 +1957,40 @@ function findCallsInCode(code, parser) {
1189
1957
  isMethod: true,
1190
1958
  receiver,
1191
1959
  ...(receiverType && { receiverType }),
1960
+ ...(receiver && localVarStdlibContracts.has(receiver) && {
1961
+ receiverTypeStdlibModule:
1962
+ localVarStdlibContracts.get(receiver),
1963
+ }),
1192
1964
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1965
+ ...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
1966
+ ...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
1193
1967
  ...(receiverIsModule && { receiverIsModule: true }),
1968
+ ...(receiver && objNode?.type === 'identifier' &&
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
+ }),
1194
1984
  ...(selfAttribute && { selfAttribute }),
1195
1985
  ...(receiverCall && { receiverCall }),
1196
1986
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1197
1987
  ...(receiverCallAwaited && { receiverCallAwaited: true }),
1198
1988
  ...(receiverCallLine && { receiverCallLine }),
1989
+ ...(capabilityGuard && {
1990
+ receiverCapabilityGuard: capabilityGuard,
1991
+ }),
1199
1992
  ...(assignedTo && { assignedTo }),
1993
+ ...(assignedContext && { assignedContext: true }),
1200
1994
  argCount,
1201
1995
  ...(argSpread && { argSpread: true }),
1202
1996
  enclosingFunction,
@@ -1268,11 +2062,53 @@ function findCallsInCode(code, parser) {
1268
2062
  localVarTypes.clear();
1269
2063
  for (const [k, v] of saved) localVarTypes.set(k, v);
1270
2064
  }
2065
+ const savedDeclared = declaredVarTypesStack.pop();
2066
+ if (savedDeclared) {
2067
+ declaredVarTypes.clear();
2068
+ for (const [k, v] of savedDeclared) declaredVarTypes.set(k, v);
2069
+ }
1271
2070
  const savedQualifiers = localVarTypeQualifiersStack.pop();
1272
2071
  if (savedQualifiers) {
1273
2072
  localVarTypeQualifiers.clear();
1274
2073
  for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
1275
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
+ }
2106
+ const savedConstructed = constructedReceiverVarsStack.pop();
2107
+ constructedReceiverVars.clear();
2108
+ if (savedConstructed) for (const name of savedConstructed) constructedReceiverVars.add(name);
2109
+ const savedWithBindings = withBindingVarsStack.pop();
2110
+ withBindingVars.clear();
2111
+ if (savedWithBindings) for (const name of savedWithBindings) withBindingVars.add(name);
1276
2112
  const savedAliases = memberAliasesStack.pop();
1277
2113
  if (savedAliases) {
1278
2114
  memberAliases.clear();
@@ -1304,13 +2140,33 @@ function findImportsInCode(code, parser) {
1304
2140
  for (let i = 0; i < node.namedChildCount; i++) {
1305
2141
  const child = node.namedChild(i);
1306
2142
  if (child.type === 'dotted_name') {
1307
- // import os
1308
- imports.push({
1309
- module: child.text,
1310
- names: [child.text.split('.').pop()],
1311
- type: 'import',
1312
- line
1313
- });
2143
+ // `import pkg.submodule` binds `pkg`, while also loading
2144
+ // the complete submodule. Record both ownership edges so
2145
+ // `pkg.public_api()` can resolve through pkg/__init__.py
2146
+ // and `pkg.submodule.member` can still resolve to the
2147
+ // imported child module.
2148
+ const parts = child.text.split('.');
2149
+ if (parts.length > 1) {
2150
+ imports.push({
2151
+ module: parts[0],
2152
+ names: [parts[0]],
2153
+ type: 'import',
2154
+ line
2155
+ });
2156
+ imports.push({
2157
+ module: child.text,
2158
+ names: [],
2159
+ type: 'import-submodule',
2160
+ line
2161
+ });
2162
+ } else {
2163
+ imports.push({
2164
+ module: child.text,
2165
+ names: [child.text],
2166
+ type: 'import',
2167
+ line
2168
+ });
2169
+ }
1314
2170
  } else if (child.type === 'aliased_import') {
1315
2171
  // import sys as system
1316
2172
  const nameNode = child.namedChild(0);
@@ -1414,6 +2270,33 @@ function findExportsInCode(code, parser) {
1414
2270
  const exports = [];
1415
2271
 
1416
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
+ }
1417
2300
  // Look for __all__ = [...]
1418
2301
  if (node.type === 'expression_statement') {
1419
2302
  const child = node.namedChild(0);
@@ -1537,6 +2420,16 @@ function findUsagesInCode(code, name, parser, tree) {
1537
2420
  usages.push({ line, column, usageType, receiver: object.text });
1538
2421
  return true;
1539
2422
  }
2423
+ // Constructed receiver: ColorTriplet(...).normalized is a
2424
+ // class-associated property reference, not an unowned bare
2425
+ // name. This matters to class-scoped `tests` queries.
2426
+ if (object && object.type === 'call') {
2427
+ const ctor = object.childForFieldName('function');
2428
+ if (ctor?.type === 'identifier') {
2429
+ usages.push({ line, column, usageType, receiver: ctor.text });
2430
+ return true;
2431
+ }
2432
+ }
1540
2433
  // self.attr receiver (unittest setUp idiom: self.w = Widget(3);
1541
2434
  // self.w.render()) — record the ATTR name so the instance-type
1542
2435
  // map built from the assignment line ('w' → Widget) matches
@@ -1567,9 +2460,10 @@ function findUsagesInCode(code, name, parser, tree) {
1567
2460
  * @param {object} parser - Tree-sitter parser instance
1568
2461
  * @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
1569
2462
  */
1570
- function findInstanceAttributeTypes(code, parser) {
2463
+ function findInstanceAttributeTypes(code, parser, options = {}) {
1571
2464
  const tree = parseTree(parser, code);
1572
2465
  const result = new Map(); // className -> Map(attrName -> typeName)
2466
+ const explicitContracts = explicitInstanceFieldContracts(tree, parser);
1573
2467
 
1574
2468
  const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
1575
2469
 
@@ -1584,6 +2478,23 @@ function findInstanceAttributeTypes(code, parser) {
1584
2478
  if (!body) return false;
1585
2479
 
1586
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
+ }
1587
2498
 
1588
2499
  // Scan annotated class-level fields: name: Type [= ...]. Originally
1589
2500
  // @dataclass-only (fix #28); a BARE class-body annotation is the
@@ -1691,6 +2602,19 @@ function findInstanceAttributeTypes(code, parser) {
1691
2602
  const typeName = extractConstructorName(rhs);
1692
2603
  if (typeName) {
1693
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
+ }
1694
2618
  } else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
1695
2619
  // self.X = param where param has type annotation
1696
2620
  attrTypes.set(attrName, paramTypes.get(rhs.text));
@@ -1700,6 +2624,63 @@ function findInstanceAttributeTypes(code, parser) {
1700
2624
  });
1701
2625
  }
1702
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
+
1703
2684
  if (attrTypes.size > 0) {
1704
2685
  result.set(className, attrTypes);
1705
2686
  }
@@ -1799,6 +2780,32 @@ function isEntryPoint(symbol) {
1799
2780
  return getEntryPointKind(symbol) !== null;
1800
2781
  }
1801
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
+
1802
2809
  module.exports = {
1803
2810
  findFunctions,
1804
2811
  findClasses,
@@ -1808,6 +2815,8 @@ module.exports = {
1808
2815
  findExportsInCode,
1809
2816
  findUsagesInCode,
1810
2817
  findInstanceAttributeTypes,
2818
+ getBuiltinCallReturnType,
2819
+ getBuiltinFieldType,
1811
2820
  isEntryPoint,
1812
2821
  getEntryPointKind,
1813
2822
  parse