mirror of https://github.com/astral-sh/ruff
[ty] Don't include already-bound legacy typevars in function generic context (#19558)
We now correctly exclude legacy typevars from enclosing scopes when
constructing the generic context for a generic function.
more detail:
A function is generic if it refers to legacy typevars in its signature:
```py
from typing import TypeVar
T = TypeVar("T")
def f(t: T) -> T:
return t
```
Generic functions are allowed to appear inside of other generic
contexts. When they do, they can refer to the typevars of those
enclosing generic contexts, and that should not rebind the typevar:
```py
from typing import TypeVar, Generic
T = TypeVar("T")
U = TypeVar("U")
class C(Generic[T]):
@staticmethod
def method(t: T, u: U) -> None: ...
# revealed: def method(t: int, u: U) -> None
reveal_type(C[int].method)
```
This substitution was already being performed correctly, but we were
also still including the enclosing legacy typevars in the method's own
generic context, which can be seen via `ty_extensions.generic_context`
(which has been updated to work on generic functions and methods):
```py
from ty_extensions import generic_context
# before: tuple[T, U]
# after: tuple[U]
reveal_type(generic_context(C[int].method))
```
---------
Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
72fdb7d439
commit
e867830848
|
|
@ -433,17 +433,31 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva
|
||||||
scope for the method.
|
scope for the method.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
from ty_extensions import generic_context
|
||||||
from typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
U = TypeVar("U")
|
U = TypeVar("U")
|
||||||
|
|
||||||
class C(Generic[T]):
|
class C(Generic[T]):
|
||||||
def method(self, u: U) -> U:
|
def method(self, u: int) -> int:
|
||||||
return u
|
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]()
|
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
|
## Specializations propagate
|
||||||
|
|
|
||||||
|
|
@ -392,8 +392,13 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva
|
||||||
scope for the method.
|
scope for the method.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
from ty_extensions import generic_context
|
||||||
|
|
||||||
class C[T]:
|
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
|
return u
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
def cannot_use_outside_of_method(self, u: U): ...
|
def cannot_use_outside_of_method(self, u: U): ...
|
||||||
|
|
@ -401,8 +406,18 @@ class C[T]:
|
||||||
# TODO: error
|
# TODO: error
|
||||||
def cannot_shadow_class_typevar[T](self, t: T): ...
|
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]()
|
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
|
## Specializations propagate
|
||||||
|
|
|
||||||
|
|
@ -141,10 +141,22 @@ class Legacy(Generic[T]):
|
||||||
def m(self, x: T, y: S) -> S:
|
def m(self, x: T, y: S) -> S:
|
||||||
return y
|
return y
|
||||||
|
|
||||||
legacy: Legacy[int] = Legacy()
|
legacy: Legacy[int] = Legacy[int]()
|
||||||
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
|
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:
|
With PEP 695 syntax, it is clearer that the method uses a separate typevar:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -623,19 +623,38 @@ impl<'db> Bindings<'db> {
|
||||||
|
|
||||||
Some(KnownFunction::GenericContext) => {
|
Some(KnownFunction::GenericContext) => {
|
||||||
if let [Some(ty)] = overload.parameter_types() {
|
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
|
// TODO: Handle generic functions, and unions/intersections of
|
||||||
// generic types
|
// generic types
|
||||||
overload.set_return_type(match ty {
|
overload.set_return_type(match ty {
|
||||||
Type::ClassLiteral(class) => match class.generic_context(db) {
|
Type::ClassLiteral(class) => class
|
||||||
Some(generic_context) => TupleType::from_elements(
|
.generic_context(db)
|
||||||
db,
|
.map(|generic_context| generic_context.as_tuple(db))
|
||||||
generic_context
|
.unwrap_or_else(|| Type::none(db)),
|
||||||
.variables(db)
|
|
||||||
.iter()
|
Type::FunctionLiteral(function) => {
|
||||||
.map(|typevar| Type::TypeVar(*typevar)),
|
function_generic_context(*function)
|
||||||
),
|
}
|
||||||
None => Type::none(db),
|
|
||||||
},
|
Type::BoundMethod(bound_method) => {
|
||||||
|
function_generic_context(bound_method.function(db))
|
||||||
|
}
|
||||||
|
|
||||||
_ => Type::none(db),
|
_ => Type::none(db),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,7 @@ impl<'db> OverloadLiteral<'db> {
|
||||||
let index = semantic_index(db, scope.file(db));
|
let index = semantic_index(db, scope.file(db));
|
||||||
GenericContext::from_type_params(db, index, type_params)
|
GenericContext::from_type_params(db, index, type_params)
|
||||||
});
|
});
|
||||||
|
|
||||||
Signature::from_function(
|
Signature::from_function(
|
||||||
db,
|
db,
|
||||||
generic_context,
|
generic_context,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use rustc_hash::FxHashMap;
|
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::ClassType;
|
||||||
use crate::types::class_base::ClassBase;
|
use crate::types::class_base::ClassBase;
|
||||||
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
|
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::tuple::{TupleSpec, TupleType};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints,
|
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints,
|
||||||
TypeVarInstance, TypeVarVariance, UnionType, declaration_type,
|
TypeVarInstance, TypeVarVariance, UnionType, binding_type, declaration_type,
|
||||||
};
|
};
|
||||||
use crate::{Db, FxOrderSet};
|
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<Item = GenericContext<'db>> {
|
||||||
|
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<Item = TypeVarInstance<'db>> {
|
||||||
|
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.
|
/// 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
|
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
|
||||||
|
|
@ -82,9 +126,11 @@ impl<'db> GenericContext<'db> {
|
||||||
/// list.
|
/// list.
|
||||||
pub(crate) fn from_function_params(
|
pub(crate) fn from_function_params(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
|
definition: Definition<'db>,
|
||||||
parameters: &Parameters<'db>,
|
parameters: &Parameters<'db>,
|
||||||
return_type: Option<Type<'db>>,
|
return_type: Option<Type<'db>>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
|
// Find all of the legacy typevars mentioned in the function signature.
|
||||||
let mut variables = FxOrderSet::default();
|
let mut variables = FxOrderSet::default();
|
||||||
for param in parameters {
|
for param in parameters {
|
||||||
if let Some(ty) = param.annotated_type() {
|
if let Some(ty) = param.annotated_type() {
|
||||||
|
|
@ -97,6 +143,16 @@ impl<'db> GenericContext<'db> {
|
||||||
if let Some(ty) = return_type {
|
if let Some(ty) = return_type {
|
||||||
ty.find_legacy_typevars(db, &mut variables);
|
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() {
|
if variables.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -171,6 +227,16 @@ impl<'db> GenericContext<'db> {
|
||||||
self.specialize(db, types.into())
|
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 {
|
pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool {
|
||||||
self.variables(db).is_subset(other.variables(db))
|
self.variables(db).is_subset(other.variables(db))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ impl<'db> Signature<'db> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let legacy_generic_context =
|
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() {
|
if generic_context.is_some() && legacy_generic_context.is_some() {
|
||||||
// TODO: Raise a diagnostic!
|
// TODO: Raise a diagnostic!
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue