Skip to content

Commit 0837cdd

Browse files
authored
[RUF] Add rule to detect empty literal in deque call (RUF025) (#15104)
1 parent 0dbfa8d commit 0837cdd

File tree

9 files changed

+318
-5
lines changed

9 files changed

+318
-5
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from collections import deque
2+
import collections
3+
4+
5+
def f():
6+
queue = collections.deque([]) # RUF025
7+
8+
9+
def f():
10+
queue = collections.deque([], maxlen=10) # RUF025
11+
12+
13+
def f():
14+
queue = deque([]) # RUF025
15+
16+
17+
def f():
18+
queue = deque(()) # RUF025
19+
20+
21+
def f():
22+
queue = deque({}) # RUF025
23+
24+
25+
def f():
26+
queue = deque(set()) # RUF025
27+
28+
29+
def f():
30+
queue = collections.deque([], maxlen=10) # RUF025
31+
32+
33+
def f():
34+
class FakeDeque:
35+
pass
36+
37+
deque = FakeDeque
38+
queue = deque([]) # Ok
39+
40+
41+
def f():
42+
class FakeSet:
43+
pass
44+
45+
set = FakeSet
46+
queue = deque(set()) # Ok
47+
48+
49+
def f():
50+
queue = deque([1, 2]) # Ok
51+
52+
53+
def f():
54+
queue = deque([1, 2], maxlen=10) # Ok
55+
56+
57+
def f():
58+
queue = deque() # Ok

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
11171117
if checker.enabled(Rule::UnnecessaryRound) {
11181118
ruff::rules::unnecessary_round(checker, call);
11191119
}
1120+
if checker.enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) {
1121+
ruff::rules::unnecessary_literal_within_deque_call(checker, call);
1122+
}
11201123
}
11211124
Expr::Dict(dict) => {
11221125
if checker.any_enabled(&[

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
971971
(Ruff, "022") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderAll),
972972
(Ruff, "023") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderSlots),
973973
(Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue),
974+
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryEmptyIterableWithinDequeCall),
974975
(Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg),
975976
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
976977
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod tests {
5151
#[test_case(Rule::UnsortedDunderAll, Path::new("RUF022.py"))]
5252
#[test_case(Rule::UnsortedDunderSlots, Path::new("RUF023.py"))]
5353
#[test_case(Rule::MutableFromkeysValue, Path::new("RUF024.py"))]
54+
#[test_case(Rule::UnnecessaryEmptyIterableWithinDequeCall, Path::new("RUF025.py"))]
5455
#[test_case(Rule::DefaultFactoryKwarg, Path::new("RUF026.py"))]
5556
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_0.py"))]
5657
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_1.py"))]

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub(crate) use test_rules::*;
3636
pub(crate) use unnecessary_cast_to_int::*;
3737
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
3838
pub(crate) use unnecessary_key_check::*;
39+
pub(crate) use unnecessary_literal_within_deque_call::*;
3940
pub(crate) use unnecessary_nested_literal::*;
4041
pub(crate) use unnecessary_regular_expression::*;
4142
pub(crate) use unnecessary_round::*;
@@ -89,6 +90,7 @@ pub(crate) mod test_rules;
8990
mod unnecessary_cast_to_int;
9091
mod unnecessary_iterable_allocation_for_first_element;
9192
mod unnecessary_key_check;
93+
mod unnecessary_literal_within_deque_call;
9294
mod unnecessary_nested_literal;
9395
mod unnecessary_regular_expression;
9496
mod unnecessary_round;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use crate::checkers::ast::Checker;
2+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
3+
use ruff_macros::{derive_message_formats, ViolationMetadata};
4+
use ruff_python_ast::{self as ast, Expr};
5+
use ruff_text_size::Ranged;
6+
7+
/// ## What it does
8+
/// Checks for usages of `collections.deque` that have an empty iterable as the first argument.
9+
///
10+
/// ## Why is this bad?
11+
/// It's unnecessary to use an empty literal as a deque's iterable, since this is already the default behavior.
12+
///
13+
/// ## Examples
14+
///
15+
/// ```python
16+
/// from collections import deque
17+
///
18+
/// queue = deque(set())
19+
/// queue = deque([], 10)
20+
/// ```
21+
///
22+
/// Use instead:
23+
///
24+
/// ```python
25+
/// from collections import deque
26+
///
27+
/// queue = deque()
28+
/// queue = deque(maxlen=10)
29+
/// ```
30+
///
31+
/// ## References
32+
/// - [Python documentation: `collections.deque`](https://docs.python.org/3/library/collections.html#collections.deque)
33+
#[derive(ViolationMetadata)]
34+
pub(crate) struct UnnecessaryEmptyIterableWithinDequeCall {
35+
has_maxlen: bool,
36+
}
37+
38+
impl AlwaysFixableViolation for UnnecessaryEmptyIterableWithinDequeCall {
39+
#[derive_message_formats]
40+
fn message(&self) -> String {
41+
"Unnecessary empty iterable within a deque call".to_string()
42+
}
43+
44+
fn fix_title(&self) -> String {
45+
let title = if self.has_maxlen {
46+
"Replace with `deque(maxlen=...)`"
47+
} else {
48+
"Replace with `deque()`"
49+
};
50+
title.to_string()
51+
}
52+
}
53+
54+
/// RUF025
55+
pub(crate) fn unnecessary_literal_within_deque_call(checker: &mut Checker, deque: &ast::ExprCall) {
56+
let ast::ExprCall {
57+
func, arguments, ..
58+
} = deque;
59+
60+
let Some(qualified) = checker.semantic().resolve_qualified_name(func) else {
61+
return;
62+
};
63+
if !matches!(qualified.segments(), ["collections", "deque"]) || arguments.len() > 2 {
64+
return;
65+
}
66+
67+
let Some(iterable) = arguments.find_argument_value("iterable", 0) else {
68+
return;
69+
};
70+
71+
let maxlen = arguments.find_argument_value("maxlen", 1);
72+
73+
let is_empty_literal = match iterable {
74+
Expr::Dict(dict) => dict.is_empty(),
75+
Expr::List(list) => list.is_empty(),
76+
Expr::Tuple(tuple) => tuple.is_empty(),
77+
Expr::Call(call) => {
78+
checker
79+
.semantic()
80+
.resolve_builtin_symbol(&call.func)
81+
// other lints should handle empty list/dict/tuple calls,
82+
// but this means that the lint still applies before those are fixed
83+
.is_some_and(|name| {
84+
name == "set" || name == "list" || name == "dict" || name == "tuple"
85+
})
86+
&& call.arguments.is_empty()
87+
}
88+
_ => false,
89+
};
90+
if !is_empty_literal {
91+
return;
92+
}
93+
94+
let mut diagnostic = Diagnostic::new(
95+
UnnecessaryEmptyIterableWithinDequeCall {
96+
has_maxlen: maxlen.is_some(),
97+
},
98+
deque.range,
99+
);
100+
101+
diagnostic.set_fix(fix_unnecessary_literal_in_deque(checker, deque, maxlen));
102+
103+
checker.diagnostics.push(diagnostic);
104+
}
105+
106+
fn fix_unnecessary_literal_in_deque(
107+
checker: &Checker,
108+
deque: &ast::ExprCall,
109+
maxlen: Option<&Expr>,
110+
) -> Fix {
111+
let deque_name = checker.locator().slice(deque.func.range());
112+
let deque_str = match maxlen {
113+
Some(maxlen) => {
114+
let len_str = checker.locator().slice(maxlen);
115+
format!("{deque_name}(maxlen={len_str})")
116+
}
117+
None => format!("{deque_name}()"),
118+
};
119+
Fix::safe_edit(Edit::range_replacement(deque_str, deque.range))
120+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
---
2+
source: crates/ruff_linter/src/rules/ruff/mod.rs
3+
snapshot_kind: text
4+
---
5+
RUF025.py:6:13: RUF025 [*] Unnecessary empty iterable within a deque call
6+
|
7+
5 | def f():
8+
6 | queue = collections.deque([]) # RUF025
9+
| ^^^^^^^^^^^^^^^^^^^^^ RUF025
10+
|
11+
= help: Replace with `deque()`
12+
13+
Safe fix
14+
3 3 |
15+
4 4 |
16+
5 5 | def f():
17+
6 |- queue = collections.deque([]) # RUF025
18+
6 |+ queue = collections.deque() # RUF025
19+
7 7 |
20+
8 8 |
21+
9 9 | def f():
22+
23+
RUF025.py:10:13: RUF025 [*] Unnecessary empty iterable within a deque call
24+
|
25+
9 | def f():
26+
10 | queue = collections.deque([], maxlen=10) # RUF025
27+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF025
28+
|
29+
= help: Replace with `deque(maxlen=...)`
30+
31+
Safe fix
32+
7 7 |
33+
8 8 |
34+
9 9 | def f():
35+
10 |- queue = collections.deque([], maxlen=10) # RUF025
36+
10 |+ queue = collections.deque(maxlen=10) # RUF025
37+
11 11 |
38+
12 12 |
39+
13 13 | def f():
40+
41+
RUF025.py:14:13: RUF025 [*] Unnecessary empty iterable within a deque call
42+
|
43+
13 | def f():
44+
14 | queue = deque([]) # RUF025
45+
| ^^^^^^^^^ RUF025
46+
|
47+
= help: Replace with `deque()`
48+
49+
Safe fix
50+
11 11 |
51+
12 12 |
52+
13 13 | def f():
53+
14 |- queue = deque([]) # RUF025
54+
14 |+ queue = deque() # RUF025
55+
15 15 |
56+
16 16 |
57+
17 17 | def f():
58+
59+
RUF025.py:18:13: RUF025 [*] Unnecessary empty iterable within a deque call
60+
|
61+
17 | def f():
62+
18 | queue = deque(()) # RUF025
63+
| ^^^^^^^^^ RUF025
64+
|
65+
= help: Replace with `deque()`
66+
67+
Safe fix
68+
15 15 |
69+
16 16 |
70+
17 17 | def f():
71+
18 |- queue = deque(()) # RUF025
72+
18 |+ queue = deque() # RUF025
73+
19 19 |
74+
20 20 |
75+
21 21 | def f():
76+
77+
RUF025.py:22:13: RUF025 [*] Unnecessary empty iterable within a deque call
78+
|
79+
21 | def f():
80+
22 | queue = deque({}) # RUF025
81+
| ^^^^^^^^^ RUF025
82+
|
83+
= help: Replace with `deque()`
84+
85+
Safe fix
86+
19 19 |
87+
20 20 |
88+
21 21 | def f():
89+
22 |- queue = deque({}) # RUF025
90+
22 |+ queue = deque() # RUF025
91+
23 23 |
92+
24 24 |
93+
25 25 | def f():
94+
95+
RUF025.py:26:13: RUF025 [*] Unnecessary empty iterable within a deque call
96+
|
97+
25 | def f():
98+
26 | queue = deque(set()) # RUF025
99+
| ^^^^^^^^^^^^ RUF025
100+
|
101+
= help: Replace with `deque()`
102+
103+
Safe fix
104+
23 23 |
105+
24 24 |
106+
25 25 | def f():
107+
26 |- queue = deque(set()) # RUF025
108+
26 |+ queue = deque() # RUF025
109+
27 27 |
110+
28 28 |
111+
29 29 | def f():
112+
113+
RUF025.py:30:13: RUF025 [*] Unnecessary empty iterable within a deque call
114+
|
115+
29 | def f():
116+
30 | queue = collections.deque([], maxlen=10) # RUF025
117+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF025
118+
|
119+
= help: Replace with `deque(maxlen=...)`
120+
121+
Safe fix
122+
27 27 |
123+
28 28 |
124+
29 29 | def f():
125+
30 |- queue = collections.deque([], maxlen=10) # RUF025
126+
30 |+ queue = collections.deque(maxlen=10) # RUF025
127+
31 31 |
128+
32 32 |
129+
33 33 | def f():

crates/ruff_python_ast/src/nodes.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3774,14 +3774,14 @@ pub struct Arguments {
37743774
}
37753775

37763776
/// An entry in the argument list of a function call.
3777-
#[derive(Clone, Debug, PartialEq)]
3777+
#[derive(Copy, Clone, Debug, PartialEq)]
37783778
pub enum ArgOrKeyword<'a> {
37793779
Arg(&'a Expr),
37803780
Keyword(&'a Keyword),
37813781
}
37823782

37833783
impl<'a> ArgOrKeyword<'a> {
3784-
pub const fn value(&self) -> &'a Expr {
3784+
pub const fn value(self) -> &'a Expr {
37853785
match self {
37863786
ArgOrKeyword::Arg(argument) => argument,
37873787
ArgOrKeyword::Keyword(keyword) => &keyword.value,
@@ -3841,9 +3841,7 @@ impl Arguments {
38413841
/// argument exists. Used to retrieve argument values that can be provided _either_ as keyword or
38423842
/// positional arguments.
38433843
pub fn find_argument_value(&self, name: &str, position: usize) -> Option<&Expr> {
3844-
self.find_keyword(name)
3845-
.map(|keyword| &keyword.value)
3846-
.or_else(|| self.find_positional(position))
3844+
self.find_argument(name, position).map(ArgOrKeyword::value)
38473845
}
38483846

38493847
/// Return the the argument with the given name or at the given position, or `None` if no such

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)