mirror of https://github.com/astral-sh/ruff
Support concatenated string key removals (#4976)
This commit is contained in:
parent
63fdcea29e
commit
d647105e97
|
|
@ -7,3 +7,5 @@ hidden = {"a": "!"}
|
||||||
|
|
||||||
"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
||||||
"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
|
|
||||||
|
'' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
use anyhow::{bail, Ok, Result};
|
use anyhow::{bail, Ok, Result};
|
||||||
use libcst_native::{DictElement, Expression};
|
|
||||||
use ruff_text_size::TextRange;
|
use ruff_text_size::TextRange;
|
||||||
use rustpython_parser::ast::{Excepthandler, Expr, Ranged};
|
use rustpython_parser::ast::{Excepthandler, Expr, Ranged};
|
||||||
use rustpython_parser::{lexer, Mode, Tok};
|
use rustpython_parser::{lexer, Mode, Tok};
|
||||||
|
|
||||||
use ruff_diagnostics::Edit;
|
use ruff_diagnostics::Edit;
|
||||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||||
use ruff_python_ast::str::raw_contents;
|
|
||||||
|
|
||||||
use crate::autofix::codemods::CodegenStylist;
|
use crate::autofix::codemods::CodegenStylist;
|
||||||
use crate::cst::matchers::{match_call_mut, match_dict, match_expression};
|
use crate::cst::matchers::{match_call_mut, match_dict, match_expression};
|
||||||
|
|
||||||
/// Generate a [`Edit`] to remove unused keys from format dict.
|
/// Generate a [`Edit`] to remove unused keys from format dict.
|
||||||
pub(crate) fn remove_unused_format_arguments_from_dict(
|
pub(crate) fn remove_unused_format_arguments_from_dict(
|
||||||
unused_arguments: &[&str],
|
unused_arguments: &[usize],
|
||||||
stmt: &Expr,
|
stmt: &Expr,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
stylist: &Stylist,
|
stylist: &Stylist,
|
||||||
|
|
@ -22,11 +20,12 @@ pub(crate) fn remove_unused_format_arguments_from_dict(
|
||||||
let mut tree = match_expression(module_text)?;
|
let mut tree = match_expression(module_text)?;
|
||||||
let dict = match_dict(&mut tree)?;
|
let dict = match_dict(&mut tree)?;
|
||||||
|
|
||||||
dict.elements.retain(|e| {
|
// Remove the elements at the given indexes.
|
||||||
!matches!(e, DictElement::Simple {
|
let mut index = 0;
|
||||||
key: Expression::SimpleString(name),
|
dict.elements.retain(|_| {
|
||||||
..
|
let is_unused = unused_arguments.contains(&index);
|
||||||
} if raw_contents(name.value).map_or(false, |name| unused_arguments.contains(&name)))
|
index += 1;
|
||||||
|
!is_unused
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Edit::range_replacement(
|
Ok(Edit::range_replacement(
|
||||||
|
|
@ -37,7 +36,7 @@ pub(crate) fn remove_unused_format_arguments_from_dict(
|
||||||
|
|
||||||
/// Generate a [`Edit`] to remove unused keyword arguments from a `format` call.
|
/// Generate a [`Edit`] to remove unused keyword arguments from a `format` call.
|
||||||
pub(crate) fn remove_unused_keyword_arguments_from_format_call(
|
pub(crate) fn remove_unused_keyword_arguments_from_format_call(
|
||||||
unused_arguments: &[&str],
|
unused_arguments: &[usize],
|
||||||
location: TextRange,
|
location: TextRange,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
stylist: &Stylist,
|
stylist: &Stylist,
|
||||||
|
|
@ -46,8 +45,17 @@ pub(crate) fn remove_unused_keyword_arguments_from_format_call(
|
||||||
let mut tree = match_expression(module_text)?;
|
let mut tree = match_expression(module_text)?;
|
||||||
let call = match_call_mut(&mut tree)?;
|
let call = match_call_mut(&mut tree)?;
|
||||||
|
|
||||||
call.args
|
// Remove the keyword arguments at the given indexes.
|
||||||
.retain(|e| !matches!(&e.keyword, Some(kw) if unused_arguments.contains(&kw.value)));
|
let mut index = 0;
|
||||||
|
call.args.retain(|arg| {
|
||||||
|
if arg.keyword.is_none() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_unused = unused_arguments.contains(&index);
|
||||||
|
index += 1;
|
||||||
|
!is_unused
|
||||||
|
});
|
||||||
|
|
||||||
Ok(Edit::range_replacement(
|
Ok(Edit::range_replacement(
|
||||||
tree.codegen_stylist(stylist),
|
tree.codegen_stylist(stylist),
|
||||||
|
|
|
||||||
|
|
@ -574,13 +574,15 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||||
let Expr::Dict(ast::ExprDict { keys, .. }) = &right else {
|
let Expr::Dict(ast::ExprDict { keys, .. }) = &right else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// If any of the keys are spread, abort.
|
||||||
if keys.iter().any(Option::is_none) {
|
if keys.iter().any(Option::is_none) {
|
||||||
return; // contains **x splat
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let missing: Vec<&str> = keys
|
let missing: Vec<(usize, &str)> = keys
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|k| match k {
|
.enumerate()
|
||||||
|
.filter_map(|(index, key)| match key {
|
||||||
Some(Expr::Constant(ast::ExprConstant {
|
Some(Expr::Constant(ast::ExprConstant {
|
||||||
value: Constant::Str(value),
|
value: Constant::Str(value),
|
||||||
..
|
..
|
||||||
|
|
@ -588,7 +590,7 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||||
if summary.keywords.contains(value) {
|
if summary.keywords.contains(value) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(value.as_str())
|
Some((index, value.as_str()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -599,16 +601,19 @@ pub(crate) fn percent_format_extra_named_arguments(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let names: Vec<String> = missing
|
||||||
|
.iter()
|
||||||
|
.map(|(_, name)| (*name).to_string())
|
||||||
|
.collect();
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
PercentFormatExtraNamedArguments {
|
PercentFormatExtraNamedArguments { missing: names },
|
||||||
missing: missing.iter().map(|&arg| arg.to_string()).collect(),
|
|
||||||
},
|
|
||||||
location,
|
location,
|
||||||
);
|
);
|
||||||
if checker.patch(diagnostic.kind.rule()) {
|
if checker.patch(diagnostic.kind.rule()) {
|
||||||
|
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
|
||||||
diagnostic.try_set_fix(|| {
|
diagnostic.try_set_fix(|| {
|
||||||
let edit = remove_unused_format_arguments_from_dict(
|
let edit = remove_unused_format_arguments_from_dict(
|
||||||
&missing,
|
&indexes,
|
||||||
right,
|
right,
|
||||||
checker.locator,
|
checker.locator,
|
||||||
checker.stylist,
|
checker.stylist,
|
||||||
|
|
@ -742,21 +747,22 @@ pub(crate) fn string_dot_format_extra_named_arguments(
|
||||||
keywords: &[Keyword],
|
keywords: &[Keyword],
|
||||||
location: TextRange,
|
location: TextRange,
|
||||||
) {
|
) {
|
||||||
|
// If there are any **kwargs, abort.
|
||||||
if has_star_star_kwargs(keywords) {
|
if has_star_star_kwargs(keywords) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let keywords = keywords.iter().filter_map(|k| {
|
let keywords = keywords
|
||||||
let Keyword { arg, .. } = &k;
|
.iter()
|
||||||
arg.as_ref()
|
.filter_map(|Keyword { arg, .. }| arg.as_ref());
|
||||||
});
|
|
||||||
|
|
||||||
let missing: Vec<&str> = keywords
|
let missing: Vec<(usize, &str)> = keywords
|
||||||
.filter_map(|arg| {
|
.enumerate()
|
||||||
if summary.keywords.contains(arg.as_ref()) {
|
.filter_map(|(index, keyword)| {
|
||||||
|
if summary.keywords.contains(keyword.as_ref()) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(arg.as_str())
|
Some((index, keyword.as_str()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -765,16 +771,19 @@ pub(crate) fn string_dot_format_extra_named_arguments(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let names: Vec<String> = missing
|
||||||
|
.iter()
|
||||||
|
.map(|(_, name)| (*name).to_string())
|
||||||
|
.collect();
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
StringDotFormatExtraNamedArguments {
|
StringDotFormatExtraNamedArguments { missing: names },
|
||||||
missing: missing.iter().map(|&arg| arg.to_string()).collect(),
|
|
||||||
},
|
|
||||||
location,
|
location,
|
||||||
);
|
);
|
||||||
if checker.patch(diagnostic.kind.rule()) {
|
if checker.patch(diagnostic.kind.rule()) {
|
||||||
|
let indexes: Vec<usize> = missing.iter().map(|(index, _)| *index).collect();
|
||||||
diagnostic.try_set_fix(|| {
|
diagnostic.try_set_fix(|| {
|
||||||
let edit = remove_unused_keyword_arguments_from_format_call(
|
let edit = remove_unused_keyword_arguments_from_format_call(
|
||||||
&missing,
|
&indexes,
|
||||||
location,
|
location,
|
||||||
checker.locator,
|
checker.locator,
|
||||||
checker.stylist,
|
checker.stylist,
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,42 @@ F504.py:8:1: F504 [*] `%`-format string has unused named argument(s): b
|
||||||
8 |-"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
8 |-"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
||||||
8 |+"%(a)s" % {"a": 1, } # F504 ("b" not used)
|
8 |+"%(a)s" % {"a": 1, } # F504 ("b" not used)
|
||||||
9 9 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
9 9 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
|
10 10 |
|
||||||
|
11 11 | '' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
|
||||||
F504.py:9:1: F504 [*] `%`-format string has unused named argument(s): b
|
F504.py:9:1: F504 [*] `%`-format string has unused named argument(s): b
|
||||||
|
|
|
|
||||||
9 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
9 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
||||||
10 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
10 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F504
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F504
|
||||||
|
11 |
|
||||||
|
12 | '' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
|
|
|
||||||
= help: Remove extra named arguments: b
|
= help: Remove extra named arguments: b
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat)
|
6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat)
|
||||||
7 7 |
|
7 7 |
|
||||||
8 8 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
8 8 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
||||||
9 |-"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
9 |-"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
9 |+"%(a)s" % {'a': 1, } # F504 ("b" not used)
|
9 |+"%(a)s" % {'a': 1, } # F504 ("b" not used)
|
||||||
|
10 10 |
|
||||||
|
11 11 | '' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
|
||||||
|
F504.py:11:1: F504 [*] `%`-format string has unused named argument(s): ab
|
||||||
|
|
|
||||||
|
11 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
|
12 |
|
||||||
|
13 | '' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ F504
|
||||||
|
|
|
||||||
|
= help: Remove extra named arguments: ab
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
8 8 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
|
||||||
|
9 9 | "%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
|
||||||
|
10 10 |
|
||||||
|
11 |-'' % {'a''b' : ''} # F504 ("ab" not used)
|
||||||
|
11 |+'' % {} # F504 ("ab" not used)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue