Skip to content

Commit f283ddc

Browse files
RenzoMXDdylwil3
andauthored
[pyflakes] Flag annotated variable redeclarations as F811 in preview mode (#24244)
## Summary - In preview mode, F811 now detects annotated variable redeclarations like `bar: int = 1; bar: int = 2` as unused redefinitions - Plain reassignments (`x = 1; x = 2`) remain unflagged, as this is normal Python - Only triggers when **both** the original and the shadowing binding are annotated assignments with values - `bar: int = 1; bar: int = 2` — flagged - `bar = 1; bar = 2` — not flagged (plain reassignment) - `bar = 1; bar: int = 2` — not flagged (mixed) - `bar: int = 1; bar = 2` — not flagged (mixed) - `bar: int = 1; print(bar); bar: int = 2` — not flagged (first is used) Closes #23802 --------- Co-authored-by: dylwil3 <dylwil3@gmail.com>
1 parent 29bf84e commit f283ddc

5 files changed

Lines changed: 99 additions & 3 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Regression test for: https://github.com/astral-sh/ruff/issues/23802"""
2+
3+
# F811: both annotated assignments, first unused
4+
bar: int = 1
5+
bar: int = 2 # F811
6+
7+
x: str = "hello"
8+
x: str = "world" # F811
9+
10+
# OK: plain reassignment (no annotation)
11+
y = 1
12+
y = 2
13+
14+
# OK: first is plain, second is annotated
15+
z = 1
16+
z: int = 2
17+
18+
# OK: first is annotated, second is plain
19+
w: int = 1
20+
w = 2
21+
22+
# OK: used between assignments
23+
a: int = 1
24+
print(a)
25+
a: int = 2

crates/ruff_linter/src/preview.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ use crate::settings::{LinterSettings, types::PreviewMode};
99

1010
// Rule-specific behavior
1111

12+
// https://github.com/astral-sh/ruff/issues/23802
13+
pub(crate) const fn is_annotated_assignment_redefinition_enabled(
14+
settings: &LinterSettings,
15+
) -> bool {
16+
settings.preview.is_enabled()
17+
}
18+
1219
// https://github.com/astral-sh/ruff/pull/21382
1320
pub(crate) const fn is_custom_exception_checking_enabled(settings: &LinterSettings) -> bool {
1421
settings.preview.is_enabled()

crates/ruff_linter/src/rules/pyflakes/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,19 @@ mod tests {
749749
Ok(())
750750
}
751751

752+
#[test]
753+
fn f811_annotated_assignment_redefinition() -> Result<()> {
754+
let diagnostics = test_path(
755+
Path::new("pyflakes/F811_34.py"),
756+
&LinterSettings {
757+
preview: PreviewMode::Enabled,
758+
..LinterSettings::for_rule(Rule::RedefinedWhileUnused)
759+
},
760+
)?;
761+
assert_diagnostics!(diagnostics);
762+
Ok(())
763+
}
764+
752765
#[test]
753766
fn extend_generics() -> Result<()> {
754767
let snapshot = "extend_immutable_calls".to_string();

crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use ruff_macros::{ViolationMetadata, derive_message_formats};
2+
use ruff_python_ast::Stmt;
23
use ruff_python_semantic::analyze::visibility;
3-
use ruff_python_semantic::{BindingKind, Imported, Scope, ScopeId};
4+
use ruff_python_semantic::{Binding, BindingKind, Imported, Scope, ScopeId, SemanticModel};
45
use ruff_source_file::SourceRow;
56
use ruff_text_size::Ranged;
67

78
use crate::checkers::ast::Checker;
89
use crate::fix::edits;
10+
use crate::preview::is_annotated_assignment_redefinition_enabled;
911
use crate::{Fix, FixAvailability, Violation};
1012

1113
use rustc_hash::FxHashMap;
@@ -31,6 +33,14 @@ use rustc_hash::FxHashMap;
3133
/// import bar
3234
/// ```
3335
///
36+
/// ## Preview
37+
/// When [preview] is enabled, this rule also flags annotated variable
38+
/// redeclarations. For example, `bar: int = 1` followed by `bar: int = 2`
39+
/// will be flagged as a redefinition of an unused variable, whereas plain
40+
/// reassignments like `bar = 1` followed by `bar = 2` remain unflagged.
41+
///
42+
/// [preview]: https://docs.astral.sh/ruff/preview/
43+
///
3444
/// ## Options
3545
///
3646
/// This rule ignores dummy variables, as determined by:
@@ -79,9 +89,15 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
7989
}
8090

8191
// If the shadowing binding isn't considered a "redefinition" of the
82-
// shadowed binding, abort.
92+
// shadowed binding, abort — unless both are annotated assignments
93+
// and preview mode is enabled (see #23802).
8394
if !binding.redefines(shadowed) {
84-
continue;
95+
if !(is_annotated_assignment_redefinition_enabled(checker.settings())
96+
&& is_annotated_assignment(binding, checker.semantic())
97+
&& is_annotated_assignment(shadowed, checker.semantic()))
98+
{
99+
continue;
100+
}
85101
}
86102

87103
if shadow.same_scope() {
@@ -224,3 +240,9 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
224240
}
225241
}
226242
}
243+
244+
fn is_annotated_assignment(binding: &Binding, semantic: &SemanticModel) -> bool {
245+
binding
246+
.statement(semantic)
247+
.is_some_and(Stmt::is_ann_assign_stmt)
248+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
3+
---
4+
F811 Redefinition of unused `bar` from line 4
5+
--> F811_34.py:4:1
6+
|
7+
3 | # F811: both annotated assignments, first unused
8+
4 | bar: int = 1
9+
| --- previous definition of `bar` here
10+
5 | bar: int = 2 # F811
11+
| ^^^ `bar` redefined here
12+
6 |
13+
7 | x: str = "hello"
14+
|
15+
help: Remove definition: `bar`
16+
17+
F811 Redefinition of unused `x` from line 7
18+
--> F811_34.py:7:1
19+
|
20+
5 | bar: int = 2 # F811
21+
6 |
22+
7 | x: str = "hello"
23+
| - previous definition of `x` here
24+
8 | x: str = "world" # F811
25+
| ^ `x` redefined here
26+
9 |
27+
10 | # OK: plain reassignment (no annotation)
28+
|
29+
help: Remove definition: `x`

0 commit comments

Comments
 (0)