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"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) {
flake8_comprehensions::rules::unnecessary_collection_call(
checker,
expr,
func,
args,
keywords,
call,
&checker.settings.flake8_comprehensions,
);
}

View file

@ -5,13 +5,13 @@ use itertools::Itertools;
use libcst_native::{
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
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_semantic::SemanticModel;
use ruff_source_file::Locator;
@ -25,7 +25,7 @@ use crate::{
checkers::ast::Checker,
cst::matchers::{
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,126 +213,82 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
))
}
/// (C408)
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> {
enum Collection {
Tuple,
List,
Dict,
}
/// (C408) Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
pub(crate) fn fix_unnecessary_collection_call(
expr: &ast::ExprCall,
checker: &Checker,
) -> Result<Edit> {
let locator = checker.locator();
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 mut tree = match_expression(module_text)?;
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,
// below.
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(),
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
// Quote each argument.
for arg in &call.args {
let quoted = format!(
"{}{}{}",
quote,
arg.keyword
.as_ref()
.expect("Expected dictionary argument to be kwarg")
.value,
quote,
);
arena.push(quoted);
}
let elements = call
.args
.iter()
.enumerate()
.map(|(i, arg)| DictElement::Simple {
key: Expression::SimpleString(Box::new(SimpleString {
value: &arena[i],
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());
})),
value: arg.value.clone(),
comma: arg.comma.clone(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
})
.collect();
// Quote each argument.
for arg in &call.args {
let quoted = format!(
"{}{}{}",
quote,
arg.keyword
.as_ref()
.expect("Expected dictionary argument to be kwarg")
.value,
quote,
);
arena.push(quoted);
}
let elements = call
.args
.iter()
.enumerate()
.map(|(i, arg)| DictElement::Simple {
key: Expression::SimpleString(Box::new(SimpleString {
value: &arena[i],
lpar: vec![],
rpar: vec![],
})),
value: arg.value.clone(),
comma: arg.comma.clone(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
SimpleWhitespace(" "),
),
})
.collect();
tree = Expression::Dict(Box::new(Dict {
elements,
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: call
.args
.last()
.expect("Arguments should be non-empty")
.whitespace_after_arg
.clone(),
},
lpar: vec![],
rpar: vec![],
}));
}
}
};
tree = Expression::Dict(Box::new(Dict {
elements,
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
rbrace: RightCurlyBrace {
whitespace_before: call
.args
.last()
.expect("Arguments should be non-empty")
.whitespace_after_arg
.clone(),
},
lpar: vec![],
rpar: vec![],
}));
Ok(Edit::range_replacement(
if matches!(collection, Collection::Dict) {
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
)
} else {
tree.codegen_stylist(stylist)
},
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
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_python_ast::{Expr, Keyword};
use ruff_text_size::Ranged;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
use crate::rules::flake8_comprehensions::settings::Settings;
/// ## What it does
@ -56,42 +56,88 @@ impl AlwaysFixableViolation for UnnecessaryCollectionCall {
/// C408
pub(crate) fn unnecessary_collection_call(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
call: &ast::ExprCall,
settings: &Settings,
) {
if !args.is_empty() {
if !call.arguments.args.is_empty() {
return;
}
let Some(func) = func.as_name_expr() else {
let Some(func) = call.func.as_name_expr() else {
return;
};
match func.id.as_str() {
let collection = match func.id.as_str() {
"dict"
if keywords.is_empty()
if call.arguments.keywords.is_empty()
|| (!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)`)
Collection::Dict
}
"list" | "tuple" if keywords.is_empty() => {
// `list()` or `tuple()`
"list" if call.arguments.keywords.is_empty() => {
// `list()
Collection::List
}
"tuple" if call.arguments.keywords.is_empty() => {
// `tuple()`
Collection::Tuple
}
_ => return,
};
if !checker.semantic().is_builtin(func.id.as_str()) {
return;
}
let mut diagnostic = Diagnostic::new(
UnnecessaryCollectionCall {
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);
}
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') }"
21 21 | 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)
|
@ -236,6 +237,7 @@ C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
20 |+f"{ dict(x='y') | {'y': 'z'} }"
21 21 | 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)
|
@ -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 { {'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)
|
@ -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') | {'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)
|
@ -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"
22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408
23 |
24 | dict(
|
= 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"
22 |-f"a { dict(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)
|
@ -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"
22 | f"a { dict(x='y') | dict(y='z') } b"
| ^^^^^^^^^^^ C408
23 |
24 | dict(
|
= 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"
22 |-f"a { dict(x='y') | dict(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')}"
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 | )