From 37218fd4d638bec14333c111b86429aa4cc27b61 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Sep 2025 02:22:54 +0100 Subject: [PATCH 1/3] Try some aliases speed-up --- mypy/constraints.py | 6 +- mypy/indirection.py | 9 ++- mypy/semanal_typeargs.py | 11 ++- mypy/stats.py | 6 +- mypy/test/testtypes.py | 12 ---- mypy/type_visitor.py | 39 +++++----- mypy/typeanal.py | 22 ++---- mypy/typeops.py | 6 +- mypy/types.py | 87 ++++++++--------------- test-data/unit/check-recursive-types.test | 6 +- test-data/unit/fine-grained.test | 4 +- 11 files changed, 81 insertions(+), 127 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 6416791fa74a..96c0c7ccaf35 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -21,6 +21,7 @@ ArgKind, TypeInfo, ) +from mypy.type_visitor import ALL_STRATEGY, BoolTypeQuery from mypy.types import ( TUPLE_LIKE_INSTANCE_NAMES, AnyType, @@ -41,7 +42,6 @@ TypeAliasType, TypedDictType, TypeOfAny, - TypeQuery, TypeType, TypeVarId, TypeVarLikeType, @@ -670,9 +670,9 @@ def is_complete_type(typ: Type) -> bool: return typ.accept(CompleteTypeVisitor()) -class CompleteTypeVisitor(TypeQuery[bool]): +class CompleteTypeVisitor(BoolTypeQuery): def __init__(self) -> None: - super().__init__(all) + super().__init__(ALL_STRATEGY) def visit_uninhabited_type(self, t: UninhabitedType) -> bool: return False diff --git a/mypy/indirection.py b/mypy/indirection.py index 88258b94d94a..1473ada21ba2 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -37,16 +37,15 @@ def find_modules(self, typs: Iterable[types.Type]) -> set[str]: return self.modules def _visit(self, typ: types.Type) -> None: - if isinstance(typ, types.TypeAliasType): + if isinstance(typ, types.TypeAliasType) and typ.is_recursive: # Avoid infinite recursion for recursive type aliases. - if typ not in self.seen_aliases: - self.seen_aliases.add(typ) + self.seen_aliases.add(typ) typ.accept(self) def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: # Micro-optimization: Specialized version of _visit for lists for typ in typs: - if isinstance(typ, types.TypeAliasType): + if isinstance(typ, types.TypeAliasType) and typ.is_recursive: # Avoid infinite recursion for recursive type aliases. if typ in self.seen_aliases: continue @@ -56,7 +55,7 @@ def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: def _visit_type_list(self, typs: list[types.Type]) -> None: # Micro-optimization: Specialized version of _visit for tuples for typ in typs: - if isinstance(typ, types.TypeAliasType): + if isinstance(typ, types.TypeAliasType) and typ.is_recursive: # Avoid infinite recursion for recursive type aliases. if typ in self.seen_aliases: continue diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index be39a8259c2e..79241d43f658 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -83,12 +83,11 @@ def visit_block(self, o: Block) -> None: 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 been reported. - return - self.seen_aliases.add(t) + if t.is_recursive: + if t in self.seen_aliases: + # Avoid infinite recursion on recursive type aliases. + return + self.seen_aliases.add(t) assert t.alias is not None, f"Unfixed type alias {t.type_ref}" is_error, is_invalid = self.validate_args( t.alias.name, tuple(t.args), t.alias.alias_tvars, t diff --git a/mypy/stats.py b/mypy/stats.py index 6bad400ce5d5..e3499d234563 100644 --- a/mypy/stats.py +++ b/mypy/stats.py @@ -43,6 +43,7 @@ YieldFromExpr, ) from mypy.traverser import TraverserVisitor +from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery from mypy.typeanal import collect_all_inner_types from mypy.types import ( AnyType, @@ -52,7 +53,6 @@ TupleType, Type, TypeOfAny, - TypeQuery, TypeVarType, get_proper_type, get_proper_types, @@ -453,9 +453,9 @@ def is_imprecise(t: Type) -> bool: return t.accept(HasAnyQuery()) -class HasAnyQuery(TypeQuery[bool]): +class HasAnyQuery(BoolTypeQuery): def __init__(self) -> None: - super().__init__(any) + super().__init__(ANY_STRATEGY) def visit_any(self, t: AnyType) -> bool: return not is_special_form_any(t) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 0fe41bc28ecd..fc68d9aa6eac 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -201,18 +201,6 @@ def test_type_alias_expand_once(self) -> None: assert get_proper_type(A) == target assert get_proper_type(target) == target - def test_type_alias_expand_all(self) -> None: - A, _ = self.fx.def_alias_1(self.fx.a) - assert A.expand_all_if_possible() is None - A, _ = self.fx.def_alias_2(self.fx.a) - assert A.expand_all_if_possible() is None - - B = self.fx.non_rec_alias(self.fx.a) - C = self.fx.non_rec_alias(TupleType([B, B], Instance(self.fx.std_tuplei, [B]))) - assert C.expand_all_if_possible() == TupleType( - [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) T = TypeVarType( diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 15494393cae6..8445fb679395 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -15,7 +15,7 @@ from abc import abstractmethod from collections.abc import Iterable, Sequence -from typing import Any, Callable, Final, Generic, TypeVar, cast +from typing import Any, Final, Generic, TypeVar, cast from mypy_extensions import mypyc_attr, trait @@ -353,16 +353,19 @@ class TypeQuery(SyntheticTypeVisitor[T]): # TODO: check that we don't have existing violations of this rule. """ - def __init__(self, strategy: Callable[[list[T]], T]) -> None: - self.strategy = strategy + def __init__(self) -> None: # Keep track of the type aliases already visited. This is needed to avoid # infinite recursion on types like A = Union[int, List[A]]. - self.seen_aliases: set[TypeAliasType] = set() + self.seen_aliases: set[TypeAliasType] | None = None # By default, we eagerly expand type aliases, and query also types in the # alias target. In most cases this is a desired behavior, but we may want # to skip targets in some cases (e.g. when collecting type variables). self.skip_alias_target = False + @abstractmethod + def strategy(self, items: list[T]) -> T: + raise NotImplementedError + def visit_unbound_type(self, t: UnboundType, /) -> T: return self.query_types(t.args) @@ -440,14 +443,15 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> T: return self.query_types(t.args) def visit_type_alias_type(self, t: TypeAliasType, /) -> T: - # Skip type aliases already visited types to avoid infinite recursion. - # TODO: Ideally we should fire subvisitors here (or use caching) if we care - # about duplicates. - if t in self.seen_aliases: - return self.strategy([]) - self.seen_aliases.add(t) if self.skip_alias_target: return self.query_types(t.args) + # Skip type aliases already visited types to avoid infinite recursion. + if t.is_recursive: + if self.seen_aliases is None: + self.seen_aliases = set() + elif t in self.seen_aliases: + return self.strategy([]) + self.seen_aliases.add(t) return get_proper_type(t).accept(self) def query_types(self, types: Iterable[Type]) -> T: @@ -580,16 +584,15 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> bool: return self.query_types(t.args) def visit_type_alias_type(self, t: TypeAliasType, /) -> bool: - # Skip type aliases already visited types to avoid infinite recursion. - # TODO: Ideally we should fire subvisitors here (or use caching) if we care - # about duplicates. - if self.seen_aliases is None: - self.seen_aliases = set() - elif t in self.seen_aliases: - return self.default - self.seen_aliases.add(t) if self.skip_alias_target: return self.query_types(t.args) + # Skip type aliases already visited types to avoid infinite recursion. + if t.is_recursive: + if self.seen_aliases is None: + self.seen_aliases = set() + elif t in self.seen_aliases: + return self.default + self.seen_aliases.add(t) return get_proper_type(t).accept(self) def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 658730414763..81fb87fbf9ee 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2377,9 +2377,9 @@ def has_explicit_any(t: Type) -> bool: return t.accept(HasExplicitAny()) -class HasExplicitAny(TypeQuery[bool]): +class HasExplicitAny(BoolTypeQuery): def __init__(self) -> None: - super().__init__(any) + super().__init__(ANY_STRATEGY) def visit_any(self, t: AnyType) -> bool: return t.type_of_any == TypeOfAny.explicit @@ -2418,15 +2418,11 @@ def collect_all_inner_types(t: Type) -> list[Type]: class CollectAllInnerTypesQuery(TypeQuery[list[Type]]): - def __init__(self) -> None: - super().__init__(self.combine_lists_strategy) - def query_types(self, types: Iterable[Type]) -> list[Type]: return self.strategy([t.accept(self) for t in types]) + list(types) - @classmethod - def combine_lists_strategy(cls, it: Iterable[list[Type]]) -> list[Type]: - return list(itertools.chain.from_iterable(it)) + def strategy(self, items: Iterable[list[Type]]) -> list[Type]: + return list(itertools.chain.from_iterable(items)) def make_optional_type(t: Type) -> Type: @@ -2556,7 +2552,6 @@ def __init__(self, api: SemanticAnalyzerCoreInterface, scope: TypeVarLikeScope) self.scope = scope self.type_var_likes: list[tuple[str, TypeVarLikeExpr]] = [] self.has_self_type = False - self.seen_aliases: set[TypeAliasType] | None = None self.include_callables = True def _seems_like_callable(self, type: UnboundType) -> bool: @@ -2653,7 +2648,8 @@ def visit_union_type(self, t: UnionType) -> None: self.process_types(t.items) def visit_overloaded(self, t: Overloaded) -> None: - self.process_types(t.items) # type: ignore[arg-type] + for it in t.items: + it.accept(self) def visit_type_type(self, t: TypeType) -> None: t.item.accept(self) @@ -2665,12 +2661,6 @@ def visit_placeholder_type(self, t: PlaceholderType) -> None: return self.process_types(t.args) def visit_type_alias_type(self, t: TypeAliasType) -> None: - # Skip type aliases in already visited types to avoid infinite recursion. - if self.seen_aliases is None: - self.seen_aliases = set() - elif t in self.seen_aliases: - return - self.seen_aliases.add(t) self.process_types(t.args) def process_types(self, types: list[Type] | tuple[Type, ...]) -> None: diff --git a/mypy/typeops.py b/mypy/typeops.py index 87a4d8cefd13..298ad4d16f8c 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1114,12 +1114,12 @@ def get_all_type_vars(tp: Type) -> list[TypeVarLikeType]: class TypeVarExtractor(TypeQuery[list[TypeVarLikeType]]): def __init__(self, include_all: bool = False) -> None: - super().__init__(self._merge) + super().__init__() self.include_all = include_all - def _merge(self, iter: Iterable[list[TypeVarLikeType]]) -> list[TypeVarLikeType]: + def strategy(self, items: Iterable[list[TypeVarLikeType]]) -> list[TypeVarLikeType]: out = [] - for item in iter: + for item in items: out.extend(item) return out diff --git a/mypy/types.py b/mypy/types.py index 3f4bd94b5b24..e0e897e04cad 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -369,29 +369,6 @@ def _expand_once(self) -> Type: return self.alias.target.accept(InstantiateAliasVisitor(mapping)) - def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]: - # Private method mostly for debugging and testing. - unroller = UnrollAliasVisitor(set(), {}) - 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, 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. If nothing_args is True, replace all type arguments with an - UninhabitedType() (used to detect recursively defined aliases). - """ - 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. @@ -404,7 +381,7 @@ def is_recursive(self) -> bool: 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(nothing_args=True) is None + is_recursive = self.alias in self.alias.target.accept(CollectAliasesVisitor()) # 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 @@ -3654,8 +3631,8 @@ class TypeStrVisitor(SyntheticTypeVisitor[str]): def __init__(self, id_mapper: IdMapper | None = None, *, options: Options) -> None: self.id_mapper = id_mapper - self.any_as_dots = False self.options = options + self.dotted_aliases: set[TypeAliasType] | None = None def visit_unbound_type(self, t: UnboundType, /) -> str: s = t.name + "?" @@ -3674,8 +3651,6 @@ def visit_callable_argument(self, t: CallableArgument, /) -> str: return f"{t.constructor}({typ}, {t.name})" def visit_any(self, t: AnyType, /) -> str: - if self.any_as_dots and t.type_of_any == TypeOfAny.special_form: - return "..." return "Any" def visit_none_type(self, t: NoneType, /) -> str: @@ -3902,13 +3877,18 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> str: return f"" def visit_type_alias_type(self, t: TypeAliasType, /) -> str: - if t.alias is not None: - unrolled, recursed = t._partial_expansion() - self.any_as_dots = recursed - type_str = unrolled.accept(self) - self.any_as_dots = False - return type_str - return "" + if t.alias is None: + return "" + if not t.is_recursive: + return get_proper_type(t).accept(self) + if self.dotted_aliases is None: + self.dotted_aliases = set() + elif t in self.dotted_aliases: + return "..." + self.dotted_aliases.add(t) + type_str = get_proper_type(t).accept(self) + self.dotted_aliases.discard(t) + return type_str def visit_unpack_type(self, t: UnpackType, /) -> str: return f"Unpack[{t.type.accept(self)}]" @@ -3943,28 +3923,23 @@ def visit_type_list(self, t: TypeList, /) -> Type: return t -class UnrollAliasVisitor(TrivialSyntheticTypeTranslator): - def __init__( - self, initial_aliases: set[TypeAliasType], cache: dict[Type, Type] | None - ) -> None: - assert cache is not None - super().__init__(cache) - self.recursed = False - self.initial_aliases = initial_aliases - - def visit_type_alias_type(self, t: TypeAliasType) -> Type: - if t in self.initial_aliases: - self.recursed = True - return AnyType(TypeOfAny.special_form) - # Create a new visitor on encountering a new type alias, so that an alias like - # A = Tuple[B, B] - # B = int - # will not be detected as recursive on the second encounter of B. - subvisitor = UnrollAliasVisitor(self.initial_aliases | {t}, self.cache) - result = get_proper_type(t).accept(subvisitor) - if subvisitor.recursed: - self.recursed = True - return result +class CollectAliasesVisitor(TypeQuery[list[mypy.nodes.TypeAlias]]): + def __init__(self) -> None: + super().__init__() + self.seen_alias_nodes: set[mypy.nodes.TypeAlias] = set() + + def strategy(self, items: list[list[mypy.nodes.TypeAlias]]) -> list[mypy.nodes.TypeAlias]: + out = [] + for item in items: + out.extend(item) + return out + + def visit_type_alias_type(self, t: TypeAliasType, /) -> list[mypy.nodes.TypeAlias]: + assert t.alias is not None + if t.alias not in self.seen_alias_nodes: + self.seen_alias_nodes.add(t.alias) + return [t.alias] + t.alias.target.accept(self) + return [] def is_named_instance(t: Type, fullnames: str | tuple[str, ...]) -> TypeGuard[Instance]: diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 86e9f02b5263..28616b7f003a 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -189,7 +189,7 @@ class A(NamedTuple('A', [('attr', List[Exp])])): pass class B(NamedTuple('B', [('val', object)])): pass def my_eval(exp: Exp) -> int: - reveal_type(exp) # N: Revealed type is "Union[tuple[builtins.list[...], fallback=__main__.A], tuple[builtins.object, fallback=__main__.B]]" + reveal_type(exp) # N: Revealed type is "Union[tuple[builtins.list[Union[..., tuple[builtins.object, fallback=__main__.B]]], fallback=__main__.A], tuple[builtins.object, fallback=__main__.B]]" if isinstance(exp, A): my_eval(exp[0][0]) return my_eval(exp.attr[0]) @@ -578,7 +578,7 @@ B = NamedTuple('B', [('x', A), ('y', int)]) A = NamedTuple('A', [('x', str), ('y', 'B')]) n: A def f(m: B) -> None: pass -reveal_type(n) # N: Revealed type is "tuple[builtins.str, tuple[..., builtins.int, fallback=__main__.B], fallback=__main__.A]" +reveal_type(n) # N: Revealed type is "tuple[builtins.str, tuple[tuple[builtins.str, ..., fallback=__main__.A], builtins.int, fallback=__main__.B], fallback=__main__.A]" reveal_type(f) # N: Revealed type is "def (m: tuple[tuple[builtins.str, ..., fallback=__main__.A], builtins.int, fallback=__main__.B])" f(n) # E: Argument 1 to "f" has incompatible type "A"; expected "B" [builtins fixtures/tuple.pyi] @@ -1013,4 +1013,4 @@ from typing import Callable from bogus import Foo # type: ignore A = Callable[[Foo, "B"], Foo] # E: Type alias target becomes "Callable[[Any, B], Any]" due to an unfollowed import -B = Callable[[Foo, A], Foo] # E: Type alias target becomes "Callable[[Any, A], Any]" due to an unfollowed import +B = Callable[[Foo, A], Foo] # E: Type alias target becomes "Callable[[Any, Callable[[Any, B], Any]], Any]" due to an unfollowed import diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 1bddee0e5ed2..4a30c8a3828f 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3528,9 +3528,9 @@ reveal_type(a.n) [out] == == -c.py:4: note: Revealed type is "tuple[Union[tuple[Union[..., None], builtins.int, fallback=b.M], None], builtins.int, fallback=a.N]" +c.py:4: note: Revealed type is "tuple[Union[tuple[Union[tuple[Union[..., None], builtins.int, fallback=a.N], None], builtins.int, fallback=b.M], None], builtins.int, fallback=a.N]" c.py:5: error: Incompatible types in assignment (expression has type "Optional[N]", variable has type "int") -c.py:7: note: Revealed type is "tuple[Union[tuple[Union[..., None], builtins.int, fallback=b.M], None], builtins.int, fallback=a.N]" +c.py:7: note: Revealed type is "tuple[Union[tuple[Union[tuple[Union[..., None], builtins.int, fallback=a.N], None], builtins.int, fallback=b.M], None], builtins.int, fallback=a.N]" [case testTupleTypeUpdateNonRecursiveToRecursiveFine] import c From 14573d5cc6c37a1ed0e85ff957c71de98f78d595 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Sep 2025 15:40:57 +0100 Subject: [PATCH 2/3] Some tweaks to use implicit caching --- mypy/indirection.py | 6 ++--- mypy/semanal_typeargs.py | 7 +++++- mypy/type_visitor.py | 28 +++++++++++------------ test-data/unit/check-recursive-types.test | 6 ++--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/mypy/indirection.py b/mypy/indirection.py index 1473ada21ba2..4e566194632b 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -37,7 +37,7 @@ def find_modules(self, typs: Iterable[types.Type]) -> set[str]: return self.modules def _visit(self, typ: types.Type) -> None: - if isinstance(typ, types.TypeAliasType) and typ.is_recursive: + if isinstance(typ, types.TypeAliasType): # Avoid infinite recursion for recursive type aliases. self.seen_aliases.add(typ) typ.accept(self) @@ -45,7 +45,7 @@ def _visit(self, typ: types.Type) -> None: def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: # Micro-optimization: Specialized version of _visit for lists for typ in typs: - if isinstance(typ, types.TypeAliasType) and typ.is_recursive: + if isinstance(typ, types.TypeAliasType): # Avoid infinite recursion for recursive type aliases. if typ in self.seen_aliases: continue @@ -55,7 +55,7 @@ def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: def _visit_type_list(self, typs: list[types.Type]) -> None: # Micro-optimization: Specialized version of _visit for tuples for typ in typs: - if isinstance(typ, types.TypeAliasType) and typ.is_recursive: + if isinstance(typ, types.TypeAliasType): # Avoid infinite recursion for recursive type aliases. if typ in self.seen_aliases: continue diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 79241d43f658..a19285b5b487 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -100,9 +100,14 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None: if not is_error: # If there was already an error for the alias itself, there is no point in checking # the expansion, most likely it will result in the same kind of error. - get_proper_type(t).accept(self) if t.alias is not None: t.alias.accept(self) + if t.args: + # Since we always allow unbounded type variables in alias definitions, we need + # to verify the arguments satisfy the upper bounds of the expansion as well. + get_proper_type(t).accept(self) + if t.is_recursive: + self.seen_aliases.discard(t) def visit_tuple_type(self, t: TupleType) -> None: t.items = flatten_nested_tuples(t.items) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 8445fb679395..86ef6ade8471 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -445,13 +445,13 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> T: def visit_type_alias_type(self, t: TypeAliasType, /) -> T: if self.skip_alias_target: return self.query_types(t.args) - # Skip type aliases already visited types to avoid infinite recursion. - if t.is_recursive: - if self.seen_aliases is None: - self.seen_aliases = set() - elif t in self.seen_aliases: - return self.strategy([]) - self.seen_aliases.add(t) + # Skip type aliases already visited types to avoid infinite recursion + # (also use this as a simple-minded cache). + if self.seen_aliases is None: + self.seen_aliases = set() + elif t in self.seen_aliases: + return self.strategy([]) + self.seen_aliases.add(t) return get_proper_type(t).accept(self) def query_types(self, types: Iterable[Type]) -> T: @@ -586,13 +586,13 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> bool: def visit_type_alias_type(self, t: TypeAliasType, /) -> bool: if self.skip_alias_target: return self.query_types(t.args) - # Skip type aliases already visited types to avoid infinite recursion. - if t.is_recursive: - if self.seen_aliases is None: - self.seen_aliases = set() - elif t in self.seen_aliases: - return self.default - self.seen_aliases.add(t) + # Skip type aliases already visited types to avoid infinite recursion + # (also use this as a simple-minded cache). + if self.seen_aliases is None: + self.seen_aliases = set() + elif t in self.seen_aliases: + return self.default + self.seen_aliases.add(t) return get_proper_type(t).accept(self) def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool: diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 28616b7f003a..86e9f02b5263 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -189,7 +189,7 @@ class A(NamedTuple('A', [('attr', List[Exp])])): pass class B(NamedTuple('B', [('val', object)])): pass def my_eval(exp: Exp) -> int: - reveal_type(exp) # N: Revealed type is "Union[tuple[builtins.list[Union[..., tuple[builtins.object, fallback=__main__.B]]], fallback=__main__.A], tuple[builtins.object, fallback=__main__.B]]" + reveal_type(exp) # N: Revealed type is "Union[tuple[builtins.list[...], fallback=__main__.A], tuple[builtins.object, fallback=__main__.B]]" if isinstance(exp, A): my_eval(exp[0][0]) return my_eval(exp.attr[0]) @@ -578,7 +578,7 @@ B = NamedTuple('B', [('x', A), ('y', int)]) A = NamedTuple('A', [('x', str), ('y', 'B')]) n: A def f(m: B) -> None: pass -reveal_type(n) # N: Revealed type is "tuple[builtins.str, tuple[tuple[builtins.str, ..., fallback=__main__.A], builtins.int, fallback=__main__.B], fallback=__main__.A]" +reveal_type(n) # N: Revealed type is "tuple[builtins.str, tuple[..., builtins.int, fallback=__main__.B], fallback=__main__.A]" reveal_type(f) # N: Revealed type is "def (m: tuple[tuple[builtins.str, ..., fallback=__main__.A], builtins.int, fallback=__main__.B])" f(n) # E: Argument 1 to "f" has incompatible type "A"; expected "B" [builtins fixtures/tuple.pyi] @@ -1013,4 +1013,4 @@ from typing import Callable from bogus import Foo # type: ignore A = Callable[[Foo, "B"], Foo] # E: Type alias target becomes "Callable[[Any, B], Any]" due to an unfollowed import -B = Callable[[Foo, A], Foo] # E: Type alias target becomes "Callable[[Any, Callable[[Any, B], Any]], Any]" due to an unfollowed import +B = Callable[[Foo, A], Foo] # E: Type alias target becomes "Callable[[Any, A], Any]" due to an unfollowed import From 02d3d12c4273cf9df09ee5bf6899ef22e40795f4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 8 Sep 2025 16:34:44 +0100 Subject: [PATCH 3/3] Avoid duplicate checking of alias targets --- mypy/mixedtraverser.py | 2 ++ mypy/semanal_typeargs.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/mixedtraverser.py b/mypy/mixedtraverser.py index 324e8a87c1bd..f47d762934bc 100644 --- a/mypy/mixedtraverser.py +++ b/mypy/mixedtraverser.py @@ -47,6 +47,8 @@ def visit_class_def(self, o: ClassDef, /) -> None: if info: for base in info.bases: base.accept(self) + if info.special_alias: + info.special_alias.accept(self) def visit_type_alias_expr(self, o: TypeAliasExpr, /) -> None: super().visit_type_alias_expr(o) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index a19285b5b487..686e7a57042d 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -100,8 +100,6 @@ def visit_type_alias_type(self, t: TypeAliasType) -> None: if not is_error: # If there was already an error for the alias itself, there is no point in checking # the expansion, most likely it will result in the same kind of error. - if t.alias is not None: - t.alias.accept(self) if t.args: # Since we always allow unbounded type variables in alias definitions, we need # to verify the arguments satisfy the upper bounds of the expansion as well.