Skip to content

Commit eda2355

Browse files
[ty] Show Final source in final assignment diagnostic (#24194)
## Summary Addresses a TODO from #23880 to show the `Final` annotation in the final assignment diagnostic.
1 parent 929eb52 commit eda2355

4 files changed

Lines changed: 295 additions & 13 deletions

File tree

crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_`typing.Final`_-_Full_diagnostics_(174fdd8134fb325b).snap

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,36 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
3939
24 | c.x = 2 # error: [invalid-assignment]
4040
25 | from typing import Final
4141
26 |
42-
27 | UNINITIALIZED: Final[int] # error: [final-without-value]
42+
27 | class C:
43+
28 | x: Final[int] # error: [final-without-value]
44+
29 |
45+
30 | def f(self):
46+
31 | self.x = 2 # error: [invalid-assignment]
47+
32 | from typing import Final
48+
33 |
49+
34 | class C:
50+
35 | x: Final[int] = 1
51+
36 |
52+
37 | def __init__(self):
53+
38 | self.x = 2 # error: [invalid-assignment]
54+
39 | from typing import Final
55+
40 |
56+
41 | class Base:
57+
42 | x: Final[int] = 1
58+
43 |
59+
44 | class Child(Base):
60+
45 | def f(self):
61+
46 | self.x = 2 # error: [invalid-assignment]
62+
47 | from typing import Final
63+
48 |
64+
49 | class C:
65+
50 | x: int
66+
51 |
67+
52 | def f(self):
68+
53 | self.x: Final[int] = 1 # error: [invalid-assignment]
69+
54 | from typing import Final
70+
55 |
71+
56 | UNINITIALIZED: Final[int] # error: [final-without-value]
4372
```
4473

4574
# Diagnostics
@@ -79,8 +108,12 @@ info: rule `invalid-assignment` is enabled by default
79108

80109
```
81110
error[invalid-assignment]: Cannot assign to final attribute `x` on type `Self@f`
82-
--> src/mdtest_snippet.py:17:9
111+
--> src/mdtest_snippet.py:14:8
83112
|
113+
13 | class C:
114+
14 | x: Final[int] = 1
115+
| ---------- Attribute declared as `Final` here
116+
15 |
84117
16 | def f(self):
85118
17 | self.x = 2 # error: [invalid-assignment]
86119
| ^^^^^^ `Final` attributes can only be assigned in the class body or `__init__`
@@ -92,8 +125,12 @@ info: rule `invalid-assignment` is enabled by default
92125

93126
```
94127
error[invalid-assignment]: Cannot assign to final attribute `x` on type `C`
95-
--> src/mdtest_snippet.py:24:5
128+
--> src/mdtest_snippet.py:21:8
96129
|
130+
20 | class C:
131+
21 | x: Final[int] = 1
132+
| ---------- Attribute declared as `Final` here
133+
22 |
97134
23 | def __init__(c: C):
98135
24 | c.x = 2 # error: [invalid-assignment]
99136
| ^^^ `Final` attributes can only be assigned in the class body or `__init__`
@@ -103,13 +140,92 @@ info: rule `invalid-assignment` is enabled by default
103140
104141
```
105142

143+
```
144+
error[final-without-value]: `Final` symbol `x` is not assigned a value
145+
--> src/mdtest_snippet.py:28:5
146+
|
147+
27 | class C:
148+
28 | x: Final[int] # error: [final-without-value]
149+
| ^^^^^^^^^^^^^
150+
29 |
151+
30 | def f(self):
152+
|
153+
info: rule `final-without-value` is enabled by default
154+
155+
```
156+
157+
```
158+
error[invalid-assignment]: Cannot assign to final attribute `x` on type `Self@f`
159+
--> src/mdtest_snippet.py:28:8
160+
|
161+
27 | class C:
162+
28 | x: Final[int] # error: [final-without-value]
163+
| ---------- Attribute declared as `Final` here
164+
29 |
165+
30 | def f(self):
166+
31 | self.x = 2 # error: [invalid-assignment]
167+
| ^^^^^^ `Final` attributes can only be assigned in the class body or `__init__`
168+
32 | from typing import Final
169+
|
170+
info: rule `invalid-assignment` is enabled by default
171+
172+
```
173+
174+
```
175+
error[invalid-assignment]: Invalid assignment to final attribute
176+
--> src/mdtest_snippet.py:35:8
177+
|
178+
34 | class C:
179+
35 | x: Final[int] = 1
180+
| ---------- Attribute declared as `Final` here
181+
36 |
182+
37 | def __init__(self):
183+
38 | self.x = 2 # error: [invalid-assignment]
184+
| ^^^^^^ `x` already has a value in the class body
185+
39 | from typing import Final
186+
|
187+
info: rule `invalid-assignment` is enabled by default
188+
189+
```
190+
191+
```
192+
error[invalid-assignment]: Cannot assign to final attribute `x` on type `Self@f`
193+
--> src/mdtest_snippet.py:42:8
194+
|
195+
41 | class Base:
196+
42 | x: Final[int] = 1
197+
| ---------- Attribute declared as `Final` here
198+
43 |
199+
44 | class Child(Base):
200+
45 | def f(self):
201+
46 | self.x = 2 # error: [invalid-assignment]
202+
| ^^^^^^ `Final` attributes can only be assigned in the class body or `__init__`
203+
47 | from typing import Final
204+
|
205+
info: rule `invalid-assignment` is enabled by default
206+
207+
```
208+
209+
```
210+
error[invalid-assignment]: Cannot assign to final attribute `x` on type `Self@f`
211+
--> src/mdtest_snippet.py:53:9
212+
|
213+
52 | def f(self):
214+
53 | self.x: Final[int] = 1 # error: [invalid-assignment]
215+
| ^^^^^^ `Final` attributes can only be assigned in the class body or `__init__`
216+
54 | from typing import Final
217+
|
218+
info: rule `invalid-assignment` is enabled by default
219+
220+
```
221+
106222
```
107223
error[final-without-value]: `Final` symbol `UNINITIALIZED` is not assigned a value
108-
--> src/mdtest_snippet.py:27:1
224+
--> src/mdtest_snippet.py:56:1
109225
|
110-
25 | from typing import Final
111-
26 |
112-
27 | UNINITIALIZED: Final[int] # error: [final-without-value]
226+
54 | from typing import Final
227+
55 |
228+
56 | UNINITIALIZED: Final[int] # error: [final-without-value]
113229
| ^^^^^^^^^^^^^^^^^^^^^^^^^
114230
|
115231
info: rule `final-without-value` is enabled by default

crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,29 @@ class E:
11381138
self.x = 2 # Error: `self` is the second parameter, not the implicit receiver
11391139
```
11401140

1141+
## Cross-module final attribute assignment
1142+
1143+
Assigning to an inherited `Final` attribute where the base class is in a different module:
1144+
1145+
`base.py`:
1146+
1147+
```py
1148+
from typing import Final
1149+
1150+
class Base:
1151+
x: Final[int] = 1
1152+
```
1153+
1154+
`child.py`:
1155+
1156+
```py
1157+
from base import Base
1158+
1159+
class Child(Base):
1160+
def f(self):
1161+
self.x = 2 # error: [invalid-assignment]
1162+
```
1163+
11411164
## Full diagnostics
11421165

11431166
<!-- snapshot-diagnostics -->
@@ -1186,6 +1209,55 @@ def __init__(c: C):
11861209
c.x = 2 # error: [invalid-assignment]
11871210
```
11881211

1212+
Class-body `Final` declaration without value:
1213+
1214+
```py
1215+
from typing import Final
1216+
1217+
class C:
1218+
x: Final[int] # error: [final-without-value]
1219+
1220+
def f(self):
1221+
self.x = 2 # error: [invalid-assignment]
1222+
```
1223+
1224+
`__init__` assignment after class-body value:
1225+
1226+
```py
1227+
from typing import Final
1228+
1229+
class C:
1230+
x: Final[int] = 1
1231+
1232+
def __init__(self):
1233+
self.x = 2 # error: [invalid-assignment]
1234+
```
1235+
1236+
Inherited final attribute assignment:
1237+
1238+
```py
1239+
from typing import Final
1240+
1241+
class Base:
1242+
x: Final[int] = 1
1243+
1244+
class Child(Base):
1245+
def f(self):
1246+
self.x = 2 # error: [invalid-assignment]
1247+
```
1248+
1249+
Method-local `Final` annotation should not point at non-`Final` class annotation:
1250+
1251+
```py
1252+
from typing import Final
1253+
1254+
class C:
1255+
x: int
1256+
1257+
def f(self):
1258+
self.x: Final[int] = 1 # error: [invalid-assignment]
1259+
```
1260+
11891261
`Final` declaration without value:
11901262

11911263
```py

crates/ty_python_semantic/src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,15 @@ impl<'db> Type<'db> {
10571057
Type::NominalInstance(instance) => Some(instance.class(db)),
10581058
Type::ProtocolInstance(instance) => instance.to_nominal_instance().map(|i| i.class(db)),
10591059
Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db),
1060+
Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db),
1061+
Type::TypeVar(typevar) => {
1062+
let TypeVarBoundOrConstraints::UpperBound(bound) =
1063+
typevar.typevar(db).bound_or_constraints(db)?
1064+
else {
1065+
return None;
1066+
};
1067+
bound.nominal_class(db)
1068+
}
10601069
_ => None,
10611070
}
10621071
}

0 commit comments

Comments
 (0)