Skip to content

Commit 504e6cb

Browse files
committed
Fix crash on invalid recursive variadic alias
1 parent 49ccd26 commit 504e6cb

7 files changed

Lines changed: 60 additions & 21 deletions

File tree

mypy/expandtype.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,16 @@ class ExpandTypeVisitor(TrivialSyntheticTypeTranslator):
182182

183183
variables: Mapping[TypeVarId, Type] # TypeVar id -> TypeVar value
184184

185-
def __init__(self, variables: Mapping[TypeVarId, Type]) -> None:
185+
def __init__(
186+
self, variables: Mapping[TypeVarId, Type], skip_normalization: bool = False
187+
) -> None:
186188
super().__init__()
187189
self.variables = variables
190+
# This flag will skip normalizations that are semantically not needed, but
191+
# require calling get_proper_type(). This is needed during very early stages
192+
# to avoid infinite recursion when detecting invalid/divergent recursive
193+
# type aliases.
194+
self.skip_normalization = skip_normalization
188195

189196
def visit_unbound_type(self, t: UnboundType) -> Type:
190197
return t
@@ -228,11 +235,9 @@ def visit_instance(self, t: Instance) -> Type:
228235
if t.type.fullname == "builtins.tuple":
229236
# Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
230237
arg = args[0]
231-
if isinstance(arg, UnpackType):
238+
if not self.skip_normalization and isinstance(arg, UnpackType):
232239
unpacked = get_proper_type(arg.type)
233240
if isinstance(unpacked, Instance):
234-
# TODO: this and similar asserts below may be unsafe because get_proper_type()
235-
# may be called during semantic analysis before all invalid types are removed.
236241
assert unpacked.type.fullname == "builtins.tuple"
237242
args = list(unpacked.args)
238243
return t.copy_modified(args=args)
@@ -535,7 +540,7 @@ def visit_tuple_type(self, t: TupleType) -> Type:
535540
if len(items) == 1:
536541
# Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
537542
item = items[0]
538-
if isinstance(item, UnpackType):
543+
if not self.skip_normalization and isinstance(item, UnpackType):
539544
unpacked = get_proper_type(item.type)
540545
if isinstance(unpacked, Instance):
541546
# expand_type() may be called during semantic analysis, before invalid unpacks are fixed.

mypy/semanal.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4263,6 +4263,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
42634263
# An alias gets updated.
42644264
updated = False
42654265
if isinstance(existing.node, TypeAlias):
4266+
# Invalidate recursive status cache in case it was previously set.
4267+
existing.node._is_recursive = None
42664268
if existing.node.target != res:
42674269
# Copy expansion to the existing alias, this matches how we update base classes
42684270
# for a TypeInfo _in place_ if there are nested placeholders.
@@ -4271,8 +4273,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
42714273
existing.node.alias_tvars = alias_tvars
42724274
existing.node.no_args = no_args
42734275
updated = True
4274-
# Invalidate recursive status cache in case it was previously set.
4275-
existing.node._is_recursive = None
42764276
else:
42774277
# Otherwise just replace existing placeholder with type alias *in place*.
42784278
existing._node = alias_node
@@ -5830,6 +5830,8 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
58305830
):
58315831
updated = False
58325832
if isinstance(existing.node, TypeAlias):
5833+
# Invalidate recursive status cache in case it was previously set.
5834+
existing.node._is_recursive = None
58335835
if (
58345836
existing.node.target != res
58355837
or existing.node.alias_tvars != alias_node.alias_tvars
@@ -5840,8 +5842,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
58405842
existing.node.default_depends = default_depends
58415843
existing.node.alias_tvars = alias_tvars
58425844
updated = True
5843-
# Invalidate recursive status cache in case it was previously set.
5844-
existing.node._is_recursive = None
58455845
else:
58465846
# Otherwise just replace existing placeholder with type alias *in place*.
58475847
existing._node = alias_node

mypy/server/astmerge.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ def visit_var(self, node: Var) -> None:
340340
super().visit_var(node)
341341

342342
def visit_type_alias(self, node: TypeAlias) -> None:
343+
# Updating alias target can invalidate its recursive status.
344+
node._is_recursive = None
343345
self.fixup_type(node.target)
344346
for v in node.alias_tvars:
345347
self.fixup_type(v)

mypy/typeanal.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
BoolTypeQuery,
7676
CallableArgument,
7777
CallableType,
78+
CollectAliasesVisitor,
7879
DeletedType,
7980
EllipsisType,
8081
ErasedType,
@@ -275,7 +276,9 @@ def __init__(
275276
self.prohibit_special_class_field_types = prohibit_special_class_field_types
276277
# Allow variables typed as Type[Any] and type (useful for base classes).
277278
self.allow_type_any = allow_type_any
278-
self.allow_type_var_tuple = False
279+
# Level of nesting at which a TypeVarTuple is allowed. Note we specify exact level
280+
# to prohibit things like Unpack[list[Ts]], which are not supported.
281+
self.allow_type_var_tuple = -1
279282
self.allow_unpack = allow_unpack
280283
# Set when we are analyzing a default of a type variable.
281284
self.analyzing_tvar_def = analyzing_tvar_def
@@ -453,7 +456,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
453456
self.fail(msg, t, code=codes.VALID_TYPE)
454457
return AnyType(TypeOfAny.from_error)
455458
assert isinstance(tvar_def, TypeVarTupleType)
456-
if not self.allow_type_var_tuple:
459+
if self.allow_type_var_tuple != self.nesting_level:
457460
self.fail(
458461
f'TypeVarTuple "{t.name}" is only valid with an unpack',
459462
t,
@@ -808,9 +811,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
808811
if not self.allow_unpack:
809812
self.fail(message_registry.INVALID_UNPACK_POSITION, t, code=codes.VALID_TYPE)
810813
return AnyType(TypeOfAny.from_error)
811-
self.allow_type_var_tuple = True
814+
self.allow_type_var_tuple = self.nesting_level + 1
812815
result = UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column)
813-
self.allow_type_var_tuple = False
816+
self.allow_type_var_tuple = -1
814817
return result
815818
elif fullname in SELF_TYPE_NAMES:
816819
if t.args:
@@ -1161,9 +1164,9 @@ def visit_unpack_type(self, t: UnpackType) -> Type:
11611164
if not self.allow_unpack:
11621165
self.fail(message_registry.INVALID_UNPACK_POSITION, t.type, code=codes.VALID_TYPE)
11631166
return AnyType(TypeOfAny.from_error)
1164-
self.allow_type_var_tuple = True
1167+
self.allow_type_var_tuple = self.nesting_level + 1
11651168
result = UnpackType(self.anal_type(t.type), from_star_syntax=t.from_star_syntax)
1166-
self.allow_type_var_tuple = False
1169+
self.allow_type_var_tuple = -1
11671170
return result
11681171

11691172
def visit_parameters(self, t: Parameters) -> Type:
@@ -2500,7 +2503,7 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
25002503
return t
25012504
new_nodes = self.seen_nodes | {t.alias}
25022505
visitor = DivergingAliasDetector(new_nodes)
2503-
_ = get_proper_type(t).accept(visitor)
2506+
_ = t.expand_once(skip_normalization=True).accept(visitor)
25042507
if visitor.diverging:
25052508
self.diverging = True
25062509
return t
@@ -2518,6 +2521,15 @@ def detect_diverging_alias(node: TypeAlias, target: Type) -> bool:
25182521
They may be handy in rare cases, e.g. to express a union of non-mixed nested lists:
25192522
Nested = Union[T, Nested[List[T]]] ~> Union[T, List[T], List[List[T]], ...]
25202523
"""
2524+
is_recursive = node._is_recursive
2525+
if is_recursive is None:
2526+
is_recursive = node in node.target.accept(CollectAliasesVisitor())
2527+
if not is_recursive:
2528+
# Fast path: this is not a recursive alias at all.
2529+
return False
2530+
# Note we only cache positive case, caching negative case is risky, as this type alias
2531+
# (or importantly any other alias it uses) may be not ready yet.
2532+
node._is_recursive = True
25212533
visitor = DivergingAliasDetector({node})
25222534
_ = target.accept(visitor)
25232535
return visitor.diverging

mypy/types.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,11 @@ def __init__(
350350
self.args = args
351351
self.type_ref: str | None = None
352352

353-
def _expand_once(self) -> Type:
353+
def expand_once(self, skip_normalization: bool = False) -> Type:
354354
"""Expand to the target type exactly once.
355355
356356
This doesn't do full expansion, i.e. the result can contain another
357-
(or even this same) type alias. Use this internal helper only when really needed,
357+
(or even this same) type alias. Use this helper only when needed,
358358
its public wrapper mypy.types.get_proper_type() is preferred.
359359
"""
360360
assert self.alias is not None
@@ -381,7 +381,8 @@ def _expand_once(self) -> Type:
381381
):
382382
mapping[tvar.id] = sub
383383

384-
return self.alias.target.accept(InstantiateAliasVisitor(mapping))
384+
visitor = InstantiateAliasVisitor(mapping, skip_normalization=skip_normalization)
385+
return self.alias.target.accept(visitor)
385386

386387
@property
387388
def is_recursive(self) -> bool:
@@ -3707,7 +3708,7 @@ def get_proper_type(typ: Type | None) -> ProperType | None:
37073708
if isinstance(typ, TypeGuardedType):
37083709
typ = typ.type_guard
37093710
while isinstance(typ, TypeAliasType):
3710-
typ = typ._expand_once()
3711+
typ = typ.expand_once()
37113712
# TODO: store the name of original type alias on this type, so we can show it in errors.
37123713
return cast(ProperType, typ)
37133714

mypy/types_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def is_invalid_recursive_alias(seen_nodes: set[TypeAlias], target: Type) -> bool
6565
if target.alias in seen_nodes:
6666
return True
6767
assert target.alias, f"Unfixed type alias {target.type_ref}"
68-
return is_invalid_recursive_alias(seen_nodes | {target.alias}, get_proper_type(target))
68+
return is_invalid_recursive_alias(
69+
seen_nodes | {target.alias}, target.expand_once(skip_normalization=True)
70+
)
6971
assert isinstance(target, ProperType)
7072
if not isinstance(target, (UnionType, TupleType)):
7173
return False

test-data/unit/check-typevar-tuple.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,24 @@ z: C
882882
reveal_type(x) # N: Revealed type is "Any"
883883
reveal_type(y) # N: Revealed type is "Any"
884884
reveal_type(z) # N: Revealed type is "tuple[builtins.int, Unpack[builtins.tuple[Any, ...]]]"
885+
[builtins fixtures/tuple.pyi]
886+
887+
[case testBanPathologicalRecursiveTuplesGeneric]
888+
from typing import TypeVarTuple, Unpack
889+
890+
Ts = TypeVarTuple("Ts")
891+
A = tuple[Unpack[B[Unpack[Ts]]]] # E: Invalid recursive alias: a tuple item of itself \
892+
# E: Name "B" is used before definition
893+
B = tuple[Unpack[A[Unpack[Ts]]]]
894+
[builtins fixtures/tuple.pyi]
895+
896+
[case testBanTypeVarTupleNotImmediatelyInsideUnpack]
897+
from typing import TypeVarTuple, Unpack
885898

899+
Ts = TypeVarTuple("Ts")
900+
A = tuple[Unpack[tuple[Ts]]] # E: TypeVarTuple "Ts" is only valid with an unpack
901+
x: A[int, str]
902+
reveal_type(x) # N: Revealed type is "tuple[Any]"
886903
[builtins fixtures/tuple.pyi]
887904

888905
[case testInferenceAgainstGenericVariadicWithBadType]

0 commit comments

Comments
 (0)