[ty] Add subdiagnostic suggestion to unresolved-reference diagnostic when variable exists on self (#18444)

## Summary

Closes https://github.com/astral-sh/ty/issues/502.

In the following example:
```py
class Foo:
    x: int

    def method(self):
        y = x
```
The user may intended to use `y = self.x` in `method`. 

This is now added as a subdiagnostic in the following form : 

`info: An attribute with the same name as 'x' is defined, consider using
'self.x'`

## Test Plan

Added mdtest with snapshot diagnostics.
This commit is contained in:
lipefree 2025-06-04 17:13:50 +02:00 committed by GitHub
parent f1883d71a4
commit e658778ced
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 146 additions and 2 deletions

View file

@ -1778,7 +1778,11 @@ pub(super) fn report_possibly_unbound_attribute(
));
}
pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
pub(super) fn report_unresolved_reference(
context: &InferContext,
expr_name_node: &ast::ExprName,
attribute_exists: bool,
) {
let Some(builder) = context.report_lint(&UNRESOLVED_REFERENCE, expr_name_node) else {
return;
};
@ -1795,6 +1799,12 @@ pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node
"resolving types",
);
}
if attribute_exists {
diagnostic.info(format_args!(
"An attribute `{id}` is available, consider using `self.{id}`"
));
}
}
pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) {

View file

@ -1718,6 +1718,9 @@ impl<'db> TypeInferenceBuilder<'db> {
fn class_context_of_current_method(&self) -> Option<ClassLiteral<'db>> {
let current_scope_id = self.scope().file_scope_id(self.db());
let current_scope = self.index.scope(current_scope_id);
if current_scope.kind() != ScopeKind::Function {
return None;
}
let parent_scope_id = current_scope.parent()?;
let parent_scope = self.index.scope(parent_scope_id);
@ -5899,7 +5902,20 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound(qualifiers) => {
if self.is_reachable(name_node) {
report_unresolved_reference(&self.context, name_node);
let attribute_exists =
if let Some(class) = self.class_context_of_current_method() {
let symbol = Type::instance(db, class.default_specialization(db))
.member(db, symbol_name)
.symbol;
match symbol {
Symbol::Type(..) => true,
Symbol::Unbound => false,
}
} else {
false
};
report_unresolved_reference(&self.context, name_node, attribute_exists);
}
TypeAndQualifiers::new(Type::unknown(), qualifiers)
}