Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 105 additions & 72 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

436 changes: 435 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/final.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - A possibly-undefined `@final` method is overridden
mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import final
2 |
3 | def coinflip() -> bool:
4 | return False
5 |
6 | class A:
7 | if coinflip():
8 | @final
9 | def method1(self) -> None: ...
10 | else:
11 | def method1(self) -> None: ...
12 |
13 | if coinflip():
14 | def method2(self) -> None: ...
15 | else:
16 | @final
17 | def method2(self) -> None: ...
18 |
19 | if coinflip():
20 | @final
21 | def method3(self) -> None: ...
22 | else:
23 | @final
24 | def method3(self) -> None: ...
25 |
26 | if coinflip():
27 | def method4(self) -> None: ...
28 | elif coinflip():
29 | @final
30 | def method4(self) -> None: ...
31 | else:
32 | def method4(self) -> None: ...
33 |
34 | class B(A):
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
41 | class C(A):
42 | if coinflip():
43 | # TODO: the autofix here introduces invalid syntax because there are now no
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
46 | def method1(self) -> None: ... # error: [override-of-final-method]
47 | else:
48 | pass
49 |
50 | if coinflip():
51 | def method2(self) -> None: ... # error: [override-of-final-method]
52 | def method3(self) -> None: ... # error: [override-of-final-method]
53 | def method4(self) -> None: ... # error: [override-of-final-method]
```

# Diagnostics

```
error[override-of-final-method]: Cannot override `A.method1`
--> src/mdtest_snippet.py:35:9
|
34 | class B(A):
35 | def method1(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method1` from superclass `A`
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:8:9
|
6 | class A:
7 | if coinflip():
8 | @final
| ------
9 | def method1(self) -> None: ...
| ------- `A.method1` defined here
10 | else:
11 | def method1(self) -> None: ...
|
help: Remove the override of `method1`
info: rule `override-of-final-method` is enabled by default
32 | def method4(self) -> None: ...
33 |
34 | class B(A):
- def method1(self) -> None: ... # error: [override-of-final-method]
35 + # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method2`
--> src/mdtest_snippet.py:36:9
|
34 | class B(A):
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method2` from superclass `A`
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:16:9
|
14 | def method2(self) -> None: ...
15 | else:
16 | @final
| ------
17 | def method2(self) -> None: ...
| ------- `A.method2` defined here
18 |
19 | if coinflip():
|
help: Remove the override of `method2`
info: rule `override-of-final-method` is enabled by default
33 |
34 | class B(A):
35 | def method1(self) -> None: ... # error: [override-of-final-method]
- def method2(self) -> None: ... # error: [override-of-final-method]
36 + # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method3`
--> src/mdtest_snippet.py:37:9
|
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method3` from superclass `A`
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method3` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:20:9
|
19 | if coinflip():
20 | @final
| ------
21 | def method3(self) -> None: ...
| ------- `A.method3` defined here
22 | else:
23 | @final
|
help: Remove the override of `method3`
info: rule `override-of-final-method` is enabled by default
34 | class B(A):
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
- def method3(self) -> None: ... # error: [override-of-final-method]
37 + # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method4`
--> src/mdtest_snippet.py:38:9
|
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
38 | def method4(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method4` from superclass `A`
39 |
40 | # Possible overrides of possibly `@final` methods...
|
info: `A.method4` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:29:9
|
27 | def method4(self) -> None: ...
28 | elif coinflip():
29 | @final
| ------
30 | def method4(self) -> None: ...
| ------- `A.method4` defined here
31 | else:
32 | def method4(self) -> None: ...
|
help: Remove the override of `method4`
info: rule `override-of-final-method` is enabled by default
35 | def method1(self) -> None: ... # error: [override-of-final-method]
36 | def method2(self) -> None: ... # error: [override-of-final-method]
37 | def method3(self) -> None: ... # error: [override-of-final-method]
- def method4(self) -> None: ... # error: [override-of-final-method]
38 + # error: [override-of-final-method]
39 |
40 | # Possible overrides of possibly `@final` methods...
41 | class C(A):
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method1`
--> src/mdtest_snippet.py:46:13
|
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
46 | def method1(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method1` from superclass `A`
47 | else:
48 | pass
|
info: `A.method1` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:8:9
|
6 | class A:
7 | if coinflip():
8 | @final
| ------
9 | def method1(self) -> None: ...
| ------- `A.method1` defined here
10 | else:
11 | def method1(self) -> None: ...
|
help: Remove the override of `method1`
info: rule `override-of-final-method` is enabled by default
43 | # TODO: the autofix here introduces invalid syntax because there are now no
44 | # statements inside the `if:` branch
45 | # (but it might still be a useful autofix in an IDE context?)
- def method1(self) -> None: ... # error: [override-of-final-method]
46 + # error: [override-of-final-method]
47 | else:
48 | pass
49 |
Copy link
Member Author

@AlexWaygood AlexWaygood Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaReiser, I feel like I remember Ruff having this issue in the past -- can you remember what the solution was? Ideally we'd mark the autofix as display-only if we can see that it's going to introduce invalid syntax. (Or use a range_replacement that replaces the definition with pass.)

Copy link
Member

@MichaReiser MichaReiser Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a DisplayOnly Applicability. I don't think they are shown anywhere by default (ever?). The IDE also filters out manual only edits.

I don't think we should add fixes that introduce syntax errors. I'm also not convinced that it's the right fix. It's as likely that the proper fix is to remove the @final

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was more wondering if Ruff had a nice little routine somewhere to easily detect that this was the only statement in the if: branch (and therefore that a naive deletion would introduce an unsafe fix)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Return the [`Edit`] to use when deleting a [`Stmt`].
///
/// In some cases, this is as simple as deleting the [`TextRange`] of the [`Stmt`]
/// itself. However, there are a few exceptions:
/// - If the [`Stmt`] is _not_ the terminal statement in a multi-statement line,
/// we need to delete up to the start of the next statement (and avoid
/// deleting any content that precedes the statement).
/// - If the [`Stmt`] is the terminal statement in a multi-statement line, we need
/// to avoid deleting any content that precedes the statement.
/// - If the [`Stmt`] has no trailing and leading content, then it's convenient to
/// remove the entire start and end lines.
/// - If the [`Stmt`] is the last statement in its parent body, replace it with a
/// `pass` instead.
pub(crate) fn delete_stmt(
stmt: &Stmt,
parent: Option<&Stmt>,
locator: &Locator,
indexer: &Indexer,
) -> Edit {
if parent.is_some_and(|parent| is_lone_child(stmt, parent)) {
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Edit::range_replacement("pass".to_string(), stmt.range())
} else {
if let Some(semicolon) = trailing_semicolon(stmt.end(), locator) {
let next = next_stmt_break(semicolon, locator);
Edit::deletion(stmt.start(), next)
} else if has_leading_content(stmt.start(), locator.contents()) {
Edit::range_deletion(stmt.range())
} else if let Some(start) =
indexer.preceded_by_continuations(stmt.start(), locator.contents())
{
Edit::deletion(start, stmt.end())
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
}
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems complicated, I'll leave this for a followup

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yeah

note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method2`
--> src/mdtest_snippet.py:51:13
|
50 | if coinflip():
51 | def method2(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method2` from superclass `A`
52 | def method3(self) -> None: ... # error: [override-of-final-method]
53 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method2` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:16:9
|
14 | def method2(self) -> None: ...
15 | else:
16 | @final
| ------
17 | def method2(self) -> None: ...
| ------- `A.method2` defined here
18 |
19 | if coinflip():
|
help: Remove the override of `method2`
info: rule `override-of-final-method` is enabled by default
48 | pass
49 |
50 | if coinflip():
- def method2(self) -> None: ... # error: [override-of-final-method]
51 + # error: [override-of-final-method]
52 | def method3(self) -> None: ... # error: [override-of-final-method]
53 | def method4(self) -> None: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method3`
--> src/mdtest_snippet.py:52:13
|
50 | if coinflip():
51 | def method2(self) -> None: ... # error: [override-of-final-method]
52 | def method3(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method3` from superclass `A`
53 | def method4(self) -> None: ... # error: [override-of-final-method]
|
info: `A.method3` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:20:9
|
19 | if coinflip():
20 | @final
| ------
21 | def method3(self) -> None: ...
| ------- `A.method3` defined here
22 | else:
23 | @final
|
help: Remove the override of `method3`
info: rule `override-of-final-method` is enabled by default
49 |
50 | if coinflip():
51 | def method2(self) -> None: ... # error: [override-of-final-method]
- def method3(self) -> None: ... # error: [override-of-final-method]
52 + # error: [override-of-final-method]
53 | def method4(self) -> None: ... # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior

```

```
error[override-of-final-method]: Cannot override `A.method4`
--> src/mdtest_snippet.py:53:13
|
51 | def method2(self) -> None: ... # error: [override-of-final-method]
52 | def method3(self) -> None: ... # error: [override-of-final-method]
53 | def method4(self) -> None: ... # error: [override-of-final-method]
| ^^^^^^^ Override of `method4` from superclass `A`
|
info: `A.method4` is decorated with `@final`, forbidding overrides
--> src/mdtest_snippet.py:29:9
|
27 | def method4(self) -> None: ...
28 | elif coinflip():
29 | @final
| ------
30 | def method4(self) -> None: ...
| ------- `A.method4` defined here
31 | else:
32 | def method4(self) -> None: ...
|
help: Remove the override of `method4`
info: rule `override-of-final-method` is enabled by default
50 | if coinflip():
51 | def method2(self) -> None: ... # error: [override-of-final-method]
52 | def method3(self) -> None: ... # error: [override-of-final-method]
- def method4(self) -> None: ... # error: [override-of-final-method]
53 + # error: [override-of-final-method]
note: This is an unsafe fix and may change runtime behavior

```
Loading
Loading