mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[ty] Unify Type::is_subtype_of()
and Type::is_assignable_to()
(#18430)
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 / 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 / mkdocs (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 (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 / 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 / mkdocs (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 (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
This commit is contained in:
parent
1274521f9f
commit
6e785867c3
11 changed files with 394 additions and 588 deletions
|
@ -305,10 +305,13 @@ simplify to `Never`, even in the presence of other types:
|
|||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(Generic[T_co]): ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Not[P]],
|
||||
|
@ -317,6 +320,8 @@ def _(
|
|||
i4: Intersection[Not[P], Q, P],
|
||||
i5: Intersection[P, Any, Not[P]],
|
||||
i6: Intersection[Not[P], Any, P],
|
||||
i7: Intersection[R[P], Not[R[P]]],
|
||||
i8: Intersection[R[P], Not[R[Q]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
|
@ -324,6 +329,8 @@ def _(
|
|||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
reveal_type(i7) # revealed: Never
|
||||
reveal_type(i8) # revealed: R[P] & ~R[Q]
|
||||
```
|
||||
|
||||
### Union of a type and its negation
|
||||
|
@ -332,20 +339,28 @@ Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `o
|
|||
|
||||
```py
|
||||
from ty_extensions import Intersection, Not
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(Generic[T_co]): ...
|
||||
|
||||
def _(
|
||||
i1: P | Not[P],
|
||||
i2: Not[P] | P,
|
||||
i3: P | Q | Not[P],
|
||||
i4: Not[P] | Q | P,
|
||||
i5: R[P] | Not[R[P]],
|
||||
i6: R[P] | Not[R[Q]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: object
|
||||
reveal_type(i2) # revealed: object
|
||||
reveal_type(i3) # revealed: object
|
||||
reveal_type(i4) # revealed: object
|
||||
reveal_type(i5) # revealed: object
|
||||
reveal_type(i6) # revealed: R[P] | ~R[Q]
|
||||
```
|
||||
|
||||
### Negation is an involution
|
||||
|
|
|
@ -902,8 +902,7 @@ from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf
|
|||
class HasX(Protocol):
|
||||
x: int
|
||||
|
||||
# TODO: this should pass
|
||||
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[module], HasX))
|
||||
static_assert(is_assignable_to(TypeOf[module], HasX))
|
||||
|
||||
class ExplicitProtocolSubtype(HasX, Protocol):
|
||||
|
|
|
@ -209,6 +209,34 @@ class AnyMeta(metaclass=Any): ...
|
|||
static_assert(is_assignable_to(type[AnyMeta], type))
|
||||
static_assert(is_assignable_to(type[AnyMeta], type[object]))
|
||||
static_assert(is_assignable_to(type[AnyMeta], type[Any]))
|
||||
|
||||
from typing import TypeVar, Generic, Any
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
class Foo(Generic[T_co]): ...
|
||||
class Bar(Foo[T_co], Generic[T_co]): ...
|
||||
|
||||
static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
|
||||
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
|
||||
|
||||
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types)
|
||||
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## `type[]` is not assignable to types disjoint from `builtins.type`
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import is_assignable_to, static_assert
|
||||
|
||||
static_assert(not is_assignable_to(type[Any], None))
|
||||
```
|
||||
|
||||
## Class-literals that inherit from `Any`
|
||||
|
@ -717,6 +745,53 @@ def f(x: int, y: str) -> None: ...
|
|||
c1: Callable[[int], None] = partial(f, y="a")
|
||||
```
|
||||
|
||||
### Generic classes with `__call__`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
class Foo[T]:
|
||||
def __call__(self): ...
|
||||
|
||||
class FooLegacy(Generic[T]):
|
||||
def __call__(self): ...
|
||||
|
||||
class Bar[T, **P]:
|
||||
def __call__(self): ...
|
||||
|
||||
# TODO: should not error
|
||||
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
def __call__(self): ...
|
||||
|
||||
static_assert(is_assignable_to(Foo, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(FooLegacy, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(Bar, Callable[..., Any]))
|
||||
static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
|
||||
|
||||
class Spam[T]: ...
|
||||
class SpamLegacy(Generic[T]): ...
|
||||
class Eggs[T, **P]: ...
|
||||
|
||||
# TODO: should not error
|
||||
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
|
||||
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
|
||||
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
|
||||
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))
|
||||
|
||||
# TODO: should pass
|
||||
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
### Classes with `__call__` as attribute
|
||||
|
||||
An instance type is assignable to a compatible callable type if the instance type's class has a
|
||||
|
|
|
@ -611,6 +611,10 @@ impl<'db> Type<'db> {
|
|||
matches!(self, Type::GenericAlias(_))
|
||||
}
|
||||
|
||||
const fn is_dynamic(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
/// Replace references to the class `class` with a self-reference marker. This is currently
|
||||
/// used for recursive protocols, but could probably be extended to self-referential type-
|
||||
/// aliases and similar.
|
||||
|
@ -1050,34 +1054,26 @@ impl<'db> Type<'db> {
|
|||
///
|
||||
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
// Two equivalent types are always subtypes of each other.
|
||||
//
|
||||
// "Equivalent to" here means that the two types are both fully static
|
||||
// and describe exactly the same set of possible runtime objects.
|
||||
// For example, `int` is a subtype of `int` because `int` and `int` are equivalent to each other.
|
||||
// Equally, `type[object]` is a subtype of `type`,
|
||||
// because the former type expresses "all subclasses of `object`"
|
||||
// while the latter expresses "all instances of `type`",
|
||||
// and these are exactly the same set of objects at runtime.
|
||||
if self.is_equivalent_to(db, target) {
|
||||
self.has_relation_to(db, target, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
self.has_relation_to(db, target, TypeRelation::Assignability)
|
||||
}
|
||||
|
||||
fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool {
|
||||
if !relation.applies_to(db, self, target) {
|
||||
return false;
|
||||
}
|
||||
if relation.are_equivalent(db, self, target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-fully-static types do not participate in subtyping.
|
||||
//
|
||||
// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
|
||||
// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
|
||||
// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
|
||||
// so the question is simply unanswerable for non-fully-static types.
|
||||
if !self.is_fully_static(db) || !target.is_fully_static(db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (self, target) {
|
||||
// We should have handled these immediately above.
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => {
|
||||
unreachable!("Non-fully-static types do not participate in subtyping!")
|
||||
}
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true,
|
||||
|
||||
// `Never` is the bottom type, the empty set.
|
||||
// It is a subtype of all other fully static types.
|
||||
|
@ -1115,12 +1111,12 @@ impl<'db> Type<'db> {
|
|||
match typevar.bound_or_constraints(db) {
|
||||
None => unreachable!(),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.is_subtype_of(db, target)
|
||||
bound.has_relation_to(db, target, relation)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| constraint.is_subtype_of(db, target)),
|
||||
.all(|constraint| constraint.has_relation_to(db, target, relation)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1131,7 +1127,7 @@ impl<'db> Type<'db> {
|
|||
if typevar.constraints(db).is_some_and(|constraints| {
|
||||
constraints
|
||||
.iter()
|
||||
.all(|constraint| self.is_subtype_of(db, *constraint))
|
||||
.all(|constraint| self.has_relation_to(db, *constraint, relation))
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
|
@ -1140,12 +1136,12 @@ impl<'db> Type<'db> {
|
|||
(Type::Union(union), _) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
.all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
|
||||
|
||||
(_, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
|
||||
.any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)),
|
||||
|
||||
// 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,
|
||||
|
@ -1154,7 +1150,7 @@ impl<'db> Type<'db> {
|
|||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.all(|&pos_ty| self.is_subtype_of(db, pos_ty))
|
||||
.all(|&pos_ty| self.has_relation_to(db, pos_ty, relation))
|
||||
&& intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
|
@ -1164,7 +1160,7 @@ impl<'db> Type<'db> {
|
|||
(Type::Intersection(intersection), _) => intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| elem_ty.is_subtype_of(db, target)),
|
||||
.any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
|
||||
|
||||
// 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.
|
||||
|
@ -1179,7 +1175,7 @@ impl<'db> Type<'db> {
|
|||
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
|
||||
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
|
||||
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
|
||||
target.is_equivalent_to(db, Type::object(db))
|
||||
relation.are_equivalent(db, target, Type::object(db))
|
||||
}
|
||||
|
||||
// These clauses handle type variants that include function literals. A function
|
||||
|
@ -1188,13 +1184,13 @@ impl<'db> Type<'db> {
|
|||
// applied to the signature. Different specializations of the same function literal are
|
||||
// only subtypes of each other if they result in the same signature.
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_subtype_of(db, target_function)
|
||||
self_function.has_relation_to(db, target_function, relation)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_subtype_of(db, target_method)
|
||||
self_method.has_relation_to(db, target_method, relation)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_subtype_of(db, target_method)
|
||||
self_method.has_relation_to(db, target_method, relation)
|
||||
}
|
||||
|
||||
// No literal type is a subtype of any other literal type, unless they are the same
|
||||
|
@ -1216,6 +1212,31 @@ impl<'db> Type<'db> {
|
|||
| Type::ModuleLiteral(_),
|
||||
) => false,
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// If the type of __call__ is a subtype of a callable type, this instance is.
|
||||
// Don't add other special cases here; our subtyping of a callable type
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.has_relation_to(db, target, relation)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.has_relation_to(db, right, relation)
|
||||
}
|
||||
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
(Type::StringLiteral(_), Type::LiteralString) => true,
|
||||
|
||||
|
@ -1231,45 +1252,37 @@ impl<'db> Type<'db> {
|
|||
| Type::ModuleLiteral(_),
|
||||
_,
|
||||
) => (self.literal_fallback_instance(db))
|
||||
.is_some_and(|instance| instance.is_subtype_of(db, target)),
|
||||
|
||||
// Function-like callables are subtypes of `FunctionType`
|
||||
(Type::Callable(callable), Type::NominalInstance(target))
|
||||
if callable.is_function_like(db)
|
||||
&& target.class.is_known(db, KnownClass::FunctionType) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
.is_some_and(|instance| instance.has_relation_to(db, target, relation)),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
.is_subtype_of(db, target)
|
||||
.has_relation_to(db, target, relation)
|
||||
}
|
||||
|
||||
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
|
||||
.into_callable_type(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
|
||||
// so it also, for now, just delegates to its instance fallback.
|
||||
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// The same reasoning applies for these special callable types:
|
||||
(Type::BoundMethod(_), _) => KnownClass::MethodType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
|
||||
self_callable.is_subtype_of(db, other_callable)
|
||||
self_callable.has_relation_to(db, other_callable, relation)
|
||||
}
|
||||
|
||||
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
|
||||
|
@ -1277,29 +1290,15 @@ impl<'db> Type<'db> {
|
|||
false
|
||||
}
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// If the type of __call__ is a subtype of a callable type, this instance is.
|
||||
// Don't add other special cases here; our subtyping of a callable type
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.is_subtype_of(db, target)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// Function-like callables are subtypes of `FunctionType`
|
||||
(Type::Callable(callable), _)
|
||||
if callable.is_function_like(db)
|
||||
&& KnownClass::FunctionType
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.is_subtype_of(db, right)
|
||||
}
|
||||
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
(Type::Callable(_), _) => {
|
||||
// TODO: Implement subtyping between callable types and other types like
|
||||
|
@ -1319,54 +1318,81 @@ impl<'db> Type<'db> {
|
|||
self_elements.len() == target_elements.len()
|
||||
&& self_elements.iter().zip(target_elements).all(
|
||||
|(self_element, target_element)| {
|
||||
self_element.is_subtype_of(db, *target_element)
|
||||
self_element.has_relation_to(db, *target_element, relation)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
|
||||
(Type::Tuple(tuple), _) => tuple.homogeneous_supertype(db).is_subtype_of(db, target),
|
||||
(Type::Tuple(tuple), _) => tuple
|
||||
.homogeneous_supertype(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
|
||||
(Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target),
|
||||
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
|
||||
(Type::BoundSuper(_), _) => KnownClass::Super
|
||||
.to_instance(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
|
||||
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
|
||||
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_some_and(|target_class| class.is_subclass_of(db, None, target_class)),
|
||||
.is_none_or(|subclass_of_class| {
|
||||
ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
.subclass_of()
|
||||
.into_class()
|
||||
.is_some_and(|target_class| {
|
||||
ClassType::from(alias).is_subclass_of(db, target_class)
|
||||
.is_none_or(|subclass_of_class| {
|
||||
ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation)
|
||||
}),
|
||||
|
||||
// This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`?
|
||||
(Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => {
|
||||
self_subclass_ty.is_subtype_of(db, target_subclass_ty)
|
||||
self_subclass_ty.has_relation_to(db, target_subclass_ty, relation)
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
|
||||
ClassType::NonGeneric(class_literal)
|
||||
.into_callable(db)
|
||||
.is_subtype_of(db, target)
|
||||
.has_relation_to(db, target, relation)
|
||||
}
|
||||
|
||||
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
|
||||
.into_callable(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
|
||||
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
|
||||
// is an instance of its metaclass `abc.ABCMeta`.
|
||||
(Type::ClassLiteral(class), _) => {
|
||||
class.metaclass_instance_type(db).is_subtype_of(db, target)
|
||||
}
|
||||
(Type::ClassLiteral(class), _) => class
|
||||
.metaclass_instance_type(db)
|
||||
.has_relation_to(db, target, relation),
|
||||
(Type::GenericAlias(alias), _) => ClassType::from(alias)
|
||||
.metaclass_instance_type(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// This branch upholds two properties:
|
||||
// - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`.
|
||||
// - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`.
|
||||
//
|
||||
// This is really the same as the very first branch in this `match` statement that handles dynamic types.
|
||||
// That branch upholds two properties:
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any`
|
||||
// - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`.
|
||||
//
|
||||
// The only difference between this branch and the first branch is that the first branch deals with the type
|
||||
// `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`.
|
||||
//
|
||||
// See also: <https://github.com/astral-sh/ty/issues/222>
|
||||
(Type::SubclassOf(subclass_of_ty), other)
|
||||
| (other, Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
|
||||
// of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str`
|
||||
|
@ -1379,30 +1405,31 @@ impl<'db> Type<'db> {
|
|||
.subclass_of()
|
||||
.into_class()
|
||||
.map(|class| class.metaclass_instance_type(db))
|
||||
.is_some_and(|metaclass_instance_type| {
|
||||
metaclass_instance_type.is_subtype_of(db, target)
|
||||
}),
|
||||
.unwrap_or_else(|| KnownClass::Type.to_instance(db))
|
||||
.has_relation_to(db, target, relation),
|
||||
|
||||
// For example: `Type::SpecialForm(SpecialFormType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`,
|
||||
// because `Type::SpecialForm(SpecialFormType::Type)` is a set with exactly one runtime value in it
|
||||
// (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime.
|
||||
(Type::SpecialForm(left), right) => left.instance_fallback(db).is_subtype_of(db, right),
|
||||
(Type::SpecialForm(left), right) => left
|
||||
.instance_fallback(db)
|
||||
.has_relation_to(db, right, relation),
|
||||
|
||||
(Type::KnownInstance(left), right) => {
|
||||
left.instance_fallback(db).is_subtype_of(db, right)
|
||||
}
|
||||
(Type::KnownInstance(left), right) => left
|
||||
.instance_fallback(db)
|
||||
.has_relation_to(db, right, relation),
|
||||
|
||||
// `bool` is a subtype of `int`, because `bool` subclasses `int`,
|
||||
// which means that all instances of `bool` are also instances of `int`
|
||||
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
|
||||
self_instance.is_subtype_of(db, target_instance)
|
||||
self_instance.has_relation_to(db, target_instance, relation)
|
||||
}
|
||||
|
||||
(Type::PropertyInstance(_), _) => KnownClass::Property
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
.has_relation_to(db, target, relation),
|
||||
(_, Type::PropertyInstance(_)) => {
|
||||
self.is_subtype_of(db, KnownClass::Property.to_instance(db))
|
||||
self.has_relation_to(db, KnownClass::Property.to_instance(db), relation)
|
||||
}
|
||||
|
||||
// Other than the special cases enumerated above, `Instance` types and typevars are
|
||||
|
@ -1411,292 +1438,6 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_gradual_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match (self, target) {
|
||||
// Never can be assigned to any type.
|
||||
(Type::Never, _) => true,
|
||||
|
||||
// The dynamic type is assignable-to and assignable-from any type.
|
||||
(Type::Dynamic(_), _) => true,
|
||||
(_, Type::Dynamic(_)) => true,
|
||||
|
||||
// All types are assignable to `object`.
|
||||
// TODO this special case might be removable once the below cases are comprehensive
|
||||
(_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
|
||||
|
||||
// In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `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 assignable to `S`.
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be assignable to any union containing `T`.
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true,
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(_))
|
||||
if intersection.negative(db).contains(&target) =>
|
||||
{
|
||||
false
|
||||
}
|
||||
|
||||
// A typevar is assignable to its upper bound, and to something similar to the union of
|
||||
// its constraints. An unbound, unconstrained typevar has an implicit upper bound of
|
||||
// `object` (which is handled above).
|
||||
(Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => {
|
||||
match typevar.bound_or_constraints(db) {
|
||||
None => unreachable!(),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.is_assignable_to(db, target)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|constraint| constraint.is_assignable_to(db, target)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// A union is assignable to a type T iff every element of the union is assignable to T.
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
|
||||
|
||||
// A type T is assignable to a union iff T is assignable to any element of the union.
|
||||
(ty, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
|
||||
|
||||
// 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,
|
||||
// but none of A, B, or C is assignable to (A & B).
|
||||
//
|
||||
// A type S is assignable to an intersection type T if
|
||||
// S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and
|
||||
// S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]).
|
||||
(ty, Type::Intersection(intersection)) => {
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| ty.is_assignable_to(db, elem_ty))
|
||||
&& intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
.all(|&neg_ty| ty.is_disjoint_from(db, neg_ty))
|
||||
}
|
||||
|
||||
// An intersection type S is assignable to a type T if
|
||||
// Any element of S is assignable to T (e.g. `A & B` is assignable to `A`)
|
||||
// Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T.
|
||||
(Type::Intersection(intersection), ty) => intersection
|
||||
.positive(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)) => {
|
||||
let self_elements = self_tuple.elements(db);
|
||||
let target_elements = target_tuple.elements(db);
|
||||
self_elements.len() == target_elements.len()
|
||||
&& self_elements.iter().zip(target_elements).all(
|
||||
|(self_element, target_element)| {
|
||||
self_element.is_assignable_to(db, *target_element)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// This special case is required because the left-hand side tuple might be a
|
||||
// gradual type, so we can not rely on subtyping. This allows us to assign e.g.
|
||||
// `tuple[Any, int]` to `tuple`.
|
||||
//
|
||||
// `tuple[A, B, C]` is assignable to `tuple[A | B | C, ...]`
|
||||
(Type::Tuple(tuple), _)
|
||||
if tuple.homogeneous_supertype(db).is_assignable_to(db, target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// These clauses handle type variants that include function literals. A function
|
||||
// literal is assignable to itself, and not to any other function literal. However, our
|
||||
// representation of a function literal includes any specialization that should be
|
||||
// applied to the signature. Different specializations of the same function literal are
|
||||
// only assignable to each other if they result in the same signature.
|
||||
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
|
||||
self_function.is_assignable_to(db, target_function)
|
||||
}
|
||||
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
|
||||
self_method.is_assignable_to(db, target_method)
|
||||
}
|
||||
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
|
||||
self_method.is_assignable_to(db, target_method)
|
||||
}
|
||||
|
||||
// `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can
|
||||
// materialize to any `type[...]` type.
|
||||
(Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_))
|
||||
if subclass_of_ty.is_dynamic() =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class), Type::SubclassOf(_))
|
||||
if class
|
||||
.iter_mro(db, None)
|
||||
.any(class_base::ClassBase::is_dynamic) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// Every `type[...]` is assignable to `type`
|
||||
(Type::SubclassOf(_), _)
|
||||
if KnownClass::Type
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can
|
||||
// materialize to any `type[...]` type.
|
||||
//
|
||||
// Every class literal type is also assignable to `type[Any]`, because the class
|
||||
// literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable
|
||||
// to `type[Any]`.
|
||||
(
|
||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_),
|
||||
Type::SubclassOf(target_subclass_of),
|
||||
) if target_subclass_of.is_dynamic() => true,
|
||||
|
||||
// `type[Any]` is assignable to any type that `type[object]` is assignable to, because
|
||||
// `type[Any]` can materialize to `type[object]`.
|
||||
//
|
||||
// `type[Any]` is also assignable to any subtype of `type[object]`, because all
|
||||
// subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can
|
||||
// materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to
|
||||
// `Never`.)
|
||||
(Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& (KnownClass::Type
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target)
|
||||
|| target.is_subtype_of(db, KnownClass::Type.to_instance(db))) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
// Any type that is assignable to `type[object]` is also assignable to `type[Any]`,
|
||||
// because `type[Any]` can materialize to `type[object]`.
|
||||
(Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_dynamic()
|
||||
&& self.is_assignable_to(db, KnownClass::Type.to_instance(db)) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
|
||||
self_instance.is_assignable_to(db, target_instance)
|
||||
}
|
||||
|
||||
(Type::Callable(self_callable), Type::Callable(target_callable)) => {
|
||||
self_callable.is_assignable_to(db, target_callable)
|
||||
}
|
||||
|
||||
(Type::NominalInstance(instance), Type::Callable(_))
|
||||
if instance.class.is_subclass_of_any_or_unknown(db) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
Name::new_static("__call__"),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||
)
|
||||
.place;
|
||||
// shouldn't get out of sync with the calls we will actually allow.
|
||||
if let Place::Type(t, Boundness::Bound) = call_symbol {
|
||||
t.is_assignable_to(db, target)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
_ if self
|
||||
.literal_fallback_instance(db)
|
||||
.is_some_and(|instance| instance.is_assignable_to(db, target)) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
|
||||
ClassType::NonGeneric(class_literal)
|
||||
.into_callable(db)
|
||||
.is_assignable_to(db, target)
|
||||
}
|
||||
|
||||
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
|
||||
.into_callable(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
.is_assignable_to(db, target)
|
||||
}
|
||||
|
||||
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
|
||||
.into_callable_type(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||
left.is_assignable_to(db, right)
|
||||
}
|
||||
// Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
|
||||
// a protocol instance can never be assignable to a nominal type,
|
||||
// with the *sole* exception of `object`.
|
||||
(Type::ProtocolInstance(_), _) => false,
|
||||
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
|
||||
|
||||
// TODO other types containing gradual forms
|
||||
_ => self.is_subtype_of(db, target),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is [equivalent to] type `other`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
|
@ -7027,6 +6768,45 @@ impl<'db> ConstructorCallError<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum TypeRelation {
|
||||
Subtyping,
|
||||
Assignability,
|
||||
}
|
||||
|
||||
impl TypeRelation {
|
||||
/// Non-fully-static types do not participate in subtyping, only assignability,
|
||||
/// so the subtyping relation does not even apply to them.
|
||||
///
|
||||
/// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
|
||||
/// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
|
||||
/// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
|
||||
/// so the question is simply unanswerable for non-fully-static types.
|
||||
///
|
||||
/// However, the assignability relation applies to all types, even non-fully-static ones.
|
||||
fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db),
|
||||
TypeRelation::Assignability => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether `type_1` and `type_2` are equivalent.
|
||||
///
|
||||
/// Depending on whether the context is a subtyping test or an assignability test,
|
||||
/// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`].
|
||||
fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
|
||||
match self {
|
||||
TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2),
|
||||
TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2),
|
||||
}
|
||||
}
|
||||
|
||||
const fn applies_to_non_fully_static_types(self) -> bool {
|
||||
matches!(self, TypeRelation::Assignability)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Truthiness {
|
||||
/// For an object `x`, `bool(x)` will always return `True`
|
||||
|
@ -7139,26 +6919,16 @@ impl<'db> BoundMethodType<'db> {
|
|||
)
|
||||
}
|
||||
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
// A bound method is a typically a subtype of itself. However, we must explicitly verify
|
||||
// the subtyping of the underlying function signatures (since they might be specialized
|
||||
// differently), and of the bound self parameter (taking care that parameters, including a
|
||||
// bound self parameter, are contravariant.)
|
||||
self.function(db).is_subtype_of(db, other.function(db))
|
||||
self.function(db)
|
||||
.has_relation_to(db, other.function(db), relation)
|
||||
&& other
|
||||
.self_instance(db)
|
||||
.is_subtype_of(db, self.self_instance(db))
|
||||
}
|
||||
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A bound method is a typically assignable to itself. However, we must explicitly verify
|
||||
// the assignability of the underlying function signatures (since they might be specialized
|
||||
// differently), and of the bound self parameter (taking care that parameters, including a
|
||||
// bound self parameter, are contravariant.)
|
||||
self.function(db).is_assignable_to(db, other.function(db))
|
||||
&& other
|
||||
.self_instance(db)
|
||||
.is_assignable_to(db, self.self_instance(db))
|
||||
.has_relation_to(db, self.self_instance(db), relation)
|
||||
}
|
||||
|
||||
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
|
@ -7276,26 +7046,15 @@ impl<'db> CallableType<'db> {
|
|||
self.signatures(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Check whether this callable type is a subtype of another callable type.
|
||||
/// Check whether this callable type has the given relation to another callable type.
|
||||
///
|
||||
/// See [`Type::is_subtype_of`] for more details.
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_is_function_like = self.is_function_like(db);
|
||||
let other_is_function_like = other.is_function_like(db);
|
||||
(self_is_function_like || !other_is_function_like)
|
||||
&& self.signatures(db).is_subtype_of(db, other.signatures(db))
|
||||
}
|
||||
|
||||
/// Check whether this callable type is assignable to another callable type.
|
||||
///
|
||||
/// See [`Type::is_assignable_to`] for more details.
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_is_function_like = self.is_function_like(db);
|
||||
let other_is_function_like = other.is_function_like(db);
|
||||
(self_is_function_like || !other_is_function_like)
|
||||
&& self
|
||||
.signatures(db)
|
||||
.is_assignable_to(db, other.signatures(db))
|
||||
/// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details.
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
if other.is_function_like(db) && !self.is_function_like(db) {
|
||||
return false;
|
||||
}
|
||||
self.signatures(db)
|
||||
.has_relation_to(db, other.signatures(db), relation)
|
||||
}
|
||||
|
||||
/// Check whether this callable type is equivalent to another callable type.
|
||||
|
@ -7348,50 +7107,17 @@ pub enum MethodWrapperKind<'db> {
|
|||
}
|
||||
|
||||
impl<'db> MethodWrapperKind<'db> {
|
||||
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(other_function),
|
||||
) => self_function.is_subtype_of(db, other_function),
|
||||
) => self_function.has_relation_to(db, other_function, relation),
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderCall(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderCall(other_function),
|
||||
) => self_function.is_subtype_of(db, other_function),
|
||||
|
||||
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
|
||||
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
|
||||
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
|
||||
self == other
|
||||
}
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(_)
|
||||
| MethodWrapperKind::FunctionTypeDunderCall(_)
|
||||
| MethodWrapperKind::PropertyDunderGet(_)
|
||||
| MethodWrapperKind::PropertyDunderSet(_)
|
||||
| MethodWrapperKind::StrStartswith(_),
|
||||
) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderGet(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderGet(other_function),
|
||||
) => self_function.is_assignable_to(db, other_function),
|
||||
|
||||
(
|
||||
MethodWrapperKind::FunctionTypeDunderCall(self_function),
|
||||
MethodWrapperKind::FunctionTypeDunderCall(other_function),
|
||||
) => self_function.is_assignable_to(db, other_function),
|
||||
) => self_function.has_relation_to(db, other_function, relation),
|
||||
|
||||
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
|
||||
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
|||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeVarInstance,
|
||||
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxOrderSet, KnownModule, Program,
|
||||
|
@ -29,8 +29,8 @@ use crate::{
|
|||
place_table, semantic_index, use_def_map,
|
||||
},
|
||||
types::{
|
||||
CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType,
|
||||
UnionBuilder, UnionType, definition_expression_type,
|
||||
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder,
|
||||
UnionType, definition_expression_type,
|
||||
},
|
||||
};
|
||||
use indexmap::IndexSet;
|
||||
|
@ -340,23 +340,22 @@ impl<'db> ClassType<'db> {
|
|||
class_literal.is_final(db)
|
||||
}
|
||||
|
||||
/// Is this class a subclass of `Any` or `Unknown`?
|
||||
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
matches!(
|
||||
base,
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if `other` is present in this class's MRO.
|
||||
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
self.has_relation_to(db, other, TypeRelation::Subtyping)
|
||||
}
|
||||
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate.
|
||||
ClassBase::Dynamic(_) => false,
|
||||
ClassBase::Dynamic(_) => {
|
||||
relation.applies_to_non_fully_static_types() && !other.is_final(db)
|
||||
}
|
||||
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic => false,
|
||||
|
@ -365,9 +364,11 @@ impl<'db> ClassType<'db> {
|
|||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_subtype_of(db, other.specialization(db))
|
||||
&& base.specialization(db).has_relation_to(
|
||||
db,
|
||||
other.specialization(db),
|
||||
relation,
|
||||
)
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
|
@ -390,30 +391,6 @@ impl<'db> ClassType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db),
|
||||
ClassBase::Dynamic(_) => false,
|
||||
|
||||
// Protocol and Generic are not represented by a ClassType.
|
||||
ClassBase::Protocol | ClassBase::Generic => false,
|
||||
|
||||
ClassBase::Class(base) => match (base, other) {
|
||||
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
|
||||
(ClassType::Generic(base), ClassType::Generic(other)) => {
|
||||
base.origin(db) == other.origin(db)
|
||||
&& base
|
||||
.specialization(db)
|
||||
.is_assignable_to(db, other.specialization(db))
|
||||
}
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
||||
match (self, other) {
|
||||
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,
|
||||
|
|
|
@ -279,10 +279,6 @@ impl<'db> ClassBase<'db> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_dynamic(self) -> bool {
|
||||
matches!(self, Self::Dynamic(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassType<'db>> for ClassBase<'db> {
|
||||
|
|
|
@ -67,7 +67,9 @@ use crate::semantic_index::semantic_index;
|
|||
use crate::types::generics::GenericContext;
|
||||
use crate::types::narrow::ClassInfoConstraintFunction;
|
||||
use crate::types::signatures::{CallableSignature, Signature};
|
||||
use crate::types::{BoundMethodType, CallableType, Type, TypeMapping, TypeVarInstance};
|
||||
use crate::types::{
|
||||
BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
/// A collection of useful spans for annotating functions.
|
||||
|
@ -707,6 +709,18 @@ impl<'db> FunctionType<'db> {
|
|||
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match relation {
|
||||
TypeRelation::Subtyping => self.is_subtype_of(db, other),
|
||||
TypeRelation::Assignability => self.is_assignable_to(db, other),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// A function type is the subtype of itself, and not of any other function type. However,
|
||||
// our representation of a function type includes any specialization that should be applied
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::types::class_base::ClassBase;
|
|||
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
|
||||
use crate::types::signatures::{Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
KnownInstanceType, Type, TypeMapping, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
TypeVarVariance, UnionType, declaration_type, todo_type,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
@ -358,7 +358,12 @@ impl<'db> Specialization<'db> {
|
|||
Self::new(db, self.generic_context(db), types)
|
||||
}
|
||||
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
let generic_context = self.generic_context(db);
|
||||
if generic_context != other.generic_context(db) {
|
||||
return false;
|
||||
|
@ -368,20 +373,31 @@ impl<'db> Specialization<'db> {
|
|||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
|
||||
return false;
|
||||
if self_type.is_dynamic() || other_type.is_dynamic() {
|
||||
match relation {
|
||||
TypeRelation::Assignability => continue,
|
||||
TypeRelation::Subtyping => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// Subtyping of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// Subtyping/assignability of each type in the specialization depends on the variance
|
||||
// of the corresponding typevar:
|
||||
// - covariant: verify that self_type <: other_type
|
||||
// - contravariant: verify that other_type <: self_type
|
||||
// - invariant: verify that self_type == other_type
|
||||
// - bivariant: skip, can't make subtyping false
|
||||
// - invariant: verify that self_type <: other_type AND other_type <: self_type
|
||||
// - bivariant: skip, can't make subtyping/assignability false
|
||||
let compatible = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant => self_type.is_equivalent_to(db, *other_type),
|
||||
TypeVarVariance::Covariant => self_type.is_subtype_of(db, *other_type),
|
||||
TypeVarVariance::Contravariant => other_type.is_subtype_of(db, *self_type),
|
||||
TypeVarVariance::Invariant => match relation {
|
||||
TypeRelation::Subtyping => self_type.is_equivalent_to(db, *other_type),
|
||||
TypeRelation::Assignability => {
|
||||
self_type.is_assignable_to(db, *other_type)
|
||||
&& other_type.is_assignable_to(db, *self_type)
|
||||
}
|
||||
},
|
||||
TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation),
|
||||
TypeVarVariance::Contravariant => {
|
||||
other_type.has_relation_to(db, *self_type, relation)
|
||||
}
|
||||
TypeVarVariance::Bivariant => true,
|
||||
};
|
||||
if !compatible {
|
||||
|
@ -426,43 +442,6 @@ impl<'db> Specialization<'db> {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
|
||||
let generic_context = self.generic_context(db);
|
||||
if generic_context != other.generic_context(db) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
|
||||
.zip(self.types(db))
|
||||
.zip(other.types(db))
|
||||
{
|
||||
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assignability of each type in the specialization depends on the variance of the
|
||||
// corresponding typevar:
|
||||
// - covariant: verify that self_type <: other_type
|
||||
// - contravariant: verify that other_type <: self_type
|
||||
// - invariant: verify that self_type <: other_type AND other_type <: self_type
|
||||
// - bivariant: skip, can't make assignability false
|
||||
let compatible = match typevar.variance(db) {
|
||||
TypeVarVariance::Invariant => {
|
||||
self_type.is_assignable_to(db, *other_type)
|
||||
&& other_type.is_assignable_to(db, *self_type)
|
||||
}
|
||||
TypeVarVariance::Covariant => self_type.is_assignable_to(db, *other_type),
|
||||
TypeVarVariance::Contravariant => other_type.is_assignable_to(db, *self_type),
|
||||
TypeVarVariance::Bivariant => true,
|
||||
};
|
||||
if !compatible {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_gradual_equivalent_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
@ -5,14 +5,16 @@ use std::marker::PhantomData;
|
|||
use super::protocol_class::ProtocolInterface;
|
||||
use super::{ClassType, KnownClass, SubclassOfType, Type};
|
||||
use crate::place::{Boundness, Place, PlaceAndQualifiers};
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance};
|
||||
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
pub(super) use synthesized_protocol::SynthesizedProtocolType;
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
|
||||
if class.class_literal(db).0.is_protocol(db) {
|
||||
if class.is_known(db, KnownClass::Any) {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
} else if class.class_literal(db).0.is_protocol(db) {
|
||||
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
|
||||
} else {
|
||||
Self::NominalInstance(NominalInstanceType::from_class(class))
|
||||
|
@ -78,19 +80,19 @@ impl<'db> NominalInstanceType<'db> {
|
|||
Self::from_class(self.class.normalized(db))
|
||||
}
|
||||
|
||||
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
// N.B. The subclass relation is fully static
|
||||
self.class.is_subclass_of(db, other.class)
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
self.class.has_relation_to(db, other.class, relation)
|
||||
}
|
||||
|
||||
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.class.is_equivalent_to(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.class.is_assignable_to(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
|
||||
return true;
|
||||
|
@ -254,16 +256,20 @@ impl<'db> ProtocolInstanceType<'db> {
|
|||
self.inner.interface(db).is_fully_static(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is a subtype of the protocol `other`.
|
||||
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other)
|
||||
}
|
||||
|
||||
/// Return `true` if this protocol type is assignable to the protocol `other`.
|
||||
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
|
||||
///
|
||||
/// TODO: consider the types of the members as well as their existence
|
||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
other
|
||||
pub(super) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
relation.applies_to(
|
||||
db,
|
||||
Type::ProtocolInstance(self),
|
||||
Type::ProtocolInstance(other),
|
||||
) && other
|
||||
.inner
|
||||
.interface(db)
|
||||
.is_sub_interface_of(db, self.inner.interface(db))
|
||||
|
|
|
@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
|
|||
use super::{DynamicType, Type, definition_expression_type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance, todo_type};
|
||||
use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
|
@ -98,11 +98,23 @@ impl<'db> CallableSignature<'db> {
|
|||
.all(|signature| signature.is_fully_static(db))
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
other: &Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match relation {
|
||||
TypeRelation::Subtyping => self.is_subtype_of(db, other),
|
||||
TypeRelation::Assignability => self.is_assignable_to(db, other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this callable type is a subtype of another callable type.
|
||||
///
|
||||
/// See [`Type::is_subtype_of`] for more details.
|
||||
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature),
|
||||
|
@ -113,7 +125,7 @@ impl<'db> CallableSignature<'db> {
|
|||
///
|
||||
/// See [`Type::is_assignable_to`] for more details.
|
||||
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
&self.overloads,
|
||||
&other.overloads,
|
||||
&|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature),
|
||||
|
@ -124,7 +136,7 @@ impl<'db> CallableSignature<'db> {
|
|||
/// types.
|
||||
///
|
||||
/// The `check_signature` closure is used to check the relation between two [`Signature`]s.
|
||||
fn is_assignable_to_impl<F>(
|
||||
fn has_relation_to_impl<F>(
|
||||
self_signatures: &[Signature<'db>],
|
||||
other_signatures: &[Signature<'db>],
|
||||
check_signature: &F,
|
||||
|
@ -140,7 +152,7 @@ impl<'db> CallableSignature<'db> {
|
|||
|
||||
// `self` is possibly overloaded while `other` is definitely not overloaded.
|
||||
(_, [_]) => self_signatures.iter().any(|self_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
std::slice::from_ref(self_signature),
|
||||
other_signatures,
|
||||
check_signature,
|
||||
|
@ -149,7 +161,7 @@ impl<'db> CallableSignature<'db> {
|
|||
|
||||
// `self` is definitely not overloaded while `other` is possibly overloaded.
|
||||
([_], _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
|
@ -158,7 +170,7 @@ impl<'db> CallableSignature<'db> {
|
|||
|
||||
// `self` is definitely overloaded while `other` is possibly overloaded.
|
||||
(_, _) => other_signatures.iter().all(|other_signature| {
|
||||
Self::is_assignable_to_impl(
|
||||
Self::has_relation_to_impl(
|
||||
self_signatures,
|
||||
std::slice::from_ref(other_signature),
|
||||
check_signature,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::place::PlaceAndQualifiers;
|
||||
use crate::types::{
|
||||
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeVarInstance,
|
||||
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation,
|
||||
TypeVarInstance,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
|
@ -30,10 +31,14 @@ impl<'db> SubclassOfType<'db> {
|
|||
SubclassOfInner::Class(class) => {
|
||||
if class.is_final(db) {
|
||||
Type::from(class)
|
||||
} else if class.is_object(db) {
|
||||
KnownClass::Type.to_instance(db)
|
||||
} else {
|
||||
Type::SubclassOf(Self { subclass_of })
|
||||
match class.known(db) {
|
||||
Some(KnownClass::Object) => KnownClass::Type.to_instance(db),
|
||||
Some(KnownClass::Any) => Type::SubclassOf(Self {
|
||||
subclass_of: SubclassOfInner::Dynamic(DynamicType::Any),
|
||||
}),
|
||||
_ => Type::SubclassOf(Self { subclass_of }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,21 +108,23 @@ impl<'db> SubclassOfType<'db> {
|
|||
Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
///
|
||||
/// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant;
|
||||
/// only fully static types participate in subtyping.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
|
||||
/// Return `true` if `self` has a certain relation to `other`.
|
||||
pub(crate) fn has_relation_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other: SubclassOfType<'db>,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
// Non-fully-static types do not participate in subtyping
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => {
|
||||
relation.applies_to_non_fully_static_types()
|
||||
}
|
||||
|
||||
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
|
||||
// and `type[int]` describes all possible runtime subclasses of the class `int`.
|
||||
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
|
||||
(SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => {
|
||||
// N.B. The subclass relation is fully static
|
||||
self_class.is_subclass_of(db, other_class)
|
||||
self_class.has_relation_to(db, other_class, relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue