[red-knot] Acknowledge that T & anything is assignable to T (#17413)
This reworks the assignability/subtyping relations a bit to handle typevars better: 1. For the most part, types are not assignable to typevars, since there's no guarantee what type the typevar will be specialized to. 2. An intersection is an exception, if it contains the typevar itself as one of the positive elements. This should fall out from the other clauses automatically, since a typevar is assignable to itself, and an intersection is assignable to something if any positive element is assignable to that something. 3. Constrained typevars are an exception, since they must be specialized to _exactly_ one of the constraints, not to a _subtype_ of a constraint. If a type is assignable to every constraint, then the type is also assignable to the constrained typevar. We already had a special case for (3), but the ordering of it relative to the intersection clauses meant we weren't catching (2) correctly. To fix this, we keep the special case for (3), but fall through to the other match arms for non-constrained typevars and if the special case isn't true for a constrained typevar. Closes https://github.com/astral-sh/ruff/issues/17364
This commit is contained in:
@@ -507,6 +507,20 @@ def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
The intersection of a typevar with any other type is assignable to (and if fully static, a subtype
|
||||
of) itself.
|
||||
|
||||
```py
|
||||
from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not
|
||||
|
||||
def intersection_is_assignable[T](t: T) -> None:
|
||||
static_assert(is_assignable_to(Intersection[T, None], T))
|
||||
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
|
||||
|
||||
static_assert(is_subtype_of(Intersection[T, None], T))
|
||||
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
||||
|
||||
@@ -890,23 +890,18 @@ impl<'db> Type<'db> {
|
||||
.iter()
|
||||
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
|
||||
|
||||
(_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) {
|
||||
// No types are a subtype of a bounded typevar, or of an unbounded unconstrained
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized
|
||||
// to. If the typevar is bounded, it might be specialized to a smaller type than
|
||||
// the bound. (This is true even if the bound is a final class, since the typevar
|
||||
// can still be specialized to `Never`.)
|
||||
None => false,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
|
||||
// If the typevar is constrained, there must be multiple constraints, and the
|
||||
// typevar might be specialized to any one of them. However, the constraints do not
|
||||
// have to be disjoint, which means an lhs type might be a subtype of all of the
|
||||
// constraints.
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| self.is_subtype_of(db, *constraint)),
|
||||
},
|
||||
// If the typevar is constrained, there must be multiple constraints, and the typevar
|
||||
// might be specialized to any one of them. However, the constraints do not have to be
|
||||
// disjoint, which means an lhs type might be a subtype of all of the constraints.
|
||||
(_, Type::TypeVar(typevar))
|
||||
if typevar.constraints(db).is_some_and(|constraints| {
|
||||
constraints
|
||||
.iter()
|
||||
.all(|constraint| self.is_subtype_of(db, *constraint))
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B,
|
||||
@@ -927,6 +922,13 @@ impl<'db> Type<'db> {
|
||||
.iter()
|
||||
.any(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
|
||||
// Other than the special cases checked above, no other types are a subtype of a
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized to.
|
||||
// (If the typevar is bounded, it might be specialized to a smaller type than the
|
||||
// bound. This is true even if the bound is a final class, since the typevar can still
|
||||
// be specialized to `Never`.)
|
||||
(_, Type::TypeVar(_)) => false,
|
||||
|
||||
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
|
||||
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
|
||||
(left, Type::AlwaysFalsy) => left.bool(db).is_always_false(),
|
||||
@@ -1186,23 +1188,18 @@ impl<'db> Type<'db> {
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
|
||||
|
||||
(_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) {
|
||||
// No types are assignable to a bounded typevar, or to an unbounded unconstrained
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized
|
||||
// to. If the typevar is bounded, it might be specialized to a smaller type than
|
||||
// the bound. (This is true even if the bound is a final class, since the typevar
|
||||
// can still be specialized to `Never`.)
|
||||
None => false,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
|
||||
// If the typevar is constrained, there must be multiple constraints, and the
|
||||
// typevar might be specialized to any one of them. However, the constraints do not
|
||||
// have to be disjoint, which means an lhs type might be assignable to all of the
|
||||
// constraints.
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| self.is_assignable_to(db, *constraint)),
|
||||
},
|
||||
// If the typevar is constrained, there must be multiple constraints, and the typevar
|
||||
// might be specialized to any one of them. However, the constraints do not have to be
|
||||
// disjoint, which means an lhs type might be assignable to all of the constraints.
|
||||
(_, Type::TypeVar(typevar))
|
||||
if typevar.constraints(db).is_some_and(|constraints| {
|
||||
constraints
|
||||
.iter()
|
||||
.all(|constraint| self.is_assignable_to(db, *constraint))
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// If both sides are intersections we need to handle the right side first
|
||||
// (A & B & C) is assignable to (A & B) because the left is assignable to both A and B,
|
||||
@@ -1230,6 +1227,13 @@ impl<'db> Type<'db> {
|
||||
.iter()
|
||||
.any(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// Other than the special cases checked above, no other types are assignable to a
|
||||
// typevar, since there's no guarantee what type the typevar will be specialized to.
|
||||
// (If the typevar is bounded, it might be specialized to a smaller type than the
|
||||
// bound. This is true even if the bound is a final class, since the typevar can still
|
||||
// be specialized to `Never`.)
|
||||
(_, Type::TypeVar(_)) => false,
|
||||
|
||||
// A tuple type S is assignable to a tuple type T if their lengths are the same, and
|
||||
// each element of S is assignable to the corresponding element of T.
|
||||
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
|
||||
|
||||
Reference in New Issue
Block a user