Skip to content

Commit fe98edc

Browse files
authored
Fix new style union aliases with not ready items (#22172)
Fixes #21263 Fixes #11887 Old style aliases with new style unions are quite tricky, since this requires some heuristics to figure out whether this is a type alias or a regular variable. Things get even trickier when some items are not ready, because our heuristics must be robust w.r.t. to this. I spent few hours today playing with this, and I don't think there is a fully-principled fix that doesn't involve rewriting a bunch of logic. But I think I have something that is still fairly principled. Namely, in this PR: * Handle `becomes_typeinfo` status for imported placeholders consistently. * Put `should_wait_rhs()` back in sync with `can_be_type_alias()` w.r.t. `OpExpr`. * Add "backtracking" logic (maybe type alias -> actually a regular variable) for one rare case that is tricky to handle otherwise. Note, the root cause of the crash is that we normally never change a type alias back to a regular variable. Normal flow for a symbol table entry is always like this `None -> PlaceholderNode -> PlaceholderNode(becomes_typeinfo=True) -> SymbolNode` (where some steps may be skipped). A lot of logic depends on this, but in the relevant scenarios it is tricky to follow this principle.
1 parent 69dcb38 commit fe98edc

3 files changed

Lines changed: 88 additions & 3 deletions

File tree

‎mypy/semanal.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,7 +3136,7 @@ def process_imported_symbol(
31363136
node.node,
31373137
module_public=module_public,
31383138
module_hidden=module_hidden,
3139-
becomes_typeinfo=True,
3139+
becomes_typeinfo=node.node.becomes_typeinfo,
31403140
)
31413141
# NOTE: we take the original node even for final `Var`s. This is to support
31423142
# a common pattern when constants are re-exported (same applies to import *).
@@ -3385,6 +3385,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
33853385
special_form = True
33863386
elif self.analyze_enum_assign(s):
33873387
special_form = True
3388+
elif isinstance(s.rvalue, OpExpr):
3389+
# In rare cases we may need to "backtrack", and make something we thought is
3390+
# an alias into a regular variable, see testUnionNewSyntaxUndefinedAttrNoCrash.
3391+
s.rvalue.analyzed = None
33883392

33893393
if special_form:
33903394
self.record_special_form_lvalue(s)
@@ -3533,6 +3537,11 @@ def should_wait_rhs(self, rv: Expression) -> bool:
35333537
return True
35343538
elif isinstance(rv, IndexExpr) and isinstance(rv.base, RefExpr):
35353539
return self.should_wait_rhs(rv.base)
3540+
if isinstance(rv, OpExpr) and rv.op == "|":
3541+
if self.is_stub_file:
3542+
return False
3543+
if self.should_wait_rhs(rv.left) or self.should_wait_rhs(rv.right):
3544+
return True
35363545
elif isinstance(rv, CallExpr) and isinstance(rv.callee, RefExpr):
35373546
# This is only relevant for builtin SCC where things like 'TypeVar'
35383547
# may be not ready.
@@ -3543,7 +3552,7 @@ def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool:
35433552
"""Is this a valid r.h.s. for an alias definition?
35443553
35453554
Note: this function should be only called for expressions where self.should_wait_rhs()
3546-
returns False.
3555+
returns False, keep these two functions "in sync".
35473556
"""
35483557
if isinstance(rv, RefExpr) and self.is_type_ref(rv, bare=True):
35493558
return True
@@ -4251,6 +4260,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
42514260
# annotations (see the second rule).
42524261
return False
42534262
if not pep_613 and not pep_695 and not self.can_be_type_alias(rvalue):
4263+
# Backtrack potential alias to a regular variable.
4264+
if existing and isinstance(existing.node, TypeAlias):
4265+
del self.current_symbol_table()[lvalue.name]
42544266
return False
42554267

42564268
if existing and not isinstance(existing.node, (PlaceholderNode, TypeAlias)):
@@ -6862,6 +6874,10 @@ def lookup_qualified(
68626874
nextsym = self.get_module_symbol(node, part)
68636875
namespace = node.fullname
68646876
elif isinstance(node, PlaceholderNode):
6877+
# TODO: find a cleaner way to handle this.
6878+
# We don't know if the resolved placeholder will have the attribute.
6879+
# But if we try to be strict here, we will break some use cases like:
6880+
# Alias = Outer.NestedOne | Outer.NestedTwo
68656881
return sym
68666882
elif isinstance(node, TypeAlias) and node.no_args:
68676883
assert isinstance(node.target, ProperType)

‎test-data/unit/check-newsemanal.test‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ from b import bad # E: Module "b" has no attribute "bad"
7575
[file b.py]
7676
# TODO: Could we generate an error here as well?
7777
from a import bad
78-
[targets a, b, a, b, a, b, a, b, __main__]
78+
[targets a, b, a, b, a, b, __main__]
7979

8080
[case testNewAnalyzerExportedValuesInImportAll]
8181
from m import *
@@ -3248,3 +3248,71 @@ def test() -> None:
32483248

32493249
class Model:
32503250
pass
3251+
3252+
[case testUnionNewSyntaxUndefinedAttrNoCrash]
3253+
class Test(Flag): # E: Name "Flag" is not defined
3254+
A = auto() # E: Name "auto" is not defined
3255+
B = auto() # E: Name "auto" is not defined
3256+
3257+
AB = Test.A | Test.B
3258+
3259+
[case testUnionNewSyntaxUndefinedAttrRegular]
3260+
class Test(Flag): # E: Name "Flag" is not defined
3261+
A = 1
3262+
B = 2
3263+
3264+
AB = Test.A | Test.B
3265+
reveal_type(AB) # N: Revealed type is "builtins.int"
3266+
[builtins fixtures/ops.pyi]
3267+
3268+
[case testUnionNewSyntaxUndefinedAttrRegularReassign]
3269+
class Test(Flag): # E: Name "Flag" is not defined
3270+
A = 1
3271+
B = 2
3272+
3273+
AB = Test.A | Test.B
3274+
C = AB
3275+
reveal_type(AB) # N: Revealed type is "builtins.int"
3276+
reveal_type(C) # N: Revealed type is "builtins.int"
3277+
[builtins fixtures/ops.pyi]
3278+
3279+
[case testUnionNewSyntaxNested]
3280+
class Test(Flag): # E: Name "Flag" is not defined
3281+
class A: ...
3282+
class B: ...
3283+
3284+
AB = Test.A | Test.B
3285+
ab: AB
3286+
reveal_type(ab) # N: Revealed type is "__main__.Test.A | __main__.Test.B"
3287+
3288+
[case testUnionNewSyntaxImportCycleNoCrash]
3289+
import m
3290+
[file m.py]
3291+
from lib import A
3292+
3293+
AB = int | A
3294+
3295+
[file lib.py]
3296+
from m import whatever # type: ignore
3297+
3298+
A = whatever.A
3299+
3300+
[case testAliasToNestedClassInsideClassDependingOnAlias]
3301+
class D(list[A]):
3302+
class C: ...
3303+
3304+
A = D.C
3305+
x: A
3306+
reveal_type(x) # N: Revealed type is "__main__.D.C"
3307+
3308+
[case testUnionNewSyntaxDeepDeferral]
3309+
# flags: --disable-error-code=used-before-def
3310+
3311+
A = str | B
3312+
3313+
B = C
3314+
C = D
3315+
class D: ...
3316+
3317+
x: A
3318+
reveal_type(x) # N: Revealed type is "builtins.str | __main__.D"

‎test-data/unit/fixtures/ops.pyi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class int:
4343
def __rtruediv__(self, x: 'int') -> 'int': pass
4444
def __mod__(self, x: 'int') -> 'int': pass
4545
def __floordiv__(self, x: 'int') -> 'int': pass
46+
def __or__(self, x: 'int') -> 'int': pass
4647
def __pow__(self, x: 'int', __modulo: Optional[int] = ...) -> Any: pass
4748
def __pos__(self) -> 'int': pass
4849
def __neg__(self) -> 'int': pass

0 commit comments

Comments
 (0)