mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
[red-knot] Implement disjointness for Instance types where the underlying class is @final
(#15539)
## Summary Closes https://github.com/astral-sh/ruff/issues/15508 For any two instance types `T` and `S`, we know they are disjoint if either `T` is final and `T` is not a subclass of `S` or `S` is final and `S` is not a subclass of `T`. Correspondingly, for any two types `type[T]` and `S` where `S` is an instance type, `type[T]` can be said to be disjoint from `S` if `S` is disjoint from `U`, where `U` is the type that represents all instances of `T`'s metaclass. And a heterogeneous tuple type can be said to be disjoint from an instance type if the instance type is disjoint from `tuple` (a type representing all instances of the `tuple` class at runtime). ## Test Plan - A new mdtest added. Most of our `is_disjoint_from()` tests are not written as mdtests just yet, but it's pretty hard to test some of these edge cases from a Rust unit test! - Ran `QUICKCHECK_TESTS=1000000 cargo test --release -p red_knot_python_semantic -- --ignored types::property_tests::stable` --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
e84c82424d
commit
3950b00ee4
3 changed files with 63 additions and 45 deletions
|
@ -199,24 +199,20 @@ def f(
|
||||||
if isinstance(a, bool):
|
if isinstance(a, bool):
|
||||||
reveal_type(a) # revealed: Never
|
reveal_type(a) # revealed: Never
|
||||||
else:
|
else:
|
||||||
# TODO: `bool` is final, so `& ~bool` is redundant here
|
reveal_type(a) # revealed: P & AlwaysTruthy
|
||||||
reveal_type(a) # revealed: P & AlwaysTruthy & ~bool
|
|
||||||
|
|
||||||
if isinstance(b, bool):
|
if isinstance(b, bool):
|
||||||
reveal_type(b) # revealed: Never
|
reveal_type(b) # revealed: Never
|
||||||
else:
|
else:
|
||||||
# TODO: `bool` is final, so `& ~bool` is redundant here
|
reveal_type(b) # revealed: P & AlwaysFalsy
|
||||||
reveal_type(b) # revealed: P & AlwaysFalsy & ~bool
|
|
||||||
|
|
||||||
if isinstance(c, bool):
|
if isinstance(c, bool):
|
||||||
reveal_type(c) # revealed: Never
|
reveal_type(c) # revealed: Never
|
||||||
else:
|
else:
|
||||||
# TODO: `bool` is final, so `& ~bool` is redundant here
|
reveal_type(c) # revealed: P & ~AlwaysTruthy
|
||||||
reveal_type(c) # revealed: P & ~AlwaysTruthy & ~bool
|
|
||||||
|
|
||||||
if isinstance(d, bool):
|
if isinstance(d, bool):
|
||||||
reveal_type(d) # revealed: Never
|
reveal_type(d) # revealed: Never
|
||||||
else:
|
else:
|
||||||
# TODO: `bool` is final, so `& ~bool` is redundant here
|
reveal_type(d) # revealed: P & ~AlwaysFalsy
|
||||||
reveal_type(d) # revealed: P & ~AlwaysFalsy & ~bool
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Tests for disjointness
|
||||||
|
|
||||||
|
If two types can be disjoint, it means that it is known that no possible runtime object could ever
|
||||||
|
inhabit both types simultaneously.
|
||||||
|
|
||||||
|
TODO: Most of our disjointness tests are still Rust tests; they should be moved to this file.
|
||||||
|
|
||||||
|
## Instance types versus `type[T]` types
|
||||||
|
|
||||||
|
An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of
|
||||||
|
the instance type is not a subclass of `T`'s metaclass.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
from knot_extensions import is_disjoint_from, static_assert
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Foo: ...
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(Foo, type[int]))
|
||||||
|
static_assert(is_disjoint_from(type[object], Foo))
|
||||||
|
static_assert(is_disjoint_from(type[dict], Foo))
|
||||||
|
|
||||||
|
# Instance types can be disjoint from `type[]` types
|
||||||
|
# even if the instance type is a subtype of `type`
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Meta1(type): ...
|
||||||
|
|
||||||
|
class UsesMeta1(metaclass=Meta1): ...
|
||||||
|
|
||||||
|
static_assert(not is_disjoint_from(Meta1, type[UsesMeta1]))
|
||||||
|
|
||||||
|
class Meta2(type): ...
|
||||||
|
class UsesMeta2(metaclass=Meta2): ...
|
||||||
|
|
||||||
|
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2]))
|
||||||
|
static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
|
||||||
|
```
|
|
@ -1278,14 +1278,17 @@ impl<'db> Type<'db> {
|
||||||
|
|
||||||
(Type::SubclassOf(_), Type::SubclassOf(_)) => false,
|
(Type::SubclassOf(_), Type::SubclassOf(_)) => false,
|
||||||
|
|
||||||
(Type::SubclassOf(_), Type::Instance(instance))
|
(Type::SubclassOf(subclass_of_ty), instance @ Type::Instance(_))
|
||||||
| (Type::Instance(instance), Type::SubclassOf(_)) => {
|
| (instance @ Type::Instance(_), Type::SubclassOf(subclass_of_ty)) => {
|
||||||
// TODO this should be `true` if the instance is of a final type which is not a
|
// `type[T]` is disjoint from `S`, where `S` is an instance type,
|
||||||
// subclass of type. (With non-final types, we never know whether a subclass might
|
// if `U` is disjoint from `S`,
|
||||||
// multiply-inherit `type` or a subclass of it, and thus not be disjoint with
|
// where `U` represents all instances of `T`'s metaclass
|
||||||
// `type[...]`.) Until we support finality, hardcode None, which is known to be
|
let metaclass_instance = subclass_of_ty
|
||||||
// final.
|
.subclass_of()
|
||||||
instance.class.is_known(db, KnownClass::NoneType)
|
.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -1339,24 +1342,6 @@ impl<'db> Type<'db> {
|
||||||
known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
|
known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
|
||||||
Type::Instance(InstanceType { class: class_none }),
|
|
||||||
Type::Instance(InstanceType { class: class_other }),
|
|
||||||
)
|
|
||||||
| (
|
|
||||||
Type::Instance(InstanceType { class: class_other }),
|
|
||||||
Type::Instance(InstanceType { class: class_none }),
|
|
||||||
) if class_none.is_known(db, KnownClass::NoneType) => {
|
|
||||||
!class_none.is_subclass_of(db, class_other)
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::Instance(InstanceType { class: class_none }), _)
|
|
||||||
| (_, Type::Instance(InstanceType { class: class_none }))
|
|
||||||
if class_none.is_known(db, KnownClass::NoneType) =>
|
|
||||||
{
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::BooleanLiteral(..), Type::Instance(InstanceType { class }))
|
(Type::BooleanLiteral(..), Type::Instance(InstanceType { class }))
|
||||||
| (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => {
|
| (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => {
|
||||||
// A `Type::BooleanLiteral()` must be an instance of exactly `bool`
|
// A `Type::BooleanLiteral()` must be an instance of exactly `bool`
|
||||||
|
@ -1430,15 +1415,12 @@ impl<'db> Type<'db> {
|
||||||
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
|
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::Instance(..), Type::Instance(..)) => {
|
(
|
||||||
// TODO: once we have support for `final`, there might be some cases where
|
Type::Instance(InstanceType { class: left_class }),
|
||||||
// we can determine that two types are disjoint. Once we do this, some cases
|
Type::Instance(InstanceType { class: right_class }),
|
||||||
// above (e.g. NoneType) can be removed. For non-final classes, we return
|
) => {
|
||||||
// false (multiple inheritance).
|
(left_class.is_final(db) && !left_class.is_subclass_of(db, right_class))
|
||||||
|
|| (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class))
|
||||||
// TODO: is there anything specific to do for instances of KnownClass::Type?
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
|
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
|
||||||
|
@ -1451,7 +1433,8 @@ impl<'db> Type<'db> {
|
||||||
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
|
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::Tuple(..), Type::Instance(..)) | (Type::Instance(..), Type::Tuple(..)) => {
|
(Type::Tuple(..), instance @ Type::Instance(_))
|
||||||
|
| (instance @ Type::Instance(_), Type::Tuple(..)) => {
|
||||||
// We cannot be sure if the tuple is disjoint from the instance because:
|
// We cannot be sure if the tuple is disjoint from the instance because:
|
||||||
// - 'other' might be the homogeneous arbitrary-length tuple type
|
// - 'other' might be the homogeneous arbitrary-length tuple type
|
||||||
// tuple[T, ...] (which we don't have support for yet); if all of
|
// tuple[T, ...] (which we don't have support for yet); if all of
|
||||||
|
@ -1460,7 +1443,7 @@ impl<'db> Type<'db> {
|
||||||
// over the same or compatible *Ts, would overlap with tuple.
|
// over the same or compatible *Ts, would overlap with tuple.
|
||||||
//
|
//
|
||||||
// TODO: add checks for the above cases once we support them
|
// TODO: add checks for the above cases once we support them
|
||||||
false
|
instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue