Skip to content

Commit bfabdb3

Browse files
authored
New semantic analyzer: fix corner cases in aliases to not ready classes (and other aliases) (#6390)
Fixes #6355 This does three non-trivial things: * Creates placeholder with `becomes_typeinfo=True` if r.h.s. is such a placeholder (i.e. the definition is a potential type alias). * Defers all `Invalid type` and use of corresponding unbound types until final iteration. * Moves few tests in newly enabled test file to new semantic analyzer only (those where new behaviour is clearly better). I added a bunch of new tests for things that was broken, plus some variations (although some of them are trivial, replacing `NameExpr` with `MemberExpr` or `IndexExpr`). I also verified that this PR fixes the crash in mypy self-check.
1 parent e8c7ee7 commit bfabdb3

File tree

7 files changed

+262
-30
lines changed

7 files changed

+262
-30
lines changed

mypy/newsemanal/semanal.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ class NewSemanticAnalyzer(NodeVisitor[None],
213213
# Stack of functions being analyzed
214214
function_stack = None # type: List[FuncItem]
215215

216+
# Is this the final iteration of semantic analysis?
217+
final_iteration = False
218+
216219
loop_depth = 0 # Depth of breakable loops
217220
cur_mod_id = '' # Current module id (or None) (phase 2)
218221
is_stub_file = False # Are we analyzing a stub file?
@@ -1875,16 +1878,49 @@ def add_type_alias_deps(self, aliases_used: Iterable[str],
18751878
target = self.scope.current_target()
18761879
self.cur_mod_node.alias_deps[target].update(aliases_used)
18771880

1881+
def is_not_ready_type_ref(self, rv: Expression) -> bool:
1882+
"""Does this expression refers to a not-ready class?
1883+
1884+
This includes 'Ref' and 'Ref[Arg1, Arg2, ...]', where 'Ref'
1885+
refers to a PlaceholderNode with becomes_typeinfo=True.
1886+
"""
1887+
if isinstance(rv, IndexExpr) and isinstance(rv.base, RefExpr):
1888+
return self.is_not_ready_type_ref(rv.base)
1889+
if isinstance(rv, NameExpr):
1890+
n = self.lookup(rv.name, rv)
1891+
if n and isinstance(n.node, PlaceholderNode) and n.node.becomes_typeinfo:
1892+
return True
1893+
elif isinstance(rv, MemberExpr):
1894+
fname = get_member_expr_fullname(rv)
1895+
if fname:
1896+
n = self.lookup_qualified(fname, rv)
1897+
if n and isinstance(n.node, PlaceholderNode) and n.node.becomes_typeinfo:
1898+
return True
1899+
return False
1900+
18781901
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
18791902
s.is_final_def = self.unwrap_final(s)
18801903
tag = self.track_incomplete_refs()
18811904
s.rvalue.accept(self)
1882-
if self.found_incomplete_ref(tag):
1905+
if isinstance(s.rvalue, IndexExpr) and isinstance(s.rvalue.base, RefExpr):
1906+
# Special case: analyze index expression _as a type_ to trigger
1907+
# incomplete refs for string forward references, for example
1908+
# Union['ClassA', 'ClassB'].
1909+
# We throw away the results of the analysis and we only care about
1910+
# the detection of incomplete references (this doesn't change the expression
1911+
# in place).
1912+
self.analyze_alias(s.rvalue, allow_placeholder=True)
1913+
top_level_not_ready = self.is_not_ready_type_ref(s.rvalue)
1914+
# NOTE: the first check is insufficient. We want to defer creation of a Var.
1915+
if self.found_incomplete_ref(tag) or top_level_not_ready:
18831916
# Initializer couldn't be fully analyzed. Defer the current node and give up.
18841917
# Make sure that if we skip the definition of some local names, they can't be
18851918
# added later in this scope, since an earlier definition should take precedence.
18861919
for expr in names_modified_by_assignment(s):
1887-
self.mark_incomplete(expr.name, expr)
1920+
# NOTE: Currently for aliases like 'X = List[Y]', where 'Y' is not ready
1921+
# we proceed forward and create a Var. The latter will be replaced with
1922+
# a type alias it r.h.s. is a valid alias.
1923+
self.mark_incomplete(expr.name, expr, becomes_typeinfo=top_level_not_ready)
18881924
return
18891925
if self.analyze_namedtuple_assign(s):
18901926
return
@@ -2119,8 +2155,9 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Opt
21192155

21202156
return None
21212157

2122-
def analyze_alias(self, rvalue: Expression) -> Tuple[Optional[Type], List[str],
2123-
Set[str], List[str]]:
2158+
def analyze_alias(self, rvalue: Expression,
2159+
allow_placeholder: bool = False) -> Tuple[Optional[Type], List[str],
2160+
Set[str], List[str]]:
21242161
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).
21252162
21262163
If yes, return the corresponding type, a list of
@@ -2140,6 +2177,7 @@ def analyze_alias(self, rvalue: Expression) -> Tuple[Optional[Type], List[str],
21402177
self.options,
21412178
self.is_typeshed_stub_file,
21422179
allow_unnormalized=self.is_stub_file,
2180+
allow_placeholder=allow_placeholder,
21432181
in_dynamic_func=dynamic,
21442182
global_scope=global_scope)
21452183
typ = None # type: Optional[Type]
@@ -2182,8 +2220,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
21822220
# annotations (see the second rule).
21832221
return
21842222
rvalue = s.rvalue
2223+
tag = self.track_incomplete_refs()
21852224
res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(rvalue)
2186-
if not res:
2225+
if not res or self.found_incomplete_ref(tag):
21872226
return
21882227
if (isinstance(res, Instance) and res.type.name() == lvalue.name and
21892228
res.type.module_name == self.cur_mod_id):
@@ -2220,7 +2259,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
22202259
s.rvalue.analyzed.column = res.column
22212260
elif isinstance(s.rvalue, RefExpr):
22222261
s.rvalue.is_alias_rvalue = True
2223-
node.node = TypeAlias(res, node.node.fullname(), s.line, s.column,
2262+
node.node = TypeAlias(res, self.qualified_name(lvalue.name), s.line, s.column,
22242263
alias_tvars=alias_tvars, no_args=no_args)
22252264
if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias):
22262265
node.node.normalized = rvalue.node.normalized

mypy/newsemanal/semanal_main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ def process_top_level_function(analyzer: 'NewSemanticAnalyzer',
138138
# OK, this is one last pass, now missing names will be reported.
139139
more_iterations = False
140140
analyzer.incomplete_namespaces.discard(module)
141-
deferred, incomplete = semantic_analyze_target(target, state, node, active_type, False)
141+
deferred, incomplete = semantic_analyze_target(target, state, node, active_type,
142+
not more_iterations)
142143

144+
analyzer.incomplete_namespaces.discard(module)
143145
# After semantic analysis is done, discard local namespaces
144146
# to avoid memory hoarding.
145147
analyzer.saved_locals.clear()

mypy/newsemanal/semanal_shared.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ def is_incomplete_namespace(self, fullname: str) -> bool:
6868
"""Is a module or class namespace potentially missing some definitions?"""
6969
raise NotImplementedError
7070

71+
@abstractproperty
72+
def final_iteration(self) -> bool:
73+
"""Is this the final iteration of semantic analysis?"""
74+
raise NotImplementedError
75+
7176

7277
@trait
7378
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):

mypy/newsemanal/typeanal.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def analyze_type_alias(node: Expression,
6565
options: Options,
6666
is_typeshed_stub: bool,
6767
allow_unnormalized: bool = False,
68+
allow_placeholder: bool = False,
6869
in_dynamic_func: bool = False,
6970
global_scope: bool = True) -> Optional[Tuple[Type, Set[str]]]:
7071
"""Analyze r.h.s. of a (potential) type alias definition.
@@ -121,7 +122,8 @@ def analyze_type_alias(node: Expression,
121122
api.fail('Invalid type alias', node)
122123
return None
123124
analyzer = TypeAnalyser(api, tvar_scope, plugin, options, is_typeshed_stub,
124-
allow_unnormalized=allow_unnormalized, defining_alias=True)
125+
allow_unnormalized=allow_unnormalized, defining_alias=True,
126+
allow_placeholder=allow_placeholder)
125127
analyzer.in_dynamic_func = in_dynamic_func
126128
analyzer.global_scope = global_scope
127129
res = type.accept(analyzer)
@@ -407,7 +409,13 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl
407409
if self.allow_unbound_tvars and unbound_tvar and not self.third_pass:
408410
return t
409411
# None of the above options worked, we give up.
410-
self.fail('Invalid type "{}"'.format(name), t)
412+
# NOTE: 'final_iteration' is iteration when we hit the maximum number of iterations limit.
413+
if self.api.final_iteration:
414+
# TODO: This is problematic, since we will have to wait until the maximum number
415+
# of iterations to report an invalid type.
416+
self.fail('Invalid type "{}"'.format(name), t)
417+
else:
418+
self.api.defer()
411419
if self.third_pass and isinstance(sym.node, TypeVarExpr):
412420
self.note_func("Forward references to type variables are prohibited", t)
413421
return AnyType(TypeOfAny.from_error)
@@ -678,7 +686,7 @@ def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> Optional[L
678686
# We report an error in only the first two cases. In the third case, we assume
679687
# some other region of the code has already reported a more relevant error.
680688
#
681-
# TODO: Once we start adding support for enums, make sure we reprt a custom
689+
# TODO: Once we start adding support for enums, make sure we report a custom
682690
# error for case 2 as well.
683691
if arg.type_of_any != TypeOfAny.from_error:
684692
self.fail('Parameter {} of Literal[...] cannot be of type "Any"'.format(idx), ctx)

mypy/test/hacks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
'check-serialize.test',
4444
'check-statements.test',
4545
'check-tuples.test',
46-
'check-type-aliases.test',
4746
'check-typeddict.test',
4847
'check-typevar-values.test',
4948
'check-unions.test',

test-data/unit/check-newsemanal.test

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,174 @@ class Test:
10551055
def __init__(self) -> None:
10561056
some_module = self.a
10571057

1058+
[case testNewAnalyzerAliasToNotReadyClass]
1059+
import a
1060+
[file a.py]
1061+
from b import B
1062+
1063+
x: A
1064+
A = B
1065+
[file b.py]
1066+
from typing import List
1067+
from a import x
1068+
1069+
class B(List[B]): pass
1070+
1071+
reveal_type(x[0][0]) # E: Revealed type is 'b.B*'
1072+
[builtins fixtures/list.pyi]
1073+
1074+
[case testNewAnalyzerAliasToNotReadyClass2]
1075+
from typing import List
1076+
1077+
x: A
1078+
1079+
class A(List[B]): pass
1080+
B = A
1081+
1082+
reveal_type(x[0][0]) # E: Revealed type is '__main__.A*'
1083+
[builtins fixtures/list.pyi]
1084+
1085+
[case testNewAnalyzerAliasToNotReadyClass3]
1086+
from typing import List
1087+
1088+
x: B
1089+
B = A
1090+
A = C
1091+
class C(List[B]): pass
1092+
1093+
reveal_type(x[0][0]) # E: Revealed type is '__main__.C*'
1094+
[builtins fixtures/list.pyi]
1095+
1096+
[case testNewAnalyzerAliasToNotReadyNestedClass]
1097+
import a
1098+
[file a.py]
1099+
from b import Out
1100+
1101+
x: A
1102+
A = Out.B
1103+
[file b.py]
1104+
from typing import List
1105+
from a import x
1106+
1107+
class Out:
1108+
class B(List[B]): pass
1109+
1110+
reveal_type(x[0][0]) # E: Revealed type is 'b.Out.B*'
1111+
[builtins fixtures/list.pyi]
1112+
1113+
[case testNewAnalyzerAliasToNotReadyNestedClass2]
1114+
from typing import List
1115+
1116+
x: Out.A
1117+
1118+
class Out:
1119+
class A(List[B]): pass
1120+
B = Out.A
1121+
1122+
reveal_type(x[0][0]) # E: Revealed type is '__main__.Out.A*'
1123+
[builtins fixtures/list.pyi]
1124+
1125+
[case testNewAnalyzerAliasToNotReadyClassGeneric]
1126+
import a
1127+
[file a.py]
1128+
from typing import Tuple
1129+
from b import B, T
1130+
1131+
x: A[int]
1132+
A = B[Tuple[T, T]]
1133+
[file b.py]
1134+
from typing import List, Generic, TypeVar
1135+
from a import x
1136+
1137+
class B(List[B], Generic[T]): pass
1138+
T = TypeVar('T')
1139+
reveal_type(x) # E: Revealed type is 'b.B[Tuple[builtins.int, builtins.int]]'
1140+
[builtins fixtures/list.pyi]
1141+
1142+
[case testNewAnalyzerAliasToNotReadyClassInGeneric]
1143+
import a
1144+
[file a.py]
1145+
from typing import Tuple
1146+
from b import B
1147+
1148+
x: A
1149+
A = Tuple[B, B]
1150+
[file b.py]
1151+
from typing import List
1152+
from a import x
1153+
1154+
class B(List[B]): pass
1155+
1156+
reveal_type(x) # E: Revealed type is 'Tuple[b.B, b.B]'
1157+
[builtins fixtures/list.pyi]
1158+
1159+
[case testNewAnalyzerAliasToNotReadyClassDoubleGeneric]
1160+
from typing import List, TypeVar, Union
1161+
1162+
T = TypeVar('T')
1163+
1164+
x: B[int]
1165+
B = A[List[T]]
1166+
A = Union[int, T]
1167+
class C(List[B[int]]): pass
1168+
1169+
reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.list[builtins.int]]'
1170+
reveal_type(y[0]) # E: Revealed type is 'Union[builtins.int, builtins.list[builtins.int]]'
1171+
y: C
1172+
[builtins fixtures/list.pyi]
1173+
1174+
[case testNewAnalyzerForwardAliasFromUnion]
1175+
from typing import Union, List
1176+
1177+
A = Union['B', 'C']
1178+
1179+
class D:
1180+
x: List[A]
1181+
1182+
def test(self) -> None:
1183+
reveal_type(self.x[0].y) # E: Revealed type is 'builtins.int'
1184+
1185+
class B:
1186+
y: int
1187+
class C:
1188+
y: int
1189+
[builtins fixtures/list.pyi]
1190+
1191+
[case testNewAnalyzerAliasToNotReadyTwoDeferrals-skip]
1192+
from typing import List
1193+
1194+
x: B
1195+
B = List[C]
1196+
A = C
1197+
class C(List[A]): pass
1198+
1199+
reveal_type(x)
1200+
[builtins fixtures/list.pyi]
1201+
1202+
[case testNewAnalyzerAliasToNotReadyDirectBase-skip]
1203+
from typing import List
1204+
1205+
x: B
1206+
B = List[C]
1207+
class C(B): pass
1208+
1209+
reveal_type(x) # E: Revealed type is 'builtins.list[__main__.C]'
1210+
reveal_type(x[0][0]) # E: Revealed type is '__main__.C'
1211+
[builtins fixtures/list.pyi]
1212+
1213+
[case testNewAnalyzerAliasToNotReadyMixed]
1214+
from typing import List, Union
1215+
x: A
1216+
1217+
A = Union[B, C]
1218+
1219+
class B(List[A]): pass
1220+
class C(List[A]): pass
1221+
1222+
reveal_type(x) # E: Revealed type is 'Union[__main__.B, __main__.C]'
1223+
reveal_type(x[0]) # E: Revealed type is 'Union[__main__.B, __main__.C]'
1224+
[builtins fixtures/list.pyi]
1225+
10581226
[case testNewAnalyzerListComprehension]
10591227
from typing import List
10601228
a: List[A]

0 commit comments

Comments
 (0)