[ty] Rework disjointness of protocol instances vs types with possibly unbound attributes (#19043)

This commit is contained in:
Alex Waygood 2025-07-01 12:47:27 +01:00 committed by GitHub
parent c6fd11fe36
commit ebf59e2bef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 207 additions and 52 deletions

View file

@ -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)
)
})
}

View file

@ -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),
}
}

View file

@ -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