[ty] Understand classes that inherit from subscripted Protocol[] as generic (#17832)

This commit is contained in:
Alex Waygood 2025-05-09 17:39:15 +01:00 committed by GitHub
parent 2370297cde
commit d1bb10a66b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 451 additions and 183 deletions

View file

@ -662,7 +662,7 @@ impl<'db> Type<'db> {
pub fn contains_todo(&self, db: &'db dyn Db) -> bool {
match self {
Self::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) => true,
Self::Dynamic(DynamicType::Todo(_)) => true,
Self::AlwaysFalsy
| Self::AlwaysTruthy
@ -703,9 +703,7 @@ impl<'db> Type<'db> {
}
Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Dynamic(
DynamicType::Todo(_) | DynamicType::SubscriptedProtocol,
) => true,
SubclassOfInner::Dynamic(DynamicType::Todo(_)) => true,
SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false,
SubclassOfInner::Class(_) => false,
},
@ -722,12 +720,10 @@ impl<'db> Type<'db> {
Self::BoundSuper(bound_super) => {
matches!(
bound_super.pivot_class(db),
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol)
ClassBase::Dynamic(DynamicType::Todo(_))
) || matches!(
bound_super.owner(db),
SuperOwnerKind::Dynamic(
DynamicType::Todo(_) | DynamicType::SubscriptedProtocol
)
SuperOwnerKind::Dynamic(DynamicType::Todo(_))
)
}
@ -3939,6 +3935,9 @@ impl<'db> Type<'db> {
);
Signatures::single(signature)
}
Some(KnownClass::NamedTuple) => {
Signatures::single(CallableSignature::todo("functional `NamedTuple` syntax"))
}
Some(KnownClass::Object) => {
// ```py
// class object:
@ -4328,6 +4327,12 @@ impl<'db> Type<'db> {
return Ok(UnionType::from_elements(db, tuple_type.elements(db)));
}
if let Type::GenericAlias(alias) = self {
if alias.origin(db).is_known(db, KnownClass::Tuple) {
return Ok(todo_type!("*tuple[] annotations"));
}
}
let try_call_dunder_getitem = || {
self.try_call_dunder(
db,
@ -4826,7 +4831,7 @@ impl<'db> Type<'db> {
KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")),
KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")),
KnownInstanceType::Protocol => Err(InvalidTypeExpressionError {
KnownInstanceType::Protocol(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol],
fallback_type: Type::unknown(),
}),
@ -4931,9 +4936,6 @@ impl<'db> Type<'db> {
Some(KnownClass::UnionType) => Ok(todo_type!(
"Support for `types.UnionType` instances in type expressions"
)),
Some(KnownClass::NamedTuple) => Ok(todo_type!(
"Support for functional `typing.NamedTuple` syntax"
)),
_ => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
*self
@ -5093,6 +5095,10 @@ impl<'db> Type<'db> {
instance.apply_type_mapping(db, type_mapping),
),
Type::ProtocolInstance(instance) => {
Type::ProtocolInstance(instance.apply_specialization(db, type_mapping))
}
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(
function.apply_type_mapping(db, type_mapping),
@ -5176,8 +5182,6 @@ impl<'db> Type<'db> {
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::BoundSuper(_)
// Same for `ProtocolInstance`
| Type::ProtocolInstance(_)
| Type::KnownInstance(_) => self,
}
}
@ -5498,9 +5502,6 @@ pub enum DynamicType {
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// Temporary type until we support generic protocols.
/// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly.
SubscriptedProtocol,
}
impl std::fmt::Display for DynamicType {
@ -5511,11 +5512,6 @@ impl std::fmt::Display for DynamicType {
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
// any other type
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
DynamicType::SubscriptedProtocol => f.write_str(if cfg!(debug_assertions) {
"@Todo(`Protocol[]` subscript)"
} else {
"@Todo"
}),
}
}
}

View file

@ -312,7 +312,7 @@ impl<'db> ClassType<'db> {
ClassBase::Dynamic(_) => false,
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic(_) => false,
ClassBase::Protocol(_) | ClassBase::Generic(_) => false,
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
@ -350,7 +350,7 @@ impl<'db> ClassType<'db> {
ClassBase::Dynamic(_) => false,
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic(_) => false,
ClassBase::Protocol(_) | ClassBase::Generic(_) => false,
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
@ -536,7 +536,10 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
self.explicit_bases(db).iter().find_map(|base| match base {
Type::KnownInstance(KnownInstanceType::Generic(generic_context)) => *generic_context,
Type::KnownInstance(
KnownInstanceType::Generic(generic_context)
| KnownInstanceType::Protocol(generic_context),
) => *generic_context,
_ => None,
})
}
@ -608,6 +611,17 @@ impl<'db> ClassLiteral<'db> {
}
}
/// Returns a specialization of this class with a `@Todo`-type
pub(crate) fn todo_specialization(self, db: &'db dyn Db, todo: &'static str) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.todo_specialization(db, todo);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
}
/// Returns the unknown specialization of this class. For non-generic classes, the class is
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
/// maps each of the class's typevars to `Unknown`.
@ -678,13 +692,11 @@ impl<'db> ClassLiteral<'db> {
// - OR be the last-but-one base (with the final base being `Generic[]` or `object`)
// - OR be the last-but-two base (with the penultimate base being `Generic[]`
// and the final base being `object`)
self.explicit_bases(db).iter().rev().take(3).any(|base| {
matches!(
base,
Type::KnownInstance(KnownInstanceType::Protocol)
| Type::Dynamic(DynamicType::SubscriptedProtocol)
)
})
self.explicit_bases(db)
.iter()
.rev()
.take(3)
.any(|base| matches!(base, Type::KnownInstance(KnownInstanceType::Protocol(_))))
})
}
@ -1011,12 +1023,8 @@ impl<'db> ClassLiteral<'db> {
for superclass in mro_iter {
match superclass {
ClassBase::Dynamic(DynamicType::SubscriptedProtocol)
| ClassBase::Generic(_)
| ClassBase::Protocol => {
// TODO: We currently skip `Protocol` when looking up class members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
ClassBase::Generic(_) | ClassBase::Protocol(_) => {
// Skip over these very special class bases that aren't really classes.
}
ClassBase::Dynamic(_) => {
// Note: calling `Type::from(superclass).member()` would be incorrect here.
@ -1354,12 +1362,8 @@ impl<'db> ClassLiteral<'db> {
for superclass in self.iter_mro(db, specialization) {
match superclass {
ClassBase::Dynamic(DynamicType::SubscriptedProtocol)
| ClassBase::Generic(_)
| ClassBase::Protocol => {
// TODO: We currently skip these when looking up instance members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
ClassBase::Generic(_) | ClassBase::Protocol(_) => {
// Skip over these very special class bases that aren't really classes.
}
ClassBase::Dynamic(_) => {
return SymbolAndQualifiers::todo(

View file

@ -18,7 +18,7 @@ pub enum ClassBase<'db> {
Class(ClassType<'db>),
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
/// and can appear in the MRO of a class.
Protocol,
Protocol(Option<GenericContext<'db>>),
/// Bare `Generic` cannot be subclassed directly in user code,
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
/// `Protocol[T]`, or bare `Protocol`.
@ -50,11 +50,17 @@ impl<'db> ClassBase<'db> {
ClassBase::Class(ClassType::Generic(alias)) => {
write!(f, "<class '{}'>", alias.display(self.db))
}
ClassBase::Protocol => f.write_str("typing.Protocol"),
ClassBase::Protocol(generic_context) => {
f.write_str("typing.Protocol")?;
if let Some(generic_context) = generic_context {
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}
ClassBase::Generic(generic_context) => {
f.write_str("typing.Generic")?;
if let Some(generic_context) = generic_context {
write!(f, "{}", generic_context.display(self.db))?;
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}
@ -71,9 +77,7 @@ impl<'db> ClassBase<'db> {
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(DynamicType::Todo(_)) => "@Todo",
ClassBase::Protocol | ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => {
"Protocol"
}
ClassBase::Protocol(_) => "Protocol",
ClassBase::Generic(_) => "Generic",
}
}
@ -199,7 +203,9 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::Callable => {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
KnownInstanceType::Protocol => Some(ClassBase::Protocol),
KnownInstanceType::Protocol(generic_context) => {
Some(ClassBase::Protocol(generic_context))
}
KnownInstanceType::Generic(generic_context) => {
Some(ClassBase::Generic(generic_context))
}
@ -210,14 +216,14 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => None,
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => None,
}
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
match self {
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => self,
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => self,
}
}
@ -241,7 +247,7 @@ impl<'db> ClassBase<'db> {
.try_mro(db, specialization)
.is_err_and(MroError::is_cycle)
}
ClassBase::Dynamic(_) | ClassBase::Generic(_) | ClassBase::Protocol => false,
ClassBase::Dynamic(_) | ClassBase::Generic(_) | ClassBase::Protocol(_) => false,
}
}
@ -252,11 +258,8 @@ impl<'db> ClassBase<'db> {
additional_specialization: Option<Specialization<'db>>,
) -> impl Iterator<Item = ClassBase<'db>> {
match self {
ClassBase::Protocol => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None))
}
ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None))
ClassBase::Protocol(context) => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(context))
}
ClassBase::Dynamic(_) | ClassBase::Generic(_) => {
ClassBaseMroIterator::length_2(db, self)
@ -279,7 +282,9 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
match value {
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Class(class) => class.into(),
ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol),
ClassBase::Protocol(generic_context) => {
Type::KnownInstance(KnownInstanceType::Protocol(generic_context))
}
ClassBase::Generic(generic_context) => {
Type::KnownInstance(KnownInstanceType::Generic(generic_context))
}

View file

@ -174,7 +174,9 @@ impl Display for DisplayRepresentation<'_> {
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization.display_short(self.db).to_string()
specialization
.display_short(self.db, TupleSpecialization::No)
.to_string()
} else {
String::new()
},
@ -187,7 +189,9 @@ impl Display for DisplayRepresentation<'_> {
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization.display_short(self.db).to_string()
specialization
.display_short(self.db, TupleSpecialization::No)
.to_string()
} else {
String::new()
},
@ -274,7 +278,10 @@ impl Display for DisplayGenericAlias<'_> {
f,
"{origin}{specialization}",
origin = self.origin.name(self.db),
specialization = self.specialization.display_short(self.db),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)
),
)
}
}
@ -327,22 +334,32 @@ impl Display for DisplayGenericContext<'_> {
impl<'db> Specialization<'db> {
/// Renders the specialization in full, e.g. `{T = int, U = str}`.
pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> {
pub fn display(
&'db self,
db: &'db dyn Db,
tuple_specialization: TupleSpecialization,
) -> DisplaySpecialization<'db> {
DisplaySpecialization {
typevars: self.generic_context(db).variables(db),
types: self.types(db),
db,
full: true,
tuple_specialization,
}
}
/// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`.
pub fn display_short(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> {
pub fn display_short(
&'db self,
db: &'db dyn Db,
tuple_specialization: TupleSpecialization,
) -> DisplaySpecialization<'db> {
DisplaySpecialization {
typevars: self.generic_context(db).variables(db),
types: self.types(db),
db,
full: false,
tuple_specialization,
}
}
}
@ -352,6 +369,7 @@ pub struct DisplaySpecialization<'db> {
types: &'db [Type<'db>],
db: &'db dyn Db,
full: bool,
tuple_specialization: TupleSpecialization,
}
impl Display for DisplaySpecialization<'_> {
@ -373,11 +391,34 @@ impl Display for DisplaySpecialization<'_> {
}
ty.display(self.db).fmt(f)?;
}
if self.tuple_specialization.is_yes() {
f.write_str(", ...")?;
}
f.write_char(']')
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TupleSpecialization {
Yes,
No,
}
impl TupleSpecialization {
const fn is_yes(self) -> bool {
matches!(self, Self::Yes)
}
fn from_class(db: &dyn Db, class: ClassLiteral) -> Self {
if class.is_known(db, KnownClass::Tuple) {
Self::Yes
} else {
Self::No
}
}
}
impl<'db> CallableType<'db> {
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayCallableType<'db> {
DisplayCallableType {

View file

@ -4,8 +4,8 @@ use rustc_hash::FxHashMap;
use crate::semantic_index::SemanticIndex;
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarVariance, UnionType,
declaration_type, todo_type, KnownInstanceType, Type, TypeVarBoundOrConstraints,
TypeVarInstance, TypeVarVariance, UnionType,
};
use crate::{Db, FxOrderSet};
@ -17,6 +17,7 @@ use crate::{Db, FxOrderSet};
pub struct GenericContext<'db> {
#[returns(ref)]
pub(crate) variables: FxOrderSet<TypeVarInstance<'db>>,
pub(crate) origin: GenericContextOrigin,
}
impl<'db> GenericContext<'db> {
@ -30,7 +31,7 @@ impl<'db> GenericContext<'db> {
.iter()
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
.collect();
Self::new(db, variables)
Self::new(db, variables, GenericContextOrigin::TypeParameterList)
}
fn variable_from_type_param(
@ -76,7 +77,11 @@ impl<'db> GenericContext<'db> {
if variables.is_empty() {
return None;
}
Some(Self::new(db, variables))
Some(Self::new(
db,
variables,
GenericContextOrigin::LegacyGenericFunction,
))
}
/// Creates a generic context from the legacy `TypeVar`s that appear in class's base class
@ -92,7 +97,7 @@ impl<'db> GenericContext<'db> {
if variables.is_empty() {
return None;
}
Some(Self::new(db, variables))
Some(Self::new(db, variables, GenericContextOrigin::Inherited))
}
pub(crate) fn len(self, db: &'db dyn Db) -> usize {
@ -133,6 +138,20 @@ impl<'db> GenericContext<'db> {
self.specialize_partial(db, &vec![None; self.variables(db).len()])
}
#[allow(unused_variables)] // Only unused in release builds
pub(crate) fn todo_specialization(
self,
db: &'db dyn Db,
todo: &'static str,
) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| typevar.default_ty(db).unwrap_or(todo_type!(todo)))
.collect();
self.specialize(db, types)
}
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
@ -209,6 +228,58 @@ impl<'db> GenericContext<'db> {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum GenericContextOrigin {
LegacyBase(LegacyGenericBase),
Inherited,
LegacyGenericFunction,
TypeParameterList,
}
impl GenericContextOrigin {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::LegacyBase(base) => base.as_str(),
Self::Inherited => "inherited",
Self::LegacyGenericFunction => "legacy generic function",
Self::TypeParameterList => "type parameter list",
}
}
}
impl std::fmt::Display for GenericContextOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum LegacyGenericBase {
Generic,
Protocol,
}
impl LegacyGenericBase {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Generic => "`typing.Generic`",
Self::Protocol => "subscripted `typing.Protocol`",
}
}
}
impl std::fmt::Display for LegacyGenericBase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<LegacyGenericBase> for GenericContextOrigin {
fn from(base: LegacyGenericBase) -> Self {
Self::LegacyBase(base)
}
}
/// An assignment of a specific type to each type variable in a generic scope.
///
/// TODO: Handle nested specializations better, with actual parent links to the specialization of

View file

@ -107,6 +107,7 @@ use super::diagnostic::{
report_unresolved_reference, INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL,
REDUNDANT_CAST, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
};
use super::generics::{GenericContextOrigin, LegacyGenericBase};
use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
@ -1020,14 +1021,16 @@ impl<'db> TypeInferenceBuilder<'db> {
// (5) Check that a generic class does not have invalid or conflicting generic
// contexts.
if class.pep695_generic_context(self.db()).is_some()
&& class.legacy_generic_context(self.db()).is_some()
{
if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(
"Cannot both inherit from `Generic` and use PEP 695 type variables",
);
if class.pep695_generic_context(self.db()).is_some() {
if let Some(legacy_context) = class.legacy_generic_context(self.db()) {
if let Some(builder) =
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(format_args!(
"Cannot both inherit from {} and use PEP 695 type variables",
legacy_context.origin(self.db())
));
}
}
}
@ -4734,6 +4737,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| KnownClass::Property
| KnownClass::Super
| KnownClass::TypeVar
| KnownClass::NamedTuple
)
)
// temporary special-casing for all subclasses of `enum.Enum`
@ -5795,8 +5799,6 @@ impl<'db> TypeInferenceBuilder<'db> {
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
(todo @ Type::Dynamic(DynamicType::Todo(_)), _, _)
| (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo),
(todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _)
| (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo),
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
@ -6884,6 +6886,13 @@ impl<'db> TypeInferenceBuilder<'db> {
// special cases, too.
let value_ty = self.infer_expression(value);
if let Type::ClassLiteral(class) = value_ty {
if class.is_known(self.db(), KnownClass::Tuple) {
self.infer_expression(slice);
// TODO heterogeneous and homogeneous tuples in value expressions
return Type::from(
class.todo_specialization(self.db(), "Generic tuple specializations"),
);
}
if let Some(generic_context) = class.generic_context(self.db()) {
return self.infer_explicit_class_specialization(
subscript,
@ -7072,14 +7081,44 @@ impl<'db> TypeInferenceBuilder<'db> {
value_ty,
Type::IntLiteral(i64::from(bool)),
),
(Type::KnownInstance(KnownInstanceType::Protocol), _, _) => {
Type::Dynamic(DynamicType::SubscriptedProtocol)
(Type::KnownInstance(KnownInstanceType::Protocol(None)), Type::Tuple(typevars), _) => {
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
LegacyGenericBase::Protocol,
)
.map(|context| Type::KnownInstance(KnownInstanceType::Protocol(Some(context))))
.unwrap_or_else(Type::unknown)
}
(Type::KnownInstance(KnownInstanceType::Protocol(None)), typevar, _) => self
.legacy_generic_class_context(
value_node,
std::slice::from_ref(&typevar),
LegacyGenericBase::Protocol,
)
.map(|context| Type::KnownInstance(KnownInstanceType::Protocol(Some(context))))
.unwrap_or_else(Type::unknown),
(Type::KnownInstance(KnownInstanceType::Protocol(Some(_))), _, _) => {
// TODO: emit a diagnostic
todo_type!("doubly-specialized typing.Protocol")
}
(Type::KnownInstance(KnownInstanceType::Generic(None)), Type::Tuple(typevars), _) => {
self.infer_subscript_legacy_generic_class(value_node, typevars.elements(self.db()))
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
LegacyGenericBase::Generic,
)
.map(|context| Type::KnownInstance(KnownInstanceType::Generic(Some(context))))
.unwrap_or_else(Type::unknown)
}
(Type::KnownInstance(KnownInstanceType::Generic(None)), typevar, _) => self
.infer_subscript_legacy_generic_class(value_node, std::slice::from_ref(&typevar)),
.legacy_generic_class_context(
value_node,
std::slice::from_ref(&typevar),
LegacyGenericBase::Generic,
)
.map(|context| Type::KnownInstance(KnownInstanceType::Generic(Some(context))))
.unwrap_or_else(Type::unknown),
(Type::KnownInstance(KnownInstanceType::Generic(Some(_))), _, _) => {
// TODO: emit a diagnostic
todo_type!("doubly-specialized typing.Generic")
@ -7238,11 +7277,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
fn infer_subscript_legacy_generic_class(
fn legacy_generic_class_context(
&mut self,
value_node: &ast::Expr,
typevars: &[Type<'db>],
) -> Type<'db> {
origin: LegacyGenericBase,
) -> Option<GenericContext<'db>> {
let typevars: Option<FxOrderSet<_>> = typevars
.iter()
.map(|typevar| match typevar {
@ -7252,7 +7292,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
{
builder.into_diagnostic(format_args!(
"`{}` is not a valid argument to `typing.Generic`",
"`{}` is not a valid argument to {origin}",
typevar.display(self.db()),
));
}
@ -7260,11 +7300,9 @@ impl<'db> TypeInferenceBuilder<'db> {
}
})
.collect();
let Some(typevars) = typevars else {
return Type::unknown();
};
let generic_context = GenericContext::new(self.db(), typevars);
Type::KnownInstance(KnownInstanceType::Generic(Some(generic_context)))
typevars.map(|typevars| {
GenericContext::new(self.db(), typevars, GenericContextOrigin::from(origin))
})
}
fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> {
@ -8420,9 +8458,14 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_expression(arguments_slice);
todo_type!("`Unpack[]` special form")
}
KnownInstanceType::Protocol => {
KnownInstanceType::Protocol(_) => {
self.infer_type_expression(arguments_slice);
Type::Dynamic(DynamicType::SubscriptedProtocol)
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.Protocol` is not allowed in type expressions",
));
}
Type::unknown()
}
KnownInstanceType::Generic(_) => {
self.infer_type_expression(arguments_slice);

View file

@ -260,6 +260,21 @@ impl<'db> ProtocolInstanceType<'db> {
.unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)),
}
}
pub(super) fn apply_specialization<'a>(
self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
match self.0 {
Protocol::FromClass(class) => Self(Protocol::FromClass(
class.apply_type_mapping(db, type_mapping),
)),
Protocol::Synthesized(synthesized) => Self(Protocol::Synthesized(
synthesized.apply_type_mapping(db, type_mapping),
)),
}
}
}
/// An enumeration of the two kinds of protocol types: those that originate from a class
@ -287,6 +302,7 @@ impl<'db> Protocol<'db> {
mod synthesized_protocol {
use crate::db::Db;
use crate::types::generics::TypeMapping;
use crate::types::protocol_class::ProtocolInterface;
/// A "synthesized" protocol type that is dissociated from a class definition in source code.
@ -306,6 +322,14 @@ mod synthesized_protocol {
Self(interface.normalized(db))
}
pub(super) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
Self(self.0.specialized_and_normalized(db, type_mapping))
}
pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> {
self.0
}

View file

@ -60,7 +60,7 @@ pub enum KnownInstanceType<'db> {
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
Protocol(Option<GenericContext<'db>>),
/// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`)
Generic(Option<GenericContext<'db>>),
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
@ -146,7 +146,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Deque
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Protocol(_)
| Self::Generic(_)
| Self::ReadOnly
| Self::TypeAliasType(_)
@ -203,7 +203,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Deque => KnownClass::StdlibAlias,
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Protocol(_) => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Generic(_) => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
@ -249,7 +249,7 @@ impl<'db> KnownInstanceType<'db> {
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Generic" => Self::Generic(None),
"Protocol" => Self::Protocol,
"Protocol" => Self::Protocol(None),
"Optional" => Self::Optional,
"Union" => Self::Union,
"NoReturn" => Self::NoReturn,
@ -311,7 +311,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Generic(_)
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Protocol(_)
| Self::Literal
| Self::LiteralString
| Self::Never
@ -384,11 +384,17 @@ impl Display for KnownInstanceRepr<'_> {
KnownInstanceType::Deque => f.write_str("typing.Deque"),
KnownInstanceType::ChainMap => f.write_str("typing.ChainMap"),
KnownInstanceType::OrderedDict => f.write_str("typing.OrderedDict"),
KnownInstanceType::Protocol => f.write_str("typing.Protocol"),
KnownInstanceType::Protocol(generic_context) => {
f.write_str("typing.Protocol")?;
if let Some(generic_context) = generic_context {
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}
KnownInstanceType::Generic(generic_context) => {
f.write_str("typing.Generic")?;
if let Some(generic_context) = generic_context {
write!(f, "{}", generic_context.display(self.db))?;
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}

View file

@ -174,7 +174,9 @@ impl<'db> Mro<'db> {
continue;
}
match base {
ClassBase::Class(_) | ClassBase::Generic(_) | ClassBase::Protocol => {
ClassBase::Class(_)
| ClassBase::Generic(_)
| ClassBase::Protocol(_) => {
errors.push(DuplicateBaseError {
duplicate_base: base,
first_index: *first_index,

View file

@ -8,7 +8,7 @@ use crate::{
db::Db,
semantic_index::{symbol_table, use_def_map},
symbol::{symbol_from_bindings, symbol_from_declarations},
types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeQualifiers},
types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers},
};
impl<'db> ClassLiteral<'db> {
@ -146,6 +146,29 @@ impl<'db> ProtocolInterface<'db> {
Self::SelfReference => Self::SelfReference,
}
}
pub(super) fn specialized_and_normalized<'a>(
self,
db: &'db dyn Db,
type_mapping: TypeMapping<'a, 'db>,
) -> Self {
match self {
Self::Members(members) => Self::Members(ProtocolInterfaceMembers::new(
db,
members
.inner(db)
.iter()
.map(|(name, data)| {
(
name.clone(),
data.apply_type_mapping(db, type_mapping).normalized(db),
)
})
.collect::<BTreeMap<_, _>>(),
)),
Self::SelfReference => Self::SelfReference,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)]
@ -161,6 +184,13 @@ impl<'db> ProtocolMemberData<'db> {
qualifiers: self.qualifiers,
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
Self {
ty: self.ty.apply_type_mapping(db, type_mapping),
qualifiers: self.qualifiers,
}
}
}
/// A single member of a protocol interface.

View file

@ -153,11 +153,15 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right),
(ClassBase::Class(_), _) => Ordering::Less,
(_, ClassBase::Class(_)) => Ordering::Greater,
(ClassBase::Protocol, _) => Ordering::Less,
(_, ClassBase::Protocol) => Ordering::Greater,
(ClassBase::Protocol(left), ClassBase::Protocol(right)) => left.cmp(&right),
(ClassBase::Protocol(_), _) => Ordering::Less,
(_, ClassBase::Protocol(_)) => Ordering::Greater,
(ClassBase::Generic(left), ClassBase::Generic(right)) => left.cmp(&right),
(ClassBase::Generic(_), _) => Ordering::Less,
(_, ClassBase::Generic(_)) => Ordering::Greater,
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
dynamic_elements_ordering(left, right)
}
@ -253,8 +257,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(KnownInstanceType::Generic(_), _) => Ordering::Less,
(_, KnownInstanceType::Generic(_)) => Ordering::Greater,
(KnownInstanceType::Protocol, _) => Ordering::Less,
(_, KnownInstanceType::Protocol) => Ordering::Greater,
(KnownInstanceType::Protocol(left), KnownInstanceType::Protocol(right)) => {
left.cmp(right)
}
(KnownInstanceType::Protocol(_), _) => Ordering::Less,
(_, KnownInstanceType::Protocol(_)) => Ordering::Greater,
(KnownInstanceType::NoReturn, _) => Ordering::Less,
(_, KnownInstanceType::NoReturn) => Ordering::Greater,
@ -379,8 +386,5 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
#[cfg(not(debug_assertions))]
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
(DynamicType::SubscriptedProtocol, _) => Ordering::Less,
(_, DynamicType::SubscriptedProtocol) => Ordering::Greater,
}
}