Skip to content

Commit 678ea18

Browse files
authored
Fail gracefully on invalid and/or unsupported recursive type aliases (#13336)
This is a follow up for #13297. See some motivation in the original PR (also in the docstrings).
1 parent 5d3eeea commit 678ea18

File tree

5 files changed

+84
-7
lines changed

5 files changed

+84
-7
lines changed

mypy/semanal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@
275275
UnboundType,
276276
get_proper_type,
277277
get_proper_types,
278+
invalid_recursive_alias,
278279
is_named_instance,
279280
)
280281
from mypy.typevars import fill_typevars
@@ -3087,7 +3088,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
30873088
)
30883089
if not res:
30893090
return False
3090-
if self.options.enable_recursive_aliases:
3091+
if self.options.enable_recursive_aliases and not self.is_func_scope():
30913092
# Only marking incomplete for top-level placeholders makes recursive aliases like
30923093
# `A = Sequence[str | A]` valid here, similar to how we treat base classes in class
30933094
# definitions, allowing `class str(Sequence[str]): ...`
@@ -3131,6 +3132,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
31313132
no_args=no_args,
31323133
eager=eager,
31333134
)
3135+
if invalid_recursive_alias({alias_node}, alias_node.target):
3136+
self.fail("Invalid recursive alias: a union item of itself", rvalue)
31343137
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
31353138
s.rvalue.analyzed = TypeAliasExpr(alias_node)
31363139
s.rvalue.analyzed.line = s.line
@@ -5564,6 +5567,8 @@ def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:
55645567

55655568
def cannot_resolve_name(self, name: str, kind: str, ctx: Context) -> None:
55665569
self.fail(f'Cannot resolve {kind} "{name}" (possible cyclic definition)', ctx)
5570+
if self.options.enable_recursive_aliases and self.is_func_scope():
5571+
self.note("Recursive types are not allowed at function scope", ctx)
55675572

55685573
def qualified_name(self, name: str) -> str:
55695574
if self.type is not None:

mypy/semanal_typeargs.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
UnpackType,
3131
get_proper_type,
3232
get_proper_types,
33+
invalid_recursive_alias,
3334
)
3435

3536

@@ -68,10 +69,16 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None:
6869
super().visit_type_alias_type(t)
6970
if t in self.seen_aliases:
7071
# Avoid infinite recursion on recursive type aliases.
71-
# Note: it is fine to skip the aliases we have already seen in non-recursive types,
72-
# since errors there have already already reported.
72+
# Note: it is fine to skip the aliases we have already seen in non-recursive
73+
# types, since errors there have already been reported.
7374
return
7475
self.seen_aliases.add(t)
76+
assert t.alias is not None, f"Unfixed type alias {t.type_ref}"
77+
if invalid_recursive_alias({t.alias}, t.alias.target):
78+
# Fix type arguments for invalid aliases (error is already reported).
79+
t.args = []
80+
t.alias.target = AnyType(TypeOfAny.from_error)
81+
return
7582
get_proper_type(t).accept(self)
7683

7784
def visit_instance(self, t: Instance) -> None:

mypy/typeanal.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
UninhabitedType,
8383
UnionType,
8484
UnpackType,
85+
bad_type_type_item,
8586
callable_with_ellipsis,
8687
get_proper_type,
8788
union_items,
@@ -374,7 +375,6 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
374375
unexpanded_type=t,
375376
)
376377
if node.eager:
377-
# TODO: Generate error if recursive (once we have recursive types)
378378
res = get_proper_type(res)
379379
return res
380380
elif isinstance(node, TypeInfo):
@@ -487,7 +487,10 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
487487
type_str = "Type[...]" if fullname == "typing.Type" else "type[...]"
488488
self.fail(type_str + " must have exactly one type argument", t)
489489
item = self.anal_type(t.args[0])
490-
return TypeType.make_normalized(item, line=t.line)
490+
if bad_type_type_item(item):
491+
self.fail("Type[...] can't contain another Type[...]", t)
492+
item = AnyType(TypeOfAny.from_error)
493+
return TypeType.make_normalized(item, line=t.line, column=t.column)
491494
elif fullname == "typing.ClassVar":
492495
if self.nesting_level > 0:
493496
self.fail("Invalid type: ClassVar nested inside other type", t)

mypy/types.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,6 @@ def is_singleton_type(self) -> bool:
236236
class TypeAliasType(Type):
237237
"""A type alias to another type.
238238
239-
NOTE: this is not being used yet, and the implementation is still incomplete.
240-
241239
To support recursive type aliases we don't immediately expand a type alias
242240
during semantic analysis, but create an instance of this type that records the target alias
243241
definition node (mypy.nodes.TypeAlias) and type arguments (for generic aliases).
@@ -3197,6 +3195,40 @@ def union_items(typ: Type) -> List[ProperType]:
31973195
return [typ]
31983196

31993197

3198+
def invalid_recursive_alias(seen_nodes: Set[mypy.nodes.TypeAlias], target: Type) -> bool:
3199+
"""Flag aliases like A = Union[int, A] (and similar mutual aliases).
3200+
3201+
Such aliases don't make much sense, and cause problems in later phases.
3202+
"""
3203+
if isinstance(target, TypeAliasType):
3204+
if target.alias in seen_nodes:
3205+
return True
3206+
assert target.alias, f"Unfixed type alias {target.type_ref}"
3207+
return invalid_recursive_alias(seen_nodes | {target.alias}, get_proper_type(target))
3208+
assert isinstance(target, ProperType)
3209+
if not isinstance(target, UnionType):
3210+
return False
3211+
return any(invalid_recursive_alias(seen_nodes, item) for item in target.items)
3212+
3213+
3214+
def bad_type_type_item(item: Type) -> bool:
3215+
"""Prohibit types like Type[Type[...]].
3216+
3217+
Such types are explicitly prohibited by PEP 484. Also they cause problems
3218+
with recursive types like T = Type[T], because internal representation of
3219+
TypeType item is normalized (i.e. always a proper type).
3220+
"""
3221+
item = get_proper_type(item)
3222+
if isinstance(item, TypeType):
3223+
return True
3224+
if isinstance(item, UnionType):
3225+
return any(
3226+
isinstance(get_proper_type(i), TypeType)
3227+
for i in flatten_nested_unions(item.items, handle_type_alias_type=True)
3228+
)
3229+
return False
3230+
3231+
32003232
def is_union_with_any(tp: Type) -> bool:
32013233
"""Is this a union with Any or a plain Any type?"""
32023234
tp = get_proper_type(tp)

test-data/unit/check-recursive-types.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,33 @@ reveal_type(bar(la)) # N: Revealed type is "__main__.A"
388388
reveal_type(bar(lla)) # N: Revealed type is "__main__.A"
389389
reveal_type(bar(llla)) # N: Revealed type is "__main__.A"
390390
[builtins fixtures/isinstancelist.pyi]
391+
392+
[case testRecursiveAliasesProhibitBadAliases]
393+
# flags: --enable-recursive-aliases
394+
from typing import Union, Type, List, TypeVar
395+
396+
NR = List[int]
397+
NR2 = Union[NR, NR]
398+
NR3 = Union[NR, Union[NR2, NR2]]
399+
400+
A = Union[B, int] # E: Invalid recursive alias: a union item of itself
401+
B = Union[int, A] # E: Invalid recursive alias: a union item of itself
402+
def f() -> A: ...
403+
reveal_type(f()) # N: Revealed type is "Union[Any, builtins.int]"
404+
405+
T = TypeVar("T")
406+
G = Union[T, G[T]] # E: Invalid recursive alias: a union item of itself
407+
def g() -> G[int]: ...
408+
reveal_type(g()) # N: Revealed type is "Any"
409+
410+
def local() -> None:
411+
L = List[Union[int, L]] # E: Cannot resolve name "L" (possible cyclic definition) \
412+
# N: Recursive types are not allowed at function scope
413+
x: L
414+
reveal_type(x) # N: Revealed type is "builtins.list[Union[builtins.int, Any]]"
415+
416+
S = Type[S] # E: Type[...] cannot contain another Type[...]
417+
U = Type[Union[int, U]] # E: Type[...] cannot contain another Type[...]
418+
x: U
419+
reveal_type(x) # N: Revealed type is "Type[Any]"
420+
[builtins fixtures/isinstancelist.pyi]

0 commit comments

Comments
 (0)