[ty] Fix bug where ty would think all types had an __mro__ attribute (#20995)

This commit is contained in:
Alex Waygood 2025-10-27 11:19:12 +00:00 committed by GitHub
parent 3c7f56f582
commit db0e921db1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 780 additions and 599 deletions

View file

@ -4364,14 +4364,6 @@ impl<'db> Type<'db> {
}
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect(
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
);
if name == "__mro__" {
return class_attr_plain;
}
if let Some(enum_class) = match self {
Type::ClassLiteral(literal) => Some(literal),
Type::SubclassOf(subclass_of) => subclass_of
@ -4392,6 +4384,10 @@ impl<'db> Type<'db> {
}
}
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect(
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
);
let class_attr_fallback = Self::try_call_dunder_get_on_attribute(
db,
class_attr_plain,

View file

@ -1981,11 +1981,6 @@ impl<'db> ClassLiteral<'db> {
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
if name == "__mro__" {
let tuple_elements = self.iter_mro(db, specialization);
return Place::bound(Type::heterogeneous_tuple(db, tuple_elements)).into();
}
self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization))
}

View file

@ -348,6 +348,27 @@ impl<'db> ClassBase<'db> {
}
}
}
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display {
struct ClassBaseDisplay<'db> {
db: &'db dyn Db,
base: ClassBase<'db>,
}
impl std::fmt::Display for ClassBaseDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.base {
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
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"),
}
}
}
ClassBaseDisplay { db, base: self }
}
}
impl<'db> From<ClassType<'db>> for ClassBase<'db> {

View file

@ -1319,6 +1319,8 @@ pub enum KnownFunction {
HasMember,
/// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface,
/// `ty_extensions.reveal_mro`
RevealMro,
/// `ty_extensions.range_constraint`
RangeConstraint,
/// `ty_extensions.negated_range_constraint`
@ -1397,6 +1399,7 @@ impl KnownFunction {
| Self::RevealProtocolInterface
| Self::RangeConstraint
| Self::NegatedRangeConstraint
| Self::RevealMro
| Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(),
@ -1619,6 +1622,85 @@ impl KnownFunction {
}
}
KnownFunction::RevealMro => {
let [Some(param_type)] = parameter_types else {
return;
};
let mut good_argument = true;
let classes = match param_type {
Type::ClassLiteral(class) => vec![ClassType::NonGeneric(*class)],
Type::GenericAlias(generic_alias) => vec![ClassType::Generic(*generic_alias)],
Type::Union(union) => {
let elements = union.elements(db);
let mut classes = Vec::with_capacity(elements.len());
for element in elements {
match element {
Type::ClassLiteral(class) => {
classes.push(ClassType::NonGeneric(*class));
}
Type::GenericAlias(generic_alias) => {
classes.push(ClassType::Generic(*generic_alias));
}
_ => {
good_argument = false;
break;
}
}
}
classes
}
_ => {
good_argument = false;
vec![]
}
};
if !good_argument {
let Some(builder) =
context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression)
else {
return;
};
let mut diagnostic =
builder.into_diagnostic("Invalid argument to `reveal_mro`");
diagnostic.set_primary_message(format_args!(
"Can only pass a class object, generic alias or a union thereof"
));
return;
}
if let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
{
let mut diag = builder.into_diagnostic("Revealed MRO");
let span = context.span(&call_expression.arguments.args[0]);
let mut message = String::new();
for (i, class) in classes.iter().enumerate() {
message.push('(');
for class in class.iter_mro(db) {
message.push_str(&class.display(db).to_string());
// Omit the comma for the last element (which is always `object`)
if class
.into_class()
.is_none_or(|base| !base.is_object(context.db()))
{
message.push_str(", ");
}
}
// If the last element was also the first element
// (i.e., it's a length-1 tuple -- which can only happen if we're revealing
// the MRO for `object` itself), add a trailing comma so that it's still a
// valid tuple display.
if class.is_object(db) {
message.push(',');
}
message.push(')');
if i < classes.len() - 1 {
message.push_str(" | ");
}
}
diag.annotate(Annotation::primary(span).message(message));
}
}
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
let [Some(first_arg), Some(second_argument)] = parameter_types else {
return;
@ -1822,6 +1904,7 @@ pub(crate) mod tests {
| KnownFunction::RevealProtocolInterface
| KnownFunction::RangeConstraint
| KnownFunction::NegatedRangeConstraint
| KnownFunction::RevealMro
| KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib,