diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 18f32d9acb..0b74ab50aa 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -928,6 +928,42 @@ def _(flag1: bool, flag2: bool): reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] ``` +## Invalid access to attribute + + + +If a non-declared variable is used and an attribute with the same name is defined and accessible, +then we emit a subdiagnostic suggesting the use of `self.`. +(`An attribute with the same name as 'x' is defined, consider using 'self.x'` in these cases) + +```py +class Foo: + x: int + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +```py +class Foo: + x: int = 1 + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +```py +class Foo: + def __init__(self): + self.x = 1 + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + ## Unions of attributes If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at…_(5457445ffed43a87).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at…_(5457445ffed43a87).snap new file mode 100644 index 0000000000..41b32f8ac8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at…_(5457445ffed43a87).snap @@ -0,0 +1,82 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attributes.md - Attributes - Invalid access to attribute +mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Foo: + 2 | x: int + 3 | + 4 | def method(self): + 5 | # error: [unresolved-reference] "Name `x` used when not defined" + 6 | y = x + 7 | class Foo: + 8 | x: int = 1 + 9 | +10 | def method(self): +11 | # error: [unresolved-reference] "Name `x` used when not defined" +12 | y = x +13 | class Foo: +14 | def __init__(self): +15 | self.x = 1 +16 | +17 | def method(self): +18 | # error: [unresolved-reference] "Name `x` used when not defined" +19 | y = x +``` + +# Diagnostics + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:6:13 + | +4 | def method(self): +5 | # error: [unresolved-reference] "Name `x` used when not defined" +6 | y = x + | ^ +7 | class Foo: +8 | x: int = 1 + | +info: An attribute `x` is available, consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:12:13 + | +10 | def method(self): +11 | # error: [unresolved-reference] "Name `x` used when not defined" +12 | y = x + | ^ +13 | class Foo: +14 | def __init__(self): + | +info: An attribute `x` is available, consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:19:13 + | +17 | def method(self): +18 | # error: [unresolved-reference] "Name `x` used when not defined" +19 | y = x + | ^ + | +info: An attribute `x` is available, consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 39277b142b..addffb6e23 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -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) { diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index af77abaabc..7537573b60 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1718,6 +1718,9 @@ impl<'db> TypeInferenceBuilder<'db> { fn class_context_of_current_method(&self) -> Option> { 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) }