Skip to content

Commit cf384d4

Browse files
committed
Tweaks
1 parent 1b20392 commit cf384d4

File tree

3 files changed

+114
-81
lines changed

3 files changed

+114
-81
lines changed

crates/ruff_linter/resources/test/fixtures/refurb/FURB187.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# these should match
1+
# Errors
2+
23

3-
# using functions to ensure `l` doesn't change type
44
def a():
55
l = []
66
l = reversed(l)
@@ -18,7 +18,7 @@ def c():
1818

1919
# False negative
2020
def c2():
21-
class Wrapper():
21+
class Wrapper:
2222
l: list[int]
2323

2424
w = Wrapper()
@@ -27,7 +27,8 @@ class Wrapper():
2727
w.l = reversed(w.l)
2828

2929

30-
# these should not
30+
# OK
31+
3132

3233
def d():
3334
l = []
@@ -45,7 +46,7 @@ def e():
4546
def f():
4647
d = {}
4748

48-
# dont warn since d is a dict and does not have a .reverse() method
49+
# Don't warn: `d` is a dictionary, which doesn't have a `reverse` method.
4950
d = reversed(d)
5051

5152

Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
1-
use crate::checkers::ast::Checker;
21
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
32
use ruff_macros::{derive_message_formats, violation};
43
use ruff_python_ast::{
54
Expr, ExprCall, ExprName, ExprSlice, ExprSubscript, ExprUnaryOp, Int, StmtAssign, UnaryOp,
65
};
76
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;
811

912
/// ## What it does
10-
/// Checks for uses of assignment of "reversed" expression on the list to the same binging.
13+
/// Checks for list reversals that can be performed in-place in lieu of
14+
/// creating a new list.
1115
///
1216
/// ## Why is this bad?
13-
///
14-
/// Use of in-place method `.reverse()` is faster and allows to avoid copying the name of variable.
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.
1520
///
1621
/// ## Example
1722
/// ```python
1823
/// l = [1, 2, 3]
1924
/// l = reversed(l)
25+
///
26+
/// l = [1, 2, 3]
2027
/// l = list(reversed(l))
28+
///
29+
/// l = [1, 2, 3]
2130
/// l = l[::-1]
2231
/// ```
2332
///
2433
/// Use instead:
2534
/// ```python
2635
/// l = [1, 2, 3]
2736
/// l.reverse()
28-
/// l.reverse()
29-
/// l.reverse()
3037
/// ```
3138
///
3239
/// ## References
@@ -39,52 +46,116 @@ pub struct ListAssignReversed {
3946
impl AlwaysFixableViolation for ListAssignReversed {
4047
#[derive_message_formats]
4148
fn message(&self) -> String {
42-
format!("Use of assignment of `reversed` on list `{}`", self.name)
49+
let ListAssignReversed { name } = self;
50+
format!("Use of assignment of `reversed` on list `{name}`")
4351
}
4452

4553
fn fix_title(&self) -> String {
46-
format!("Use `{}.reverse()` instead", self.name)
54+
let ListAssignReversed { name } = self;
55+
format!("Replace with `{name}.reverse()`")
4756
}
4857
}
4958

50-
fn extract_name_from_reversed(expr: &Expr) -> Option<&ExprName> {
51-
let ExprCall {
52-
func, arguments, ..
53-
} = expr.as_call_expr()?;
54-
if !arguments.keywords.is_empty() {
55-
return None;
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;
5671
}
57-
let [arg] = arguments.args.as_ref() else {
58-
return None;
72+
73+
let Some(binding) = checker
74+
.semantic()
75+
.only_binding(reversed_expr)
76+
.map(|id| checker.semantic().binding(id))
77+
else {
78+
return;
5979
};
80+
if !typing::is_list(binding, checker.semantic()) {
81+
return;
82+
}
6083

61-
func.as_name_expr()
62-
.is_some_and(|name_expr| name_expr.id == "reversed")
63-
.then(|| arg.as_name_expr())
64-
.flatten()
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+
);
6596
}
6697

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.
67102
fn peel_lists(expr: &Expr) -> &Expr {
68103
let Some(ExprCall {
69104
func, arguments, ..
70105
}) = expr.as_call_expr()
71106
else {
72107
return expr;
73108
};
74-
if !arguments.keywords.is_empty()
75-
|| func
76-
.as_name_expr()
77-
.map_or(true, |expr_name| expr_name.id != "list")
78-
{
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 {
79119
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+
if !arguments.keywords.is_empty() {
136+
return None;
80137
}
81-
if let [arg] = arguments.args.as_ref() {
82-
peel_lists(arg)
83-
} else {
84-
expr
138+
let [arg] = arguments.args.as_ref() else {
139+
return None;
140+
};
141+
let Some(arg) = func
142+
.as_name_expr()
143+
.is_some_and(|name| name.id == "reversed")
144+
.then(|| arg.as_name_expr())
145+
.flatten()
146+
else {
147+
return None;
148+
};
149+
if !semantic.is_builtin("reversed") {
150+
return None;
85151
}
152+
153+
Some(arg)
86154
}
87155

156+
/// Given a slice expression, returns the inner argument if it's a reversed slice.
157+
///
158+
/// For example, given `l[::-1]`, this function would return `l`.
88159
fn extract_name_from_sliced_reversed(expr: &Expr) -> Option<&ExprName> {
89160
let ExprSubscript { value, slice, .. } = expr.as_subscript_expr()?;
90161
let ExprSlice {
@@ -101,57 +172,18 @@ fn extract_name_from_sliced_reversed(expr: &Expr) -> Option<&ExprName> {
101172
else {
102173
return None;
103174
};
104-
if operand
175+
if !operand
105176
.as_number_literal_expr()
106177
.and_then(|num| num.value.as_int())
107178
.and_then(Int::as_u8)
108-
!= Some(1)
179+
.is_some_and(|value| value == 1)
109180
{
110181
return None;
111182
};
112183
value.as_name_expr()
113184
}
114185

115-
fn extract_name_from_general_reversed(expr: &Expr) -> Option<&ExprName> {
186+
fn extract_reversed<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a ExprName> {
116187
let expr = peel_lists(expr);
117-
extract_name_from_reversed(expr).or_else(|| extract_name_from_sliced_reversed(expr))
118-
}
119-
120-
// FURB187
121-
pub(crate) fn list_assign_reversed(checker: &mut Checker, assign: &StmtAssign) {
122-
let [Expr::Name(target_name_expr)] = assign.targets.as_slice() else {
123-
return;
124-
};
125-
126-
let Some(arg_name_expr) = extract_name_from_general_reversed(assign.value.as_ref()) else {
127-
return;
128-
};
129-
130-
if arg_name_expr.id != target_name_expr.id {
131-
return;
132-
}
133-
134-
let Some(binding) = checker
135-
.semantic()
136-
.only_binding(arg_name_expr)
137-
.map(|id| checker.semantic().binding(id))
138-
else {
139-
return;
140-
};
141-
if !typing::is_list(binding, checker.semantic()) {
142-
return;
143-
}
144-
145-
checker.diagnostics.push(
146-
Diagnostic::new(
147-
ListAssignReversed {
148-
name: target_name_expr.id.to_string(),
149-
},
150-
assign.range,
151-
)
152-
.with_fix(Fix::safe_edit(Edit::range_replacement(
153-
format!("{}.reverse()", target_name_expr.id),
154-
assign.range,
155-
))),
156-
);
188+
extract_name_from_reversed(expr, semantic).or_else(|| extract_name_from_sliced_reversed(expr))
157189
}

crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ FURB187.py:6:5: FURB187 [*] Use of assignment of `reversed` on list `l`
88
6 | l = reversed(l)
99
| ^^^^^^^^^^^^^^^ FURB187
1010
|
11-
= help: Use `l.reverse()` instead
11+
= help: Replace with `l.reverse()`
1212

1313
Safe fix
14-
3 3 | # using functions to ensure `l` doesn't change type
14+
3 3 |
1515
4 4 | def a():
1616
5 5 | l = []
1717
6 |- l = reversed(l)
@@ -27,7 +27,7 @@ FURB187.py:11:5: FURB187 [*] Use of assignment of `reversed` on list `l`
2727
11 | l = list(reversed(l))
2828
| ^^^^^^^^^^^^^^^^^^^^^ FURB187
2929
|
30-
= help: Use `l.reverse()` instead
30+
= help: Replace with `l.reverse()`
3131

3232
Safe fix
3333
8 8 |
@@ -46,7 +46,7 @@ FURB187.py:16:5: FURB187 [*] Use of assignment of `reversed` on list `l`
4646
16 | l = l[::-1]
4747
| ^^^^^^^^^^^ FURB187
4848
|
49-
= help: Use `l.reverse()` instead
49+
= help: Replace with `l.reverse()`
5050

5151
Safe fix
5252
13 13 |

0 commit comments

Comments
 (0)