Skip to content

Commit 01fe268

Browse files
authored
[refurb] Implement list_assign_reversed lint (FURB187) (#10212)
## Summary Implement [use_reverse (FURB187)](https://github.com/dosisod/refurb/blob/master/refurb/checks/readability/use_reverse.py) lint. Tests were copied from original https://github.com/dosisod/refurb/blob/master/test/data/err_187.py. ## Test Plan cargo test
1 parent c62184d commit 01fe268

File tree

8 files changed

+319
-0
lines changed

8 files changed

+319
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Errors
2+
3+
4+
def a():
5+
l = []
6+
l = reversed(l)
7+
8+
9+
def b():
10+
l = []
11+
l = list(reversed(l))
12+
13+
14+
def c():
15+
l = []
16+
l = l[::-1]
17+
18+
19+
# False negative
20+
def c2():
21+
class Wrapper:
22+
l: list[int]
23+
24+
w = Wrapper()
25+
w.l = list(reversed(w.l))
26+
w.l = w.l[::-1]
27+
w.l = reversed(w.l)
28+
29+
30+
# OK
31+
32+
33+
def d():
34+
l = []
35+
_ = reversed(l)
36+
37+
38+
def e():
39+
l = []
40+
l = l[::-2]
41+
l = l[1:]
42+
l = l[1::-1]
43+
l = l[:1:-1]
44+
45+
46+
def f():
47+
d = {}
48+
49+
# Don't warn: `d` is a dictionary, which doesn't have a `reverse` method.
50+
d = reversed(d)
51+
52+
53+
def g():
54+
l = "abc"[::-1]
55+
56+
57+
def h():
58+
l = reversed([1, 2, 3])
59+
60+
61+
def i():
62+
l = list(reversed([1, 2, 3]))

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

+3
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
15031503
}
15041504
}
15051505
}
1506+
if checker.enabled(Rule::ListAssignReversed) {
1507+
refurb::rules::list_assign_reversed(checker, assign);
1508+
}
15061509
}
15071510
Stmt::AnnAssign(
15081511
assign_stmt @ ast::StmtAnnAssign {

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10561056
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
10571057
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
10581058
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
1059+
(Refurb, "187") => (RuleGroup::Preview, rules::refurb::rules::ListAssignReversed),
10591060

10601061
// flake8-logging
10611062
(Flake8Logging, "001") => (RuleGroup::Stable, rules::flake8_logging::rules::DirectLoggerInstantiation),

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod tests {
3535
#[test_case(Rule::RedundantLogBase, Path::new("FURB163.py"))]
3636
#[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))]
3737
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
38+
#[test_case(Rule::ListAssignReversed, Path::new("FURB187.py"))]
3839
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
3940
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
4041
let diagnostics = test_path(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::{
4+
Expr, ExprCall, ExprName, ExprSlice, ExprSubscript, ExprUnaryOp, Int, StmtAssign, UnaryOp,
5+
};
6+
use ruff_python_semantic::analyze::typing;
7+
use ruff_python_semantic::SemanticModel;
8+
use ruff_text_size::Ranged;
9+
10+
use crate::checkers::ast::Checker;
11+
12+
/// ## What it does
13+
/// Checks for list reversals that can be performed in-place in lieu of
14+
/// creating a new list.
15+
///
16+
/// ## Why is this bad?
17+
/// When reversing a list, it's more efficient to use the in-place method
18+
/// `.reverse()` instead of creating a new list, if the original list is
19+
/// no longer needed.
20+
///
21+
/// ## Example
22+
/// ```python
23+
/// l = [1, 2, 3]
24+
/// l = reversed(l)
25+
///
26+
/// l = [1, 2, 3]
27+
/// l = list(reversed(l))
28+
///
29+
/// l = [1, 2, 3]
30+
/// l = l[::-1]
31+
/// ```
32+
///
33+
/// Use instead:
34+
/// ```python
35+
/// l = [1, 2, 3]
36+
/// l.reverse()
37+
/// ```
38+
///
39+
/// ## References
40+
/// - [Python documentation: More on Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists)
41+
#[violation]
42+
pub struct ListAssignReversed {
43+
name: String,
44+
}
45+
46+
impl AlwaysFixableViolation for ListAssignReversed {
47+
#[derive_message_formats]
48+
fn message(&self) -> String {
49+
let ListAssignReversed { name } = self;
50+
format!("Use of assignment of `reversed` on list `{name}`")
51+
}
52+
53+
fn fix_title(&self) -> String {
54+
let ListAssignReversed { name } = self;
55+
format!("Replace with `{name}.reverse()`")
56+
}
57+
}
58+
59+
/// FURB187
60+
pub(crate) fn list_assign_reversed(checker: &mut Checker, assign: &StmtAssign) {
61+
let [Expr::Name(target_expr)] = assign.targets.as_slice() else {
62+
return;
63+
};
64+
65+
let Some(reversed_expr) = extract_reversed(assign.value.as_ref(), checker.semantic()) else {
66+
return;
67+
};
68+
69+
if reversed_expr.id != target_expr.id {
70+
return;
71+
}
72+
73+
let Some(binding) = checker
74+
.semantic()
75+
.only_binding(reversed_expr)
76+
.map(|id| checker.semantic().binding(id))
77+
else {
78+
return;
79+
};
80+
if !typing::is_list(binding, checker.semantic()) {
81+
return;
82+
}
83+
84+
checker.diagnostics.push(
85+
Diagnostic::new(
86+
ListAssignReversed {
87+
name: target_expr.id.to_string(),
88+
},
89+
assign.range(),
90+
)
91+
.with_fix(Fix::safe_edit(Edit::range_replacement(
92+
format!("{}.reverse()", target_expr.id),
93+
assign.range(),
94+
))),
95+
);
96+
}
97+
98+
/// Recursively removes any `list` wrappers from the expression.
99+
///
100+
/// For example, given `list(list(list([1, 2, 3])))`, this function
101+
/// would return the inner `[1, 2, 3]` expression.
102+
fn peel_lists(expr: &Expr) -> &Expr {
103+
let Some(ExprCall {
104+
func, arguments, ..
105+
}) = expr.as_call_expr()
106+
else {
107+
return expr;
108+
};
109+
110+
if !arguments.keywords.is_empty() {
111+
return expr;
112+
}
113+
114+
if !func.as_name_expr().is_some_and(|name| name.id == "list") {
115+
return expr;
116+
}
117+
118+
let [arg] = arguments.args.as_ref() else {
119+
return expr;
120+
};
121+
122+
peel_lists(arg)
123+
}
124+
125+
/// Given a call to `reversed`, returns the inner argument.
126+
///
127+
/// For example, given `reversed(l)`, this function would return `l`.
128+
fn extract_name_from_reversed<'a>(
129+
expr: &'a Expr,
130+
semantic: &SemanticModel,
131+
) -> Option<&'a ExprName> {
132+
let ExprCall {
133+
func, arguments, ..
134+
} = expr.as_call_expr()?;
135+
136+
if !arguments.keywords.is_empty() {
137+
return None;
138+
}
139+
140+
let [arg] = arguments.args.as_ref() else {
141+
return None;
142+
};
143+
144+
let arg = func
145+
.as_name_expr()
146+
.is_some_and(|name| name.id == "reversed")
147+
.then(|| arg.as_name_expr())
148+
.flatten()?;
149+
150+
if !semantic.is_builtin("reversed") {
151+
return None;
152+
}
153+
154+
Some(arg)
155+
}
156+
157+
/// Given a slice expression, returns the inner argument if it's a reversed slice.
158+
///
159+
/// For example, given `l[::-1]`, this function would return `l`.
160+
fn extract_name_from_sliced_reversed(expr: &Expr) -> Option<&ExprName> {
161+
let ExprSubscript { value, slice, .. } = expr.as_subscript_expr()?;
162+
let ExprSlice {
163+
lower, upper, step, ..
164+
} = slice.as_slice_expr()?;
165+
if lower.is_some() || upper.is_some() {
166+
return None;
167+
}
168+
let Some(ExprUnaryOp {
169+
op: UnaryOp::USub,
170+
operand,
171+
..
172+
}) = step.as_ref().and_then(|expr| expr.as_unary_op_expr())
173+
else {
174+
return None;
175+
};
176+
if !operand
177+
.as_number_literal_expr()
178+
.and_then(|num| num.value.as_int())
179+
.and_then(Int::as_u8)
180+
.is_some_and(|value| value == 1)
181+
{
182+
return None;
183+
};
184+
value.as_name_expr()
185+
}
186+
187+
fn extract_reversed<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a ExprName> {
188+
let expr = peel_lists(expr);
189+
extract_name_from_reversed(expr, semantic).or_else(|| extract_name_from_sliced_reversed(expr))
190+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub(crate) use hashlib_digest_hex::*;
55
pub(crate) use if_expr_min_max::*;
66
pub(crate) use implicit_cwd::*;
77
pub(crate) use isinstance_type_none::*;
8+
pub(crate) use list_assign_reversed::*;
89
pub(crate) use math_constant::*;
910
pub(crate) use metaclass_abcmeta::*;
1011
pub(crate) use print_empty_string::*;
@@ -27,6 +28,7 @@ mod hashlib_digest_hex;
2728
mod if_expr_min_max;
2829
mod implicit_cwd;
2930
mod isinstance_type_none;
31+
mod list_assign_reversed;
3032
mod math_constant;
3133
mod metaclass_abcmeta;
3234
mod print_empty_string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
source: crates/ruff_linter/src/rules/refurb/mod.rs
3+
---
4+
FURB187.py:6:5: FURB187 [*] Use of assignment of `reversed` on list `l`
5+
|
6+
4 | def a():
7+
5 | l = []
8+
6 | l = reversed(l)
9+
| ^^^^^^^^^^^^^^^ FURB187
10+
|
11+
= help: Replace with `l.reverse()`
12+
13+
Safe fix
14+
3 3 |
15+
4 4 | def a():
16+
5 5 | l = []
17+
6 |- l = reversed(l)
18+
6 |+ l.reverse()
19+
7 7 |
20+
8 8 |
21+
9 9 | def b():
22+
23+
FURB187.py:11:5: FURB187 [*] Use of assignment of `reversed` on list `l`
24+
|
25+
9 | def b():
26+
10 | l = []
27+
11 | l = list(reversed(l))
28+
| ^^^^^^^^^^^^^^^^^^^^^ FURB187
29+
|
30+
= help: Replace with `l.reverse()`
31+
32+
Safe fix
33+
8 8 |
34+
9 9 | def b():
35+
10 10 | l = []
36+
11 |- l = list(reversed(l))
37+
11 |+ l.reverse()
38+
12 12 |
39+
13 13 |
40+
14 14 | def c():
41+
42+
FURB187.py:16:5: FURB187 [*] Use of assignment of `reversed` on list `l`
43+
|
44+
14 | def c():
45+
15 | l = []
46+
16 | l = l[::-1]
47+
| ^^^^^^^^^^^ FURB187
48+
|
49+
= help: Replace with `l.reverse()`
50+
51+
Safe fix
52+
13 13 |
53+
14 14 | def c():
54+
15 15 | l = []
55+
16 |- l = l[::-1]
56+
16 |+ l.reverse()
57+
17 17 |
58+
18 18 |
59+
19 19 | # False negative

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)