mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
[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:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user