Skip to content

Commit a3c79d8

Browse files
MatthewMckee4AlexWaygoodcarljm
authored
[ty] Don't add incorrect subdiagnostic for unresolved reference (#18487)
Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Carl Meyer <[email protected]>
1 parent 57bd7d0 commit a3c79d8

File tree

5 files changed

+422
-73
lines changed

5 files changed

+422
-73
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -946,9 +946,9 @@ def _(flag1: bool, flag2: bool):
946946

947947
<!-- snapshot-diagnostics -->
948948

949-
If a non-declared variable is used and an attribute with the same name is defined and accessible,
950-
then we emit a subdiagnostic suggesting the use of `self.`.
951-
(`An attribute with the same name as 'x' is defined, consider using 'self.x'` in these cases)
949+
If an undefined variable is used in a method, and an attribute with the same name is defined and
950+
accessible, then we emit a subdiagnostic suggesting the use of `self.`. (These don't appear inline
951+
here; see the diagnostic snapshots.)
952952

953953
```py
954954
class Foo:
@@ -978,6 +978,107 @@ class Foo:
978978
y = x
979979
```
980980

981+
In a staticmethod, we don't suggest that it might be an attribute.
982+
983+
```py
984+
class Foo:
985+
def __init__(self):
986+
self.x = 42
987+
988+
@staticmethod
989+
def static_method():
990+
# error: [unresolved-reference] "Name `x` used when not defined"
991+
y = x
992+
```
993+
994+
In a classmethod, if the name matches a class attribute, we suggest `cls.`.
995+
996+
```py
997+
from typing import ClassVar
998+
999+
class Foo:
1000+
x: ClassVar[int] = 42
1001+
1002+
@classmethod
1003+
def class_method(cls):
1004+
# error: [unresolved-reference] "Name `x` used when not defined"
1005+
y = x
1006+
```
1007+
1008+
In a classmethod, if the name matches an instance-only attribute, we don't suggest anything.
1009+
1010+
```py
1011+
class Foo:
1012+
def __init__(self):
1013+
self.x = 42
1014+
1015+
@classmethod
1016+
def class_method(cls):
1017+
# error: [unresolved-reference] "Name `x` used when not defined"
1018+
y = x
1019+
```
1020+
1021+
We also don't suggest anything if the method is (invalidly) decorated with both `@classmethod` and
1022+
`@staticmethod`:
1023+
1024+
```py
1025+
class Foo:
1026+
x: ClassVar[int]
1027+
1028+
@classmethod
1029+
@staticmethod
1030+
def class_method(cls):
1031+
# error: [unresolved-reference] "Name `x` used when not defined"
1032+
y = x
1033+
```
1034+
1035+
In an instance method that uses some other parameter name in place of `self`, we use that parameter
1036+
name in the sub-diagnostic.
1037+
1038+
```py
1039+
class Foo:
1040+
def __init__(self):
1041+
self.x = 42
1042+
1043+
def method(other):
1044+
# error: [unresolved-reference] "Name `x` used when not defined"
1045+
y = x
1046+
```
1047+
1048+
In a classmethod that uses some other parameter name in place of `cls`, we use that parameter name
1049+
in the sub-diagnostic.
1050+
1051+
```py
1052+
from typing import ClassVar
1053+
1054+
class Foo:
1055+
x: ClassVar[int] = 42
1056+
1057+
@classmethod
1058+
def class_method(c_other):
1059+
# error: [unresolved-reference] "Name `x` used when not defined"
1060+
y = x
1061+
```
1062+
1063+
We don't suggest anything if an instance method or a classmethod only has variadic arguments, or if
1064+
the first parameter is keyword-only:
1065+
1066+
```py
1067+
from typing import ClassVar
1068+
1069+
class Foo:
1070+
x: ClassVar[int] = 42
1071+
1072+
def instance_method(*args, **kwargs):
1073+
# error: [unresolved-reference] "Name `x` used when not defined"
1074+
print(x)
1075+
1076+
@classmethod
1077+
def class_method(*, cls):
1078+
# error: [unresolved-reference] "Name `x` used when not defined"
1079+
y = x
1080+
```
1081+
9811082
## Unions of attributes
9821083

9831084
If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we

crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at…_(5457445ffed43a87).snap

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,68 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
3131
17 | def method(self):
3232
18 | # error: [unresolved-reference] "Name `x` used when not defined"
3333
19 | y = x
34+
20 | class Foo:
35+
21 | def __init__(self):
36+
22 | self.x = 42
37+
23 |
38+
24 | @staticmethod
39+
25 | def static_method():
40+
26 | # error: [unresolved-reference] "Name `x` used when not defined"
41+
27 | y = x
42+
28 | from typing import ClassVar
43+
29 |
44+
30 | class Foo:
45+
31 | x: ClassVar[int] = 42
46+
32 |
47+
33 | @classmethod
48+
34 | def class_method(cls):
49+
35 | # error: [unresolved-reference] "Name `x` used when not defined"
50+
36 | y = x
51+
37 | class Foo:
52+
38 | def __init__(self):
53+
39 | self.x = 42
54+
40 |
55+
41 | @classmethod
56+
42 | def class_method(cls):
57+
43 | # error: [unresolved-reference] "Name `x` used when not defined"
58+
44 | y = x
59+
45 | class Foo:
60+
46 | x: ClassVar[int]
61+
47 |
62+
48 | @classmethod
63+
49 | @staticmethod
64+
50 | def class_method(cls):
65+
51 | # error: [unresolved-reference] "Name `x` used when not defined"
66+
52 | y = x
67+
53 | class Foo:
68+
54 | def __init__(self):
69+
55 | self.x = 42
70+
56 |
71+
57 | def method(other):
72+
58 | # error: [unresolved-reference] "Name `x` used when not defined"
73+
59 | y = x
74+
60 | from typing import ClassVar
75+
61 |
76+
62 | class Foo:
77+
63 | x: ClassVar[int] = 42
78+
64 |
79+
65 | @classmethod
80+
66 | def class_method(c_other):
81+
67 | # error: [unresolved-reference] "Name `x` used when not defined"
82+
68 | y = x
83+
69 | from typing import ClassVar
84+
70 |
85+
71 | class Foo:
86+
72 | x: ClassVar[int] = 42
87+
73 |
88+
74 | def instance_method(*args, **kwargs):
89+
75 | # error: [unresolved-reference] "Name `x` used when not defined"
90+
76 | print(x)
91+
77 |
92+
78 | @classmethod
93+
79 | def class_method(*, cls):
94+
80 | # error: [unresolved-reference] "Name `x` used when not defined"
95+
81 | y = x
3496
```
3597

3698
# Diagnostics
@@ -75,8 +137,128 @@ error[unresolved-reference]: Name `x` used when not defined
75137
18 | # error: [unresolved-reference] "Name `x` used when not defined"
76138
19 | y = x
77139
| ^
140+
20 | class Foo:
141+
21 | def __init__(self):
78142
|
79143
info: An attribute `x` is available: consider using `self.x`
80144
info: rule `unresolved-reference` is enabled by default
81145
82146
```
147+
148+
```
149+
error[unresolved-reference]: Name `x` used when not defined
150+
--> src/mdtest_snippet.py:27:13
151+
|
152+
25 | def static_method():
153+
26 | # error: [unresolved-reference] "Name `x` used when not defined"
154+
27 | y = x
155+
| ^
156+
28 | from typing import ClassVar
157+
|
158+
info: rule `unresolved-reference` is enabled by default
159+
160+
```
161+
162+
```
163+
error[unresolved-reference]: Name `x` used when not defined
164+
--> src/mdtest_snippet.py:36:13
165+
|
166+
34 | def class_method(cls):
167+
35 | # error: [unresolved-reference] "Name `x` used when not defined"
168+
36 | y = x
169+
| ^
170+
37 | class Foo:
171+
38 | def __init__(self):
172+
|
173+
info: An attribute `x` is available: consider using `cls.x`
174+
info: rule `unresolved-reference` is enabled by default
175+
176+
```
177+
178+
```
179+
error[unresolved-reference]: Name `x` used when not defined
180+
--> src/mdtest_snippet.py:44:13
181+
|
182+
42 | def class_method(cls):
183+
43 | # error: [unresolved-reference] "Name `x` used when not defined"
184+
44 | y = x
185+
| ^
186+
45 | class Foo:
187+
46 | x: ClassVar[int]
188+
|
189+
info: rule `unresolved-reference` is enabled by default
190+
191+
```
192+
193+
```
194+
error[unresolved-reference]: Name `x` used when not defined
195+
--> src/mdtest_snippet.py:52:13
196+
|
197+
50 | def class_method(cls):
198+
51 | # error: [unresolved-reference] "Name `x` used when not defined"
199+
52 | y = x
200+
| ^
201+
53 | class Foo:
202+
54 | def __init__(self):
203+
|
204+
info: rule `unresolved-reference` is enabled by default
205+
206+
```
207+
208+
```
209+
error[unresolved-reference]: Name `x` used when not defined
210+
--> src/mdtest_snippet.py:59:13
211+
|
212+
57 | def method(other):
213+
58 | # error: [unresolved-reference] "Name `x` used when not defined"
214+
59 | y = x
215+
| ^
216+
60 | from typing import ClassVar
217+
|
218+
info: An attribute `x` is available: consider using `other.x`
219+
info: rule `unresolved-reference` is enabled by default
220+
221+
```
222+
223+
```
224+
error[unresolved-reference]: Name `x` used when not defined
225+
--> src/mdtest_snippet.py:68:13
226+
|
227+
66 | def class_method(c_other):
228+
67 | # error: [unresolved-reference] "Name `x` used when not defined"
229+
68 | y = x
230+
| ^
231+
69 | from typing import ClassVar
232+
|
233+
info: An attribute `x` is available: consider using `c_other.x`
234+
info: rule `unresolved-reference` is enabled by default
235+
236+
```
237+
238+
```
239+
error[unresolved-reference]: Name `x` used when not defined
240+
--> src/mdtest_snippet.py:76:15
241+
|
242+
74 | def instance_method(*args, **kwargs):
243+
75 | # error: [unresolved-reference] "Name `x` used when not defined"
244+
76 | print(x)
245+
| ^
246+
77 |
247+
78 | @classmethod
248+
|
249+
info: rule `unresolved-reference` is enabled by default
250+
251+
```
252+
253+
```
254+
error[unresolved-reference]: Name `x` used when not defined
255+
--> src/mdtest_snippet.py:81:13
256+
|
257+
79 | def class_method(*, cls):
258+
80 | # error: [unresolved-reference] "Name `x` used when not defined"
259+
81 | y = x
260+
| ^
261+
|
262+
info: rule `unresolved-reference` is enabled by default
263+
264+
```

crates/ty_python_semantic/src/semantic_index/place.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ pub enum ScopeKind {
581581
}
582582

583583
impl ScopeKind {
584-
pub(crate) fn is_eager(self) -> bool {
584+
pub(crate) const fn is_eager(self) -> bool {
585585
match self {
586586
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true,
587587
ScopeKind::Annotation
@@ -591,7 +591,7 @@ impl ScopeKind {
591591
}
592592
}
593593

594-
pub(crate) fn is_function_like(self) -> bool {
594+
pub(crate) const fn is_function_like(self) -> bool {
595595
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
596596
// place table also uses the term "function-like" for these scopes.
597597
matches!(
@@ -604,13 +604,17 @@ impl ScopeKind {
604604
)
605605
}
606606

607-
pub(crate) fn is_class(self) -> bool {
607+
pub(crate) const fn is_class(self) -> bool {
608608
matches!(self, ScopeKind::Class)
609609
}
610610

611-
pub(crate) fn is_type_parameter(self) -> bool {
611+
pub(crate) const fn is_type_parameter(self) -> bool {
612612
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
613613
}
614+
615+
pub(crate) const fn is_non_lambda_function(self) -> bool {
616+
matches!(self, ScopeKind::Function)
617+
}
614618
}
615619

616620
/// [`PlaceExpr`] table for a specific [`Scope`].

0 commit comments

Comments
 (0)