mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[ty] Don't include already-bound legacy typevars in function generic context (#19558)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
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
7 changed files with 145 additions and 18 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<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.
|
||||
///
|
||||
/// 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<Type<'db>>,
|
||||
) -> Option<Self> {
|
||||
// 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))
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue