mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:54:42 +00:00
[ty] Improve ability to solve TypeVars when they appear in unions (#19829)
This commit is contained in:
parent
6b0eadfb4d
commit
8489816edc
3 changed files with 98 additions and 32 deletions
|
@ -395,3 +395,40 @@ def decorated(t: T) -> None:
|
||||||
# error: [redundant-cast]
|
# error: [redundant-cast]
|
||||||
reveal_type(cast(T, t)) # revealed: T@decorated
|
reveal_type(cast(T, t)) # revealed: T@decorated
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Solving TypeVars with upper bounds in unions
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=A)
|
||||||
|
|
||||||
|
class B(Generic[T]):
|
||||||
|
x: T
|
||||||
|
|
||||||
|
def f(c: T | None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def g(b: B[T]):
|
||||||
|
return f(b.x) # Fine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constrained TypeVar in a union
|
||||||
|
|
||||||
|
This is a regression test for an issue that surfaced in the primer report of an early version of
|
||||||
|
<https://github.com/astral-sh/ruff/pull/19811>, where we failed to solve the `TypeVar` here due to
|
||||||
|
the fact that it only appears in the function's type annotations as part of a union:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T", str, bytes)
|
||||||
|
|
||||||
|
def NamedTemporaryFile(suffix: T | None, prefix: T | None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def f(x: str):
|
||||||
|
NamedTemporaryFile(prefix=x, suffix=".tar.gz") # Fine
|
||||||
|
```
|
||||||
|
|
|
@ -404,3 +404,32 @@ def decorated[T](t: T) -> None:
|
||||||
# error: [redundant-cast]
|
# error: [redundant-cast]
|
||||||
reveal_type(cast(T, t)) # revealed: T@decorated
|
reveal_type(cast(T, t)) # revealed: T@decorated
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Solving TypeVars with upper bounds in unions
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
class B[T: A]:
|
||||||
|
x: T
|
||||||
|
|
||||||
|
def f[T: A](c: T | None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def g[T: A](b: B[T]):
|
||||||
|
return f(b.x) # Fine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constrained TypeVar in a union
|
||||||
|
|
||||||
|
This is a regression test for an issue that surfaced in the primer report of an early version of
|
||||||
|
<https://github.com/astral-sh/ruff/pull/19811>, where we failed to solve the `TypeVar` here due to
|
||||||
|
the fact that it only appears in the function's type annotations as part of a union:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f[T: (str, bytes)](suffix: T | None, prefix: T | None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def g(x: str):
|
||||||
|
f(prefix=x, suffix=".tar.gz")
|
||||||
|
```
|
||||||
|
|
|
@ -792,6 +792,38 @@ impl<'db> SpecializationBuilder<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
match (formal, actual) {
|
match (formal, actual) {
|
||||||
|
(Type::Union(formal), _) => {
|
||||||
|
// TODO: We haven't implemented a full unification solver yet. If typevars appear
|
||||||
|
// in multiple union elements, we ideally want to express that _only one_ of them
|
||||||
|
// needs to match, and that we should infer the smallest type mapping that allows
|
||||||
|
// that.
|
||||||
|
//
|
||||||
|
// For now, we punt on handling multiple typevar elements. Instead, if _precisely
|
||||||
|
// one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead
|
||||||
|
// and add a mapping between that typevar and the actual type. (Note that we've
|
||||||
|
// already handled above the case where the actual is assignable to a _non-typevar_
|
||||||
|
// union element.)
|
||||||
|
let mut typevars = formal.iter(self.db).filter_map(|ty| match ty {
|
||||||
|
Type::TypeVar(typevar) => Some(*typevar),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let typevar = typevars.next();
|
||||||
|
let additional_typevars = typevars.next();
|
||||||
|
if let (Some(typevar), None) = (typevar, additional_typevars) {
|
||||||
|
self.add_type_mapping(typevar, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(Type::Intersection(formal), _) => {
|
||||||
|
// The actual type must be assignable to every (positive) element of the
|
||||||
|
// formal intersection, so we must infer type mappings for each of them. (The
|
||||||
|
// actual type must also be disjoint from every negative element of the
|
||||||
|
// intersection, but that doesn't help us infer any type mappings.)
|
||||||
|
for positive in formal.iter_positive(self.db) {
|
||||||
|
self.infer(positive, actual)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) => {
|
(Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) => {
|
||||||
match typevar.bound_or_constraints(self.db) {
|
match typevar.bound_or_constraints(self.db) {
|
||||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||||
|
@ -877,38 +909,6 @@ impl<'db> SpecializationBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::Union(formal), _) => {
|
|
||||||
// TODO: We haven't implemented a full unification solver yet. If typevars appear
|
|
||||||
// in multiple union elements, we ideally want to express that _only one_ of them
|
|
||||||
// needs to match, and that we should infer the smallest type mapping that allows
|
|
||||||
// that.
|
|
||||||
//
|
|
||||||
// For now, we punt on handling multiple typevar elements. Instead, if _precisely
|
|
||||||
// one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead
|
|
||||||
// and add a mapping between that typevar and the actual type. (Note that we've
|
|
||||||
// already handled above the case where the actual is assignable to a _non-typevar_
|
|
||||||
// union element.)
|
|
||||||
let mut typevars = formal.iter(self.db).filter_map(|ty| match ty {
|
|
||||||
Type::TypeVar(typevar) => Some(*typevar),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let typevar = typevars.next();
|
|
||||||
let additional_typevars = typevars.next();
|
|
||||||
if let (Some(typevar), None) = (typevar, additional_typevars) {
|
|
||||||
self.add_type_mapping(typevar, actual);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::Intersection(formal), _) => {
|
|
||||||
// The actual type must be assignable to every (positive) element of the
|
|
||||||
// formal intersection, so we must infer type mappings for each of them. (The
|
|
||||||
// actual type must also be disjoint from every negative element of the
|
|
||||||
// intersection, but that doesn't help us infer any type mappings.)
|
|
||||||
for positive in formal.iter_positive(self.db) {
|
|
||||||
self.infer(positive, actual)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add more forms that we can structurally induct into: type[C], callables
|
// TODO: Add more forms that we can structurally induct into: type[C], callables
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue