Skip to content

Commit a28776e

Browse files
[flake8-comprehensions] Handled special case for C401 which also matches C416 (#10596)
## Summary <!-- What's the purpose of the change? What does it do, and why? --> Similar to #10419, there was a case where there is a collision of C401 and C416 (as discussed in #10101). Fixed this by implementing short-circuit for the comprehension of the form `{x for x in foo}`. ## Test Plan <!-- How was it tested? --> Extended `C401.py` with the case where `set` is not builtin function, and divided the case where the short-circuit should occur. Removed the last testcase of `print(f"{ {set(a for a in 'abc')} }")` test as this is invalid as a python code, but should I keep this?
1 parent 80b4688 commit a28776e

File tree

3 files changed

+287
-234
lines changed

3 files changed

+287
-234
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1-
x = set(x for x in range(3))
2-
x = set(x for x in range(3))
3-
y = f"{set(a if a < 6 else 0 for a in range(3))}"
4-
_ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
5-
print(f"Hello {set(a for a in range(3))} World")
6-
1+
# Cannot conbime with C416. Should use set comprehension here.
2+
even_nums = set(2 * x for x in range(3))
3+
odd_nums = set(
4+
2 * x + 1 for x in range(3)
5+
)
6+
small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
77

88
def f(x):
99
return x
1010

11-
12-
print(f'Hello {set(a for a in "abc")} World')
13-
print(f"Hello {set(a for a in 'abc')} World")
1411
print(f"Hello {set(f(a) for a in 'abc')} World")
12+
print(f"Hello { set(f(a) for a in 'abc') } World")
13+
14+
15+
# Short-circuit case, combine with C416 and should produce x = set(range(3))
16+
x = set(x for x in range(3))
17+
x = set(
18+
x for x in range(3)
19+
)
20+
print(f"Hello {set(a for a in range(3))} World")
1521
print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
1622
print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
1723

18-
# The fix generated for this diagnostic is incorrect, as we add additional space
19-
# around the set comprehension.
20-
print(f"{ {set(a for a in 'abc')} }")
24+
25+
# Not built-in set.
26+
def set(*args, **kwargs):
27+
return None
28+
29+
set(2 * x for x in range(3))
30+
set(x for x in range(3))
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast as ast;
4+
use ruff_python_ast::comparable::ComparableExpr;
5+
use ruff_python_ast::ExprGenerator;
46
use ruff_text_size::{Ranged, TextSize};
57

68
use crate::checkers::ast::Checker;
@@ -10,37 +12,53 @@ use super::helpers;
1012

1113
/// ## What it does
1214
/// Checks for unnecessary generators that can be rewritten as `set`
13-
/// comprehensions.
15+
/// comprehensions (or with `set` directly).
1416
///
1517
/// ## Why is this bad?
1618
/// It is unnecessary to use `set` around a generator expression, since
1719
/// there are equivalent comprehensions for these types. Using a
1820
/// comprehension is clearer and more idiomatic.
1921
///
22+
/// Further, if the comprehension can be removed entirely, as in the case of
23+
/// `set(x for x in foo)`, it's better to use `set(foo)` directly, since it's
24+
/// even more direct.
25+
///
2026
/// ## Examples
2127
/// ```python
2228
/// set(f(x) for x in foo)
29+
/// set(x for x in foo)
2330
/// ```
2431
///
2532
/// Use instead:
2633
/// ```python
2734
/// {f(x) for x in foo}
35+
/// set(foo)
2836
/// ```
2937
///
3038
/// ## Fix safety
3139
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
3240
/// when rewriting the call. In most cases, though, comments will be preserved.
3341
#[violation]
34-
pub struct UnnecessaryGeneratorSet;
42+
pub struct UnnecessaryGeneratorSet {
43+
short_circuit: bool,
44+
}
3545

3646
impl AlwaysFixableViolation for UnnecessaryGeneratorSet {
3747
#[derive_message_formats]
3848
fn message(&self) -> String {
39-
format!("Unnecessary generator (rewrite as a `set` comprehension)")
49+
if self.short_circuit {
50+
format!("Unnecessary generator (rewrite using `set()`")
51+
} else {
52+
format!("Unnecessary generator (rewrite as a `set` comprehension)")
53+
}
4054
}
4155

4256
fn fix_title(&self) -> String {
43-
"Rewrite as a `set` comprehension".to_string()
57+
if self.short_circuit {
58+
"Rewrite using `set()`".to_string()
59+
} else {
60+
"Rewrite as a `set` comprehension".to_string()
61+
}
4462
}
4563
}
4664

@@ -57,28 +75,59 @@ pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprC
5775
if !checker.semantic().is_builtin("set") {
5876
return;
5977
}
60-
if argument.is_generator_expr() {
61-
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, call.range());
6278

63-
// Convert `set(x for x in y)` to `{x for x in y}`.
64-
diagnostic.set_fix({
65-
// Replace `set(` with `}`.
66-
let call_start = Edit::replacement(
67-
pad_start("{", call.range(), checker.locator(), checker.semantic()),
68-
call.start(),
69-
call.arguments.start() + TextSize::from(1),
70-
);
79+
let Some(ExprGenerator {
80+
elt, generators, ..
81+
}) = argument.as_generator_expr()
82+
else {
83+
return;
84+
};
85+
86+
// Short-circuit: given `set(x for x in y)`, generate `set(y)` (in lieu of `{x for x in y}`).
87+
if let [generator] = generators.as_slice() {
88+
if generator.ifs.is_empty() && !generator.is_async {
89+
if ComparableExpr::from(elt) == ComparableExpr::from(&generator.target) {
90+
let mut diagnostic = Diagnostic::new(
91+
UnnecessaryGeneratorSet {
92+
short_circuit: true,
93+
},
94+
call.range(),
95+
);
96+
let iterator = format!("set({})", checker.locator().slice(generator.iter.range()));
97+
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
98+
iterator,
99+
call.range(),
100+
)));
101+
checker.diagnostics.push(diagnostic);
102+
return;
103+
}
104+
}
105+
}
106+
107+
// Convert `set(f(x) for x in y)` to `{f(x) for x in y}`.
108+
let mut diagnostic = Diagnostic::new(
109+
UnnecessaryGeneratorSet {
110+
short_circuit: false,
111+
},
112+
call.range(),
113+
);
114+
diagnostic.set_fix({
115+
// Replace `set(` with `}`.
116+
let call_start = Edit::replacement(
117+
pad_start("{", call.range(), checker.locator(), checker.semantic()),
118+
call.start(),
119+
call.arguments.start() + TextSize::from(1),
120+
);
71121

72-
// Replace `)` with `}`.
73-
let call_end = Edit::replacement(
74-
pad_end("}", call.range(), checker.locator(), checker.semantic()),
75-
call.arguments.end() - TextSize::from(1),
76-
call.end(),
77-
);
122+
// Replace `)` with `}`.
123+
let call_end = Edit::replacement(
124+
pad_end("}", call.range(), checker.locator(), checker.semantic()),
125+
call.arguments.end() - TextSize::from(1),
126+
call.end(),
127+
);
78128

79-
Fix::unsafe_edits(call_start, [call_end])
80-
});
129+
Fix::unsafe_edits(call_start, [call_end])
130+
});
81131

82-
checker.diagnostics.push(diagnostic);
83-
}
132+
checker.diagnostics.push(diagnostic);
84133
}

0 commit comments

Comments
 (0)