Add a ProtocolInstanceType
This commit is contained in:
@@ -1711,7 +1711,7 @@ impl<'db> ProtocolClassLiteral<'db> {
|
||||
/// 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 ordermap::set::Slice<Name> {
|
||||
pub(super) fn protocol_members(self, db: &'db dyn 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
|
||||
@@ -1748,58 +1748,52 @@ impl<'db> ProtocolClassLiteral<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn cached_protocol_members<'db>(
|
||||
db: &'db dyn Db,
|
||||
class: ClassLiteral<'db>,
|
||||
) -> Box<ordermap::set::Slice<Name>> {
|
||||
let mut members = FxOrderSet::default();
|
||||
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);
|
||||
for parent_protocol in self
|
||||
.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)| {
|
||||
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.into_boxed_slice()
|
||||
}),
|
||||
)
|
||||
.map(|symbol_id| symbol_table.symbol(symbol_id).name())
|
||||
.filter(|name| !excluded_from_proto_members(name))
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
|
||||
cached_protocol_members(db, *self)
|
||||
members.sort();
|
||||
members.shrink_to_fit();
|
||||
members
|
||||
}
|
||||
|
||||
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Instance types: both nominal and structural.
|
||||
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use super::{ClassType, KnownClass, SubclassOfType, Type};
|
||||
use crate::Db;
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub(crate) const fn instance(class: ClassType<'db>) -> Self {
|
||||
@@ -92,3 +94,94 @@ impl<'db> From<NominalInstanceType<'db>> for Type<'db> {
|
||||
Self::NominalInstance(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype)]
|
||||
pub enum ProtocolInstanceType<'db> {
|
||||
FromClass(ClassType<'db>),
|
||||
Synthesized(SynthesizedProtocolType<'db>),
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> ProtocolInstanceType<'db> {
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet<Name> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Self::FromClass(class) => SubclassOfType::from(db, class),
|
||||
|
||||
// TODO: we can and should do better here.
|
||||
//
|
||||
// This is supported by mypy, and should be supported by us as well.
|
||||
// We'll need to come up with a better solution for the meta-type of
|
||||
// synthesized protocols to solve this:
|
||||
//
|
||||
// ```py
|
||||
// from typing import Callable
|
||||
//
|
||||
// def foo(x: Callable[[], int]) -> None:
|
||||
// reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]"
|
||||
// reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any"
|
||||
// ```
|
||||
Self::Synthesized(_) => KnownClass::Type.to_instance(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
match self {
|
||||
Self::FromClass(_) => {
|
||||
Self::Synthesized(SynthesizedProtocolType::new(db, self.protocol_members(db)))
|
||||
}
|
||||
Self::Synthesized(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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.protocol_members(db)
|
||||
.is_subset(other.protocol_members(db))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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.protocol_members(db).set_eq(other.protocol_members(db))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y`
|
||||
/// have a member with the same name but disjoint types
|
||||
#[expect(clippy::unused_self)]
|
||||
pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct SynthesizedProtocolType<'db> {
|
||||
members: FxOrderSet<Name>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user