mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
[ty] Rework disjointness of protocol instances vs types with possibly unbound attributes (#19043)
This commit is contained in:
parent
c6fd11fe36
commit
ebf59e2bef
4 changed files with 207 additions and 52 deletions
|
@ -964,6 +964,121 @@ def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalN
|
||||||
reveal_type(arg2) # revealed: Never
|
reveal_type(arg2) # revealed: Never
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The disjointness of a single protocol member with the type of an attribute on another type is enough
|
||||||
|
to make the whole protocol disjoint from the other type, even if all other members on the protocol
|
||||||
|
are satisfied by the other type. This applies to both `@final` types and non-final types:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Proto(Protocol):
|
||||||
|
x: int
|
||||||
|
y: str
|
||||||
|
z: bytes
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
x: int
|
||||||
|
y: str
|
||||||
|
z: None
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(Proto, Foo))
|
||||||
|
|
||||||
|
@final
|
||||||
|
class FinalFoo:
|
||||||
|
x: int
|
||||||
|
y: str
|
||||||
|
z: None
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(Proto, FinalFoo))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intersections of protocols with types that have possibly unbound attributes
|
||||||
|
|
||||||
|
Note that if a `@final` class has a possibly unbound attribute corresponding to the protocol member,
|
||||||
|
instance types and class-literal types referring to that class cannot be a subtype of the protocol
|
||||||
|
but will also not be disjoint from the protocol:
|
||||||
|
|
||||||
|
`a.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import final, ClassVar, Protocol
|
||||||
|
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
|
||||||
|
|
||||||
|
def who_knows() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Foo:
|
||||||
|
if who_knows():
|
||||||
|
x: ClassVar[int] = 42
|
||||||
|
|
||||||
|
class HasReadOnlyX(Protocol):
|
||||||
|
@property
|
||||||
|
def x(self) -> int: ...
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(Foo, HasReadOnlyX))
|
||||||
|
static_assert(not is_assignable_to(Foo, HasReadOnlyX))
|
||||||
|
static_assert(not is_disjoint_from(Foo, HasReadOnlyX))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(type[Foo], HasReadOnlyX))
|
||||||
|
static_assert(not is_assignable_to(type[Foo], HasReadOnlyX))
|
||||||
|
static_assert(not is_disjoint_from(type[Foo], HasReadOnlyX))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(TypeOf[Foo], HasReadOnlyX))
|
||||||
|
static_assert(not is_assignable_to(TypeOf[Foo], HasReadOnlyX))
|
||||||
|
static_assert(not is_disjoint_from(TypeOf[Foo], HasReadOnlyX))
|
||||||
|
```
|
||||||
|
|
||||||
|
A similar principle applies to module-literal types that have possibly unbound attributes:
|
||||||
|
|
||||||
|
`b.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def who_knows() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if who_knows():
|
||||||
|
x: int = 42
|
||||||
|
```
|
||||||
|
|
||||||
|
`c.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import b
|
||||||
|
from a import HasReadOnlyX
|
||||||
|
from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(TypeOf[b], HasReadOnlyX))
|
||||||
|
static_assert(not is_assignable_to(TypeOf[b], HasReadOnlyX))
|
||||||
|
static_assert(not is_disjoint_from(TypeOf[b], HasReadOnlyX))
|
||||||
|
```
|
||||||
|
|
||||||
|
If the possibly unbound attribute's type is disjoint from the type of the protocol member, though,
|
||||||
|
it is still disjoint from the protocol. This applies to both `@final` types and non-final types:
|
||||||
|
|
||||||
|
`d.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from a import HasReadOnlyX, who_knows
|
||||||
|
from typing import final, ClassVar, Protocol
|
||||||
|
from ty_extensions import static_assert, is_disjoint_from, TypeOf
|
||||||
|
|
||||||
|
class Proto(Protocol):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
def __init__(self):
|
||||||
|
if who_knows():
|
||||||
|
self.x: None = None
|
||||||
|
|
||||||
|
@final
|
||||||
|
class FinalFoo:
|
||||||
|
def __init__(self):
|
||||||
|
if who_knows():
|
||||||
|
self.x: None = None
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(Foo, Proto))
|
||||||
|
static_assert(is_disjoint_from(FinalFoo, Proto))
|
||||||
|
```
|
||||||
|
|
||||||
## Satisfying a protocol's interface
|
## Satisfying a protocol's interface
|
||||||
|
|
||||||
A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other
|
A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other
|
||||||
|
|
|
@ -1698,6 +1698,20 @@ impl<'db> Type<'db> {
|
||||||
/// Note: This function aims to have no false positives, but might return
|
/// Note: This function aims to have no false positives, but might return
|
||||||
/// wrong `false` answers in some cases.
|
/// wrong `false` answers in some cases.
|
||||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||||
|
fn any_protocol_members_absent_or_disjoint<'db>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
protocol: ProtocolInstanceType<'db>,
|
||||||
|
other: Type<'db>,
|
||||||
|
) -> bool {
|
||||||
|
protocol.interface(db).members(db).any(|member| {
|
||||||
|
other
|
||||||
|
.member(db, member.name())
|
||||||
|
.place
|
||||||
|
.ignore_possibly_unbound()
|
||||||
|
.is_none_or(|attribute_type| member.has_disjoint_type_from(db, attribute_type))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(Type::Never, _) | (_, Type::Never) => true,
|
(Type::Never, _) | (_, Type::Never) => true,
|
||||||
|
|
||||||
|
@ -1864,6 +1878,57 @@ impl<'db> Type<'db> {
|
||||||
Type::SubclassOf(_),
|
Type::SubclassOf(_),
|
||||||
) => true,
|
) => true,
|
||||||
|
|
||||||
|
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
|
||||||
|
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
|
||||||
|
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
|
||||||
|
ty.bool(db).is_always_false()
|
||||||
|
}
|
||||||
|
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
|
||||||
|
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
|
||||||
|
ty.bool(db).is_always_true()
|
||||||
|
}
|
||||||
|
|
||||||
|
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
||||||
|
left.is_disjoint_from(db, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
|
||||||
|
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => {
|
||||||
|
any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db))
|
||||||
|
}
|
||||||
|
|
||||||
|
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
|
||||||
|
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
|
||||||
|
any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The absence of a protocol member on one of these types guarantees
|
||||||
|
// that the type will be disjoint from the protocol,
|
||||||
|
// but the type will not be disjoint from the protocol if it has a member
|
||||||
|
// that is of the correct type but is possibly unbound.
|
||||||
|
// If accessing a member on this type returns a possibly unbound `Place`,
|
||||||
|
// the type will not be a subtype of the protocol but it will also not be
|
||||||
|
// disjoint from the protocol, since there are possible subtypes of the type
|
||||||
|
// that could satisfy the protocol.
|
||||||
|
//
|
||||||
|
// ```py
|
||||||
|
// class Foo:
|
||||||
|
// if coinflip():
|
||||||
|
// X = 42
|
||||||
|
//
|
||||||
|
// class HasX(Protocol):
|
||||||
|
// @property
|
||||||
|
// def x(self) -> int: ...
|
||||||
|
//
|
||||||
|
// # `TypeOf[Foo]` (a class-literal type) is not a subtype of `HasX`,
|
||||||
|
// # but `TypeOf[Foo]` & HasX` should not simplify to `Never`,
|
||||||
|
// # or this branch would be incorrectly understood to be unreachable,
|
||||||
|
// # since we would understand the type of `Foo` in this branch to be
|
||||||
|
// # `TypeOf[Foo] & HasX` due to `hasattr()` narrowing.
|
||||||
|
//
|
||||||
|
// if hasattr(Foo, "X"):
|
||||||
|
// print(Foo.X)
|
||||||
|
// ```
|
||||||
(
|
(
|
||||||
ty @ (Type::LiteralString
|
ty @ (Type::LiteralString
|
||||||
| Type::StringLiteral(..)
|
| Type::StringLiteral(..)
|
||||||
|
@ -1887,51 +1952,24 @@ impl<'db> Type<'db> {
|
||||||
| Type::ModuleLiteral(..)
|
| Type::ModuleLiteral(..)
|
||||||
| Type::GenericAlias(..)
|
| Type::GenericAlias(..)
|
||||||
| Type::IntLiteral(..)),
|
| Type::IntLiteral(..)),
|
||||||
) => !ty.satisfies_protocol(db, protocol, TypeRelation::Assignability),
|
) => any_protocol_members_absent_or_disjoint(db, protocol, ty),
|
||||||
|
|
||||||
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
|
|
||||||
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
|
|
||||||
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
|
|
||||||
ty.bool(db).is_always_false()
|
|
||||||
}
|
|
||||||
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
|
|
||||||
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
|
|
||||||
ty.bool(db).is_always_true()
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
|
|
||||||
left.is_disjoint_from(db, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::ProtocolInstance(protocol), Type::SpecialForm(special_form))
|
|
||||||
| (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => !special_form
|
|
||||||
.instance_fallback(db)
|
|
||||||
.satisfies_protocol(db, protocol, TypeRelation::Assignability),
|
|
||||||
|
|
||||||
(Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance))
|
|
||||||
| (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => {
|
|
||||||
!known_instance.instance_fallback(db).satisfies_protocol(
|
|
||||||
db,
|
|
||||||
protocol,
|
|
||||||
TypeRelation::Assignability,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// This is the same as the branch above --
|
||||||
|
// once guard patterns are stabilised, it could be unified with that branch
|
||||||
|
// (<https://github.com/rust-lang/rust/issues/129967>)
|
||||||
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
|
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
|
||||||
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
|
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
|
||||||
if n.class.is_final(db) =>
|
if n.class.is_final(db) =>
|
||||||
{
|
{
|
||||||
!nominal.satisfies_protocol(db, protocol, TypeRelation::Assignability)
|
any_protocol_members_absent_or_disjoint(db, protocol, nominal)
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::ProtocolInstance(protocol), other)
|
(Type::ProtocolInstance(protocol), other)
|
||||||
| (other, Type::ProtocolInstance(protocol)) => {
|
| (other, Type::ProtocolInstance(protocol)) => {
|
||||||
protocol.interface(db).members(db).any(|member| {
|
protocol.interface(db).members(db).any(|member| {
|
||||||
// TODO: implement disjointness for property/method members as well as attribute members
|
matches!(
|
||||||
member.is_attribute_member()
|
|
||||||
&& matches!(
|
|
||||||
other.member(db, member.name()).place,
|
other.member(db, member.name()).place,
|
||||||
Place::Type(ty, Boundness::Bound) if ty.is_disjoint_from(db, member.ty())
|
Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::marker::PhantomData;
|
||||||
|
|
||||||
use super::protocol_class::ProtocolInterface;
|
use super::protocol_class::ProtocolInterface;
|
||||||
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
|
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
|
||||||
use crate::place::{Place, PlaceAndQualifiers};
|
use crate::place::PlaceAndQualifiers;
|
||||||
use crate::types::tuple::TupleType;
|
use crate::types::tuple::TupleType;
|
||||||
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeVarInstance, TypeVisitor};
|
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeVarInstance, TypeVisitor};
|
||||||
use crate::{Db, FxOrderSet};
|
use crate::{Db, FxOrderSet};
|
||||||
|
@ -272,14 +272,7 @@ impl<'db> ProtocolInstanceType<'db> {
|
||||||
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||||
match self.inner {
|
match self.inner {
|
||||||
Protocol::FromClass(class) => class.instance_member(db, name),
|
Protocol::FromClass(class) => class.instance_member(db, name),
|
||||||
Protocol::Synthesized(synthesized) => synthesized
|
Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name),
|
||||||
.interface()
|
|
||||||
.member_by_name(db, name)
|
|
||||||
.map(|member| PlaceAndQualifiers {
|
|
||||||
place: Place::bound(member.ty()),
|
|
||||||
qualifiers: member.qualifiers(),
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use ruff_python_ast::name::Name;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Db, FxOrderSet,
|
Db, FxOrderSet,
|
||||||
place::{Boundness, Place, place_from_bindings, place_from_declarations},
|
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
|
||||||
semantic_index::{place_table, use_def_map},
|
semantic_index::{place_table, use_def_map},
|
||||||
types::{
|
types::{
|
||||||
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
|
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
|
||||||
|
@ -126,11 +126,7 @@ impl<'db> ProtocolInterface<'db> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn member_by_name<'a>(
|
fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option<ProtocolMember<'a, 'db>> {
|
||||||
self,
|
|
||||||
db: &'db dyn Db,
|
|
||||||
name: &'a str,
|
|
||||||
) -> Option<ProtocolMember<'a, 'db>> {
|
|
||||||
self.inner(db).get(name).map(|data| ProtocolMember {
|
self.inner(db).get(name).map(|data| ProtocolMember {
|
||||||
name,
|
name,
|
||||||
kind: data.kind,
|
kind: data.kind,
|
||||||
|
@ -138,6 +134,15 @@ impl<'db> ProtocolInterface<'db> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||||
|
self.member_by_name(db, name)
|
||||||
|
.map(|member| PlaceAndQualifiers {
|
||||||
|
place: Place::bound(member.ty()),
|
||||||
|
qualifiers: member.qualifiers(),
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| Type::object(db).instance_member(db, name))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if if all members on `self` are also members of `other`.
|
/// Return `true` if if all members on `self` are also members of `other`.
|
||||||
///
|
///
|
||||||
/// TODO: this method should consider the types of the members as well as their names.
|
/// TODO: this method should consider the types of the members as well as their names.
|
||||||
|
@ -328,7 +333,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
|
||||||
self.qualifiers
|
self.qualifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn ty(&self) -> Type<'db> {
|
fn ty(&self) -> Type<'db> {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
ProtocolMemberKind::Method(callable) => *callable,
|
ProtocolMemberKind::Method(callable) => *callable,
|
||||||
ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property),
|
ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property),
|
||||||
|
@ -336,8 +341,12 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) const fn is_attribute_member(&self) -> bool {
|
pub(super) fn has_disjoint_type_from(&self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||||
matches!(self.kind, ProtocolMemberKind::Other(_))
|
match &self.kind {
|
||||||
|
// TODO: implement disjointness for property/method members as well as attribute members
|
||||||
|
ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false,
|
||||||
|
ProtocolMemberKind::Other(ty) => ty.is_disjoint_from(db, other),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if `other` contains an attribute/method/property that satisfies
|
/// Return `true` if `other` contains an attribute/method/property that satisfies
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue