Remove CST-based fixer for C408 (#9822)

## Summary

We have to keep the fixer for a specific case: `dict` calls that include
keyword-argument members.
This commit is contained in:
Charlie Marsh 2024-02-04 19:26:51 -08:00 committed by GitHub
parent a6bc4b2e48
commit 602f8b8250
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 240 additions and 130 deletions

View file

@ -20,3 +20,10 @@ f"{dict(x='y') | dict(y='z')}"
f"{ dict(x='y') | dict(y='z') }" f"{ dict(x='y') | dict(y='z') }"
f"a {dict(x='y') | dict(y='z')} b" f"a {dict(x='y') | dict(y='z')} b"
f"a { dict(x='y') | dict(y='z') } b" f"a { dict(x='y') | dict(y='z') } b"
dict(
# comment
)
tuple( # comment
)

View file

@ -667,10 +667,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryCollectionCall) { if checker.enabled(Rule::UnnecessaryCollectionCall) {
flake8_comprehensions::rules::unnecessary_collection_call( flake8_comprehensions::rules::unnecessary_collection_call(
checker, checker,
expr, call,
func,
args,
keywords,
&checker.settings.flake8_comprehensions, &checker.settings.flake8_comprehensions,
); );
} }

View file

@ -5,13 +5,13 @@ use itertools::Itertools;
use libcst_native::{ use libcst_native::{
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement, Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket, Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace, RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple, TrailingWhitespace, Tuple,
}; };
use ruff_diagnostics::{Edit, Fix}; use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::Expr; use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -25,7 +25,7 @@ use crate::{
checkers::ast::Checker, checkers::ast::Checker,
cst::matchers::{ cst::matchers::{
match_arg, match_call, match_call_mut, match_expression, match_generator_exp, match_lambda, match_arg, match_call, match_call_mut, match_expression, match_generator_exp, match_lambda,
match_list_comp, match_name, match_tuple, match_list_comp, match_tuple,
}, },
}; };
@ -213,60 +213,23 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
)) ))
} }
/// (C408) /// (C408) Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> { pub(crate) fn fix_unnecessary_collection_call(
enum Collection { expr: &ast::ExprCall,
Tuple, checker: &Checker,
List, ) -> Result<Edit> {
Dict,
}
let locator = checker.locator(); let locator = checker.locator();
let stylist = checker.stylist(); let stylist = checker.stylist();
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict) // Expr(Call("dict")))) -> Expr(Dict)
let module_text = locator.slice(expr); let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?; let mut tree = match_expression(module_text)?;
let call = match_call(&tree)?; let call = match_call(&tree)?;
let name = match_name(&call.func)?;
let collection = match name.value {
"tuple" => Collection::Tuple,
"list" => Collection::List,
"dict" => Collection::Dict,
_ => bail!("Expected 'tuple', 'list', or 'dict'"),
};
// Arena allocator used to create formatted strings of sufficient lifetime, // Arena allocator used to create formatted strings of sufficient lifetime,
// below. // below.
let mut arena: Vec<String> = vec![]; let mut arena: Vec<String> = vec![];
match collection {
Collection::Tuple => {
tree = Expression::Tuple(Box::new(Tuple {
elements: vec![],
lpar: vec![LeftParen::default()],
rpar: vec![RightParen::default()],
}));
}
Collection::List => {
tree = Expression::List(Box::new(List {
elements: vec![],
lbracket: LeftSquareBracket::default(),
rbracket: RightSquareBracket::default(),
lpar: vec![],
rpar: vec![],
}));
}
Collection::Dict => {
if call.args.is_empty() {
tree = Expression::Dict(Box::new(Dict {
elements: vec![],
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
lpar: vec![],
rpar: vec![],
}));
} else {
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote()); let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
// Quote each argument. // Quote each argument.
@ -296,9 +259,9 @@ pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) ->
value: arg.value.clone(), value: arg.value.clone(),
comma: arg.comma.clone(), comma: arg.comma.clone(),
whitespace_before_colon: ParenthesizableWhitespace::default(), whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace( whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
SimpleWhitespace(" "), " ",
), )),
}) })
.collect(); .collect();
@ -318,21 +281,14 @@ pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) ->
lpar: vec![], lpar: vec![],
rpar: vec![], rpar: vec![],
})); }));
}
}
};
Ok(Edit::range_replacement( Ok(Edit::range_replacement(
if matches!(collection, Collection::Dict) {
pad_expression( pad_expression(
tree.codegen_stylist(stylist), tree.codegen_stylist(stylist),
expr.range(), expr.range(),
checker.locator(), checker.locator(),
checker.semantic(), checker.semantic(),
) ),
} else {
tree.codegen_stylist(stylist)
},
expr.range(), expr.range(),
)) ))
} }

View file

@ -1,11 +1,11 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword}; use ruff_python_ast as ast;
use ruff_text_size::Ranged; use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes; use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use crate::rules::flake8_comprehensions::settings::Settings; use crate::rules::flake8_comprehensions::settings::Settings;
/// ## What it does /// ## What it does
@ -56,42 +56,88 @@ impl AlwaysFixableViolation for UnnecessaryCollectionCall {
/// C408 /// C408
pub(crate) fn unnecessary_collection_call( pub(crate) fn unnecessary_collection_call(
checker: &mut Checker, checker: &mut Checker,
expr: &Expr, call: &ast::ExprCall,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
settings: &Settings, settings: &Settings,
) { ) {
if !args.is_empty() { if !call.arguments.args.is_empty() {
return; return;
} }
let Some(func) = func.as_name_expr() else { let Some(func) = call.func.as_name_expr() else {
return; return;
}; };
match func.id.as_str() { let collection = match func.id.as_str() {
"dict" "dict"
if keywords.is_empty() if call.arguments.keywords.is_empty()
|| (!settings.allow_dict_calls_with_keyword_arguments || (!settings.allow_dict_calls_with_keyword_arguments
&& keywords.iter().all(|kw| kw.arg.is_some())) => && call.arguments.keywords.iter().all(|kw| kw.arg.is_some())) =>
{ {
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`) // `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
Collection::Dict
} }
"list" | "tuple" if keywords.is_empty() => { "list" if call.arguments.keywords.is_empty() => {
// `list()` or `tuple()` // `list()
Collection::List
}
"tuple" if call.arguments.keywords.is_empty() => {
// `tuple()`
Collection::Tuple
} }
_ => return, _ => return,
}; };
if !checker.semantic().is_builtin(func.id.as_str()) { if !checker.semantic().is_builtin(func.id.as_str()) {
return; return;
} }
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
UnnecessaryCollectionCall { UnnecessaryCollectionCall {
obj_type: func.id.to_string(), obj_type: func.id.to_string(),
}, },
expr.range(), call.range(),
); );
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(expr, checker).map(Fix::unsafe_edit) // Convert `dict()` to `{}`.
if call.arguments.keywords.is_empty() {
diagnostic.set_fix({
// Replace from the start of the call to the start of the argument.
let call_start = Edit::replacement(
match collection {
Collection::Dict => {
pad_start("{", call.range(), checker.locator(), checker.semantic())
}
Collection::List => "[".to_string(),
Collection::Tuple => "(".to_string(),
},
call.start(),
call.arguments.start() + TextSize::from(1),
);
// Replace from the end of the inner list or tuple to the end of the call with `}`.
let call_end = Edit::replacement(
match collection {
Collection::Dict => {
pad_end("}", call.range(), checker.locator(), checker.semantic())
}
Collection::List => "]".to_string(),
Collection::Tuple => ")".to_string(),
},
call.arguments.end() - TextSize::from(1),
call.end(),
);
Fix::unsafe_edits(call_start, [call_end])
}); });
} else {
// Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(call, checker).map(Fix::unsafe_edit)
});
}
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
enum Collection {
Tuple,
List,
Dict,
}

View file

@ -217,6 +217,7 @@ C408.py:20:5: C408 [*] Unnecessary `dict` call (rewrite as a literal)
20 |+f"{ {'x': 'y'} | dict(y='z') }" 20 |+f"{ {'x': 'y'} | dict(y='z') }"
21 21 | f"a {dict(x='y') | dict(y='z')} b" 21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b" 22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal) C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
| |
@ -236,6 +237,7 @@ C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
20 |+f"{ dict(x='y') | {'y': 'z'} }" 20 |+f"{ dict(x='y') | {'y': 'z'} }"
21 21 | f"a {dict(x='y') | dict(y='z')} b" 21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b" 22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal) C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
| |
@ -254,6 +256,8 @@ C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 |-f"a {dict(x='y') | dict(y='z')} b" 21 |-f"a {dict(x='y') | dict(y='z')} b"
21 |+f"a { {'x': 'y'} | dict(y='z')} b" 21 |+f"a { {'x': 'y'} | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b" 22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 24 | dict(
C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal) C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
| |
@ -272,6 +276,8 @@ C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 |-f"a {dict(x='y') | dict(y='z')} b" 21 |-f"a {dict(x='y') | dict(y='z')} b"
21 |+f"a {dict(x='y') | {'y': 'z'} } b" 21 |+f"a {dict(x='y') | {'y': 'z'} } b"
22 22 | f"a { dict(x='y') | dict(y='z') } b" 22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 24 | dict(
C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal) C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
| |
@ -279,6 +285,8 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 | f"a {dict(x='y') | dict(y='z')} b" 21 | f"a {dict(x='y') | dict(y='z')} b"
22 | f"a { dict(x='y') | dict(y='z') } b" 22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408 | ^^^^^^^^^^^ C408
23 |
24 | dict(
| |
= help: Rewrite as a literal = help: Rewrite as a literal
@ -288,6 +296,9 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 21 | f"a {dict(x='y') | dict(y='z')} b" 21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 |-f"a { dict(x='y') | dict(y='z') } b" 22 |-f"a { dict(x='y') | dict(y='z') } b"
22 |+f"a { {'x': 'y'} | dict(y='z') } b" 22 |+f"a { {'x': 'y'} | dict(y='z') } b"
23 23 |
24 24 | dict(
25 25 | # comment
C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal) C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
| |
@ -295,6 +306,8 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 | f"a {dict(x='y') | dict(y='z')} b" 21 | f"a {dict(x='y') | dict(y='z')} b"
22 | f"a { dict(x='y') | dict(y='z') } b" 22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408 | ^^^^^^^^^^^ C408
23 |
24 | dict(
| |
= help: Rewrite as a literal = help: Rewrite as a literal
@ -304,5 +317,52 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
21 21 | f"a {dict(x='y') | dict(y='z')} b" 21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 |-f"a { dict(x='y') | dict(y='z') } b" 22 |-f"a { dict(x='y') | dict(y='z') } b"
22 |+f"a { dict(x='y') | {'y': 'z'} } b" 22 |+f"a { dict(x='y') | {'y': 'z'} } b"
23 23 |
24 24 | dict(
25 25 | # comment
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
22 | f"a { dict(x='y') | dict(y='z') } b"
23 |
24 | / dict(
25 | | # comment
26 | | )
| |_^ C408
27 |
28 | tuple( # comment
|
= help: Rewrite as a literal
Unsafe fix
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 |-dict(
24 |+{
25 25 | # comment
26 |-)
26 |+}
27 27 |
28 28 | tuple( # comment
29 29 | )
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
26 | )
27 |
28 | / tuple( # comment
29 | | )
| |_^ C408
|
= help: Rewrite as a literal
Unsafe fix
25 25 | # comment
26 26 | )
27 27 |
28 |-tuple( # comment
28 |+( # comment
29 29 | )

View file

@ -96,4 +96,48 @@ C408.py:17:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
19 19 | f"{dict(x='y') | dict(y='z')}" 19 19 | f"{dict(x='y') | dict(y='z')}"
20 20 | f"{ dict(x='y') | dict(y='z') }" 20 20 | f"{ dict(x='y') | dict(y='z') }"
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
22 | f"a { dict(x='y') | dict(y='z') } b"
23 |
24 | / dict(
25 | | # comment
26 | | )
| |_^ C408
27 |
28 | tuple( # comment
|
= help: Rewrite as a literal
Unsafe fix
21 21 | f"a {dict(x='y') | dict(y='z')} b"
22 22 | f"a { dict(x='y') | dict(y='z') } b"
23 23 |
24 |-dict(
24 |+{
25 25 | # comment
26 |-)
26 |+}
27 27 |
28 28 | tuple( # comment
29 29 | )
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
26 | )
27 |
28 | / tuple( # comment
29 | | )
| |_^ C408
|
= help: Rewrite as a literal
Unsafe fix
25 25 | # comment
26 26 | )
27 27 |
28 |-tuple( # comment
28 |+( # comment
29 29 | )