Skip to content

Commit 5e24828

Browse files
authored
[flake8_comprehensions] add sum/min/max to unnecessary comprehension check (C419) (#10759)
Fixes #3259 ## Summary Renames `UnnecessaryComprehensionAnyAll` to `UnnecessaryComprehensionInCall` and extends the check to `sum`, `min`, and `max`, in addition to `any` and `all`. ## Test Plan Updated snapshot test. Built docs locally and verified the docs for this rule still render correctly.
1 parent e0a8fb6 commit 5e24828

File tree

12 files changed

+178
-72
lines changed

12 files changed

+178
-72
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
all(x.id for x in bar)
1414
any(x.id for x in bar)
1515
all((x.id for x in bar))
16+
# we don't lint on these in stable yet
17+
sum([x.val for x in bar])
18+
min([x.val for x in bar])
19+
max([x.val for x in bar])
1620

1721

1822
async def f() -> bool:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sum([x.val for x in bar])
2+
min([x.val for x in bar])
3+
max([x.val for x in bar])
4+
5+
# Ok
6+
sum(x.val for x in bar)
7+
min(x.val for x in bar)
8+
max(x.val for x in bar)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# no lint if shadowed
2+
def all(x): pass
3+
all([x.id for x in bar])

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
705705
args,
706706
);
707707
}
708-
if checker.enabled(Rule::UnnecessaryComprehensionAnyAll) {
709-
flake8_comprehensions::rules::unnecessary_comprehension_any_all(
708+
if checker.enabled(Rule::UnnecessaryComprehensionInCall) {
709+
flake8_comprehensions::rules::unnecessary_comprehension_in_call(
710710
checker, expr, func, args, keywords,
711711
);
712712
}

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
398398
(Flake8Comprehensions, "16") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehension),
399399
(Flake8Comprehensions, "17") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryMap),
400400
(Flake8Comprehensions, "18") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall),
401-
(Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionAnyAll),
401+
(Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall),
402402

403403
// flake8-debugger
404404
(Flake8Debugger, "0") => (RuleGroup::Stable, rules::flake8_debugger::rules::Debugger),

crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ pub(crate) fn fix_unnecessary_map(
793793
}
794794

795795
/// (C419) Convert `[i for i in a]` into `i for i in a`
796-
pub(crate) fn fix_unnecessary_comprehension_any_all(
796+
pub(crate) fn fix_unnecessary_comprehension_in_call(
797797
expr: &Expr,
798798
locator: &Locator,
799799
stylist: &Stylist,

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ mod tests {
1212

1313
use crate::assert_messages;
1414
use crate::registry::Rule;
15+
use crate::settings::types::PreviewMode;
1516
use crate::settings::LinterSettings;
1617
use crate::test::test_path;
1718

1819
#[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))]
1920
#[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))]
2021
#[test_case(Rule::UnnecessaryComprehension, Path::new("C416.py"))]
21-
#[test_case(Rule::UnnecessaryComprehensionAnyAll, Path::new("C419.py"))]
22+
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419.py"))]
23+
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_2.py"))]
2224
#[test_case(Rule::UnnecessaryDoubleCastOrProcess, Path::new("C414.py"))]
2325
#[test_case(Rule::UnnecessaryGeneratorDict, Path::new("C402.py"))]
2426
#[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"))]
@@ -43,6 +45,24 @@ mod tests {
4345
Ok(())
4446
}
4547

48+
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_1.py"))]
49+
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
50+
let snapshot = format!(
51+
"preview__{}_{}",
52+
rule_code.noqa_code(),
53+
path.to_string_lossy()
54+
);
55+
let diagnostics = test_path(
56+
Path::new("flake8_comprehensions").join(path).as_path(),
57+
&LinterSettings {
58+
preview: PreviewMode::Enabled,
59+
..LinterSettings::for_rule(rule_code)
60+
},
61+
)?;
62+
assert_messages!(snapshot, diagnostics);
63+
Ok(())
64+
}
65+
4666
#[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))]
4767
fn allow_dict_calls_with_keyword_arguments(rule_code: Rule, path: &Path) -> Result<()> {
4868
let snapshot = format!(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pub(crate) use unnecessary_call_around_sorted::*;
22
pub(crate) use unnecessary_collection_call::*;
33
pub(crate) use unnecessary_comprehension::*;
4-
pub(crate) use unnecessary_comprehension_any_all::*;
4+
pub(crate) use unnecessary_comprehension_in_call::*;
55
pub(crate) use unnecessary_double_cast_or_process::*;
66
pub(crate) use unnecessary_generator_dict::*;
77
pub(crate) use unnecessary_generator_list::*;
@@ -21,7 +21,7 @@ mod helpers;
2121
mod unnecessary_call_around_sorted;
2222
mod unnecessary_collection_call;
2323
mod unnecessary_comprehension;
24-
mod unnecessary_comprehension_any_all;
24+
mod unnecessary_comprehension_in_call;
2525
mod unnecessary_double_cast_or_process;
2626
mod unnecessary_generator_dict;
2727
mod unnecessary_generator_list;

crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs renamed to crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ use crate::checkers::ast::Checker;
1111
use crate::rules::flake8_comprehensions::fixes;
1212

1313
/// ## What it does
14-
/// Checks for unnecessary list comprehensions passed to `any` and `all`.
14+
/// Checks for unnecessary list comprehensions passed to builtin functions that take an iterable.
1515
///
1616
/// ## Why is this bad?
17-
/// `any` and `all` take any iterators, including generators. Converting a generator to a list
18-
/// by way of a list comprehension is unnecessary and requires iterating all values, even if `any`
19-
/// or `all` could short-circuit early.
17+
/// Many builtin functions (this rule currently covers `any`, `all`, `min`, `max`, and `sum`) take
18+
/// any iterable, including a generator. Constructing a temporary list via list comprehension is
19+
/// unnecessary and wastes memory for large iterables.
2020
///
21-
/// For example, compare the performance of `all` with a list comprehension against that
22-
/// of a generator in a case where an early short-circuit is possible (almost 40x faster):
21+
/// `any` and `all` can also short-circuit iteration, saving a lot of time. The unnecessary
22+
/// comprehension forces a full iteration of the input iterable, giving up the benefits of
23+
/// short-circuiting. For example, compare the performance of `all` with a list comprehension
24+
/// against that of a generator in a case where an early short-circuit is possible (almost 40x
25+
/// faster):
2326
///
2427
/// ```console
2528
/// In [1]: %timeit all([i for i in range(1000)])
@@ -29,22 +32,30 @@ use crate::rules::flake8_comprehensions::fixes;
2932
/// 212 ns ± 0.892 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
3033
/// ```
3134
///
32-
/// This performance difference is due to short-circuiting; if the entire iterable has to be
33-
/// traversed, the comprehension version may even be a bit faster (list allocation overhead is not
34-
/// necessarily greater than generator overhead).
35+
/// This performance improvement is due to short-circuiting. If the entire iterable has to be
36+
/// traversed, the comprehension version may even be a bit faster: list allocation overhead is not
37+
/// necessarily greater than generator overhead.
3538
///
36-
/// The generator version is more memory-efficient.
39+
/// Applying this rule simplifies the code and will usually save memory, but in the absence of
40+
/// short-circuiting it may not improve performance. (It may even slightly regress performance,
41+
/// though the difference will usually be small.)
3742
///
3843
/// ## Examples
3944
/// ```python
4045
/// any([x.id for x in bar])
4146
/// all([x.id for x in bar])
47+
/// sum([x.val for x in bar])
48+
/// min([x.val for x in bar])
49+
/// max([x.val for x in bar])
4250
/// ```
4351
///
4452
/// Use instead:
4553
/// ```python
4654
/// any(x.id for x in bar)
4755
/// all(x.id for x in bar)
56+
/// sum(x.val for x in bar)
57+
/// min(x.val for x in bar)
58+
/// max(x.val for x in bar)
4859
/// ```
4960
///
5061
/// ## Fix safety
@@ -53,9 +64,9 @@ use crate::rules::flake8_comprehensions::fixes;
5364
/// rewriting some comprehensions.
5465
///
5566
#[violation]
56-
pub struct UnnecessaryComprehensionAnyAll;
67+
pub struct UnnecessaryComprehensionInCall;
5768

58-
impl Violation for UnnecessaryComprehensionAnyAll {
69+
impl Violation for UnnecessaryComprehensionInCall {
5970
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
6071

6172
#[derive_message_formats]
@@ -69,7 +80,7 @@ impl Violation for UnnecessaryComprehensionAnyAll {
6980
}
7081

7182
/// C419
72-
pub(crate) fn unnecessary_comprehension_any_all(
83+
pub(crate) fn unnecessary_comprehension_in_call(
7384
checker: &mut Checker,
7485
expr: &Expr,
7586
func: &Expr,
@@ -79,10 +90,13 @@ pub(crate) fn unnecessary_comprehension_any_all(
7990
if !keywords.is_empty() {
8091
return;
8192
}
93+
8294
let Expr::Name(ast::ExprName { id, .. }) = func else {
8395
return;
8496
};
85-
if !matches!(id.as_str(), "all" | "any") {
97+
if !(matches!(id.as_str(), "any" | "all")
98+
|| (checker.settings.preview.is_enabled() && matches!(id.as_str(), "sum" | "min" | "max")))
99+
{
86100
return;
87101
}
88102
let [arg] = args else {
@@ -100,9 +114,9 @@ pub(crate) fn unnecessary_comprehension_any_all(
100114
return;
101115
}
102116

103-
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
117+
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionInCall, arg.range());
104118
diagnostic.try_set_fix(|| {
105-
fixes::fix_unnecessary_comprehension_any_all(expr, checker.locator(), checker.stylist())
119+
fixes::fix_unnecessary_comprehension_in_call(expr, checker.locator(), checker.stylist())
106120
});
107121
checker.diagnostics.push(diagnostic);
108122
}

crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snap

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -98,67 +98,65 @@ C419.py:9:5: C419 [*] Unnecessary list comprehension
9898
11 11 | # OK
9999
12 12 | all(x.id for x in bar)
100100

101-
C419.py:24:5: C419 [*] Unnecessary list comprehension
101+
C419.py:28:5: C419 [*] Unnecessary list comprehension
102102
|
103-
22 | # Special comment handling
104-
23 | any(
105-
24 | [ # lbracket comment
103+
26 | # Special comment handling
104+
27 | any(
105+
28 | [ # lbracket comment
106106
| _____^
107-
25 | | # second line comment
108-
26 | | i.bit_count()
109-
27 | | # random middle comment
110-
28 | | for i in range(5) # rbracket comment
111-
29 | | ] # rpar comment
107+
29 | | # second line comment
108+
30 | | i.bit_count()
109+
31 | | # random middle comment
110+
32 | | for i in range(5) # rbracket comment
111+
33 | | ] # rpar comment
112112
| |_____^ C419
113-
30 | # trailing comment
114-
31 | )
113+
34 | # trailing comment
114+
35 | )
115115
|
116116
= help: Remove unnecessary list comprehension
117117

118118
Unsafe fix
119-
21 21 |
120-
22 22 | # Special comment handling
121-
23 23 | any(
122-
24 |- [ # lbracket comment
123-
25 |- # second line comment
124-
26 |- i.bit_count()
125-
24 |+ # lbracket comment
126-
25 |+ # second line comment
127-
26 |+ i.bit_count()
128-
27 27 | # random middle comment
129-
28 |- for i in range(5) # rbracket comment
130-
29 |- ] # rpar comment
131-
28 |+ for i in range(5) # rbracket comment # rpar comment
132-
30 29 | # trailing comment
133-
31 30 | )
134-
32 31 |
119+
25 25 |
120+
26 26 | # Special comment handling
121+
27 27 | any(
122+
28 |- [ # lbracket comment
123+
29 |- # second line comment
124+
30 |- i.bit_count()
125+
28 |+ # lbracket comment
126+
29 |+ # second line comment
127+
30 |+ i.bit_count()
128+
31 31 | # random middle comment
129+
32 |- for i in range(5) # rbracket comment
130+
33 |- ] # rpar comment
131+
32 |+ for i in range(5) # rbracket comment # rpar comment
132+
34 33 | # trailing comment
133+
35 34 | )
134+
36 35 |
135135

136-
C419.py:35:5: C419 [*] Unnecessary list comprehension
136+
C419.py:39:5: C419 [*] Unnecessary list comprehension
137137
|
138-
33 | # Weird case where the function call, opening bracket, and comment are all
139-
34 | # on the same line.
140-
35 | any([ # lbracket comment
138+
37 | # Weird case where the function call, opening bracket, and comment are all
139+
38 | # on the same line.
140+
39 | any([ # lbracket comment
141141
| _____^
142-
36 | | # second line comment
143-
37 | | i.bit_count() for i in range(5) # rbracket comment
144-
38 | | ] # rpar comment
142+
40 | | # second line comment
143+
41 | | i.bit_count() for i in range(5) # rbracket comment
144+
42 | | ] # rpar comment
145145
| |_____^ C419
146-
39 | )
146+
43 | )
147147
|
148148
= help: Remove unnecessary list comprehension
149149

150150
ℹ Unsafe fix
151-
32 32 |
152-
33 33 | # Weird case where the function call, opening bracket, and comment are all
153-
34 34 | # on the same line.
154-
35 |-any([ # lbracket comment
155-
36 |- # second line comment
156-
37 |- i.bit_count() for i in range(5) # rbracket comment
157-
38 |- ] # rpar comment
158-
35 |+any(
159-
36 |+# lbracket comment
160-
37 |+# second line comment
161-
38 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment
162-
39 39 | )
163-
164-
151+
36 36 |
152+
37 37 | # Weird case where the function call, opening bracket, and comment are all
153+
38 38 | # on the same line.
154+
39 |-any([ # lbracket comment
155+
40 |- # second line comment
156+
41 |- i.bit_count() for i in range(5) # rbracket comment
157+
42 |- ] # rpar comment
158+
39 |+any(
159+
40 |+# lbracket comment
160+
41 |+# second line comment
161+
42 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment
162+
43 43 | )

0 commit comments

Comments
 (0)