[red-knot] Distinguish fully static protocols from non-fully-static protocols (#17795)

This commit is contained in:
Alex Waygood 2025-05-03 11:12:23 +01:00 committed by GitHub
parent 78d4356301
commit 084352f72c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 377 additions and 204 deletions

View file

@ -232,3 +232,58 @@ def g[T](x: T) -> T | None:
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```
## Protocols as TypeVar bounds
Protocol types can be used as TypeVar bounds, just like nominal types.
```py
from typing import Any, Protocol
from knot_extensions import static_assert, is_assignable_to
class SupportsClose(Protocol):
def close(self) -> None: ...
class ClosableFullyStaticProtocol(Protocol):
x: int
def close(self) -> None: ...
class ClosableNonFullyStaticProtocol(Protocol):
x: Any
def close(self) -> None: ...
class ClosableFullyStaticNominal:
x: int
def close(self) -> None: ...
class ClosableNonFullyStaticNominal:
x: int
def close(self) -> None: ...
class NotClosableProtocol(Protocol): ...
class NotClosableNominal: ...
def close_and_return[T: SupportsClose](x: T) -> T:
x.close()
return x
def f(
a: SupportsClose,
b: ClosableFullyStaticProtocol,
c: ClosableNonFullyStaticProtocol,
d: ClosableFullyStaticNominal,
e: ClosableNonFullyStaticNominal,
f: NotClosableProtocol,
g: NotClosableNominal,
):
reveal_type(close_and_return(a)) # revealed: SupportsClose
reveal_type(close_and_return(b)) # revealed: ClosableFullyStaticProtocol
reveal_type(close_and_return(c)) # revealed: ClosableNonFullyStaticProtocol
reveal_type(close_and_return(d)) # revealed: ClosableFullyStaticNominal
reveal_type(close_and_return(e)) # revealed: ClosableNonFullyStaticNominal
# error: [invalid-argument-type] "does not satisfy upper bound"
reveal_type(close_and_return(f)) # revealed: Unknown
# error: [invalid-argument-type] "does not satisfy upper bound"
reveal_type(close_and_return(g)) # revealed: Unknown
```

View file

@ -1087,7 +1087,8 @@ from knot_extensions import is_equivalent_to
class HasMutableXAttr(Protocol):
x: int
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))
# TODO: should pass
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
@ -1350,25 +1351,43 @@ class NotFullyStatic(Protocol):
x: Any
static_assert(is_fully_static(FullyStatic))
# TODO: should pass
static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error]
static_assert(not is_fully_static(NotFullyStatic))
```
Non-fully-static protocols do not participate in subtyping, only assignability:
Non-fully-static protocols do not participate in subtyping or equivalence, only assignability and
gradual equivalence:
```py
from knot_extensions import is_subtype_of, is_assignable_to
from knot_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to
class NominalWithX:
x: int = 42
static_assert(is_assignable_to(NominalWithX, FullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(not is_subtype_of(FullyStatic, NotFullyStatic))
static_assert(is_assignable_to(FullyStatic, NotFullyStatic))
static_assert(not is_subtype_of(NotFullyStatic, FullyStatic))
static_assert(is_assignable_to(NotFullyStatic, FullyStatic))
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(is_subtype_of(NominalWithX, FullyStatic))
# TODO: this should pass
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
static_assert(is_equivalent_to(FullyStatic, FullyStatic))
static_assert(not is_equivalent_to(NotFullyStatic, NotFullyStatic))
static_assert(is_gradual_equivalent_to(FullyStatic, FullyStatic))
static_assert(is_gradual_equivalent_to(NotFullyStatic, NotFullyStatic))
class AlsoNotFullyStatic(Protocol):
x: Any
static_assert(not is_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
static_assert(is_gradual_equivalent_to(NotFullyStatic, AlsoNotFullyStatic))
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to

View file

@ -68,6 +68,7 @@ mod instance;
mod known_instance;
mod mro;
mod narrow;
mod protocol_class;
mod signatures;
mod slots;
mod string_annotation;
@ -674,7 +675,7 @@ impl<'db> Type<'db> {
.any(|ty| ty.contains_todo(db))
}
Self::ProtocolInstance(protocol) => protocol.contains_todo(),
Self::ProtocolInstance(protocol) => protocol.contains_todo(db),
}
}
@ -2061,7 +2062,7 @@ impl<'db> Type<'db> {
| Type::AlwaysTruthy
| Type::PropertyInstance(_) => true,
Type::ProtocolInstance(protocol) => protocol.is_fully_static(),
Type::ProtocolInstance(protocol) => protocol.is_fully_static(db),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => true,
@ -2515,16 +2516,7 @@ impl<'db> Type<'db> {
Type::NominalInstance(instance) => instance.class().instance_member(db, name),
Type::ProtocolInstance(protocol) => match protocol.inner() {
Protocol::FromClass(class) => class.instance_member(db, name),
Protocol::Synthesized(synthesized) => {
if synthesized.members(db).contains(name) {
SymbolAndQualifiers::todo("Capture type of synthesized protocol members")
} else {
Symbol::Unbound.into()
}
}
},
Type::ProtocolInstance(protocol) => protocol.instance_member(db, name),
Type::FunctionLiteral(_) => KnownClass::FunctionType
.to_instance(db)
@ -5415,7 +5407,7 @@ impl std::fmt::Display for DynamicType {
bitflags! {
/// Type qualifiers that appear in an annotation expression.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash)]
pub(crate) struct TypeQualifiers: u8 {
/// `typing.ClassVar`
const CLASS_VAR = 1 << 0;

View file

@ -621,9 +621,9 @@ impl<'db> Bindings<'db> {
overload.set_return_type(Type::Tuple(TupleType::new(
db,
protocol_class
.protocol_members(db)
.iter()
.map(|member| Type::string_literal(db, member))
.interface(db)
.members()
.map(|member| Type::string_literal(db, member.name()))
.collect::<Box<[Type<'db>]>>(),
)));
}

View file

@ -1,5 +1,4 @@
use std::hash::BuildHasherDefault;
use std::ops::Deref;
use std::sync::{LazyLock, Mutex};
use super::{
@ -14,7 +13,6 @@ use crate::types::signatures::{Parameter, Parameters};
use crate::types::{
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature,
};
use crate::FxOrderSet;
use crate::{
module_resolver::file_to_module,
semantic_index::{
@ -669,7 +667,7 @@ impl<'db> ClassLiteral<'db> {
.collect()
}
fn known_function_decorators(
pub(super) fn known_function_decorators(
self,
db: &'db dyn Db,
) -> impl Iterator<Item = KnownFunction> + 'db {
@ -1750,11 +1748,6 @@ impl<'db> ClassLiteral<'db> {
}
}
/// Returns `Some` if this is a protocol class, `None` otherwise.
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option<ProtocolClassLiteral<'db>> {
self.is_protocol(db).then_some(ProtocolClassLiteral(self))
}
/// Returns the [`Span`] of the class's "header": the class name
/// and any arguments passed to the `class` statement. E.g.
///
@ -1784,145 +1777,6 @@ impl<'db> From<ClassLiteral<'db>> for Type<'db> {
}
}
/// Representation of a single `Protocol` class definition.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>);
impl<'db> ProtocolClassLiteral<'db> {
/// Returns the protocol members of this class.
///
/// A protocol's members define the interface declared by the protocol.
/// They therefore determine how the protocol should behave with regards to
/// assignability and subtyping.
///
/// The list of members consists of all bindings and declarations that take place
/// in the protocol's class body, except for a list of excluded attributes which should
/// not be taken into account. (This list includes `__init__` and `__new__`, which can
/// legally be defined on protocol classes but do not constitute protocol members.)
///
/// It is illegal for a protocol class to have any instance attributes that are not declared
/// in the protocol's class body. If any are assigned to, they are not taken into account in
/// the protocol's list of members.
pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet<Name> {
/// The list of excluded members is subject to change between Python versions,
/// especially for dunders, but it probably doesn't matter *too* much if this
/// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa
/// (<https://github.com/python/cpython/blob/87b1ea016b1454b1e83b9113fa9435849b7743aa/Lib/typing.py#L1776-L1791>)
fn excluded_from_proto_members(member: &str) -> bool {
matches!(
member,
"_is_protocol"
| "__non_callable_proto_members__"
| "__static_attributes__"
| "__orig_class__"
| "__match_args__"
| "__weakref__"
| "__doc__"
| "__parameters__"
| "__module__"
| "_MutableMapping__marker"
| "__slots__"
| "__dict__"
| "__new__"
| "__protocol_attrs__"
| "__init__"
| "__class_getitem__"
| "__firstlineno__"
| "__abstractmethods__"
| "__orig_bases__"
| "_is_runtime_protocol"
| "__subclasshook__"
| "__type_params__"
| "__annotations__"
| "__annotate__"
| "__annotate_func__"
| "__annotations_cache__"
)
}
#[salsa::tracked(return_ref, cycle_fn=proto_members_cycle_recover, cycle_initial=proto_members_cycle_initial)]
fn cached_protocol_members<'db>(
db: &'db dyn Db,
class: ClassLiteral<'db>,
) -> FxOrderSet<Name> {
let mut members = FxOrderSet::default();
for parent_protocol in class
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.filter_map(|class| class.class_literal(db).0.into_protocol_class(db))
{
let parent_scope = parent_protocol.body_scope(db);
let use_def_map = use_def_map(db, parent_scope);
let symbol_table = symbol_table(db, parent_scope);
members.extend(
use_def_map
.all_public_declarations()
.flat_map(|(symbol_id, declarations)| {
symbol_from_declarations(db, declarations)
.map(|symbol| (symbol_id, symbol))
})
.filter_map(|(symbol_id, symbol)| {
symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id)
})
// Bindings in the class body that are not declared in the class body
// are not valid protocol members, and we plan to emit diagnostics for them
// elsewhere. Invalid or not, however, it's important that we still consider
// them to be protocol members. The implementation of `issubclass()` and
// `isinstance()` for runtime-checkable protocols considers them to be protocol
// members at runtime, and it's important that we accurately understand
// type narrowing that uses `isinstance()` or `issubclass()` with
// runtime-checkable protocols.
.chain(use_def_map.all_public_bindings().filter_map(
|(symbol_id, bindings)| {
symbol_from_bindings(db, bindings)
.ignore_possibly_unbound()
.map(|_| symbol_id)
},
))
.map(|symbol_id| symbol_table.symbol(symbol_id).name())
.filter(|name| !excluded_from_proto_members(name))
.cloned(),
);
}
members.sort();
members.shrink_to_fit();
members
}
fn proto_members_cycle_recover(
_db: &dyn Db,
_value: &FxOrderSet<Name>,
_count: u32,
_class: ClassLiteral,
) -> salsa::CycleRecoveryAction<FxOrderSet<Name>> {
salsa::CycleRecoveryAction::Iterate
}
fn proto_members_cycle_initial(_db: &dyn Db, _class: ClassLiteral) -> FxOrderSet<Name> {
FxOrderSet::default()
}
let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered();
cached_protocol_members(db, *self)
}
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
self.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
}
}
impl<'db> Deref for ProtocolClassLiteral<'db> {
type Target = ClassLiteral<'db>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(super) enum InheritanceCycle {
/// The class is cyclically defined and is a participant in the cycle.

View file

@ -1,5 +1,6 @@
use super::context::InferContext;
use super::ClassLiteral;
use crate::db::Db;
use crate::declare_lint;
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::suppression::FileSuppressionId;
@ -8,8 +9,7 @@ use crate::types::string_annotation::{
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type};
use crate::Db;
use crate::types::{protocol_class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type};
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::Ranged;

View file

@ -86,11 +86,11 @@ impl Display for DisplayRepresentation<'_> {
Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f),
Protocol::Synthesized(synthetic) => {
f.write_str("<Protocol with members ")?;
let member_list = synthetic.members(self.db);
let member_list = synthetic.interface(self.db).members();
let num_members = member_list.len();
for (i, member) in member_list.iter().enumerate() {
for (i, member) in member_list.enumerate() {
let is_last = i == num_members - 1;
write!(f, "'{member}'")?;
write!(f, "'{}'", member.name())?;
if !is_last {
f.write_str(", ")?;
}

View file

@ -1,9 +1,9 @@
//! Instance types: both nominal and structural.
use ruff_python_ast::name::Name;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::{Db, FxOrderSet};
use crate::symbol::{Symbol, SymbolAndQualifiers};
use crate::Db;
impl<'db> Type<'db> {
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
@ -34,9 +34,9 @@ impl<'db> Type<'db> {
// as well as whether each member *exists* on `self`.
protocol
.0
.protocol_members(db)
.iter()
.all(|member| !self.member(db, member).symbol.is_unbound())
.interface(db)
.members()
.all(|member| !self.member(db, member.name()).symbol.is_unbound())
}
}
@ -164,54 +164,51 @@ impl<'db> ProtocolInstanceType<'db> {
}
match self.0 {
Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized(
SynthesizedProtocolType::new(db, self.0.protocol_members(db)),
SynthesizedProtocolType::new(db, self.0.interface(db)),
))),
Protocol::Synthesized(_) => Type::ProtocolInstance(self),
}
}
/// TODO: this should return `true` if any of the members of this protocol type contain any `Todo` types.
#[expect(clippy::unused_self)]
pub(super) fn contains_todo(self) -> bool {
false
/// Return `true` if any of the members of this protocol type contain any `Todo` types.
pub(super) fn contains_todo(self, db: &'db dyn Db) -> bool {
self.0.interface(db).contains_todo(db)
}
/// Return `true` if this protocol type is fully static.
///
/// TODO: should not be considered fully static if any members do not have fully static types
#[expect(clippy::unused_self)]
pub(super) fn is_fully_static(self) -> bool {
true
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.0.interface(db).is_fully_static(db)
}
/// Return `true` if this protocol type is a subtype of the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
self.0
.protocol_members(db)
.is_superset(other.0.protocol_members(db))
self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other)
}
/// Return `true` if this protocol type is assignable to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
self.is_subtype_of(db, other)
other
.0
.interface(db)
.is_sub_interface_of(self.0.interface(db))
}
/// Return `true` if this protocol type is equivalent to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.normalized(db) == other.normalized(db)
self.is_fully_static(db)
&& other.is_fully_static(db)
&& self.normalized(db) == other.normalized(db)
}
/// Return `true` if this protocol type is gradually equivalent to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.is_equivalent_to(db, other)
self.normalized(db) == other.normalized(db)
}
/// Return `true` if this protocol type is disjoint from the protocol `other`.
@ -222,6 +219,20 @@ impl<'db> ProtocolInstanceType<'db> {
pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
false
}
pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
match self.inner() {
Protocol::FromClass(class) => class.instance_member(db, name),
Protocol::Synthesized(synthesized) => synthesized
.interface(db)
.member_by_name(name)
.map(|member| SymbolAndQualifiers {
symbol: Symbol::bound(member.ty()),
qualifiers: member.qualifiers(),
})
.unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)),
}
}
}
/// An enumeration of the two kinds of protocol types: those that originate from a class
@ -236,15 +247,15 @@ pub(super) enum Protocol<'db> {
impl<'db> Protocol<'db> {
/// Return the members of this protocol type
fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet<Name> {
fn interface(self, db: &'db dyn Db) -> &'db ProtocolInterface<'db> {
match self {
Self::FromClass(class) => class
.class_literal(db)
.0
.into_protocol_class(db)
.expect("Protocol class literal should be a protocol class")
.protocol_members(db),
Self::Synthesized(synthesized) => synthesized.members(db),
.interface(db),
Self::Synthesized(synthesized) => synthesized.interface(db),
}
}
}
@ -258,5 +269,5 @@ impl<'db> Protocol<'db> {
#[salsa::interned(debug)]
pub(super) struct SynthesizedProtocolType<'db> {
#[return_ref]
pub(super) members: FxOrderSet<Name>,
pub(super) interface: ProtocolInterface<'db>,
}

View file

@ -0,0 +1,242 @@
use std::{collections::BTreeMap, ops::Deref};
use itertools::Itertools;
use ruff_python_ast::name::Name;
use crate::{
db::Db,
semantic_index::{symbol_table, use_def_map},
symbol::{symbol_from_bindings, symbol_from_declarations},
types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeQualifiers},
};
impl<'db> ClassLiteral<'db> {
/// Returns `Some` if this is a protocol class, `None` otherwise.
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option<ProtocolClassLiteral<'db>> {
self.is_protocol(db).then_some(ProtocolClassLiteral(self))
}
}
/// Representation of a single `Protocol` class definition.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>);
impl<'db> ProtocolClassLiteral<'db> {
/// Returns the protocol members of this class.
///
/// A protocol's members define the interface declared by the protocol.
/// They therefore determine how the protocol should behave with regards to
/// assignability and subtyping.
///
/// The list of members consists of all bindings and declarations that take place
/// in the protocol's class body, except for a list of excluded attributes which should
/// not be taken into account. (This list includes `__init__` and `__new__`, which can
/// legally be defined on protocol classes but do not constitute protocol members.)
///
/// It is illegal for a protocol class to have any instance attributes that are not declared
/// in the protocol's class body. If any are assigned to, they are not taken into account in
/// the protocol's list of members.
pub(super) fn interface(self, db: &'db dyn Db) -> &'db ProtocolInterface<'db> {
let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered();
cached_protocol_interface(db, *self)
}
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
self.known_function_decorators(db)
.contains(&KnownFunction::RuntimeCheckable)
}
}
impl<'db> Deref for ProtocolClassLiteral<'db> {
type Target = ClassLiteral<'db>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// The interface of a protocol: the members of that protocol, and the types of those members.
#[derive(Debug, PartialEq, Eq, salsa::Update, Default, Clone, Hash)]
pub(super) struct ProtocolInterface<'db>(BTreeMap<Name, ProtocolMemberData<'db>>);
impl<'db> ProtocolInterface<'db> {
/// Iterate over the members of this protocol.
pub(super) fn members<'a>(&'a self) -> impl ExactSizeIterator<Item = ProtocolMember<'a, 'db>> {
self.0.iter().map(|(name, data)| ProtocolMember {
name,
ty: data.ty,
qualifiers: data.qualifiers,
})
}
pub(super) fn member_by_name<'a>(&self, name: &'a str) -> Option<ProtocolMember<'a, 'db>> {
self.0.get(name).map(|data| ProtocolMember {
name,
ty: data.ty,
qualifiers: data.qualifiers,
})
}
/// Return `true` if all members of this protocol are fully static.
pub(super) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.members().all(|member| member.ty.is_fully_static(db))
}
/// 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.
pub(super) fn is_sub_interface_of(&self, other: &Self) -> bool {
self.0
.keys()
.all(|member_name| other.0.contains_key(member_name))
}
/// Return `true` if any of the members of this protocol type contain any `Todo` types.
pub(super) fn contains_todo(&self, db: &'db dyn Db) -> bool {
self.members().any(|member| member.ty.contains_todo(db))
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)]
struct ProtocolMemberData<'db> {
ty: Type<'db>,
qualifiers: TypeQualifiers,
}
/// A single member of a protocol interface.
#[derive(Debug, PartialEq, Eq)]
pub(super) struct ProtocolMember<'a, 'db> {
name: &'a str,
ty: Type<'db>,
qualifiers: TypeQualifiers,
}
impl<'a, 'db> ProtocolMember<'a, 'db> {
pub(super) fn name(&self) -> &'a str {
self.name
}
pub(super) fn ty(&self) -> Type<'db> {
self.ty
}
pub(super) fn qualifiers(&self) -> TypeQualifiers {
self.qualifiers
}
}
/// Returns `true` if a declaration or binding to a given name in a protocol class body
/// should be excluded from the list of protocol members of that class.
///
/// The list of excluded members is subject to change between Python versions,
/// especially for dunders, but it probably doesn't matter *too* much if this
/// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa
/// (<https://github.com/python/cpython/blob/87b1ea016b1454b1e83b9113fa9435849b7743aa/Lib/typing.py#L1776-L1791>)
fn excluded_from_proto_members(member: &str) -> bool {
matches!(
member,
"_is_protocol"
| "__non_callable_proto_members__"
| "__static_attributes__"
| "__orig_class__"
| "__match_args__"
| "__weakref__"
| "__doc__"
| "__parameters__"
| "__module__"
| "_MutableMapping__marker"
| "__slots__"
| "__dict__"
| "__new__"
| "__protocol_attrs__"
| "__init__"
| "__class_getitem__"
| "__firstlineno__"
| "__abstractmethods__"
| "__orig_bases__"
| "_is_runtime_protocol"
| "__subclasshook__"
| "__type_params__"
| "__annotations__"
| "__annotate__"
| "__annotate_func__"
| "__annotations_cache__"
)
}
/// Inner Salsa query for [`ProtocolClassLiteral::interface`].
#[salsa::tracked(return_ref, cycle_fn=proto_interface_cycle_recover, cycle_initial=proto_interface_cycle_initial)]
fn cached_protocol_interface<'db>(
db: &'db dyn Db,
class: ClassLiteral<'db>,
) -> ProtocolInterface<'db> {
let mut members = BTreeMap::default();
for parent_protocol in class
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.filter_map(|class| class.class_literal(db).0.into_protocol_class(db))
{
let parent_scope = parent_protocol.body_scope(db);
let use_def_map = use_def_map(db, parent_scope);
let symbol_table = symbol_table(db, parent_scope);
members.extend(
use_def_map
.all_public_declarations()
.flat_map(|(symbol_id, declarations)| {
symbol_from_declarations(db, declarations).map(|symbol| (symbol_id, symbol))
})
.filter_map(|(symbol_id, symbol)| {
symbol
.symbol
.ignore_possibly_unbound()
.map(|ty| (symbol_id, ty, symbol.qualifiers))
})
// Bindings in the class body that are not declared in the class body
// are not valid protocol members, and we plan to emit diagnostics for them
// elsewhere. Invalid or not, however, it's important that we still consider
// them to be protocol members. The implementation of `issubclass()` and
// `isinstance()` for runtime-checkable protocols considers them to be protocol
// members at runtime, and it's important that we accurately understand
// type narrowing that uses `isinstance()` or `issubclass()` with
// runtime-checkable protocols.
.chain(
use_def_map
.all_public_bindings()
.filter_map(|(symbol_id, bindings)| {
symbol_from_bindings(db, bindings)
.ignore_possibly_unbound()
.map(|ty| (symbol_id, ty, TypeQualifiers::default()))
}),
)
.map(|(symbol_id, member, qualifiers)| {
(symbol_table.symbol(symbol_id).name(), member, qualifiers)
})
.filter(|(name, _, _)| !excluded_from_proto_members(name))
.map(|(name, ty, qualifiers)| {
let member = ProtocolMemberData { ty, qualifiers };
(name.clone(), member)
}),
);
}
ProtocolInterface(members)
}
fn proto_interface_cycle_recover<'db>(
_db: &dyn Db,
_value: &ProtocolInterface<'db>,
_count: u32,
_class: ClassLiteral<'db>,
) -> salsa::CycleRecoveryAction<ProtocolInterface<'db>> {
salsa::CycleRecoveryAction::Iterate
}
fn proto_interface_cycle_initial<'db>(
_db: &dyn Db,
_class: ClassLiteral<'db>,
) -> ProtocolInterface<'db> {
ProtocolInterface::default()
}