diff --git a/mypy/semanal.py b/mypy/semanal.py index c079a865af1de..6021b56e6b571 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -235,6 +235,7 @@ calculate_tuple_fallback, find_dataclass_transform_spec, has_placeholder, + has_type_var_with_name, parse_bool, require_bool_literal_argument, set_callable_name as set_callable_name, @@ -4911,6 +4912,18 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: return False variance, upper_bound, default = res + # A type variable can't be used in its own upper bound or default. Usually, + # such references are reported as cyclic definitions, but a Callable type + # can bind a type variable, e.g. TypeVar("T", bound=Callable[..., "T"]). + # Storing such a bound/default would create an infinitely recursive type. + fullname = self.qualified_name(name) + if has_type_var_with_name(upper_bound, fullname): + self.cannot_resolve_name(name, "name", s) + upper_bound = AnyType(TypeOfAny.from_error) + if has_type_var_with_name(default, fullname): + self.cannot_resolve_name(name, "name", s) + default = AnyType(TypeOfAny.from_error) + existing = self.current_symbol_table().get(name) if existing and not ( isinstance(existing.node, PlaceholderNode) diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index f4ec7eb2c5d55..b8949291ef6e1 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -47,6 +47,7 @@ TypeVarId, TypeVarLikeType, TypeVarTupleType, + TypeVarType, UnpackType, flatten_nested_tuples, get_proper_type, @@ -381,6 +382,22 @@ def has_placeholder(typ: Type) -> bool: return typ.accept(HasPlaceholders()) +class HasTypeVarWithName(BoolTypeQuery): + def __init__(self, fullname: str) -> None: + super().__init__(ANY_STRATEGY) + self.fullname = fullname + + def visit_type_var(self, t: TypeVarType) -> bool: + # Don't look into bounds/defaults of other type variables, these are + # complete types defined elsewhere (and may be recursive). + return t.fullname == self.fullname + + +def has_type_var_with_name(typ: Type, fullname: str) -> bool: + """Check if a type refers to a (regular) type variable with given full name.""" + return typ.accept(HasTypeVarWithName(fullname)) + + def find_dataclass_transform_spec(node: Node | None) -> DataclassTransformSpec | None: """ Find the dataclass transform spec for the given node, if any exists. diff --git a/test-data/unit/check-bound.test b/test-data/unit/check-bound.test index 1c713fd77c38d..c89fd7f32e538 100644 --- a/test-data/unit/check-bound.test +++ b/test-data/unit/check-bound.test @@ -227,3 +227,12 @@ def f(t: TupleT) -> None: reveal_type(a) # N: Revealed type is "builtins.int" reveal_type(b) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/tuple.pyi] + +[case testTypeVarBoundCallableReferringToItself] +from typing import Callable, TypeVar + +T = TypeVar("T", bound=Callable[..., "T"]) # E: Cannot resolve name "T" (possible cyclic definition) +S = TypeVar("S", bound=Callable[["S"], int]) # E: Cannot resolve name "S" (possible cyclic definition) + +def f(x: T) -> T: ... +reveal_type(f) # N: Revealed type is "def [T <: Any] (x: T`-1) -> T`-1" diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index a5cdaea16105c..131fc5ff25d22 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -1063,3 +1063,13 @@ A = list[T] a: A reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" [builtins fixtures/tuple.pyi] + +[case testTypeVarDefaultCallableReferringToItself] +from typing import Callable +from typing_extensions import TypeVar + +T = TypeVar("T", default=Callable[..., "T"]) # E: Cannot resolve name "T" (possible cyclic definition) + +def f(x: T) -> T: ... +reveal_type(f) # N: Revealed type is "def [T = Any] (x: T`-1 = Any) -> T`-1 = Any" +[builtins fixtures/tuple.pyi]