Compare commits

...

2 Commits

Author SHA1 Message Date
Charlie Marsh
44a938a78a Avoid eager query 2026-01-06 09:12:11 -05:00
Charlie Marsh
3aa13b7d32 [ty] Remove ClassBase::TypedDict 2026-01-06 08:41:29 -05:00
6 changed files with 81 additions and 91 deletions

View File

@@ -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 {

View File

@@ -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(),
)
)
})
}
}
@@ -3406,13 +3415,10 @@ impl<'db> ClassLiteral<'db> {
specialization: Option<Specialization<'db>>,
name: &str,
) -> PlaceAndQualifiers<'db> {
if self.is_typed_dict(db) {
return Place::Undefined.into();
}
let mut union = UnionBuilder::new(db);
let mut union_qualifiers = TypeQualifiers::empty();
let mut is_definitely_bound = false;
let mut found_declared = false;
for superclass in self.iter_mro(db, specialization) {
match superclass {
@@ -3434,12 +3440,18 @@ impl<'db> ClassLiteral<'db> {
if origin.is_declared() {
// We found a definitely-declared attribute. Discard possibly collected
// inferred types from subclasses and return the declared type.
// But first check if this is a TypedDict; its fields are accessed
// through the TypedDict type machinery, not instance_member.
if self.is_typed_dict(db) {
return Place::Undefined.into();
}
return member;
}
is_definitely_bound = true;
}
found_declared = true;
// If the attribute is not definitely declared on this class, keep looking higher
// up in the MRO, and build a union of all inferred types (and possibly-declared
// types):
@@ -3449,29 +3461,18 @@ 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(),
)
});
}
}
}
if union.is_empty() {
Place::Undefined.with_qualifiers(TypeQualifiers::empty())
} else {
// Check if this is a TypedDict; its fields are accessed through the
// TypedDict type machinery, not instance_member.
if found_declared && self.is_typed_dict(db) {
return Place::Undefined.into();
}
let boundness = if is_definitely_bound {
Definedness::AlwaysDefined
} else {

View File

@@ -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<ClassType<'db>> {
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<Item = ClassBase<'db>> {
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<ClassBase<'db>> 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),
}
}
}

View File

@@ -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,

View File

@@ -146,7 +146,6 @@ 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;
@@ -157,10 +156,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 +257,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 literal.is_typed_dict(db) {
continue;
}
let Some(superclass_type_as_callable) = superclass_type.try_upcast_to_callable(db) else {
continue;
};
@@ -286,7 +287,7 @@ fn check_class_declaration<'db>(
}
if !subclass_overrides_superclass_declaration && !has_dynamic_superclass {
if has_typeddict_in_mro {
if literal.is_typed_dict(db) {
if !KnownClass::TypedDictFallback
.to_instance(db)
.member(db, &member.name)

View File

@@ -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)
}