mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Generalize union-type subtyping fast path (#22495)
This commit is contained in:
@@ -907,6 +907,23 @@ def _(x: list[str]):
|
||||
reveal_type(accepts_callable(GenericClass)(x, x))
|
||||
```
|
||||
|
||||
### `Callable`s that return union types
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class Box[T]:
|
||||
def get(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def my_iter[T](f: Callable[[], T | None]) -> Box[T]:
|
||||
return Box()
|
||||
|
||||
def get_int() -> int | None: ...
|
||||
|
||||
reveal_type(my_iter(get_int)) # revealed: Box[int]
|
||||
```
|
||||
|
||||
### Don't include identical lower/upper bounds in type mapping multiple times
|
||||
|
||||
This is was a performance regression reported in
|
||||
|
||||
@@ -1840,7 +1840,7 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// This method may have false negatives, but it should not have false positives. It should be
|
||||
/// a cheap shallow check, not an exhaustive recursive check.
|
||||
fn subtyping_is_always_reflexive(self) -> bool {
|
||||
const fn subtyping_is_always_reflexive(self) -> bool {
|
||||
match self {
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
@@ -1861,6 +1861,9 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
// `T` is always a subtype of itself,
|
||||
// and `T` is always a subtype of `T | None`
|
||||
| Type::TypeVar(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
@@ -1872,7 +1875,6 @@ impl<'db> Type<'db> {
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::Callable(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypeGuard(_)
|
||||
|
||||
@@ -195,6 +195,17 @@ impl TypeRelation<'_> {
|
||||
pub(crate) const fn is_subtyping(self) -> bool {
|
||||
matches!(self, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool {
|
||||
match self {
|
||||
TypeRelation::Assignability
|
||||
| TypeRelation::ConstraintSetAssignability
|
||||
| TypeRelation::Redundancy => true,
|
||||
TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => {
|
||||
ty.subtyping_is_always_reflexive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
@@ -329,7 +340,7 @@ impl<'db> Type<'db> {
|
||||
//
|
||||
// Note that we could do a full equivalence check here, but that would be both expensive
|
||||
// and unnecessary. This early return is only an optimisation.
|
||||
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive()) && self == target {
|
||||
if relation.can_safely_assume_reflexivity(self) && self == target {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
|
||||
@@ -460,46 +471,43 @@ impl<'db> Type<'db> {
|
||||
},
|
||||
}),
|
||||
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// In general, a TypeVar `T` is not redundant with a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`.
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be a subtype of any union containing `T`.
|
||||
(Type::TypeVar(bound_typevar), Type::Union(union))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(_, Type::Union(union))
|
||||
if relation.can_safely_assume_reflexivity(self)
|
||||
&& union.elements(db).contains(&self) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(Type::Intersection(intersection), _)
|
||||
if relation.can_safely_assume_reflexivity(target)
|
||||
&& intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(Type::Intersection(intersection), _)
|
||||
if relation.is_assignability()
|
||||
&& intersection.positive(db).iter().any(Type::is_dynamic) =>
|
||||
{
|
||||
// If the intersection contains `Any`/`Unknown`/`@Todo`, it is assignable to any type.
|
||||
// `Any` could materialize to `Never`, `Never & T & ~S` simplifies to `Never` for any
|
||||
// `T` and any `S`, and `Never` is a subtype of all types.
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
(Type::Intersection(intersection), _)
|
||||
if relation.can_safely_assume_reflexivity(target)
|
||||
&& intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// Two identical typevars must always solve to the same type, so they are always
|
||||
// subtypes of each other and assignable to each other.
|
||||
//
|
||||
// Note that this is not handled by the early return at the beginning of this method,
|
||||
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
|
||||
(Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar))
|
||||
if !lhs_bound_typevar.is_inferable(db, inferable)
|
||||
&& lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
|
||||
// `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance
|
||||
// of `A`, and vice versa.
|
||||
(Type::SubclassOf(subclass_of), _)
|
||||
|
||||
Reference in New Issue
Block a user