Skip to content

Commit 740c08b

Browse files
authored
[pylint] - implement redeclared-assigned-name (W0128) (#9268)
## Summary Implements [`W0128`/`redeclared-assigned-name`](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/redeclared-assigned-name.html) See: #970 ## Test Plan `cargo test`
1 parent 7e652e8 commit 740c08b

File tree

8 files changed

+149
-0
lines changed

8 files changed

+149
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FIRST, FIRST = (1, 2) # PLW0128
2+
FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
3+
FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
4+
FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
5+
6+
FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
13891389
}
13901390
}
13911391
Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => {
1392+
if checker.enabled(Rule::RedeclaredAssignedName) {
1393+
pylint::rules::redeclared_assigned_name(checker, targets);
1394+
}
13921395
if checker.enabled(Rule::LambdaAssignment) {
13931396
if let [target] = &targets[..] {
13941397
pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt);

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
294294
(Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda),
295295
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
296296
(Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable),
297+
(Pylint, "W0128") => (RuleGroup::Preview, rules::pylint::rules::RedeclaredAssignedName),
297298
(Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral),
298299
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
299300
(Pylint, "W0133") => (RuleGroup::Preview, rules::pylint::rules::UselessExceptionStatement),

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

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ mod tests {
9595
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
9696
#[test_case(Rule::NonSlotAssignment, Path::new("non_slot_assignment.py"))]
9797
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
98+
#[test_case(Rule::RedeclaredAssignedName, Path::new("redeclared_assigned_name.py"))]
9899
#[test_case(
99100
Rule::RedefinedArgumentFromLocal,
100101
Path::new("redefined_argument_from_local.py")

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

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub(crate) use non_slot_assignment::*;
4747
pub(crate) use nonlocal_without_binding::*;
4848
pub(crate) use potential_index_error::*;
4949
pub(crate) use property_with_parameters::*;
50+
pub(crate) use redeclared_assigned_name::*;
5051
pub(crate) use redefined_argument_from_local::*;
5152
pub(crate) use redefined_loop_name::*;
5253
pub(crate) use repeated_equality_comparison::*;
@@ -136,6 +137,7 @@ mod non_slot_assignment;
136137
mod nonlocal_without_binding;
137138
mod potential_index_error;
138139
mod property_with_parameters;
140+
mod redeclared_assigned_name;
139141
mod redefined_argument_from_local;
140142
mod redefined_loop_name;
141143
mod repeated_equality_comparison;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use ruff_python_ast::{self as ast, Expr};
2+
3+
use ruff_diagnostics::{Diagnostic, Violation};
4+
use ruff_macros::{derive_message_formats, violation};
5+
use ruff_text_size::Ranged;
6+
7+
use crate::checkers::ast::Checker;
8+
9+
/// ## What it does
10+
/// Checks for declared assignments to the same variable multiple times
11+
/// in the same assignment.
12+
///
13+
/// ## Why is this bad?
14+
/// Assigning a variable multiple times in the same assignment is redundant,
15+
/// as the final assignment to the variable is what the value will be.
16+
///
17+
/// ## Example
18+
/// ```python
19+
/// a, b, a = (1, 2, 3)
20+
/// print(a) # 3
21+
/// ```
22+
///
23+
/// Use instead:
24+
/// ```python
25+
/// # this is assuming you want to assign 3 to `a`
26+
/// _, b, a = (1, 2, 3)
27+
/// print(a) # 3
28+
/// ```
29+
///
30+
#[violation]
31+
pub struct RedeclaredAssignedName {
32+
name: String,
33+
}
34+
35+
impl Violation for RedeclaredAssignedName {
36+
#[derive_message_formats]
37+
fn message(&self) -> String {
38+
let RedeclaredAssignedName { name } = self;
39+
format!("Redeclared variable `{name}` in assignment")
40+
}
41+
}
42+
43+
/// PLW0128
44+
pub(crate) fn redeclared_assigned_name(checker: &mut Checker, targets: &Vec<Expr>) {
45+
let mut names: Vec<String> = Vec::new();
46+
47+
for target in targets {
48+
check_expr(checker, target, &mut names);
49+
}
50+
}
51+
52+
fn check_expr(checker: &mut Checker, expr: &Expr, names: &mut Vec<String>) {
53+
match expr {
54+
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
55+
for target in elts {
56+
check_expr(checker, target, names);
57+
}
58+
}
59+
Expr::Name(ast::ExprName { id, .. }) => {
60+
if checker.settings.dummy_variable_rgx.is_match(id) {
61+
// Ignore dummy variable assignments
62+
return;
63+
}
64+
if names.contains(id) {
65+
checker.diagnostics.push(Diagnostic::new(
66+
RedeclaredAssignedName {
67+
name: id.to_string(),
68+
},
69+
expr.range(),
70+
));
71+
}
72+
names.push(id.to_string());
73+
}
74+
_ => {}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pylint/mod.rs
3+
---
4+
redeclared_assigned_name.py:1:8: PLW0128 Redeclared variable `FIRST` in assignment
5+
|
6+
1 | FIRST, FIRST = (1, 2) # PLW0128
7+
| ^^^^^ PLW0128
8+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
9+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
10+
|
11+
12+
redeclared_assigned_name.py:2:9: PLW0128 Redeclared variable `FIRST` in assignment
13+
|
14+
1 | FIRST, FIRST = (1, 2) # PLW0128
15+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
16+
| ^^^^^ PLW0128
17+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
18+
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
19+
|
20+
21+
redeclared_assigned_name.py:3:9: PLW0128 Redeclared variable `FIRST` in assignment
22+
|
23+
1 | FIRST, FIRST = (1, 2) # PLW0128
24+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
25+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
26+
| ^^^^^ PLW0128
27+
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
28+
|
29+
30+
redeclared_assigned_name.py:3:32: PLW0128 Redeclared variable `FIRST` in assignment
31+
|
32+
1 | FIRST, FIRST = (1, 2) # PLW0128
33+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
34+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
35+
| ^^^^^ PLW0128
36+
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
37+
|
38+
39+
redeclared_assigned_name.py:4:23: PLW0128 Redeclared variable `FIRST` in assignment
40+
|
41+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
42+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
43+
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
44+
| ^^^^^ PLW0128
45+
5 |
46+
6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK
47+
|
48+
49+
redeclared_assigned_name.py:4:30: PLW0128 Redeclared variable `SECOND` in assignment
50+
|
51+
2 | FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128
52+
3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128
53+
4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128
54+
| ^^^^^^ PLW0128
55+
5 |
56+
6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK
57+
|
58+
59+

ruff.schema.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)