mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-18 09:30:35 +00:00
[red-knot] type[T]
is disjoint from type[S]
if the metaclass of T
is disjoint from the metaclass of S
(#15547)
This commit is contained in:
parent
6771b8ebd2
commit
4328df7226
4 changed files with 59 additions and 21 deletions
|
@ -246,3 +246,31 @@ def _(x: type, y: type[int]):
|
||||||
if issubclass(x, y):
|
if issubclass(x, y):
|
||||||
reveal_type(x) # revealed: type[int]
|
reveal_type(x) # revealed: type[int]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Disjoint `type[]` types are narrowed to `Never`
|
||||||
|
|
||||||
|
Here, `type[UsesMeta1]` and `type[UsesMeta2]` are disjoint because a common subclass of `UsesMeta1`
|
||||||
|
and `UsesMeta2` could only exist if a common subclass of their metaclasses could exist. This is
|
||||||
|
known to be impossible due to the fact that `Meta1` is marked as `@final`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Meta1(type): ...
|
||||||
|
|
||||||
|
class Meta2(type): ...
|
||||||
|
class UsesMeta1(metaclass=Meta1): ...
|
||||||
|
class UsesMeta2(metaclass=Meta2): ...
|
||||||
|
|
||||||
|
def _(x: type[UsesMeta1], y: type[UsesMeta2]):
|
||||||
|
if issubclass(x, y):
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: type[UsesMeta1]
|
||||||
|
|
||||||
|
if issubclass(y, x):
|
||||||
|
reveal_type(y) # revealed: Never
|
||||||
|
else:
|
||||||
|
reveal_type(y) # revealed: type[UsesMeta2]
|
||||||
|
```
|
||||||
|
|
|
@ -37,3 +37,22 @@ class UsesMeta2(metaclass=Meta2): ...
|
||||||
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2]))
|
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2]))
|
||||||
static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
|
static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `type[T]` versus `type[S]`
|
||||||
|
|
||||||
|
By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the
|
||||||
|
metaclass of `S`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
from knot_extensions import static_assert, is_disjoint_from
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Meta1(type): ...
|
||||||
|
|
||||||
|
class Meta2(type): ...
|
||||||
|
class UsesMeta1(metaclass=Meta1): ...
|
||||||
|
class UsesMeta2(metaclass=Meta2): ...
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
|
||||||
|
```
|
||||||
|
|
|
@ -1276,21 +1276,6 @@ impl<'db> Type<'db> {
|
||||||
ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a),
|
ClassBase::Class(class_a) => !class_b.is_subclass_of(db, class_a),
|
||||||
},
|
},
|
||||||
|
|
||||||
(Type::SubclassOf(_), Type::SubclassOf(_)) => false,
|
|
||||||
|
|
||||||
(Type::SubclassOf(subclass_of_ty), instance @ Type::Instance(_))
|
|
||||||
| (instance @ Type::Instance(_), Type::SubclassOf(subclass_of_ty)) => {
|
|
||||||
// `type[T]` is disjoint from `S`, where `S` is an instance type,
|
|
||||||
// if `U` is disjoint from `S`,
|
|
||||||
// where `U` represents all instances of `T`'s metaclass
|
|
||||||
let metaclass_instance = subclass_of_ty
|
|
||||||
.subclass_of()
|
|
||||||
.into_class()
|
|
||||||
.map(|class| class.metaclass(db).to_instance(db))
|
|
||||||
.unwrap_or_else(|| KnownClass::Type.to_instance(db));
|
|
||||||
instance.is_disjoint_from(db, metaclass_instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
(
|
||||||
Type::SubclassOf(_),
|
Type::SubclassOf(_),
|
||||||
Type::BooleanLiteral(..)
|
Type::BooleanLiteral(..)
|
||||||
|
@ -1324,12 +1309,9 @@ impl<'db> Type<'db> {
|
||||||
ty.bool(db).is_always_true()
|
ty.bool(db).is_always_true()
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::SubclassOf(_), other) | (other, Type::SubclassOf(_)) => {
|
(Type::SubclassOf(subclass_of_ty), other)
|
||||||
// TODO we could do better here: if both variants are `SubclassOf` and they have different "solid bases",
|
| (other, Type::SubclassOf(subclass_of_ty)) => {
|
||||||
// multiple inheritance between the two is impossible, so they are disjoint.
|
other.is_disjoint_from(db, subclass_of_ty.as_instance_type_of_metaclass(db))
|
||||||
//
|
|
||||||
// Note that `type[<@final class>]` is eagerly simplified to `Literal[<@final class>]` by [`SubclassOfType::from`].
|
|
||||||
other.is_disjoint_from(db, KnownClass::Type.to_instance(db))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::KnownInstance(known_instance), Type::Instance(InstanceType { class }))
|
(Type::KnownInstance(known_instance), Type::Instance(InstanceType { class }))
|
||||||
|
|
|
@ -68,6 +68,15 @@ impl<'db> SubclassOfType<'db> {
|
||||||
Type::from(self.subclass_of).member(db, name)
|
Type::from(self.subclass_of).member(db, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class `T` is an instance of its metaclass `U`,
|
||||||
|
/// so the type `type[T]` is a subtype of the instance type `U`.
|
||||||
|
pub(crate) fn as_instance_type_of_metaclass(&self, db: &'db dyn Db) -> Type<'db> {
|
||||||
|
match self.subclass_of {
|
||||||
|
ClassBase::Dynamic(_) => KnownClass::Type.to_instance(db),
|
||||||
|
ClassBase::Class(class) => class.metaclass(db).to_instance(db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` is a subtype of `other`.
|
/// Return `true` if `self` is a subtype of `other`.
|
||||||
///
|
///
|
||||||
/// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant;
|
/// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue