Skip to content

Commit b13cc7c

Browse files
committed
Fix RecursionError in type visitors for recursive aliases with varying type arguments
1 parent 3a0207b commit b13cc7c

2 files changed

Lines changed: 32 additions & 22 deletions

File tree

mypy/type_visitor.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
from abc import abstractmethod
1717
from collections.abc import Iterable, Sequence
18-
from typing import Any, Final, Generic, TypeVar, cast
18+
from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar, cast
19+
20+
if TYPE_CHECKING:
21+
from mypy.nodes import TypeAlias
1922

2023
from mypy_extensions import mypyc_attr, trait
2124

@@ -356,9 +359,12 @@ class TypeQuery(SyntheticTypeVisitor[T]):
356359
"""
357360

358361
def __init__(self) -> None:
359-
# Keep track of the type aliases already visited. This is needed to avoid
360-
# infinite recursion on types like A = Union[int, List[A]].
361-
self.seen_aliases: set[TypeAliasType] | None = None
362+
# Keep track of the type alias definitions already visited. This is needed
363+
# to avoid infinite recursion on recursive type aliases. We track by the
364+
# underlying TypeAlias node (not TypeAliasType) so that recursive aliases
365+
# with varying type arguments (e.g. A[P] -> A[Concatenate[int, P]]) are
366+
# still caught.
367+
self.seen_aliases: set[TypeAlias] | None = None
362368
# By default, we eagerly expand type aliases, and query also types in the
363369
# alias target. In most cases this is a desired behavior, but we may want
364370
# to skip targets in some cases (e.g. when collecting type variables).
@@ -447,13 +453,14 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> T:
447453
def visit_type_alias_type(self, t: TypeAliasType, /) -> T:
448454
if self.skip_alias_target:
449455
return self.query_types(t.args)
450-
# Skip type aliases already visited types to avoid infinite recursion
451-
# (also use this as a simple-minded cache).
456+
# Skip type aliases already visited to avoid infinite recursion.
457+
# We track by the TypeAlias node so that recursive aliases with varying
458+
# type arguments are still caught.
452459
if self.seen_aliases is None:
453460
self.seen_aliases = set()
454-
elif t in self.seen_aliases:
455-
return self.strategy([])
456-
self.seen_aliases.add(t)
461+
elif t.alias in self.seen_aliases:
462+
return self.query_types(t.args)
463+
self.seen_aliases.add(t.alias)
457464
return get_proper_type(t).accept(self)
458465

459466
def query_types(self, types: Iterable[Type]) -> T:
@@ -487,10 +494,12 @@ def __init__(self, strategy: int) -> None:
487494
else:
488495
assert strategy == ALL_STRATEGY
489496
self.default = True
490-
# Keep track of the type aliases already visited. This is needed to avoid
491-
# infinite recursion on types like A = Union[int, List[A]]. An empty set is
492-
# represented as None as a micro-optimization.
493-
self.seen_aliases: set[TypeAliasType] | None = None
497+
# Keep track of the type alias definitions already visited. This is needed
498+
# to avoid infinite recursion on recursive type aliases. We track by the
499+
# underlying TypeAlias node (not TypeAliasType) so that recursive aliases
500+
# with varying type arguments (e.g. A[P] -> A[Concatenate[int, P]]) are
501+
# still caught. An empty set is represented as None as a micro-optimization.
502+
self.seen_aliases: set[TypeAlias] | None = None
494503
# By default, we eagerly expand type aliases, and query also types in the
495504
# alias target. In most cases this is a desired behavior, but we may want
496505
# to skip targets in some cases (e.g. when collecting type variables).
@@ -588,13 +597,14 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> bool:
588597
def visit_type_alias_type(self, t: TypeAliasType, /) -> bool:
589598
if self.skip_alias_target:
590599
return self.query_types(t.args)
591-
# Skip type aliases already visited types to avoid infinite recursion
592-
# (also use this as a simple-minded cache).
600+
# Skip type aliases already visited to avoid infinite recursion.
601+
# We track by the TypeAlias node so that recursive aliases with varying
602+
# type arguments are still caught.
593603
if self.seen_aliases is None:
594604
self.seen_aliases = set()
595-
elif t in self.seen_aliases:
596-
return self.default
597-
self.seen_aliases.add(t)
605+
elif t.alias in self.seen_aliases:
606+
return self.query_types(t.args)
607+
self.seen_aliases.add(t.alias)
598608
return get_proper_type(t).accept(self)
599609

600610
def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool:

mypy/types.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3705,7 +3705,7 @@ class TypeStrVisitor(SyntheticTypeVisitor[str]):
37053705
def __init__(self, id_mapper: IdMapper | None = None, *, options: Options) -> None:
37063706
self.id_mapper = id_mapper
37073707
self.options = options
3708-
self.dotted_aliases: set[TypeAliasType] | None = None
3708+
self.dotted_aliases: set[mypy.nodes.TypeAlias] | None = None
37093709

37103710
def visit_unbound_type(self, t: UnboundType, /) -> str:
37113711
s = t.name + "?"
@@ -3989,11 +3989,11 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> str:
39893989
return get_proper_type(t).accept(self)
39903990
if self.dotted_aliases is None:
39913991
self.dotted_aliases = set()
3992-
elif t in self.dotted_aliases:
3992+
elif t.alias in self.dotted_aliases:
39933993
return "..."
3994-
self.dotted_aliases.add(t)
3994+
self.dotted_aliases.add(t.alias)
39953995
type_str = get_proper_type(t).accept(self)
3996-
self.dotted_aliases.discard(t)
3996+
self.dotted_aliases.discard(t.alias)
39973997
return type_str
39983998

39993999
def visit_unpack_type(self, t: UnpackType, /) -> str:

0 commit comments

Comments
 (0)