From 47e3aa40b3377b8cae107b53241ecb348429c2d7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 5 May 2025 17:17:36 -0400 Subject: [PATCH] [ty] Specialize bound methods and nominal instances (#17865) Fixes https://github.com/astral-sh/ruff/pull/17832#issuecomment-2851224968. We had a comment that we did not need to apply specializations to generic aliases, or to the bound `self` of a bound method, because they were already specialized. But they might be specialized with a type variable, which _does_ need to be specialized, in the case of a "multi-step" specialization, such as: ```py class LinkedList[T]: ... class C[U]: def method(self) -> LinkedList[U]: return LinkedList[U]() ``` --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/decorators.md | 2 +- .../mdtest/generics/legacy/classes.md | 34 +++++++++++++++++++ .../mdtest/generics/pep695/classes.md | 29 ++++++++++++++++ crates/ty_python_semantic/src/types.rs | 17 +++------- crates/ty_python_semantic/src/types/class.rs | 26 ++++++++++++++ .../ty_python_semantic/src/types/instance.rs | 11 ++++++ 6 files changed, 106 insertions(+), 13 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 923bf2e3eb..2c947bd734 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -145,7 +145,7 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: _lru_cache_wrapper[_T] +reveal_type(f) # revealed: _lru_cache_wrapper[Unknown] # TODO: Should be `int` reveal_type(f(1)) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index cecf23de0a..03e281d03c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -361,6 +361,40 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class LinkedList(Generic[T]): ... + +class C(Generic[T, U]): + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] +``` + ## Cyclic class definitions ### F-bounded quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 948ec47f44..736687d420 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -305,6 +305,35 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +class LinkedList[T]: ... + +class C[T, U]: + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] +``` + ## Cyclic class definitions ### F-bounded quantification diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ed8f2e5496..cf991b7d03 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4962,20 +4962,16 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function.apply_specialization(db, specialization)) } - // Note that we don't need to apply the specialization to `self_instance`, since it - // must either be a non-generic class literal (which cannot have any typevars to - // specialize) or a generic alias (which has already been fully specialized). For a - // generic alias, the specialization being applied here must be for some _other_ - // generic context nested within the generic alias's class literal, which the generic - // alias's context cannot refer to. (The _method_ does need to be specialized, since it - // might be a nested generic method, whose generic context is what is now being - // specialized.) Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, method.function(db).apply_specialization(db, specialization), - method.self_instance(db), + method.self_instance(db).apply_specialization(db, specialization), )), + Type::NominalInstance(instance) => Type::NominalInstance( + instance.apply_specialization(db, specialization), + ), + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( function.apply_specialization(db, specialization), @@ -5060,9 +5056,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::BoundSuper(_) - // `NominalInstance` contains a ClassType, which has already been specialized if needed, - // like above with BoundMethod's self_instance. - | Type::NominalInstance(_) // Same for `ProtocolInstance` | Type::ProtocolInstance(_) | Type::KnownInstance(_) => self, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 039e818191..41303cce94 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -146,6 +146,19 @@ impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).definition(db) } + + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + Self::new( + db, + self.origin(db), + self.specialization(db) + .apply_specialization(db, specialization), + ) + } } impl<'db> From> for Type<'db> { @@ -210,6 +223,19 @@ impl<'db> ClassType<'db> { self.is_known(db, KnownClass::Object) } + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => { + Self::Generic(generic.apply_specialization(db, specialization)) + } + } + } + /// Iterate over the [method resolution order] ("MRO") of the class. /// /// If the MRO could not be accurately resolved, this method falls back to iterating diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 762dc9a341..e09ad686a5 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -3,6 +3,7 @@ use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::symbol::{Symbol, SymbolAndQualifiers}; +use crate::types::generics::Specialization; use crate::Db; pub(super) use synthesized_protocol::SynthesizedProtocolType; @@ -111,6 +112,16 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { SubclassOfType::from(db, self.class) } + + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + Self { + class: self.class.apply_specialization(db, specialization), + } + } } impl<'db> From> for Type<'db> {