[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:
Douglas Creager
2025-04-15 16:34:07 -04:00
committed by GitHub
parent 78dabc332d
commit 807a8a7a29
2 changed files with 52 additions and 34 deletions

View File

@@ -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:

View File

@@ -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)) => {