diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 13fafc47b4..63d17fd688 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -476,7 +476,12 @@ impl<'db> BoundSuperType<'db> { }, Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol, Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic, - Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict, + Type::SpecialForm(SpecialFormType::TypedDict) => { + match KnownClass::Dict.to_class_literal(db).to_class_type(db) { + Some(dict_class) => ClassBase::Class(dict_class), + None => ClassBase::unknown(), + } + } Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), _ => { return Err(BoundSuperError::InvalidPivotClassType { @@ -491,7 +496,7 @@ impl<'db> BoundSuperType<'db> { let pivot_class = pivot_class.class_literal(db).0; if !owner_class.iter_mro(db).any(|superclass| match superclass { ClassBase::Dynamic(_) => true, - ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict => false, + ClassBase::Generic | ClassBase::Protocol => false, ClassBase::Class(superclass) => superclass.class_literal(db).0 == pivot_class, }) { return Err(BoundSuperError::FailingConditionCheck { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 3c1864decc..8904e744aa 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -645,10 +645,8 @@ impl<'db> ClassType<'db> { } }, - // Protocol, Generic, and TypedDict are not represented by a ClassType. - ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => { - ConstraintSet::from(false) - } + // Protocol and Generic are not represented by a ClassType. + ClassBase::Protocol | ClassBase::Generic => ConstraintSet::from(false), ClassBase::Class(base) => match (base, other) { (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => { @@ -1961,8 +1959,15 @@ impl<'db> ClassLiteral<'db> { return known.is_typed_dict_subclass(); } - self.iter_mro(db, None) - .any(|base| matches!(base, ClassBase::TypedDict)) + self.explicit_bases(db).iter().any(|base| { + if matches!(base, Type::SpecialForm(SpecialFormType::TypedDict)) { + return true; + } + if let Some(class) = base.to_class_type(db) { + return class.class_literal(db).0.is_typed_dict(db); + } + false + }) } /// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing. @@ -2204,6 +2209,32 @@ impl<'db> ClassLiteral<'db> { ClassBase::Class(class) => { let known = class.known(db); + // For TypedDicts, when we reach the `dict` base, fall back to TypedDictFallback. + if known == Some(KnownClass::Dict) && self.is_typed_dict(db) { + return KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal") + .map_type(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + None, + |base| { + base.into_class().is_some_and(|c| { + c.is_known(db, KnownClass::Dict) + }) + }, + ), + }, + TypeContext::default(), + ) + }); + } + if known == Some(KnownClass::Object) // Only exclude `object` members if this is not an `object` class itself && (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object)) @@ -2230,26 +2261,6 @@ impl<'db> ClassLiteral<'db> { ) }); } - ClassBase::TypedDict => { - return KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("Will return Some() when called on class literal") - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - None, - ClassBase::is_typed_dict, - ), - }, - TypeContext::default(), - ) - }); - } } if lookup_result.is_ok() { break; @@ -3169,20 +3180,18 @@ impl<'db> ClassLiteral<'db> { .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy) .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") - .map_type(|ty| + .map_type(|ty| { ty.apply_type_mapping( db, &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - ClassBase::is_typed_dict - ) + new_upper_bound: determine_upper_bound(db, self, specialization, |base| { + base.into_class() + .is_some_and(|c| c.is_known(db, KnownClass::Dict)) + }), }, - TypeContext::default(), + TypeContext::default(), ) - ) + }) } } @@ -3449,23 +3458,6 @@ impl<'db> ClassLiteral<'db> { union_qualifiers |= qualifiers; } } - ClassBase::TypedDict => { - return KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: Type::instance( - db, - self.unknown_specialization(db), - ), - }, - TypeContext::default(), - ) - }); - } } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 9dd74c7297..15ba54e6a7 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -27,7 +27,6 @@ pub enum ClassBase<'db> { /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, /// `Protocol[T]`, or bare `Protocol`. Generic, - TypedDict, } impl<'db> ClassBase<'db> { @@ -39,7 +38,7 @@ impl<'db> ClassBase<'db> { match self { Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), - Self::Protocol | Self::Generic | Self::TypedDict => self, + Self::Protocol | Self::Generic => self, } } @@ -54,7 +53,7 @@ impl<'db> ClassBase<'db> { Self::Class(class) => Some(Self::Class( class.recursive_type_normalized_impl(db, div, nested)?, )), - Self::Protocol | Self::Generic | Self::TypedDict => Some(self), + Self::Protocol | Self::Generic => Some(self), } } @@ -69,7 +68,6 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent", ClassBase::Protocol => "Protocol", ClassBase::Generic => "Generic", - ClassBase::TypedDict => "TypedDict", } } @@ -81,10 +79,6 @@ impl<'db> ClassBase<'db> { .map_or(Self::unknown(), Self::Class) } - pub(super) const fn is_typed_dict(self) -> bool { - matches!(self, ClassBase::TypedDict) - } - /// Attempt to resolve `ty` into a `ClassBase`. /// /// Return `None` if `ty` is not an acceptable type for a class base. @@ -295,7 +289,9 @@ impl<'db> ClassBase<'db> { SpecialFormType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db), subclass) } - SpecialFormType::TypedDict => Some(Self::TypedDict), + SpecialFormType::TypedDict => { + Self::try_from_type(db, KnownClass::Dict.to_class_literal(db), subclass) + } SpecialFormType::Callable => Self::try_from_type( db, todo_type!("Support for Callable as a base class"), @@ -308,7 +304,7 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None, + Self::Dynamic(_) | Self::Generic | Self::Protocol => None, } } @@ -323,7 +319,7 @@ impl<'db> ClassBase<'db> { Self::Class(class) => { Self::Class(class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) } - Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self, + Self::Dynamic(_) | Self::Generic | Self::Protocol => self, } } @@ -365,10 +361,7 @@ impl<'db> ClassBase<'db> { .try_mro(db, specialization) .is_err_and(MroError::is_cycle) } - ClassBase::Dynamic(_) - | ClassBase::Generic - | ClassBase::Protocol - | ClassBase::TypedDict => false, + ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false, } } @@ -380,9 +373,7 @@ impl<'db> ClassBase<'db> { ) -> impl Iterator> { match self { ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), - ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::TypedDict => { - ClassBaseMroIterator::length_2(db, self) - } + ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), ClassBase::Class(class) => { ClassBaseMroIterator::from_class(db, class, additional_specialization) } @@ -402,7 +393,6 @@ impl<'db> ClassBase<'db> { ClassBase::Class(class) => Type::from(class).display(self.db).fmt(f), ClassBase::Protocol => f.write_str("typing.Protocol"), ClassBase::Generic => f.write_str("typing.Generic"), - ClassBase::TypedDict => f.write_str("typing.TypedDict"), } } } @@ -424,7 +414,6 @@ impl<'db> From> for Type<'db> { ClassBase::Class(class) => class.into(), ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol), ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic), - ClassBase::TypedDict => Type::SpecialForm(SpecialFormType::TypedDict), } } } diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 21501060da..fef171593f 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -276,10 +276,7 @@ impl<'db> Mro<'db> { continue; } match base { - ClassBase::Class(_) - | ClassBase::Generic - | ClassBase::Protocol - | ClassBase::TypedDict => { + ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => { errors.push(DuplicateBaseError { duplicate_base: base, first_index: *first_index, diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index bf6f292e56..7af08d27cf 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -146,10 +146,12 @@ fn check_class_declaration<'db>( let mut subclass_overrides_superclass_declaration = false; let mut has_dynamic_superclass = false; - let mut has_typeddict_in_mro = false; let mut liskov_diagnostic_emitted = false; let mut overridden_final_method = None; + // Check if this class is a TypedDict upfront so we can fall back to TypedDictFallback. + let is_typed_dict = literal.is_typed_dict(db); + for class_base in class.iter_mro(db).skip(1) { let superclass = match class_base { ClassBase::Protocol | ClassBase::Generic => continue, @@ -157,10 +159,6 @@ fn check_class_declaration<'db>( has_dynamic_superclass = true; continue; } - ClassBase::TypedDict => { - has_typeddict_in_mro = true; - continue; - } ClassBase::Class(class) => class, }; @@ -262,6 +260,12 @@ fn check_class_declaration<'db>( continue; } + // TypedDict classes cannot have methods, so there's no point in checking for Liskov + // violations. We already emit `invalid-typed-dict-statement` for this case. + if is_typed_dict { + continue; + } + let Some(superclass_type_as_callable) = superclass_type.try_upcast_to_callable(db) else { continue; }; @@ -286,7 +290,7 @@ fn check_class_declaration<'db>( } if !subclass_overrides_superclass_declaration && !has_dynamic_superclass { - if has_typeddict_in_mro { + if is_typed_dict { if !KnownClass::TypedDictFallback .to_instance(db) .member(db, &member.name) diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index fdf834b8b9..1098460d31 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -173,9 +173,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (ClassBase::Generic, _) => Ordering::Less, (_, ClassBase::Generic) => Ordering::Greater, - (ClassBase::TypedDict, _) => Ordering::Less, - (_, ClassBase::TypedDict) => Ordering::Greater, - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(left, right) }