[ty] Add missing error context node for protocol to protocol assignability (#24905)

## Summary

Thank you for spotting this, @carljm.

## Test Plan

New Markdown tests
This commit is contained in:
David Peter
2026-04-29 08:46:09 +02:00
committed by GitHub
parent cd325995fc
commit 524158dbd0
3 changed files with 135 additions and 72 deletions
@@ -651,6 +651,53 @@ info: type `DoesNotHaveName` is not assignable to protocol `SupportsName`
info: └── protocol member `name` is not defined on type `DoesNotHaveName`
```
Missing protocol members (protocol to protocol):
```py
class SupportsSomethingElse(Protocol):
def something_else(self) -> None: ...
def _(source: SupportsSomethingElse):
target: SupportsCheck = source # snapshot
```
```snapshot
error[invalid-assignment]: Object of type `SupportsSomethingElse` is not assignable to `SupportsCheck`
--> src/mdtest_snippet.py:28:13
|
28 | target: SupportsCheck = source # snapshot
| ------------- ^^^^^^ Incompatible value of type `SupportsSomethingElse`
| |
| Declared type
|
info: protocol `SupportsSomethingElse` is not assignable to protocol `SupportsCheck`
info: └── protocol member `check` is not defined on type `SupportsSomethingElse`
```
Incompatible types for protocol members (protocol to protocol):
```py
class SupportsCheckWithOtherSignature(Protocol):
def check(self, x: int, y: bytes) -> bool: ...
def _(source: SupportsCheckWithOtherSignature):
target: SupportsCheck = source # snapshot
```
```snapshot
error[invalid-assignment]: Object of type `SupportsCheckWithOtherSignature` is not assignable to `SupportsCheck`
--> src/mdtest_snippet.py:33:13
|
33 | target: SupportsCheck = source # snapshot
| ------------- ^^^^^^ Incompatible value of type `SupportsCheckWithOtherSignature`
| |
| Declared type
|
info: protocol `SupportsCheckWithOtherSignature` is not assignable to protocol `SupportsCheck`
info: └── protocol member `check` is incompatible
info: └── parameter `y` has an incompatible type: `str` is not assignable to `bytes`
```
## Type aliases
Type aliases should be expanded in diagnostics to understand the underlying incompatibilities:
@@ -885,7 +932,8 @@ info: type `list[str]` is not assignable to protocol `Iterable[bytes]`
info: └── protocol member `__iter__` is incompatible
info: └── incompatible return types: `Iterator[str]` is not assignable to `Iterator[bytes]`
info: └── protocol `Iterator[str]` is not assignable to protocol `Iterator[bytes]`
info: └── incompatible return types: `str` is not assignable to `bytes`
info: └── protocol member `__next__` is incompatible
info: └── incompatible return types: `str` is not assignable to `bytes`
```
## Invariant generic classes
@@ -507,6 +507,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> {
let structurally_satisfied = if let Type::ProtocolInstance(source_protocol) = ty {
self.check_protocol_interface_pair(
db,
ty,
source_protocol.interface(db),
protocol.interface(db),
)
@@ -786,91 +786,105 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> {
pub(super) fn check_protocol_interface_pair(
&self,
db: &'db dyn Db,
source_type: Type<'db>,
source: ProtocolInterface<'db>,
target: ProtocolInterface<'db>,
) -> ConstraintSet<'db, 'c> {
if source.member_count(db) < target.member_count(db) {
if source.member_count(db) < target.member_count(db)
&& !self.is_context_collection_enabled()
{
return self.never();
}
target
.members(db)
.when_all(db, self.constraints, |target_member| {
source.member_by_name(db, target_member.name).when_some_and(
db,
self.constraints,
|source_member| {
match (source_member.kind, target_member.kind) {
// Method members are always immutable;
// they can never be subtypes of/assignable to mutable attribute members.
(ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => {
self.never()
}
let source_member = source.member_by_name(db, target_member.name);
// A property member can only be a subtype of an attribute member
// if the property is readable *and* writable.
//
// TODO: this should also consider the types of the members on both sides.
(
ProtocolMemberKind::Property(property),
ProtocolMemberKind::Other(_),
) => ConstraintSet::from_bool(
if self.is_context_collection_enabled() && source_member.is_none() {
self.provide_context(|| ErrorContext::ProtocolMemberNotDefined {
member_name: target_member.name.into(),
ty: source_type,
});
return self.never();
}
let result = source_member.when_some_and(db, self.constraints, |source_member| {
match (source_member.kind, target_member.kind) {
// Method members are always immutable;
// they can never be subtypes of/assignable to mutable attribute members.
(ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => {
self.never()
}
// A property member can only be a subtype of an attribute member
// if the property is readable *and* writable.
//
// TODO: this should also consider the types of the members on both sides.
(ProtocolMemberKind::Property(property), ProtocolMemberKind::Other(_)) => {
ConstraintSet::from_bool(
self.constraints,
property.getter(db).is_some() && property.setter(db).is_some(),
),
// A `@property` member can never be a subtype of a method member, as it is not necessarily
// accessible on the meta-type, whereas a method member must be.
(ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => {
self.never()
}
// But an attribute member *can* be a subtype of a method member,
// providing it is marked `ClassVar`
(
ProtocolMemberKind::Other(source_type),
ProtocolMemberKind::Method(target_callable),
) => ConstraintSet::from_bool(
self.constraints,
source_member.qualifiers.contains(TypeQualifiers::CLASS_VAR),
)
.and(db, self.constraints, || {
self.check_type_pair(
db,
source_type,
Type::Callable(protocol_bind_self(db, target_callable, None)),
)
}),
(
ProtocolMemberKind::Method(source_method),
ProtocolMemberKind::Method(target_method),
) => self.check_callable_pair(
db,
source_method.bind_self(db, None),
protocol_bind_self(db, target_method, None),
),
(
ProtocolMemberKind::Other(source_type),
ProtocolMemberKind::Other(target_type),
) => self.check_type_pair(db, source_type, target_type).and(
db,
self.constraints,
|| self.check_type_pair(db, target_type, source_type),
),
// TODO: finish assignability/subtyping between two `@property` members,
// and between a `@property` member and a member of a different kind.
(
ProtocolMemberKind::Property(_)
| ProtocolMemberKind::Method(_)
| ProtocolMemberKind::Other(_),
ProtocolMemberKind::Property(_),
) => self.always(),
}
},
)
// A `@property` member can never be a subtype of a method member, as it is not necessarily
// accessible on the meta-type, whereas a method member must be.
(ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => {
self.never()
}
// But an attribute member *can* be a subtype of a method member,
// providing it is marked `ClassVar`
(
ProtocolMemberKind::Other(source_type),
ProtocolMemberKind::Method(target_callable),
) => ConstraintSet::from_bool(
self.constraints,
source_member.qualifiers.contains(TypeQualifiers::CLASS_VAR),
)
.and(db, self.constraints, || {
self.check_type_pair(
db,
source_type,
Type::Callable(protocol_bind_self(db, target_callable, None)),
)
}),
(
ProtocolMemberKind::Method(source_method),
ProtocolMemberKind::Method(target_method),
) => self.check_callable_pair(
db,
source_method.bind_self(db, None),
protocol_bind_self(db, target_method, None),
),
(
ProtocolMemberKind::Other(source_type),
ProtocolMemberKind::Other(target_type),
) => self.check_type_pair(db, source_type, target_type).and(
db,
self.constraints,
|| self.check_type_pair(db, target_type, source_type),
),
// TODO: finish assignability/subtyping between two `@property` members,
// and between a `@property` member and a member of a different kind.
(
ProtocolMemberKind::Property(_)
| ProtocolMemberKind::Method(_)
| ProtocolMemberKind::Other(_),
ProtocolMemberKind::Property(_),
) => self.always(),
}
});
if result.is_never_satisfied(db) {
self.provide_context(|| ErrorContext::ProtocolMemberIncompatible {
member_name: target_member.name.into(),
});
}
result
})
}
}