From 678ea184d64aadd22b6b66abaf71dedc3f83d4b2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Aug 2022 00:58:07 +0100 Subject: [PATCH] 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). --- mypy/semanal.py | 7 ++++- mypy/semanal_typeargs.py | 11 +++++-- mypy/typeanal.py | 7 +++-- mypy/types.py | 36 +++++++++++++++++++++-- test-data/unit/check-recursive-types.test | 30 +++++++++++++++++++ 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ec503d9d8ad2..974ca5a69864 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -275,6 +275,7 @@ UnboundType, get_proper_type, get_proper_types, + invalid_recursive_alias, is_named_instance, ) from mypy.typevars import fill_typevars @@ -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]): ...` @@ -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 @@ -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: diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index e6334f9e8c0a..27933d5a8051 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -30,6 +30,7 @@ UnpackType, get_proper_type, get_proper_types, + invalid_recursive_alias, ) @@ -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: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d6615d4f4c9e..9e068a39671d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -82,6 +82,7 @@ UninhabitedType, UnionType, UnpackType, + bad_type_type_item, callable_with_ellipsis, get_proper_type, union_items, @@ -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): @@ -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) diff --git a/mypy/types.py b/mypy/types.py index 8569c7e80531..7487654c4251 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -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). @@ -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) diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 04b7d634d4a9..28adb827bc08 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -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]