[flake8-comprehensions] Handle extraneous parentheses around list comprehension (C403) (#15877)

## Summary

Given the following code:

```python
set(([x for x in range(5)]))
```

the current implementation of C403 results in

```python
{(x for x in range(5))}
```

which is a set containing a generator rather than the result of the
generator.

This change removes the extraneous parentheses so that the resulting
code is:

```python
{x for x in range(5)}
```


## Test Plan

`cargo nextest run` and `cargo insta test`
This commit is contained in:
Justin Bramley 2025-02-03 13:26:03 -05:00 committed by GitHub
parent 62075afe4f
commit dc5e922221
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 27 deletions

View file

@ -21,3 +21,14 @@ s = set( # comment
s = set([ # comment
x for x in range(3)
])
s = set(([x for x in range(3)]))
s = set(((([x for x in range(3)]))))
s = set( # outer set comment
( # inner paren comment - not preserved
((
[ # comprehension comment
x for x in range(3)]
))))

View file

@ -1,6 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@ -55,32 +56,39 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &mut Checker, call: &a
if !checker.semantic().has_builtin_binding("set") {
return;
}
if argument.is_list_comp_expr() {
let diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range());
let fix = {
// Replace `set(` with `{`.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace `)` with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
call.arguments.end() - TextSize::from(1),
call.end(),
);
// Delete the open bracket (`[`).
let argument_start =
Edit::deletion(argument.start(), argument.start() + TextSize::from(1));
// Delete the close bracket (`]`).
let argument_end = Edit::deletion(argument.end() - TextSize::from(1), argument.end());
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
};
checker.diagnostics.push(diagnostic.with_fix(fix));
if !argument.is_list_comp_expr() {
return;
}
let diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range());
let one = TextSize::from(1);
// Replace `set(` with `{`.
let call_start = Edit::replacement(
pad_start("{", call.range(), checker.locator(), checker.semantic()),
call.start(),
call.arguments.start() + one,
);
// Replace `)` with `}`.
let call_end = Edit::replacement(
pad_end("}", call.range(), checker.locator(), checker.semantic()),
call.arguments.end() - one,
call.end(),
);
// If the list comprehension is parenthesized, remove the parentheses in addition to
// removing the brackets.
let replacement_range = parenthesized_range(
argument.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or_else(|| argument.range());
let span = argument.range().add_start(one).sub_end(one);
let replacement =
Edit::range_replacement(checker.source()[span].to_string(), replacement_range);
let fix = Fix::unsafe_edits(call_start, [call_end, replacement]);
checker.diagnostics.push(diagnostic.with_fix(fix));
}

View file

@ -220,6 +220,8 @@ C403.py:21:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe
22 | | x for x in range(3)
23 | | ])
| |__^ C403
24 |
25 | s = set(([x for x in range(3)]))
|
= help: Rewrite as a set comprehension
@ -232,3 +234,77 @@ C403.py:21:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe
22 22 | x for x in range(3)
23 |-])
23 |+}
24 24 |
25 25 | s = set(([x for x in range(3)]))
26 26 |
C403.py:25:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension)
|
23 | ])
24 |
25 | s = set(([x for x in range(3)]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C403
26 |
27 | s = set(((([x for x in range(3)]))))
|
= help: Rewrite as a set comprehension
Unsafe fix
22 22 | x for x in range(3)
23 23 | ])
24 24 |
25 |-s = set(([x for x in range(3)]))
25 |+s = {x for x in range(3)}
26 26 |
27 27 | s = set(((([x for x in range(3)]))))
28 28 |
C403.py:27:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension)
|
25 | s = set(([x for x in range(3)]))
26 |
27 | s = set(((([x for x in range(3)]))))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C403
28 |
29 | s = set( # outer set comment
|
= help: Rewrite as a set comprehension
Unsafe fix
24 24 |
25 25 | s = set(([x for x in range(3)]))
26 26 |
27 |-s = set(((([x for x in range(3)]))))
27 |+s = {x for x in range(3)}
28 28 |
29 29 | s = set( # outer set comment
30 30 | ( # inner paren comment - not preserved
C403.py:29:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension)
|
27 | s = set(((([x for x in range(3)]))))
28 |
29 | s = set( # outer set comment
| _____^
30 | | ( # inner paren comment - not preserved
31 | | ((
32 | | [ # comprehension comment
33 | | x for x in range(3)]
34 | | ))))
| |_____^ C403
|
= help: Rewrite as a set comprehension
Unsafe fix
26 26 |
27 27 | s = set(((([x for x in range(3)]))))
28 28 |
29 |-s = set( # outer set comment
30 |-( # inner paren comment - not preserved
31 |-((
32 |-[ # comprehension comment
33 |- x for x in range(3)]
34 |- ))))
29 |+s = { # outer set comment
30 |+ # comprehension comment
31 |+ x for x in range(3)}