Skip to content

Commit b02e821

Browse files
authored
[ty] Don't introduce invalid syntax when autofixing override-of-final-method (#21699)
1 parent 69ace00 commit b02e821

9 files changed

+430
-421
lines changed

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,13 @@ impl Diagnostic {
354354
Arc::make_mut(&mut self.inner).fix = Some(fix);
355355
}
356356

357+
/// If `fix` is `Some`, set the fix for this diagnostic.
358+
pub fn set_optional_fix(&mut self, fix: Option<Fix>) {
359+
if let Some(fix) = fix {
360+
self.set_fix(fix);
361+
}
362+
}
363+
357364
/// Remove the fix for this diagnostic.
358365
pub fn remove_fix(&mut self) {
359366
Arc::make_mut(&mut self.inner).fix = None;

crates/ty_python_semantic/resources/mdtest/final.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class Parent:
5151
@final
5252
def my_property2(self) -> int: ...
5353

54+
@property
55+
@final
56+
def my_property3(self) -> int: ...
57+
5458
@final
5559
@classmethod
5660
def class_method1(cls) -> int: ...
@@ -86,6 +90,13 @@ class Child(Parent):
8690

8791
@property
8892
def my_property2(self) -> int: ... # error: [override-of-final-method]
93+
@my_property2.setter
94+
def my_property2(self, x: int) -> None: ...
95+
96+
@property
97+
def my_property3(self) -> int: ... # error: [override-of-final-method]
98+
@my_property3.deleter
99+
def my_proeprty3(self) -> None: ...
89100

90101
@classmethod
91102
def class_method1(cls) -> int: ... # error: [override-of-final-method]
@@ -230,7 +241,7 @@ class ChildOfBad(Bad):
230241
def bar(self, x: str) -> str: ...
231242
@overload
232243
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
233-
244+
234245
@overload
235246
def baz(self, x: str) -> str: ...
236247
@overload
@@ -461,14 +472,17 @@ class B(A):
461472
def method1(self) -> None: ... # error: [override-of-final-method]
462473
def method2(self) -> None: ... # error: [override-of-final-method]
463474
def method3(self) -> None: ... # error: [override-of-final-method]
464-
def method4(self) -> None: ... # error: [override-of-final-method]
475+
476+
# check that autofixes don't introduce invalid syntax
477+
# if there are multiple statements on one line
478+
#
479+
# TODO: we should emit a Liskov violation here too
480+
# error: [override-of-final-method]
481+
method4 = 42; unrelated = 56 # fmt: skip
465482

466483
# Possible overrides of possibly `@final` methods...
467484
class C(A):
468485
if coinflip():
469-
# TODO: the autofix here introduces invalid syntax because there are now no
470-
# statements inside the `if:` branch
471-
# (but it might still be a useful autofix in an IDE context?)
472486
def method1(self) -> None: ... # error: [override-of-final-method]
473487
else:
474488
pass

crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_A_possibly-undefined…_(fc7b496fd1986deb).snap

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,29 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
4949
35 | def method1(self) -> None: ... # error: [override-of-final-method]
5050
36 | def method2(self) -> None: ... # error: [override-of-final-method]
5151
37 | def method3(self) -> None: ... # error: [override-of-final-method]
52-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
53-
39 |
54-
40 | # Possible overrides of possibly `@final` methods...
55-
41 | class C(A):
56-
42 | if coinflip():
57-
43 | # TODO: the autofix here introduces invalid syntax because there are now no
58-
44 | # statements inside the `if:` branch
59-
45 | # (but it might still be a useful autofix in an IDE context?)
60-
46 | def method1(self) -> None: ... # error: [override-of-final-method]
61-
47 | else:
62-
48 | pass
63-
49 |
64-
50 | if coinflip():
65-
51 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
66-
52 | else:
67-
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
68-
54 |
69-
55 | if coinflip():
70-
56 | def method3(self) -> None: ... # error: [override-of-final-method]
71-
57 | def method4(self) -> None: ... # error: [override-of-final-method]
52+
38 |
53+
39 | # check that autofixes don't introduce invalid syntax
54+
40 | # if there are multiple statements on one line
55+
41 | #
56+
42 | # TODO: we should emit a Liskov violation here too
57+
43 | # error: [override-of-final-method]
58+
44 | method4 = 42; unrelated = 56 # fmt: skip
59+
45 |
60+
46 | # Possible overrides of possibly `@final` methods...
61+
47 | class C(A):
62+
48 | if coinflip():
63+
49 | def method1(self) -> None: ... # error: [override-of-final-method]
64+
50 | else:
65+
51 | pass
66+
52 |
67+
53 | if coinflip():
68+
54 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
69+
55 | else:
70+
56 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
71+
57 |
72+
58 | if coinflip():
73+
59 | def method3(self) -> None: ... # error: [override-of-final-method]
74+
60 | def method4(self) -> None: ... # error: [override-of-final-method]
7275
```
7376

7477
# Diagnostics
@@ -104,7 +107,7 @@ info: rule `override-of-final-method` is enabled by default
104107
35 + # error: [override-of-final-method]
105108
36 | def method2(self) -> None: ... # error: [override-of-final-method]
106109
37 | def method3(self) -> None: ... # error: [override-of-final-method]
107-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
110+
38 |
108111
note: This is an unsafe fix and may change runtime behavior
109112
110113
```
@@ -118,7 +121,6 @@ error[override-of-final-method]: Cannot override `A.method2`
118121
36 | def method2(self) -> None: ... # error: [override-of-final-method]
119122
| ^^^^^^^ Overrides a definition from superclass `A`
120123
37 | def method3(self) -> None: ... # error: [override-of-final-method]
121-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
122124
|
123125
info: `A.method2` is decorated with `@final`, forbidding overrides
124126
--> src/mdtest_snippet.py:16:9
@@ -140,8 +142,8 @@ info: rule `override-of-final-method` is enabled by default
140142
- def method2(self) -> None: ... # error: [override-of-final-method]
141143
36 + # error: [override-of-final-method]
142144
37 | def method3(self) -> None: ... # error: [override-of-final-method]
143-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
144-
39 |
145+
38 |
146+
39 | # check that autofixes don't introduce invalid syntax
145147
note: This is an unsafe fix and may change runtime behavior
146148
147149
```
@@ -154,7 +156,8 @@ error[override-of-final-method]: Cannot override `A.method3`
154156
36 | def method2(self) -> None: ... # error: [override-of-final-method]
155157
37 | def method3(self) -> None: ... # error: [override-of-final-method]
156158
| ^^^^^^^ Overrides a definition from superclass `A`
157-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
159+
38 |
160+
39 | # check that autofixes don't introduce invalid syntax
158161
|
159162
info: `A.method3` is decorated with `@final`, forbidding overrides
160163
--> src/mdtest_snippet.py:20:9
@@ -174,23 +177,23 @@ info: rule `override-of-final-method` is enabled by default
174177
36 | def method2(self) -> None: ... # error: [override-of-final-method]
175178
- def method3(self) -> None: ... # error: [override-of-final-method]
176179
37 + # error: [override-of-final-method]
177-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
178-
39 |
179-
40 | # Possible overrides of possibly `@final` methods...
180+
38 |
181+
39 | # check that autofixes don't introduce invalid syntax
182+
40 | # if there are multiple statements on one line
180183
note: This is an unsafe fix and may change runtime behavior
181184
182185
```
183186

184187
```
185188
error[override-of-final-method]: Cannot override `A.method4`
186-
--> src/mdtest_snippet.py:38:9
189+
--> src/mdtest_snippet.py:44:5
187190
|
188-
36 | def method2(self) -> None: ... # error: [override-of-final-method]
189-
37 | def method3(self) -> None: ... # error: [override-of-final-method]
190-
38 | def method4(self) -> None: ... # error: [override-of-final-method]
191-
| ^^^^^^^ Overrides a definition from superclass `A`
192-
39 |
193-
40 | # Possible overrides of possibly `@final` methods...
191+
42 | # TODO: we should emit a Liskov violation here too
192+
43 | # error: [override-of-final-method]
193+
44 | method4 = 42; unrelated = 56 # fmt: skip
194+
| ^^^^^^^ Overrides a definition from superclass `A`
195+
45 |
196+
46 | # Possible overrides of possibly `@final` methods...
194197
|
195198
info: `A.method4` is decorated with `@final`, forbidding overrides
196199
--> src/mdtest_snippet.py:29:9
@@ -206,28 +209,19 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
206209
|
207210
help: Remove the override of `method4`
208211
info: rule `override-of-final-method` is enabled by default
209-
35 | def method1(self) -> None: ... # error: [override-of-final-method]
210-
36 | def method2(self) -> None: ... # error: [override-of-final-method]
211-
37 | def method3(self) -> None: ... # error: [override-of-final-method]
212-
- def method4(self) -> None: ... # error: [override-of-final-method]
213-
38 + # error: [override-of-final-method]
214-
39 |
215-
40 | # Possible overrides of possibly `@final` methods...
216-
41 | class C(A):
217-
note: This is an unsafe fix and may change runtime behavior
218212
219213
```
220214

221215
```
222216
error[override-of-final-method]: Cannot override `A.method1`
223-
--> src/mdtest_snippet.py:46:13
217+
--> src/mdtest_snippet.py:49:13
224218
|
225-
44 | # statements inside the `if:` branch
226-
45 | # (but it might still be a useful autofix in an IDE context?)
227-
46 | def method1(self) -> None: ... # error: [override-of-final-method]
219+
47 | class C(A):
220+
48 | if coinflip():
221+
49 | def method1(self) -> None: ... # error: [override-of-final-method]
228222
| ^^^^^^^ Overrides a definition from superclass `A`
229-
47 | else:
230-
48 | pass
223+
50 | else:
224+
51 | pass
231225
|
232226
info: `A.method1` is decorated with `@final`, forbidding overrides
233227
--> src/mdtest_snippet.py:8:9
@@ -243,26 +237,17 @@ info: `A.method1` is decorated with `@final`, forbidding overrides
243237
|
244238
help: Remove the override of `method1`
245239
info: rule `override-of-final-method` is enabled by default
246-
43 | # TODO: the autofix here introduces invalid syntax because there are now no
247-
44 | # statements inside the `if:` branch
248-
45 | # (but it might still be a useful autofix in an IDE context?)
249-
- def method1(self) -> None: ... # error: [override-of-final-method]
250-
46 + # error: [override-of-final-method]
251-
47 | else:
252-
48 | pass
253-
49 |
254-
note: This is an unsafe fix and may change runtime behavior
255240
256241
```
257242

258243
```
259244
error[override-of-final-method]: Cannot override `A.method3`
260-
--> src/mdtest_snippet.py:56:13
245+
--> src/mdtest_snippet.py:59:13
261246
|
262-
55 | if coinflip():
263-
56 | def method3(self) -> None: ... # error: [override-of-final-method]
247+
58 | if coinflip():
248+
59 | def method3(self) -> None: ... # error: [override-of-final-method]
264249
| ^^^^^^^ Overrides a definition from superclass `A`
265-
57 | def method4(self) -> None: ... # error: [override-of-final-method]
250+
60 | def method4(self) -> None: ... # error: [override-of-final-method]
266251
|
267252
info: `A.method3` is decorated with `@final`, forbidding overrides
268253
--> src/mdtest_snippet.py:20:9
@@ -277,23 +262,16 @@ info: `A.method3` is decorated with `@final`, forbidding overrides
277262
|
278263
help: Remove the override of `method3`
279264
info: rule `override-of-final-method` is enabled by default
280-
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
281-
54 |
282-
55 | if coinflip():
283-
- def method3(self) -> None: ... # error: [override-of-final-method]
284-
56 + # error: [override-of-final-method]
285-
57 | def method4(self) -> None: ... # error: [override-of-final-method]
286-
note: This is an unsafe fix and may change runtime behavior
287265
288266
```
289267

290268
```
291269
error[override-of-final-method]: Cannot override `A.method4`
292-
--> src/mdtest_snippet.py:57:13
270+
--> src/mdtest_snippet.py:60:13
293271
|
294-
55 | if coinflip():
295-
56 | def method3(self) -> None: ... # error: [override-of-final-method]
296-
57 | def method4(self) -> None: ... # error: [override-of-final-method]
272+
58 | if coinflip():
273+
59 | def method3(self) -> None: ... # error: [override-of-final-method]
274+
60 | def method4(self) -> None: ... # error: [override-of-final-method]
297275
| ^^^^^^^ Overrides a definition from superclass `A`
298276
|
299277
info: `A.method4` is decorated with `@final`, forbidding overrides
@@ -310,11 +288,5 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
310288
|
311289
help: Remove the override of `method4`
312290
info: rule `override-of-final-method` is enabled by default
313-
54 |
314-
55 | if coinflip():
315-
56 | def method3(self) -> None: ... # error: [override-of-final-method]
316-
- def method4(self) -> None: ... # error: [override-of-final-method]
317-
57 + # error: [override-of-final-method]
318-
note: This is an unsafe fix and may change runtime behavior
319291
320292
```

0 commit comments

Comments
 (0)