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 a15bc32da7..46c1296508 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -433,17 +433,31 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva scope for the method. ```py +from ty_extensions import generic_context from typing import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") class C(Generic[T]): - def method(self, u: U) -> U: + def method(self, u: int) -> int: return u + def generic_method(self, t: T, u: U) -> U: + return u + +reveal_type(generic_context(C)) # revealed: tuple[T] +reveal_type(generic_context(C.method)) # revealed: None +reveal_type(generic_context(C.generic_method)) # revealed: tuple[U] +reveal_type(generic_context(C[int])) # revealed: None +reveal_type(generic_context(C[int].method)) # revealed: None +reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U] + c: C[int] = C[int]() -reveal_type(c.method("string")) # revealed: Literal["string"] +reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"] +reveal_type(generic_context(c)) # revealed: None +reveal_type(generic_context(c.method)) # revealed: None +reveal_type(generic_context(c.generic_method)) # revealed: tuple[U] ``` ## Specializations propagate 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 9726dbae54..a6a4071df1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -392,8 +392,13 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva scope for the method. ```py +from ty_extensions import generic_context + class C[T]: - def method[U](self, u: U) -> U: + def method(self, u: int) -> int: + return u + + def generic_method[U](self, t: T, u: U) -> U: return u # error: [unresolved-reference] def cannot_use_outside_of_method(self, u: U): ... @@ -401,8 +406,18 @@ class C[T]: # TODO: error def cannot_shadow_class_typevar[T](self, t: T): ... +reveal_type(generic_context(C)) # revealed: tuple[T] +reveal_type(generic_context(C.method)) # revealed: None +reveal_type(generic_context(C.generic_method)) # revealed: tuple[U] +reveal_type(generic_context(C[int])) # revealed: None +reveal_type(generic_context(C[int].method)) # revealed: None +reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U] + c: C[int] = C[int]() -reveal_type(c.method("string")) # revealed: Literal["string"] +reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"] +reveal_type(generic_context(c)) # revealed: None +reveal_type(generic_context(c.method)) # revealed: None +reveal_type(generic_context(c.generic_method)) # revealed: tuple[U] ``` ## Specializations propagate diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 90426ff6b1..b3c8408b5c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -141,10 +141,22 @@ class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y -legacy: Legacy[int] = Legacy() +legacy: Legacy[int] = Legacy[int]() reveal_type(legacy.m(1, "string")) # revealed: Literal["string"] ``` +The class typevar in the method signature does not bind a _new_ instance of the typevar; it was +already solved and specialized when the class was specialized: + +```py +from ty_extensions import generic_context + +legacy.m("string", None) # error: [invalid-argument-type] +reveal_type(legacy.m) # revealed: bound method Legacy[int].m(x: int, y: S) -> S +reveal_type(generic_context(Legacy)) # revealed: tuple[T] +reveal_type(generic_context(legacy.m)) # revealed: tuple[S] +``` + With PEP 695 syntax, it is clearer that the method uses a separate typevar: ```py diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 6c519c609f..f9d770c845 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -623,19 +623,38 @@ impl<'db> Bindings<'db> { Some(KnownFunction::GenericContext) => { if let [Some(ty)] = overload.parameter_types() { + let function_generic_context = |function: FunctionType<'db>| { + let union = UnionType::from_elements( + db, + function + .signature(db) + .overloads + .iter() + .filter_map(|signature| signature.generic_context) + .map(|generic_context| generic_context.as_tuple(db)), + ); + if union.is_never() { + Type::none(db) + } else { + union + } + }; + // TODO: Handle generic functions, and unions/intersections of // generic types overload.set_return_type(match ty { - Type::ClassLiteral(class) => match class.generic_context(db) { - Some(generic_context) => TupleType::from_elements( - db, - generic_context - .variables(db) - .iter() - .map(|typevar| Type::TypeVar(*typevar)), - ), - None => Type::none(db), - }, + Type::ClassLiteral(class) => class + .generic_context(db) + .map(|generic_context| generic_context.as_tuple(db)) + .unwrap_or_else(|| Type::none(db)), + + Type::FunctionLiteral(function) => { + function_generic_context(*function) + } + + Type::BoundMethod(bound_method) => { + function_generic_context(bound_method.function(db)) + } _ => Type::none(db), }); diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index a9d7493993..d1c7140fd7 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -340,6 +340,7 @@ impl<'db> OverloadLiteral<'db> { let index = semantic_index(db, scope.file(db)); GenericContext::from_type_params(db, index, type_params) }); + Signature::from_function( db, generic_context, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4eafce6052..f19a1a477e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1,9 +1,12 @@ use std::borrow::Cow; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast as ast; use rustc_hash::FxHashMap; -use crate::semantic_index::SemanticIndex; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind}; +use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType}; @@ -11,10 +14,51 @@ use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::{ KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, - TypeVarInstance, TypeVarVariance, UnionType, declaration_type, + TypeVarInstance, TypeVarVariance, UnionType, binding_type, declaration_type, }; use crate::{Db, FxOrderSet}; +/// Returns an iterator of any generic context introduced by the given scope or any enclosing +/// scope. +fn enclosing_generic_contexts<'db>( + db: &'db dyn Db, + module: &ParsedModuleRef, + index: &SemanticIndex<'db>, + scope: FileScopeId, +) -> impl Iterator> { + index + .ancestor_scopes(scope) + .filter_map(|(_, ancestor_scope)| match ancestor_scope.node() { + NodeWithScopeKind::Class(class) => { + binding_type(db, index.expect_single_definition(class.node(module))) + .into_class_literal()? + .generic_context(db) + } + NodeWithScopeKind::Function(function) => { + binding_type(db, index.expect_single_definition(function.node(module))) + .into_function_literal()? + .signature(db) + .iter() + .last() + .expect("function should have at least one overload") + .generic_context + } + _ => None, + }) +} + +/// Returns the legacy typevars that have been bound in the given scope or any enclosing scope. +fn bound_legacy_typevars<'db>( + db: &'db dyn Db, + module: &ParsedModuleRef, + index: &'db SemanticIndex<'db>, + scope: FileScopeId, +) -> impl Iterator> { + enclosing_generic_contexts(db, module, index, scope) + .flat_map(|generic_context| generic_context.variables(db).iter().copied()) + .filter(|typevar| typevar.is_legacy(db)) +} + /// A list of formal type variables for a generic function, class, or type alias. /// /// TODO: Handle nested generic contexts better, with actual parent links to the lexically @@ -82,9 +126,11 @@ impl<'db> GenericContext<'db> { /// list. pub(crate) fn from_function_params( db: &'db dyn Db, + definition: Definition<'db>, parameters: &Parameters<'db>, return_type: Option>, ) -> Option { + // Find all of the legacy typevars mentioned in the function signature. let mut variables = FxOrderSet::default(); for param in parameters { if let Some(ty) = param.annotated_type() { @@ -97,6 +143,16 @@ impl<'db> GenericContext<'db> { if let Some(ty) = return_type { ty.find_legacy_typevars(db, &mut variables); } + + // Then remove any that were bound in enclosing scopes. + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let containing_scope = definition.file_scope(db); + for typevar in bound_legacy_typevars(db, &module, index, containing_scope) { + variables.remove(&typevar); + } + if variables.is_empty() { return None; } @@ -171,6 +227,16 @@ impl<'db> GenericContext<'db> { self.specialize(db, types.into()) } + /// Returns a tuple type of the typevars introduced by this generic context. + pub(crate) fn as_tuple(self, db: &'db dyn Db) -> Type<'db> { + TupleType::from_elements( + db, + self.variables(db) + .iter() + .map(|typevar| Type::TypeVar(*typevar)), + ) + } + pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool { self.variables(db).is_subset(other.variables(db)) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 8fe8d1c097..70c923bdbf 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -331,7 +331,7 @@ impl<'db> Signature<'db> { } }); let legacy_generic_context = - GenericContext::from_function_params(db, ¶meters, return_ty); + GenericContext::from_function_params(db, definition, ¶meters, return_ty); if generic_context.is_some() && legacy_generic_context.is_some() { // TODO: Raise a diagnostic!