diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 31bdd6690a7a..18948ee7f6d6 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -31,6 +31,7 @@ UninhabitedType, UnionType, get_proper_type, + has_recursive_types, ) @@ -157,6 +158,12 @@ def test_type_alias_expand_all(self) -> None: [self.fx.a, self.fx.a], Instance(self.fx.std_tuplei, [self.fx.a]) ) + def test_recursive_nested_in_non_recursive(self) -> None: + A, _ = self.fx.def_alias_1(self.fx.a) + NA = self.fx.non_rec_alias(Instance(self.fx.gi, [UnboundType("T")]), ["T"], [A]) + assert not NA.is_recursive + assert has_recursive_types(NA) + def test_indirection_no_infinite_recursion(self) -> None: A, _ = self.fx.def_alias_1(self.fx.a) visitor = TypeIndirectionVisitor() diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 380da909893a..93e5e4b0b5ca 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -339,9 +339,13 @@ def def_alias_2(self, base: Instance) -> tuple[TypeAliasType, Type]: A.alias = AN return A, target - def non_rec_alias(self, target: Type) -> TypeAliasType: - AN = TypeAlias(target, "__main__.A", -1, -1) - return TypeAliasType(AN, []) + def non_rec_alias( + self, target: Type, alias_tvars: list[str] | None = None, args: list[Type] | None = None + ) -> TypeAliasType: + AN = TypeAlias(target, "__main__.A", -1, -1, alias_tvars=alias_tvars) + if args is None: + args = [] + return TypeAliasType(AN, args) class InterfaceTypeFixture(TypeFixture): diff --git a/mypy/types.py b/mypy/types.py index 1de294f9952d..6c08b24afd80 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -278,30 +278,42 @@ def _expand_once(self) -> Type: self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column ) - def _partial_expansion(self) -> tuple[ProperType, bool]: + def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]: # Private method mostly for debugging and testing. unroller = UnrollAliasVisitor(set()) - unrolled = self.accept(unroller) + if nothing_args: + alias = self.copy_modified(args=[UninhabitedType()] * len(self.args)) + else: + alias = self + unrolled = alias.accept(unroller) assert isinstance(unrolled, ProperType) return unrolled, unroller.recursed - def expand_all_if_possible(self) -> ProperType | None: + def expand_all_if_possible(self, nothing_args: bool = False) -> ProperType | None: """Attempt a full expansion of the type alias (including nested aliases). If the expansion is not possible, i.e. the alias is (mutually-)recursive, - return None. + return None. If nothing_args is True, replace all type arguments with an + UninhabitedType() (used to detect recursively defined aliases). """ - unrolled, recursed = self._partial_expansion() + unrolled, recursed = self._partial_expansion(nothing_args=nothing_args) if recursed: return None return unrolled @property def is_recursive(self) -> bool: + """Whether this type alias is recursive. + + Note this doesn't check generic alias arguments, but only if this alias + *definition* is recursive. The property value thus can be cached on the + underlying TypeAlias node. If you want to include all nested types, use + has_recursive_types() function. + """ assert self.alias is not None, "Unfixed type alias" is_recursive = self.alias._is_recursive if is_recursive is None: - is_recursive = self.expand_all_if_possible() is None + is_recursive = self.expand_all_if_possible(nothing_args=True) is None # We cache the value on the underlying TypeAlias node as an optimization, # since the value is the same for all instances of the same alias. self.alias._is_recursive = is_recursive @@ -3259,7 +3271,7 @@ def __init__(self) -> None: super().__init__(any) def visit_type_alias_type(self, t: TypeAliasType) -> bool: - return t.is_recursive + return t.is_recursive or self.query_types(t.args) def has_recursive_types(typ: Type) -> bool: