[ty] Improve error messages for unresolved attribute diagnostics (#20963)

## Summary

- Type checkers (and type-checker authors) think in terms of types, but
I think most Python users think in terms of values. Rather than saying
that a _type_ `X` "has no attribute `foo`" (which I think sounds strange
to many users), say that "an object of type `X` has no attribute `foo`"
- Special-case certain types so that the diagnostic messages read more
like normal English: rather than saying "Type `<class 'Foo'>` has no
attribute `bar`" or "Object of type `<class 'Foo'>` has no attribute
`bar`", just say "Class `Foo` has no attribute `bar`"

## Test Plan

Mdtests and snapshots updated
This commit is contained in:
Alex Waygood 2025-10-19 10:58:25 +01:00 committed by GitHub
parent b6b96d75eb
commit 1f8297cfe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 102 additions and 63 deletions

View file

@ -2269,10 +2269,25 @@ pub(super) fn report_possibly_missing_attribute(
let Some(builder) = context.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, target) else {
return;
};
builder.into_diagnostic(format_args!(
"Attribute `{attribute}` on type `{}` may be missing",
object_ty.display(context.db()),
));
let db = context.db();
match object_ty {
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
"Member `{attribute}` may be missing on module `{}`",
module.module(db).name(db),
)),
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on class `{}`",
class.name(db),
)),
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on class `{}`",
alias.display(db),
)),
_ => builder.into_diagnostic(format_args!(
"Attribute `{attribute}` may be missing on object of type `{}`",
object_ty.display(db),
)),
};
}
pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) {

View file

@ -814,6 +814,10 @@ impl Display for DisplayFunctionType<'_> {
}
impl<'db> GenericAlias<'db> {
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> {
self.display_with(db, DisplaySettings::default())
}
pub(crate) fn display_with(
&'db self,
db: &'db dyn Db,

View file

@ -7618,25 +7618,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.context
.report_lint(&UNRESOLVED_ATTRIBUTE, attribute)
{
if bound_on_instance {
builder.into_diagnostic(
format_args!(
"Attribute `{}` can only be accessed on instances, \
not on the class object `{}` itself.",
attr.id,
value_type.display(db)
),
);
} else {
let diagnostic = builder.into_diagnostic(
format_args!(
"Type `{}` has no attribute `{}`",
value_type.display(db),
attr.id
),
);
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr);
}
if bound_on_instance {
builder.into_diagnostic(
format_args!(
"Attribute `{}` can only be accessed on instances, \
not on the class object `{}` itself.",
attr.id,
value_type.display(db)
),
);
} else {
let diagnostic = match value_type {
Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!(
"Module `{}` has no member `{}`",
module.module(db).name(db),
&attr.id
)),
Type::ClassLiteral(class) => builder.into_diagnostic(format_args!(
"Class `{}` has no attribute `{}`",
class.name(db),
&attr.id
)),
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"Class `{}` has no attribute `{}`",
alias.display(db),
&attr.id
)),
Type::FunctionLiteral(function) => builder.into_diagnostic(format_args!(
"Function `{}` has no attribute `{}`",
function.name(db),
&attr.id
)),
_ => builder.into_diagnostic(format_args!(
"Object of type `{}` has no attribute `{}`",
value_type.display(db),
&attr.id
)),
};
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr);
}
}
}