[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
4 changed files with 146 additions and 2 deletions

View File

@@ -928,6 +928,42 @@ def _(flag1: bool, flag2: bool):
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
```
## Invalid access to attribute
<!-- snapshot-diagnostics -->
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

View File

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