Skip to content

Commit

Permalink
Fail gracefully on invalid and/or unsupported recursive type aliases (#…
Browse files Browse the repository at this point in the history
…13336)

This is a follow up for #13297.

See some motivation in the original PR (also in the docstrings).
  • Loading branch information
ilevkivskyi authored Aug 5, 2022
1 parent 5d3eeea commit 678ea18
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 7 deletions.
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 @@ -3087,7 +3088,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 @@ -3131,6 +3132,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 @@ -5564,6 +5567,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]

0 comments on commit 678ea18

Please sign in to comment.