mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-13 07:08:03 +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
|
@ -1698,6 +1698,20 @@ impl<'db> Type<'db> {
|
|||
/// Note: This function aims to have no false positives, but might return
|
||||
/// wrong `false` answers in some cases.
|
||||
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) {
|
||||
(Type::Never, _) | (_, Type::Never) => true,
|
||||
|
||||
|
@ -1864,6 +1878,57 @@ impl<'db> Type<'db> {
|
|||
Type::SubclassOf(_),
|
||||
) => 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
|
||||
| Type::StringLiteral(..)
|
||||
|
@ -1887,51 +1952,24 @@ impl<'db> Type<'db> {
|
|||
| Type::ModuleLiteral(..)
|
||||
| Type::GenericAlias(..)
|
||||
| Type::IntLiteral(..)),
|
||||
) => !ty.satisfies_protocol(db, protocol, TypeRelation::Assignability),
|
||||
|
||||
(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,
|
||||
)
|
||||
}
|
||||
) => any_protocol_members_absent_or_disjoint(db, protocol, ty),
|
||||
|
||||
// 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))
|
||||
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol))
|
||||
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)
|
||||
| (other, Type::ProtocolInstance(protocol)) => {
|
||||
protocol.interface(db).members(db).any(|member| {
|
||||
// TODO: implement disjointness for property/method members as well as attribute members
|
||||
member.is_attribute_member()
|
||||
&& matches!(
|
||||
matches!(
|
||||
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::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
|
||||
use crate::place::{Place, PlaceAndQualifiers};
|
||||
use crate::place::PlaceAndQualifiers;
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeVarInstance, TypeVisitor};
|
||||
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> {
|
||||
match self.inner {
|
||||
Protocol::FromClass(class) => class.instance_member(db, name),
|
||||
Protocol::Synthesized(synthesized) => synthesized
|
||||
.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)),
|
||||
Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use ruff_python_ast::name::Name;
|
|||
|
||||
use crate::{
|
||||
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},
|
||||
types::{
|
||||
CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature,
|
||||
|
@ -126,11 +126,7 @@ impl<'db> ProtocolInterface<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(super) fn member_by_name<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &'a str,
|
||||
) -> Option<ProtocolMember<'a, 'db>> {
|
||||
fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option<ProtocolMember<'a, 'db>> {
|
||||
self.inner(db).get(name).map(|data| ProtocolMember {
|
||||
name,
|
||||
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`.
|
||||
///
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub(super) fn ty(&self) -> Type<'db> {
|
||||
fn ty(&self) -> Type<'db> {
|
||||
match &self.kind {
|
||||
ProtocolMemberKind::Method(callable) => *callable,
|
||||
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 {
|
||||
matches!(self.kind, ProtocolMemberKind::Other(_))
|
||||
pub(super) fn has_disjoint_type_from(&self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue