-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…9932) ## Summary Implement new rule: Prefer augmented assignment (#8877). It checks for the assignment statement with the form of `<expr> = <expr> <binary-operator> …` with a unsafe fix to use augmented assignment instead. ## Test Plan 1. Snapshot test is included in the PR. 2. Manually test with playground.
- Loading branch information
Showing
8 changed files
with
785 additions
and
0 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
crates/ruff_linter/resources/test/fixtures/pylint/non_augmented_assignment.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Errors | ||
some_string = "some string" | ||
index, a_number, to_multiply, to_divide, to_cube, timeDiffSeconds, flags = ( | ||
0, | ||
1, | ||
2, | ||
3, | ||
4, | ||
5, | ||
0x3, | ||
) | ||
a_list = [1, 2] | ||
some_set = {"elem"} | ||
mat1, mat2 = None, None | ||
|
||
some_string = some_string + "a very long end of string" | ||
index = index - 1 | ||
a_list = a_list + ["to concat"] | ||
some_set = some_set | {"to concat"} | ||
to_multiply = to_multiply * 5 | ||
to_divide = to_divide / 5 | ||
to_divide = to_divide // 5 | ||
to_cube = to_cube**3 | ||
to_cube = 3**to_cube | ||
to_cube = to_cube**to_cube | ||
timeDiffSeconds = timeDiffSeconds % 60 | ||
flags = flags & 0x1 | ||
flags = flags | 0x1 | ||
flags = flags ^ 0x1 | ||
flags = flags << 1 | ||
flags = flags >> 1 | ||
mat1 = mat1 @ mat2 | ||
a_list[1] = a_list[1] + 1 | ||
|
||
a_list[0:2] = a_list[0:2] * 3 | ||
a_list[:2] = a_list[:2] * 3 | ||
a_list[1:] = a_list[1:] * 3 | ||
a_list[:] = a_list[:] * 3 | ||
|
||
index = index * (index + 10) | ||
|
||
|
||
class T: | ||
def t(self): | ||
self.a = self.a + 1 | ||
|
||
|
||
obj = T() | ||
obj.a = obj.a + 1 | ||
|
||
# OK | ||
a_list[0] = a_list[:] * 3 | ||
index = a_number = a_number + 1 | ||
a_number = index = a_number + 1 | ||
index = index * index + 10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
use ast::{Expr, StmtAugAssign}; | ||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast as ast; | ||
use ruff_python_ast::comparable::ComparableExpr; | ||
use ruff_python_ast::Operator; | ||
use ruff_python_codegen::Generator; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for assignments that can be replaced with augmented assignment | ||
/// statements. | ||
/// | ||
/// ## Why is this bad? | ||
/// If an assignment statement consists of a binary operation in which one | ||
/// operand is the same as the assignment target, it can be rewritten as an | ||
/// augmented assignment. For example, `x = x + 1` can be rewritten as | ||
/// `x += 1`. | ||
/// | ||
/// When performing such an operation, augmented assignments are more concise | ||
/// and idiomatic. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// x = x + 1 | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// x += 1 | ||
/// ``` | ||
/// | ||
/// ## Fix safety | ||
/// This rule's fix is marked as unsafe, as augmented assignments have | ||
/// different semantics when the target is a mutable data type, like a list or | ||
/// dictionary. | ||
/// | ||
/// For example, consider the following: | ||
/// | ||
/// ```python | ||
/// foo = [1] | ||
/// bar = foo | ||
/// foo = foo + [2] | ||
/// assert (foo, bar) == ([1, 2], [1]) | ||
/// ``` | ||
/// | ||
/// If the assignment is replaced with an augmented assignment, the update | ||
/// operation will apply to both `foo` and `bar`, as they refer to the same | ||
/// object: | ||
/// | ||
/// ```python | ||
/// foo = [1] | ||
/// bar = foo | ||
/// foo += [2] | ||
/// assert (foo, bar) == ([1, 2], [1, 2]) | ||
/// ``` | ||
#[violation] | ||
pub struct NonAugmentedAssignment { | ||
operator: AugmentedOperator, | ||
} | ||
|
||
impl AlwaysFixableViolation for NonAugmentedAssignment { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let NonAugmentedAssignment { operator } = self; | ||
format!("Use `{operator}` to perform an augmented assignment directly") | ||
} | ||
|
||
fn fix_title(&self) -> String { | ||
"Replace with augmented assignment".to_string() | ||
} | ||
} | ||
|
||
/// PLR6104 | ||
pub(crate) fn non_augmented_assignment(checker: &mut Checker, assign: &ast::StmtAssign) { | ||
// Ignore multiple assignment targets. | ||
let [target] = assign.targets.as_slice() else { | ||
return; | ||
}; | ||
|
||
// Match, e.g., `x = x + 1`. | ||
let Expr::BinOp(value) = &*assign.value else { | ||
return; | ||
}; | ||
|
||
// Match, e.g., `x = x + 1`. | ||
if ComparableExpr::from(target) == ComparableExpr::from(&value.left) { | ||
let mut diagnostic = Diagnostic::new( | ||
NonAugmentedAssignment { | ||
operator: AugmentedOperator::from(value.op), | ||
}, | ||
assign.range(), | ||
); | ||
diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment( | ||
checker.generator(), | ||
target, | ||
value.op, | ||
&value.right, | ||
assign.range(), | ||
))); | ||
checker.diagnostics.push(diagnostic); | ||
return; | ||
} | ||
|
||
// Match, e.g., `x = 1 + x`. | ||
if ComparableExpr::from(target) == ComparableExpr::from(&value.right) { | ||
let mut diagnostic = Diagnostic::new( | ||
NonAugmentedAssignment { | ||
operator: AugmentedOperator::from(value.op), | ||
}, | ||
assign.range(), | ||
); | ||
diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment( | ||
checker.generator(), | ||
target, | ||
value.op, | ||
&value.left, | ||
assign.range(), | ||
))); | ||
checker.diagnostics.push(diagnostic); | ||
} | ||
} | ||
|
||
/// Generate a fix to convert an assignment statement to an augmented assignment. | ||
/// | ||
/// For example, given `x = x + 1`, the fix would be `x += 1`. | ||
fn augmented_assignment( | ||
generator: Generator, | ||
target: &Expr, | ||
operator: Operator, | ||
right_operand: &Expr, | ||
range: TextRange, | ||
) -> Edit { | ||
Edit::range_replacement( | ||
generator.stmt(&ast::Stmt::AugAssign(StmtAugAssign { | ||
range: TextRange::default(), | ||
target: Box::new(target.clone()), | ||
op: operator, | ||
value: Box::new(right_operand.clone()), | ||
})), | ||
range, | ||
) | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
enum AugmentedOperator { | ||
Add, | ||
BitAnd, | ||
BitOr, | ||
BitXor, | ||
Div, | ||
FloorDiv, | ||
LShift, | ||
MatMult, | ||
Mod, | ||
Mult, | ||
Pow, | ||
RShift, | ||
Sub, | ||
} | ||
|
||
impl From<Operator> for AugmentedOperator { | ||
fn from(value: Operator) -> Self { | ||
match value { | ||
Operator::Add => Self::Add, | ||
Operator::BitAnd => Self::BitAnd, | ||
Operator::BitOr => Self::BitOr, | ||
Operator::BitXor => Self::BitXor, | ||
Operator::Div => Self::Div, | ||
Operator::FloorDiv => Self::FloorDiv, | ||
Operator::LShift => Self::LShift, | ||
Operator::MatMult => Self::MatMult, | ||
Operator::Mod => Self::Mod, | ||
Operator::Mult => Self::Mult, | ||
Operator::Pow => Self::Pow, | ||
Operator::RShift => Self::RShift, | ||
Operator::Sub => Self::Sub, | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Display for AugmentedOperator { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
Self::Add => f.write_str("+="), | ||
Self::BitAnd => f.write_str("&="), | ||
Self::BitOr => f.write_str("|="), | ||
Self::BitXor => f.write_str("^="), | ||
Self::Div => f.write_str("/="), | ||
Self::FloorDiv => f.write_str("//="), | ||
Self::LShift => f.write_str("<<="), | ||
Self::MatMult => f.write_str("@="), | ||
Self::Mod => f.write_str("%="), | ||
Self::Mult => f.write_str("*="), | ||
Self::Pow => f.write_str("**="), | ||
Self::RShift => f.write_str(">>="), | ||
Self::Sub => f.write_str("-="), | ||
} | ||
} | ||
} |
Oops, something went wrong.