ucn 5.2.1 → 5.2.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.
@@ -408,6 +408,24 @@ function pythonAliasBase(node) {
408
408
  return base ? (PY_ALIAS_RUNTIME_TYPES.get(base) || base) : null;
409
409
  }
410
410
 
411
+ function pythonAliasMembers(node) {
412
+ const current = unwrapTypeNode(node);
413
+ if (!current) return [];
414
+ if (current.type === 'binary_operator' && current.text.includes('|')) {
415
+ return [...new Set([
416
+ ...pythonAliasMembers(current.namedChild(0)),
417
+ ...pythonAliasMembers(current.namedChild(1)),
418
+ ])];
419
+ }
420
+ if (current.type === 'subscript' || current.type === 'generic_type') {
421
+ const parts = genericTypeParts(current);
422
+ if (!parts || !['Union', 'Optional'].includes(parts.base)) return [];
423
+ return [...new Set(parts.args.flatMap(pythonAliasMembers))];
424
+ }
425
+ const name = typeNameFromExpr(current);
426
+ return name && name !== 'None' ? [name] : [];
427
+ }
428
+
411
429
  /**
412
430
  * Python type aliases: PEP 695 `type X = int`, annotated
413
431
  * `X: TypeAlias = ...`, and module-scope static aliases such as
@@ -448,6 +466,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
448
466
  processedRanges.add(rangeKey);
449
467
  const { startLine, endLine } = nodeToLocation(node, lines);
450
468
  const aliasOf = pythonAliasBase(valueNode);
469
+ const aliasMembers = pythonAliasMembers(valueNode);
451
470
  classes.push({
452
471
  name,
453
472
  type: 'type',
@@ -456,6 +475,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
456
475
  methods: [],
457
476
  members: [],
458
477
  ...(aliasOf && { aliasOf }),
478
+ ...(aliasMembers.length > 1 && { aliasMembers }),
459
479
  });
460
480
  return true;
461
481
  }
@@ -969,8 +989,30 @@ function isinstanceTypes(condition, receiverName) {
969
989
  function narrowedReceiverType(refNode, receiverName, declaredUnion) {
970
990
  for (let current = refNode; current?.parent; current = current.parent) {
971
991
  const parent = current.parent;
972
- if (parent.type === 'if_statement') {
992
+ if (parent.type === 'if_statement' || parent.type === 'elif_clause') {
973
993
  const condition = parent.childForFieldName('condition');
994
+ const noneIdentity = condition?.type === 'comparison_operator' &&
995
+ condition.namedChildCount === 2 &&
996
+ condition.children.find(child =>
997
+ child.type === 'is' || child.type === 'is not');
998
+ const noneIndex = noneIdentity
999
+ ? [condition.namedChild(0), condition.namedChild(1)]
1000
+ .findIndex(child => child?.type === 'none')
1001
+ : -1;
1002
+ const compared = noneIndex >= 0
1003
+ ? condition.namedChild(1 - noneIndex) : null;
1004
+ if (compared?.type === 'identifier' &&
1005
+ compared.text === receiverName && declaredUnion?.length) {
1006
+ const remaining = declaredUnion.filter(type => type !== 'None');
1007
+ const consequence = parent.childForFieldName('consequence');
1008
+ const alternative = parent.childForFieldName('alternative');
1009
+ const inNonNullBranch = noneIdentity.type === 'is not'
1010
+ ? nodeContains(consequence, refNode)
1011
+ : nodeContains(alternative, refNode);
1012
+ if (inNonNullBranch && remaining.length === 1) {
1013
+ return remaining[0];
1014
+ }
1015
+ }
974
1016
  const positive = isinstanceTypes(condition, receiverName);
975
1017
  if (positive.length === 0) continue;
976
1018
  const consequence = parent.childForFieldName('consequence');
@@ -1122,18 +1164,21 @@ function contextTargetOf(callNode) {
1122
1164
  * pkg.ClassName(...). Preserve the qualifier: dropping `threading` from
1123
1165
  * `threading.Thread()` turns an external class into an unqualified project
1124
1166
  * type name and can falsely confirm `thread.join()` against Project.join.
1125
- * Uppercase-first remains a Python class-naming heuristic, so callers use the
1126
- * qualifier as routing/provenance evidence rather than a hidden hard claim.
1167
+ * PascalCase after optional privacy underscores remains a Python class-naming
1168
+ * heuristic, so callers use the qualifier as routing/provenance evidence
1169
+ * rather than a hidden hard claim. Private classes such as `_ClassBuilder`
1170
+ * are ordinary constructor values and must retain their type identity.
1127
1171
  */
1128
1172
  function constructorTypeInfo(funcNode) {
1129
1173
  if (!funcNode) return undefined;
1130
1174
  if (funcNode.type === 'identifier') {
1131
- return /^[A-Z]/.test(funcNode.text) ? { type: funcNode.text } : undefined;
1175
+ return /^_*[A-Z]/.test(funcNode.text)
1176
+ ? { type: funcNode.text } : undefined;
1132
1177
  }
1133
1178
  if (funcNode.type === 'attribute') {
1134
1179
  const attr = funcNode.childForFieldName('attribute');
1135
1180
  const object = funcNode.childForFieldName('object');
1136
- return attr && /^[A-Z]/.test(attr.text)
1181
+ return attr && /^_*[A-Z]/.test(attr.text)
1137
1182
  ? { type: attr.text, qualifier: object?.text || undefined }
1138
1183
  : undefined;
1139
1184
  }
@@ -1307,11 +1352,73 @@ const PY_COMPREHENSIONS = new Set([
1307
1352
  'dictionary_comprehension',
1308
1353
  ]);
1309
1354
 
1355
+ // Whether this reference is evaluated with `name` bound by an enclosing
1356
+ // for/comprehension target. Keep the check lexical: a nested function has its
1357
+ // own scope, and the iterable expression of `for x in source(x)` sees the
1358
+ // outer x rather than the new loop binding.
1359
+ function isPythonIterationBindingAt(refNode, name) {
1360
+ for (let parent = refNode?.parent; parent; parent = parent.parent) {
1361
+ if (parent.type === 'for_statement' &&
1362
+ pythonTargetBindsName(parent.childForFieldName('left'), name)) {
1363
+ const body = parent.childForFieldName('body');
1364
+ if (nodeContains(body, refNode)) return true;
1365
+ }
1366
+ if (PY_COMPREHENSIONS.has(parent.type)) {
1367
+ for (let i = 0; i < parent.namedChildCount; i++) {
1368
+ const clause = parent.namedChild(i);
1369
+ if (clause.type !== 'for_in_clause' ||
1370
+ !pythonTargetBindsName(
1371
+ clause.childForFieldName('left'), name)) continue;
1372
+ const iterable = clause.childForFieldName('right');
1373
+ if (!nodeContains(iterable, refNode)) return true;
1374
+ }
1375
+ }
1376
+ if (parent.type === 'function_definition' ||
1377
+ parent.type === 'async_function_definition' ||
1378
+ parent.type === 'lambda') break;
1379
+ }
1380
+ return false;
1381
+ }
1382
+
1383
+ // Stronger subset used for dispatch tiering: an element drawn from an
1384
+ // identifier/attribute iterable has unknown provenance. Direct call
1385
+ // producers are handled separately by assignedIter flow: external calls
1386
+ // demote there, while a project-internal producer intentionally keeps the
1387
+ // measured single-owner rule (#294's counter-probe).
1388
+ function isPythonUnprovenIterationBindingAt(refNode, name) {
1389
+ for (let parent = refNode?.parent; parent; parent = parent.parent) {
1390
+ if (parent.type === 'for_statement' &&
1391
+ pythonTargetBindsName(parent.childForFieldName('left'), name)) {
1392
+ const body = parent.childForFieldName('body');
1393
+ if (nodeContains(body, refNode)) {
1394
+ return unwrapTypeNode(parent.childForFieldName('right'))?.type !== 'call';
1395
+ }
1396
+ }
1397
+ if (PY_COMPREHENSIONS.has(parent.type)) {
1398
+ for (let i = 0; i < parent.namedChildCount; i++) {
1399
+ const clause = parent.namedChild(i);
1400
+ if (clause.type !== 'for_in_clause' ||
1401
+ !pythonTargetBindsName(
1402
+ clause.childForFieldName('left'), name)) continue;
1403
+ const iterable = clause.childForFieldName('right');
1404
+ if (!nodeContains(iterable, refNode)) {
1405
+ return unwrapTypeNode(iterable)?.type !== 'call';
1406
+ }
1407
+ }
1408
+ }
1409
+ if (parent.type === 'function_definition' ||
1410
+ parent.type === 'async_function_definition' ||
1411
+ parent.type === 'lambda') break;
1412
+ }
1413
+ return false;
1414
+ }
1415
+
1310
1416
  // Python locals are function-scoped: an assignment anywhere in the function
1311
1417
  // shadows an imported module for every reference in that function. Keep this
1312
1418
  // shared between call records and reference records so plan cannot promote a
1313
1419
  // receiver that callers would reject as locally rebound.
1314
1420
  function isPythonNameShadowedAt(refNode, name) {
1421
+ if (isPythonIterationBindingAt(refNode, name)) return true;
1315
1422
  for (let parent = refNode.parent; parent; parent = parent.parent) {
1316
1423
  if (PY_COMPREHENSIONS.has(parent.type)) {
1317
1424
  for (let i = 0; i < parent.namedChildCount; i++) {
@@ -1526,14 +1633,20 @@ function findCallsInCode(code, parser) {
1526
1633
  });
1527
1634
  const functionStack = []; // Stack of { name, startLine, endLine }
1528
1635
  const aliases = new Map(); // Track local aliases: aliasName -> originalName
1636
+ const aliasesStack = [];
1529
1637
  const nonCallableNames = new Set(); // Track names assigned non-callable values
1638
+ const nonCallableNamesStack = [];
1530
1639
  const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
1531
1640
  const declaredVarTypes = new Map(); // Compiler-checked annotations survive later assignments
1532
1641
  const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
1533
- const localVarUnionTypes = new Map(); // varName -> concrete PEP 604 alternatives
1642
+ // varName -> concrete union alternatives. `None` is retained only for a
1643
+ // complete constructor/nullable branch join so a later identity guard can
1644
+ // narrow it without typing unguarded calls.
1645
+ const localVarUnionTypes = new Map();
1534
1646
  const localIterableTypes = new Map(); // iterable binding -> loop-variable types
1535
1647
  const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
1536
1648
  const localDictValueTypes = new Map(); // local dict -> exact string-key value types
1649
+ const localSubscriptSources = new Map(); // local value -> exact typed container selected by []
1537
1650
  const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
1538
1651
  const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment receiver type
1539
1652
  const constructedReceiverVars = new Set(); // exact constructor-result bindings
@@ -1547,7 +1660,14 @@ function findCallsInCode(code, parser) {
1547
1660
  // rewritten call is then receiver-blind and routes through dispatch tiering.
1548
1661
  const memberAliases = new Map();
1549
1662
  const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
1663
+ // Exact class-value aliases: `_Segment = Segment` makes later
1664
+ // `_Segment.line()` dispatch through the imported/local class object.
1665
+ // Keep this separate from callable aliases: receiver evidence must be
1666
+ // scope-local, position-aware, and invalidated by every later write.
1667
+ const classValueAliases = new Map();
1668
+ const classValueAliasesStack = [];
1550
1669
  const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
1670
+ const moduleImportSpecifiers = new Set(); // Exact dotted imports: import rich.repr
1551
1671
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
1552
1672
  const declaredVarTypesStack = [];
1553
1673
  const localVarTypeQualifiersStack = [];
@@ -1555,6 +1675,7 @@ function findCallsInCode(code, parser) {
1555
1675
  const localIterableTypesStack = [];
1556
1676
  const localIterationSourcesStack = [];
1557
1677
  const localDictValueTypesStack = [];
1678
+ const localSubscriptSourcesStack = [];
1558
1679
  const localVarStdlibContractsStack = [];
1559
1680
  const constructedReceiverVarsStack = [];
1560
1681
  const withBindingVarsStack = [];
@@ -1644,6 +1765,33 @@ function findCallsInCode(code, parser) {
1644
1765
  };
1645
1766
 
1646
1767
  const isShadowedByLocal = isPythonNameShadowedAt;
1768
+ const isDirectScopeAssignment = assignment => {
1769
+ const statement = assignment?.parent?.type === 'expression_statement'
1770
+ ? assignment.parent : assignment;
1771
+ const parent = statement?.parent;
1772
+ if (parent?.type === 'module') return true;
1773
+ if (parent?.type !== 'block') return false;
1774
+ return ['function_definition', 'async_function_definition']
1775
+ .includes(parent.parent?.type);
1776
+ };
1777
+ const functionParameterBindsName = (functionNode, name) => {
1778
+ const params = functionNode?.childForFieldName('parameters');
1779
+ if (!params) return false;
1780
+ for (let i = 0; i < params.namedChildCount; i++) {
1781
+ const param = params.namedChild(i);
1782
+ let paramName = param.type === 'identifier'
1783
+ ? param
1784
+ : (param.childForFieldName('name') || param.namedChild(0));
1785
+ if (paramName && ['dictionary_splat_pattern', 'list_splat_pattern']
1786
+ .includes(paramName.type)) {
1787
+ paramName = paramName.namedChild(0);
1788
+ }
1789
+ if (paramName?.type === 'identifier' && paramName.text === name) {
1790
+ return true;
1791
+ }
1792
+ }
1793
+ return false;
1794
+ };
1647
1795
  const exactConstructorInfo = funcNode => {
1648
1796
  const ctor = constructorTypeInfo(funcNode);
1649
1797
  if (!ctor) return undefined;
@@ -1660,6 +1808,194 @@ function findCallsInCode(code, parser) {
1660
1808
  return moduleAliases.has(root) && !isShadowedByLocal(funcNode, root)
1661
1809
  ? ctor : undefined;
1662
1810
  };
1811
+ // Compiler-stable two-branch join (#330):
1812
+ // if value is not None and not isinstance(value, T): out = T(value)
1813
+ // else: out = value
1814
+ // The result is T | None. Require one direct assignment per branch and
1815
+ // exactly the two proving conjuncts; extra control flow must abstain.
1816
+ const nullableBranchJoinType = (assignment, targetName, source) => {
1817
+ if (!assignment || !source ||
1818
+ !['identifier', 'attribute'].includes(source.type)) return null;
1819
+ const block = assignment.parent?.parent;
1820
+ const elseClause = block?.parent;
1821
+ const statement = elseClause?.parent;
1822
+ if (block?.type !== 'block' || block.namedChildCount !== 1 ||
1823
+ elseClause?.type !== 'else_clause' ||
1824
+ statement?.type !== 'if_statement') return null;
1825
+
1826
+ const directAssignment = branch => {
1827
+ if (branch?.type !== 'block' || branch.namedChildCount !== 1) {
1828
+ return null;
1829
+ }
1830
+ const expression = branch.namedChild(0);
1831
+ const candidate = expression?.type === 'expression_statement'
1832
+ ? expression.namedChild(0) : null;
1833
+ return candidate?.type === 'assignment' &&
1834
+ candidate.childForFieldName('left')?.type === 'identifier' &&
1835
+ candidate.childForFieldName('left').text === targetName
1836
+ ? candidate.childForFieldName('right') : null;
1837
+ };
1838
+ const alternative = elseClause.childForFieldName('body') ||
1839
+ elseClause.namedChild(0);
1840
+ const alternativeRhs = directAssignment(alternative);
1841
+ if (alternativeRhs?.id !== source.id) return null;
1842
+
1843
+ const condition = statement.childForFieldName('condition');
1844
+ const andTerms = node => {
1845
+ const isAnd = node?.type === 'boolean_operator' &&
1846
+ node.children.some(child => child.type === 'and');
1847
+ return isAnd
1848
+ ? node.namedChildren.flatMap(andTerms)
1849
+ : (node ? [node] : []);
1850
+ };
1851
+ const terms = andTerms(condition);
1852
+ if (terms.length !== 2) return null;
1853
+ const sameSource = node => node?.type === source.type &&
1854
+ node.text === source.text;
1855
+ const nonNull = terms.find(term => {
1856
+ if (term.type !== 'comparison_operator' ||
1857
+ !term.children.some(child => child.type === 'is not') ||
1858
+ term.namedChildCount !== 2) return false;
1859
+ const parts = [term.namedChild(0), term.namedChild(1)];
1860
+ return parts.some(part => part?.type === 'none') &&
1861
+ parts.some(sameSource);
1862
+ });
1863
+ const negativeIsinstance = terms.find(term =>
1864
+ term.type === 'not_operator' &&
1865
+ term.namedChild(0)?.type === 'call');
1866
+ if (!nonNull || !negativeIsinstance) return null;
1867
+ const check = negativeIsinstance.namedChild(0);
1868
+ if (check.childForFieldName('function')?.type !== 'identifier' ||
1869
+ check.childForFieldName('function').text !== 'isinstance') return null;
1870
+ const args = check.childForFieldName('arguments');
1871
+ if (!args || args.namedChildCount !== 2 ||
1872
+ !sameSource(args.namedChild(0))) return null;
1873
+ const narrowedType = typeNameFromExpr(args.namedChild(1));
1874
+ if (!narrowedType) return null;
1875
+
1876
+ const consequence = statement.childForFieldName('consequence');
1877
+ const consequenceRhs = directAssignment(consequence);
1878
+ if (consequenceRhs?.type !== 'call') return null;
1879
+ const constructor = exactConstructorInfo(
1880
+ consequenceRhs.childForFieldName('function'));
1881
+ return constructor?.type === narrowedType
1882
+ ? [narrowedType, 'None'] : null;
1883
+ };
1884
+
1885
+ // A function may legally reference a module global declared later in the
1886
+ // file. The source-order walk cannot see that binding yet, so collect the
1887
+ // narrow compiler-stable subset up front: one direct module assignment to
1888
+ // a bare constructor, with no other module-scope write to the name. Keep
1889
+ // qualified constructors out of this forward pass because their module
1890
+ // import must itself be proven at the assignment position.
1891
+ const moduleBindingCounts = new Map();
1892
+ const forwardModuleConstructorCandidates = new Map();
1893
+ const moduleTargetNames = target => {
1894
+ if (!target) return [];
1895
+ if (target.type === 'identifier') return [target.text];
1896
+ if (!['tuple', 'pattern_list', 'list', 'list_pattern',
1897
+ 'tuple_pattern', 'list_splat_pattern',
1898
+ 'dictionary_splat_pattern'].includes(target.type)) return [];
1899
+ const names = [];
1900
+ for (let i = 0; i < target.namedChildCount; i++) {
1901
+ names.push(...moduleTargetNames(target.namedChild(i)));
1902
+ }
1903
+ return names;
1904
+ };
1905
+ const isModuleScopeNode = node => {
1906
+ for (let parent = node?.parent; parent; parent = parent.parent) {
1907
+ if (parent.type === 'function_definition' ||
1908
+ parent.type === 'class_definition' ||
1909
+ parent.type === 'lambda') return false;
1910
+ }
1911
+ return true;
1912
+ };
1913
+ const moduleBindingNodeTypes = new Set([
1914
+ 'assignment', 'augmented_assignment', 'named_expression',
1915
+ 'for_statement', 'function_definition', 'async_function_definition',
1916
+ 'class_definition', 'import_statement', 'import_from_statement',
1917
+ 'global_statement', 'delete_statement', 'as_pattern',
1918
+ ]);
1919
+ let hasModuleWildcardImport = false;
1920
+ traverseTreeCached(tree.rootNode, node => {
1921
+ if (!moduleBindingNodeTypes.has(node.type)) return true;
1922
+ if (node.type === 'global_statement') {
1923
+ for (let i = 0; i < node.namedChildCount; i++) {
1924
+ const child = node.namedChild(i);
1925
+ if (child.type !== 'identifier') continue;
1926
+ moduleBindingCounts.set(child.text,
1927
+ (moduleBindingCounts.get(child.text) || 0) + 1);
1928
+ }
1929
+ return true;
1930
+ }
1931
+ if (!isModuleScopeNode(node)) return true;
1932
+ let names = [];
1933
+ if (node.type === 'assignment' ||
1934
+ node.type === 'augmented_assignment' ||
1935
+ node.type === 'named_expression') {
1936
+ names = moduleTargetNames(
1937
+ node.childForFieldName('left') ||
1938
+ node.childForFieldName('name'));
1939
+ } else if (node.type === 'for_statement') {
1940
+ names = moduleTargetNames(node.childForFieldName('left'));
1941
+ } else if (node.type === 'function_definition' ||
1942
+ node.type === 'async_function_definition' ||
1943
+ node.type === 'class_definition') {
1944
+ names = moduleTargetNames(node.childForFieldName('name'));
1945
+ } else if (node.type === 'import_statement') {
1946
+ for (let i = 0; i < node.namedChildCount; i++) {
1947
+ const child = node.namedChild(i);
1948
+ if (child.type === 'aliased_import') {
1949
+ const alias = child.childForFieldName('alias');
1950
+ const imported = child.childForFieldName('name');
1951
+ if (alias) names.push(alias.text);
1952
+ else if (imported) names.push(imported.text.split('.')[0]);
1953
+ } else if (child.type === 'dotted_name') {
1954
+ names.push(child.text.split('.')[0]);
1955
+ }
1956
+ }
1957
+ } else if (node.type === 'import_from_statement') {
1958
+ for (let i = 0; i < node.namedChildCount; i++) {
1959
+ const child = node.namedChild(i);
1960
+ if (child.type === 'wildcard_import') {
1961
+ hasModuleWildcardImport = true;
1962
+ } else if (child.type === 'aliased_import') {
1963
+ const alias = child.childForFieldName('alias');
1964
+ const imported = child.childForFieldName('name');
1965
+ if (alias) names.push(alias.text);
1966
+ else if (imported) names.push(imported.text.split('.').pop());
1967
+ } else if (i > 0 && child.type === 'dotted_name') {
1968
+ names.push(child.text.split('.').pop());
1969
+ }
1970
+ }
1971
+ } else if (node.type === 'delete_statement') {
1972
+ for (let i = 0; i < node.namedChildCount; i++) {
1973
+ names.push(...moduleTargetNames(node.namedChild(i)));
1974
+ }
1975
+ } else if (node.type === 'as_pattern') {
1976
+ names = moduleTargetNames(
1977
+ node.childForFieldName('alias')?.namedChild(0));
1978
+ }
1979
+ for (const name of names) {
1980
+ moduleBindingCounts.set(name,
1981
+ (moduleBindingCounts.get(name) || 0) + 1);
1982
+ }
1983
+ if (node.type !== 'assignment' || names.length !== 1) return true;
1984
+ const statement = node.parent?.type === 'expression_statement'
1985
+ ? node.parent : node;
1986
+ if (statement.parent?.type !== 'module') return true;
1987
+ const right = node.childForFieldName('right');
1988
+ if (right?.type !== 'call') return true;
1989
+ const ctor = constructorTypeInfo(right.childForFieldName('function'));
1990
+ if (!ctor || ctor.qualifier ||
1991
+ isPythonConstructorValueShadowedAt(
1992
+ right.childForFieldName('function'), ctor.type)) return true;
1993
+ forwardModuleConstructorCandidates.set(names[0], ctor);
1994
+ return true;
1995
+ });
1996
+ const stableForwardModuleConstructors = new Map(
1997
+ [...forwardModuleConstructorCandidates].filter(([name]) =>
1998
+ !hasModuleWildcardImport && moduleBindingCounts.get(name) === 1));
1663
1999
 
1664
2000
  traverseTree(tree.rootNode, (node) => {
1665
2001
  // Track module-alias bindings: `import httpx` binds 'httpx' (a module),
@@ -1672,6 +2008,7 @@ function findCallsInCode(code, parser) {
1672
2008
  if (child.type === 'dotted_name') {
1673
2009
  const first = child.namedChild(0);
1674
2010
  if (first?.type === 'identifier') moduleAliases.add(first.text);
2011
+ moduleImportSpecifiers.add(child.text);
1675
2012
  } else if (child.type === 'aliased_import') {
1676
2013
  const alias = child.childForFieldName('alias');
1677
2014
  if (alias?.type === 'identifier') moduleAliases.add(alias.text);
@@ -1708,10 +2045,47 @@ function findCallsInCode(code, parser) {
1708
2045
  localDictValueTypesStack.push(new Map(
1709
2046
  [...localDictValueTypes].map(([name, values]) =>
1710
2047
  [name, new Map(values)])));
2048
+ localSubscriptSourcesStack.push(new Map(localSubscriptSources));
1711
2049
  localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
1712
2050
  constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
1713
2051
  withBindingVarsStack.push(new Set(withBindingVars));
1714
2052
  memberAliasesStack.push(new Map(memberAliases));
2053
+ classValueAliasesStack.push(new Map(classValueAliases));
2054
+ aliasesStack.push(new Map(aliases));
2055
+ nonCallableNamesStack.push(new Set(nonCallableNames));
2056
+ const body = node.childForFieldName('body');
2057
+ for (const [name, ctor] of stableForwardModuleConstructors) {
2058
+ if (functionParameterBindsName(node, name) ||
2059
+ (body && pythonScopeBindsName(body, name))) continue;
2060
+ if (!localVarTypes.has(name)) {
2061
+ localVarTypes.set(name, ctor.type);
2062
+ constructedReceiverVars.add(name);
2063
+ }
2064
+ }
2065
+ for (const name of aliases.keys()) {
2066
+ if ((body && pythonScopeBindsName(body, name)) ||
2067
+ functionParameterBindsName(node, name)) {
2068
+ aliases.delete(name);
2069
+ }
2070
+ }
2071
+ for (const name of nonCallableNames) {
2072
+ if ((body && pythonScopeBindsName(body, name)) ||
2073
+ functionParameterBindsName(node, name)) {
2074
+ nonCallableNames.delete(name);
2075
+ }
2076
+ }
2077
+ for (const name of localSubscriptSources.keys()) {
2078
+ if ((body && pythonScopeBindsName(body, name)) ||
2079
+ functionParameterBindsName(node, name)) {
2080
+ localSubscriptSources.delete(name);
2081
+ }
2082
+ }
2083
+ for (const name of classValueAliases.keys()) {
2084
+ if ((body && pythonScopeBindsName(body, name)) ||
2085
+ functionParameterBindsName(node, name)) {
2086
+ classValueAliases.delete(name);
2087
+ }
2088
+ }
1715
2089
  }
1716
2090
 
1717
2091
  // Track parameter type annotations: def foo(x: Foo) → x is Foo
@@ -1826,6 +2200,8 @@ function findCallsInCode(code, parser) {
1826
2200
  const left = node.childForFieldName('left');
1827
2201
  const right = node.childForFieldName('right');
1828
2202
  if (left?.type === 'identifier') {
2203
+ classValueAliases.delete(left.text);
2204
+ localSubscriptSources.delete(left.text);
1829
2205
  const previousType = localVarTypes.get(left.text);
1830
2206
  if (previousType && right?.type === 'call') {
1831
2207
  const rightFunction = right.childForFieldName('function');
@@ -1837,6 +2213,8 @@ function findCallsInCode(code, parser) {
1837
2213
  }
1838
2214
  // Track type annotation: x: Foo = ... → x is Foo
1839
2215
  const typeNode = node.childForFieldName('type');
2216
+ const branchJoinUnion = !typeNode
2217
+ ? nullableBranchJoinType(node, left.text, right) : null;
1840
2218
  if (typeNode) {
1841
2219
  const typeName = typeNameFromAnnotation(typeNode);
1842
2220
  const unionTypes = typeNamesFromAnnotation(typeNode);
@@ -1865,6 +2243,9 @@ function findCallsInCode(code, parser) {
1865
2243
  localIterationSources.delete(left.text);
1866
2244
  localDictValueTypes.delete(left.text);
1867
2245
  localVarStdlibContracts.delete(left.text);
2246
+ if (branchJoinUnion) {
2247
+ localVarUnionTypes.set(left.text, branchJoinUnion);
2248
+ }
1868
2249
  // Python assignments remain constrained by a variable or
1869
2250
  // parameter annotation. Constructor/literal inference is
1870
2251
  // nearest-assignment only, but a declared contract is
@@ -1944,6 +2325,20 @@ function findCallsInCode(code, parser) {
1944
2325
  localDictValueTypes.set(left.text, valueTypes);
1945
2326
  }
1946
2327
  }
2328
+ if (!typeNode && right?.type === 'subscript') {
2329
+ const base = right.childForFieldName('value');
2330
+ if (base?.type === 'identifier' && base.text !== left.text) {
2331
+ const rootType = localVarTypes.get(base.text);
2332
+ if (rootType) {
2333
+ localSubscriptSources.set(left.text, {
2334
+ root: base.text,
2335
+ rootType,
2336
+ rootTypeQualifier:
2337
+ localVarTypeQualifiers.get(base.text),
2338
+ });
2339
+ }
2340
+ }
2341
+ }
1947
2342
  const roundTripSource = pickleRoundTripSource(right);
1948
2343
  const roundTripType = roundTripSource
1949
2344
  ? localVarTypes.get(roundTripSource) : null;
@@ -1953,6 +2348,11 @@ function findCallsInCode(code, parser) {
1953
2348
  }
1954
2349
  if (right?.type === 'identifier') {
1955
2350
  aliases.set(left.text, right.text);
2351
+ const classValue = !typeNode && isDirectScopeAssignment(node)
2352
+ ? exactConstructorInfo(right) : null;
2353
+ if (classValue) {
2354
+ classValueAliases.set(left.text, classValue.type);
2355
+ }
1956
2356
  }
1957
2357
  // Member-access alias (fix #218): append = output.append
1958
2358
  else if (right?.type === 'attribute') {
@@ -2197,13 +2597,31 @@ function findCallsInCode(code, parser) {
2197
2597
  // Literal receivers carry their builtin type: {}.get() can
2198
2598
  // never be a project class method
2199
2599
  let subscriptReceiverType;
2600
+ let receiverSubscriptRoot;
2601
+ let receiverSubscriptRootType;
2602
+ let receiverSubscriptRootTypeQualifier;
2200
2603
  if (objNode?.type === 'subscript') {
2201
2604
  const base = objNode.childForFieldName('value');
2202
2605
  const key = literalStringValue(
2203
2606
  objNode.childForFieldName('subscript'));
2204
- if (base?.type === 'identifier' && key != null) {
2205
- subscriptReceiverType =
2206
- localDictValueTypes.get(base.text)?.get(key);
2607
+ if (base?.type === 'identifier') {
2608
+ receiverSubscriptRoot = base.text;
2609
+ receiverSubscriptRootType = localVarTypes.get(base.text);
2610
+ receiverSubscriptRootTypeQualifier =
2611
+ localVarTypeQualifiers.get(base.text);
2612
+ if (key != null) {
2613
+ subscriptReceiverType =
2614
+ localDictValueTypes.get(base.text)?.get(key);
2615
+ }
2616
+ }
2617
+ }
2618
+ if (!receiverSubscriptRoot && receiver) {
2619
+ const source = localSubscriptSources.get(receiver);
2620
+ if (source) {
2621
+ receiverSubscriptRoot = source.root;
2622
+ receiverSubscriptRootType = source.rootType;
2623
+ receiverSubscriptRootTypeQualifier =
2624
+ source.rootTypeQualifier;
2207
2625
  }
2208
2626
  }
2209
2627
  const receiverType = receiver
@@ -2212,7 +2630,8 @@ function findCallsInCode(code, parser) {
2212
2630
  comprehensionReceiverType(
2213
2631
  objNode, receiver, localIterableTypes,
2214
2632
  callableIterableTypes, instanceFieldContracts) ||
2215
- localVarTypes.get(receiver))
2633
+ localVarTypes.get(receiver) ||
2634
+ classValueAliases.get(receiver))
2216
2635
  || assignmentRhsReceiverTypes.get(node.id)
2217
2636
  : (subscriptReceiverType ||
2218
2637
  (objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
@@ -2246,6 +2665,13 @@ function findCallsInCode(code, parser) {
2246
2665
  : undefined;
2247
2666
  const receiverRootType = receiverPath
2248
2667
  ? localVarTypes.get(receiverPath.root) : undefined;
2668
+ const dottedReceiver = receiverPath
2669
+ ? [receiverPath.root, ...receiverPath.fields].join('.')
2670
+ : null;
2671
+ const receiverModuleSpecifier = dottedReceiver &&
2672
+ moduleImportSpecifiers.has(dottedReceiver) &&
2673
+ !isShadowedByLocal(objNode, receiverPath.root)
2674
+ ? dottedReceiver : undefined;
2249
2675
  // Module receiver (httpx.get()) — unless locally shadowed
2250
2676
  // by a typed instance binding
2251
2677
  const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
@@ -2270,8 +2696,20 @@ function findCallsInCode(code, parser) {
2270
2696
  ...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
2271
2697
  ...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
2272
2698
  ...(receiverIsModule && { receiverIsModule: true }),
2699
+ ...(receiverModuleSpecifier && { receiverModuleSpecifier }),
2700
+ ...(receiverSubscriptRoot && { receiverSubscriptRoot }),
2701
+ ...(receiverSubscriptRootType && {
2702
+ receiverSubscriptRootType,
2703
+ }),
2704
+ ...(receiverSubscriptRootTypeQualifier && {
2705
+ receiverSubscriptRootTypeQualifier,
2706
+ }),
2273
2707
  ...(receiver && objNode?.type === 'identifier' &&
2274
2708
  isShadowedByLocal(objNode, receiver) && { receiverLocalBinding: true }),
2709
+ ...(receiver && !receiverType && objNode?.type === 'identifier' &&
2710
+ isPythonUnprovenIterationBindingAt(objNode, receiver) && {
2711
+ receiverUntypedIteration: true,
2712
+ }),
2275
2713
  ...(receiverPath && {
2276
2714
  receiverRoot: receiverPath.root,
2277
2715
  receiverField: receiverPath.fields[receiverPath.fields.length - 1],
@@ -2432,6 +2870,13 @@ function findCallsInCode(code, parser) {
2432
2870
  localDictValueTypes.set(name, values);
2433
2871
  }
2434
2872
  }
2873
+ const savedSubscriptSources = localSubscriptSourcesStack.pop();
2874
+ if (savedSubscriptSources) {
2875
+ localSubscriptSources.clear();
2876
+ for (const [name, source] of savedSubscriptSources) {
2877
+ localSubscriptSources.set(name, source);
2878
+ }
2879
+ }
2435
2880
  const savedStdlibContracts = localVarStdlibContractsStack.pop();
2436
2881
  if (savedStdlibContracts) {
2437
2882
  localVarStdlibContracts.clear();
@@ -2450,6 +2895,25 @@ function findCallsInCode(code, parser) {
2450
2895
  memberAliases.clear();
2451
2896
  for (const [k, v] of savedAliases) memberAliases.set(k, v);
2452
2897
  }
2898
+ const savedClassValueAliases = classValueAliasesStack.pop();
2899
+ if (savedClassValueAliases) {
2900
+ classValueAliases.clear();
2901
+ for (const [k, v] of savedClassValueAliases) {
2902
+ classValueAliases.set(k, v);
2903
+ }
2904
+ }
2905
+ const savedCallableAliases = aliasesStack.pop();
2906
+ if (savedCallableAliases) {
2907
+ aliases.clear();
2908
+ for (const [k, v] of savedCallableAliases) aliases.set(k, v);
2909
+ }
2910
+ const savedNonCallableNames = nonCallableNamesStack.pop();
2911
+ if (savedNonCallableNames) {
2912
+ nonCallableNames.clear();
2913
+ for (const name of savedNonCallableNames) {
2914
+ nonCallableNames.add(name);
2915
+ }
2916
+ }
2453
2917
  }
2454
2918
  }
2455
2919
  });
@@ -2834,6 +3298,50 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
2834
3298
  const explicitContracts = explicitInstanceFieldContracts(tree, parser);
2835
3299
 
2836
3300
  const PRIMITIVE_TYPES = new Set(['int', 'float', 'str', 'bool', 'bytes', 'list', 'dict', 'set', 'tuple', 'None', 'Any', 'object']);
3301
+ const conditionalFieldType = (node, parameterAlternatives) => {
3302
+ let current = node;
3303
+ while (current?.type === 'parenthesized_expression' &&
3304
+ current.namedChildCount === 1) {
3305
+ current = current.namedChild(0);
3306
+ }
3307
+ if (current?.type !== 'conditional_expression' ||
3308
+ current.namedChildCount < 3) return null;
3309
+ const consequence = current.namedChild(0);
3310
+ const condition = current.namedChild(1);
3311
+ const alternative = current.namedChild(2);
3312
+ const parameterBranches = [consequence, alternative]
3313
+ .map((branch, index) => branch?.type === 'identifier' &&
3314
+ parameterAlternatives.has(branch.text)
3315
+ ? { index, name: branch.text } : null)
3316
+ .filter(Boolean);
3317
+ if (parameterBranches.length !== 1) return null;
3318
+ const parameterBranch = parameterBranches[0];
3319
+ const positiveTypes = isinstanceTypes(condition, parameterBranch.name);
3320
+ if (positiveTypes.length === 0) return null;
3321
+ const declaredTypes = parameterAlternatives.get(parameterBranch.name);
3322
+ const narrowedTypes = parameterBranch.index === 0
3323
+ ? declaredTypes.filter(type => positiveTypes.includes(type))
3324
+ : declaredTypes.filter(type => !positiveTypes.includes(type));
3325
+ if (narrowedTypes.length !== 1) return null;
3326
+
3327
+ const valueBranch = parameterBranch.index === 0
3328
+ ? alternative : consequence;
3329
+ if (valueBranch?.type !== 'call') return null;
3330
+ const callable = valueBranch.childForFieldName('function');
3331
+ let valueType = null;
3332
+ if (callable?.type === 'identifier' && /^[A-Z]/.test(callable.text) &&
3333
+ !isPythonConstructorValueShadowedAt(callable, callable.text)) {
3334
+ valueType = callable.text;
3335
+ } else if (callable?.type === 'attribute') {
3336
+ const owner = callable.childForFieldName('object');
3337
+ const method = callable.childForFieldName('attribute');
3338
+ if (owner?.type === 'identifier' && method?.type === 'identifier' &&
3339
+ !isPythonNameShadowedAt(callable, owner.text)) {
3340
+ valueType = options.resolveCallType?.(owner.text, method.text);
3341
+ }
3342
+ }
3343
+ return valueType === narrowedTypes[0] ? valueType : null;
3344
+ };
2837
3345
 
2838
3346
  traverseTreeCached(tree.rootNode, (node) => {
2839
3347
  if (node.type !== 'class_definition') return true;
@@ -2888,10 +3396,14 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
2888
3396
  if (!typeNode) continue;
2889
3397
  if (!isDataclass && assign.childForFieldName('right')) continue;
2890
3398
 
2891
- // Extract type name from annotation
2892
- const typeIdent = typeNode.type === 'type' ? typeNode.firstChild : typeNode;
2893
- if (!typeIdent || typeIdent.type !== 'identifier') continue;
2894
- const typeName = typeIdent.text;
3399
+ // Use the same single-type annotation parser as parameters and
3400
+ // explicit instance fields. This admits compiler-equivalent bare
3401
+ // contracts such as `color: Optional[Color]` and `Color | None`,
3402
+ // while unions with several value types still abstain. Qualified
3403
+ // names need origin metadata that this compact attr map does not
3404
+ // retain, so leave those to declared field symbols.
3405
+ const typeName = typeNameFromAnnotation(typeNode);
3406
+ if (!typeName || typeQualifierFromAnnotation(typeNode)) continue;
2895
3407
 
2896
3408
  // Skip primitives and lowercase types
2897
3409
  if (PRIMITIVE_TYPES.has(typeName)) continue;
@@ -2927,6 +3439,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
2927
3439
  // Build parameter type map from __init__ annotations
2928
3440
  // e.g. def __init__(self, market: MarketDataFetcher = None) → {market: MarketDataFetcher}
2929
3441
  const paramTypes = new Map();
3442
+ const paramAlternatives = new Map();
2930
3443
  const params = child.childForFieldName('parameters');
2931
3444
  if (params) {
2932
3445
  for (let p = 0; p < params.childCount; p++) {
@@ -2936,12 +3449,25 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
2936
3449
  const pName = param.childForFieldName('name') || param.child(0);
2937
3450
  const pType = param.childForFieldName('type');
2938
3451
  if (pName && pType) {
2939
- const typeIdent = pType.type === 'type' ? pType.firstChild : pType;
2940
- if (typeIdent?.type === 'identifier') {
2941
- const tn = typeIdent.text;
2942
- if (!PRIMITIVE_TYPES.has(tn) && tn[0] >= 'A' && tn[0] <= 'Z') {
2943
- paramTypes.set(pName.text, tn);
2944
- }
3452
+ // Use the shared annotation parser so forward
3453
+ // references (`"Environment"`), dotted names,
3454
+ // and Optional/union wrappers participate in the
3455
+ // same conservative receiver-type contract as
3456
+ // ordinary parameter calls.
3457
+ const tn = typeNameFromAnnotation(pType);
3458
+ const directAlternatives = typeNamesFromAnnotation(pType);
3459
+ const aliasAlternatives = directAlternatives.length === 1
3460
+ ? options.resolveTypeAliasMembers?.(
3461
+ directAlternatives[0])
3462
+ : null;
3463
+ const alternatives = aliasAlternatives?.length > 1
3464
+ ? aliasAlternatives : directAlternatives;
3465
+ if (alternatives.length > 1) {
3466
+ paramAlternatives.set(pName.text, alternatives);
3467
+ }
3468
+ if (tn && !PRIMITIVE_TYPES.has(tn) &&
3469
+ tn[0] >= 'A' && tn[0] <= 'Z') {
3470
+ paramTypes.set(pName.text, tn);
2945
3471
  }
2946
3472
  }
2947
3473
  }
@@ -2967,7 +3493,8 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
2967
3493
  const rhs = assign.childForFieldName('right');
2968
3494
  if (!rhs) return true;
2969
3495
 
2970
- const typeName = extractConstructorName(rhs);
3496
+ const typeName = conditionalFieldType(rhs, paramAlternatives) ||
3497
+ extractConstructorName(rhs);
2971
3498
  if (typeName) {
2972
3499
  attrTypes.set(attrName, typeName);
2973
3500
  } else if (rhs.type === 'call') {
@@ -3022,13 +3549,23 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
3022
3549
  const right = assignment.childForFieldName('right');
3023
3550
  const callable = right?.type === 'call'
3024
3551
  ? right.childForFieldName('function') : null;
3025
- const moduleName = callable?.type === 'attribute'
3026
- ? callable.childForFieldName('object')?.text : null;
3552
+ const callableReceiver = callable?.type === 'attribute'
3553
+ ? callable.childForFieldName('object') : null;
3554
+ const directConstructor = callable?.type === 'identifier' &&
3555
+ /^[A-Z]/.test(callable.text) &&
3556
+ !isPythonConstructorValueShadowedAt(callable, callable.text)
3557
+ ? callable.text : null;
3558
+ const moduleName = callableReceiver?.type === 'identifier'
3559
+ ? callableReceiver.text : null;
3027
3560
  const functionName = callable?.type === 'attribute'
3028
3561
  ? callable.childForFieldName('attribute')?.text : null;
3029
- const runtimeType = moduleName && functionName
3030
- ? options.resolveBuiltinCallType?.(moduleName, functionName)
3031
- : null;
3562
+ const resolveCallType = options.resolveCallType ||
3563
+ options.resolveBuiltinCallType;
3564
+ const runtimeType = directConstructor ||
3565
+ (moduleName && functionName &&
3566
+ !isPythonNameShadowedAt(callable, moduleName)
3567
+ ? resolveCallType?.(moduleName, functionName)
3568
+ : null);
3032
3569
  if (!runtimeType) {
3033
3570
  // None is a harmless uninitialized state; any other
3034
3571
  // unknown write could replace the field with a project