Skip to content

Fail gracefully on invalid and/or unsupported recursive type aliases #13336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@
UnboundType,
get_proper_type,
get_proper_types,
invalid_recursive_alias,
is_named_instance,
)
from mypy.typevars import fill_typevars
Expand Down Expand Up @@ -3075,7 +3076,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
)
if not res:
return False
if self.options.enable_recursive_aliases:
if self.options.enable_recursive_aliases and not self.is_func_scope():
# Only marking incomplete for top-level placeholders makes recursive aliases like
# `A = Sequence[str | A]` valid here, similar to how we treat base classes in class
# definitions, allowing `class str(Sequence[str]): ...`
Expand Down Expand Up @@ -3119,6 +3120,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
no_args=no_args,
eager=eager,
)
if invalid_recursive_alias({alias_node}, alias_node.target):
self.fail("Invalid recursive alias: a union item of itself", rvalue)
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
s.rvalue.analyzed = TypeAliasExpr(alias_node)
s.rvalue.analyzed.line = s.line
Expand Down Expand Up @@ -5552,6 +5555,8 @@ def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:

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

def qualified_name(self, name: str) -> str:
if self.type is not None:
Expand Down
11 changes: 9 additions & 2 deletions mypy/semanal_typeargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
UnpackType,
get_proper_type,
get_proper_types,
invalid_recursive_alias,
)


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

def visit_instance(self, t: Instance) -> None:
Expand Down
7 changes: 5 additions & 2 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
UninhabitedType,
UnionType,
UnpackType,
bad_type_type_item,
callable_with_ellipsis,
get_proper_type,
union_items,
Expand Down Expand Up @@ -374,7 +375,6 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
unexpanded_type=t,
)
if node.eager:
# TODO: Generate error if recursive (once we have recursive types)
res = get_proper_type(res)
return res
elif isinstance(node, TypeInfo):
Expand Down Expand Up @@ -487,7 +487,10 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
type_str = "Type[...]" if fullname == "typing.Type" else "type[...]"
self.fail(type_str + " must have exactly one type argument", t)
item = self.anal_type(t.args[0])
return TypeType.make_normalized(item, line=t.line)
if bad_type_type_item(item):
self.fail("Type[...] can't contain another Type[...]", t)
item = AnyType(TypeOfAny.from_error)
return TypeType.make_normalized(item, line=t.line, column=t.column)
elif fullname == "typing.ClassVar":
if self.nesting_level > 0:
self.fail("Invalid type: ClassVar nested inside other type", t)
Expand Down
36 changes: 34 additions & 2 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,6 @@ def is_singleton_type(self) -> bool:
class TypeAliasType(Type):
"""A type alias to another type.

NOTE: this is not being used yet, and the implementation is still incomplete.

To support recursive type aliases we don't immediately expand a type alias
during semantic analysis, but create an instance of this type that records the target alias
definition node (mypy.nodes.TypeAlias) and type arguments (for generic aliases).
Expand Down Expand Up @@ -3197,6 +3195,40 @@ def union_items(typ: Type) -> List[ProperType]:
return [typ]


def invalid_recursive_alias(seen_nodes: Set[mypy.nodes.TypeAlias], target: Type) -> bool:
"""Flag aliases like A = Union[int, A] (and similar mutual aliases).

Such aliases don't make much sense, and cause problems in later phases.
"""
if isinstance(target, TypeAliasType):
if target.alias in seen_nodes:
return True
assert target.alias, f"Unfixed type alias {target.type_ref}"
return invalid_recursive_alias(seen_nodes | {target.alias}, get_proper_type(target))
assert isinstance(target, ProperType)
if not isinstance(target, UnionType):
return False
return any(invalid_recursive_alias(seen_nodes, item) for item in target.items)


def bad_type_type_item(item: Type) -> bool:
"""Prohibit types like Type[Type[...]].

Such types are explicitly prohibited by PEP 484. Also they cause problems
with recursive types like T = Type[T], because internal representation of
TypeType item is normalized (i.e. always a proper type).
"""
item = get_proper_type(item)
if isinstance(item, TypeType):
return True
if isinstance(item, UnionType):
return any(
isinstance(get_proper_type(i), TypeType)
for i in flatten_nested_unions(item.items, handle_type_alias_type=True)
)
return False


def is_union_with_any(tp: Type) -> bool:
"""Is this a union with Any or a plain Any type?"""
tp = get_proper_type(tp)
Expand Down
30 changes: 30 additions & 0 deletions test-data/unit/check-recursive-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,33 @@ reveal_type(bar(la)) # N: Revealed type is "__main__.A"
reveal_type(bar(lla)) # N: Revealed type is "__main__.A"
reveal_type(bar(llla)) # N: Revealed type is "__main__.A"
[builtins fixtures/isinstancelist.pyi]

[case testRecursiveAliasesProhibitBadAliases]
# flags: --enable-recursive-aliases
from typing import Union, Type, List, TypeVar

NR = List[int]
NR2 = Union[NR, NR]
NR3 = Union[NR, Union[NR2, NR2]]

A = Union[B, int] # E: Invalid recursive alias: a union item of itself
B = Union[int, A] # E: Invalid recursive alias: a union item of itself
def f() -> A: ...
reveal_type(f()) # N: Revealed type is "Union[Any, builtins.int]"

T = TypeVar("T")
G = Union[T, G[T]] # E: Invalid recursive alias: a union item of itself
def g() -> G[int]: ...
reveal_type(g()) # N: Revealed type is "Any"

def local() -> None:
L = List[Union[int, L]] # E: Cannot resolve name "L" (possible cyclic definition) \
# N: Recursive types are not allowed at function scope
x: L
reveal_type(x) # N: Revealed type is "builtins.list[Union[builtins.int, Any]]"

S = Type[S] # E: Type[...] cannot contain another Type[...]
U = Type[Union[int, U]] # E: Type[...] cannot contain another Type[...]
x: U
reveal_type(x) # N: Revealed type is "Type[Any]"
[builtins fixtures/isinstancelist.pyi]