[ty] Improved diagnostic for reassignments of Final symbols (#19214)

## Summary

Implement [this
suggestion](https://github.com/astral-sh/ruff/pull/19178#discussion_r2192658146)
by @AlexWaygood.


![image](https://github.com/user-attachments/assets/f183d691-ef6e-43a2-b005-3a32205bc408)
This commit is contained in:
David Peter 2025-07-08 20:29:07 +02:00 committed by GitHub
parent a8f2c26143
commit 1a099886ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 15 deletions

View file

@ -12,30 +12,47 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
## mdtest_snippet.py ## mdtest_snippet.py
``` ```
1 | from typing import Final 1 | from typing import Final
2 | 2 |
3 | MY_CONSTANT: Final[int] = 1 3 | MY_CONSTANT: Final[int] = 1
4 | 4 |
5 | # more code 5 | # more code
6 | 6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment] 7 | MY_CONSTANT = 2 # error: [invalid-assignment]
8 | from _stat import ST_INO
9 |
10 | ST_INO = 1 # error: [invalid-assignment]
``` ```
# Diagnostics # Diagnostics
``` ```
error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not allowed error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not allowed
--> src/mdtest_snippet.py:3:1 --> src/mdtest_snippet.py:3:14
| |
1 | from typing import Final 1 | from typing import Final
2 | 2 |
3 | MY_CONSTANT: Final[int] = 1 3 | MY_CONSTANT: Final[int] = 1
| --------------------------- Original definition | ---------- Symbol declared as `Final` here
4 | 4 |
5 | # more code 5 | # more code
6 | 6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment] 7 | MY_CONSTANT = 2 # error: [invalid-assignment]
| ^^^^^^^^^^^ Reassignment of `Final` symbol | ^^^^^^^^^^^^^^^ Symbol later reassigned here
8 | from _stat import ST_INO
|
info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-assignment]: Reassignment of `Final` symbol `ST_INO` is not allowed
--> src/mdtest_snippet.py:10:1
|
8 | from _stat import ST_INO
9 |
10 | ST_INO = 1 # error: [invalid-assignment]
| ^^^^^^^^^^ Reassignment of `Final` symbol
| |
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default

View file

@ -260,6 +260,8 @@ class C:
<!-- snapshot-diagnostics --> <!-- snapshot-diagnostics -->
Annotated assignment:
```py ```py
from typing import Final from typing import Final
@ -270,4 +272,12 @@ MY_CONSTANT: Final[int] = 1
MY_CONSTANT = 2 # error: [invalid-assignment] MY_CONSTANT = 2 # error: [invalid-assignment]
``` ```
Imported `Final` symbol:
```py
from _stat import ST_INO
ST_INO = 1 # error: [invalid-assignment]
```
[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final [`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final

View file

@ -1663,7 +1663,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if !is_local || previous_definition.is_some() { if !is_local || previous_definition.is_some() {
let place = place_table.place_expr(binding.place(db)); let place = place_table.place_expr(binding.place(db));
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, node) { if let Some(builder) = self.context.report_lint(
&INVALID_ASSIGNMENT,
binding.full_range(self.db(), self.module()),
) {
let mut diagnostic = builder.into_diagnostic(format_args!( let mut diagnostic = builder.into_diagnostic(format_args!(
"Reassignment of `Final` symbol `{place}` is not allowed" "Reassignment of `Final` symbol `{place}` is not allowed"
)); ));
@ -1676,10 +1679,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// module, but that information is currently not threaded through attribute // module, but that information is currently not threaded through attribute
// lookup. // lookup.
if !previous_definition.kind(db).is_import() { if !previous_definition.kind(db).is_import() {
let range = previous_definition.full_range(self.db(), self.module()); if let DefinitionKind::AnnotatedAssignment(assignment) =
previous_definition.kind(db)
{
let range = assignment.annotation(self.module()).range();
diagnostic.annotate( diagnostic.annotate(
self.context.secondary(range).message("Original definition"), self.context
.secondary(range)
.message("Symbol declared as `Final` here"),
); );
} else {
let range =
previous_definition.full_range(self.db(), self.module());
diagnostic.annotate(
self.context
.secondary(range)
.message("Symbol declared as `Final` here"),
);
}
diagnostic.set_primary_message("Symbol later reassigned here");
} }
} }
} }