Refactor FURB105 into explicit cases (#7634)

## Summary

I was having trouble keeping track of the various cases here, so opted
to refactor into a more explicit `match`.
This commit is contained in:
Charlie Marsh 2023-09-24 14:46:09 -04:00 committed by GitHub
parent f32b0eef9c
commit 39ddad7454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 162 additions and 83 deletions

View file

@ -17,6 +17,7 @@ print("foo", "", "bar", sep="")
print("", *args)
print("", *args, sep="")
print("", **kwargs)
print(sep="\t")
# OK.

View file

@ -29,7 +29,17 @@ use crate::registry::AsRule;
/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print)
#[violation]
pub struct PrintEmptyString {
separator: Option<Separator>,
reason: Reason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reason {
/// Ex) `print("")`
EmptyArgument,
/// Ex) `print("foo", sep="\t")`
UselessSeparator,
/// Ex) `print("", sep="\t")`
Both,
}
impl Violation for PrintEmptyString {
@ -37,84 +47,135 @@ impl Violation for PrintEmptyString {
#[derive_message_formats]
fn message(&self) -> String {
let PrintEmptyString { separator } = self;
match separator {
None | Some(Separator::Retain) => format!("Unnecessary empty string passed to `print`"),
Some(Separator::Remove) => {
format!("Unnecessary empty string passed to `print` with redundant separator")
}
let PrintEmptyString { reason } = self;
match reason {
Reason::EmptyArgument => format!("Unnecessary empty string passed to `print`"),
Reason::UselessSeparator => format!("Unnecessary separator passed to `print`"),
Reason::Both => format!("Unnecessary empty string and separator passed to `print`"),
}
}
fn autofix_title(&self) -> Option<String> {
let PrintEmptyString { separator } = self;
match separator {
None | Some(Separator::Retain) => Some("Remove empty string".to_string()),
Some(Separator::Remove) => Some("Remove empty string and separator".to_string()),
let PrintEmptyString { reason } = self;
match reason {
Reason::EmptyArgument => Some("Remove empty string".to_string()),
Reason::UselessSeparator => Some("Remove separator".to_string()),
Reason::Both => Some("Remove empty string and separator".to_string()),
}
}
}
/// FURB105
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
if checker
if !checker
.semantic()
.resolve_call_path(&call.func)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "print"]))
{
// Ex) `print("", sep="")` or `print("", "", **kwargs)`
let empty_separator = call
.arguments
.find_keyword("sep")
.map_or(false, |keyword| is_empty_string(&keyword.value))
&& !call
return;
}
match &call.arguments.args.as_slice() {
// Ex) `print(*args)` or `print(*args, sep="\t")`
[arg] if arg.is_starred_expr() => {}
// Ex) `print("")` or `print("", sep="\t")`
[arg] if is_empty_string(arg) => {
let reason = if call.arguments.find_keyword("sep").is_some() {
Reason::Both
} else {
Reason::EmptyArgument
};
let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
generate_suggestion(call, Separator::Remove, checker.generator()),
call.start(),
call.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
// Ex) `print(sep="\t")` or `print(obj, sep="\t")`
[] | [_] => {
// If there's a `sep` argument, remove it, regardless of what it is.
if call.arguments.find_keyword("sep").is_some() {
let mut diagnostic = Diagnostic::new(
PrintEmptyString {
reason: Reason::UselessSeparator,
},
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
generate_suggestion(call, Separator::Remove, checker.generator()),
call.start(),
call.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
}
// Ex) `print("foo", "", "bar", sep="")`
_ => {
// Ignore `**kwargs`.
let has_kwargs = call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none());
if has_kwargs {
return;
}
// Avoid flagging, e.g., `print("", "", sep="sep")`
if !empty_separator && call.arguments.args.len() != 1 {
return;
}
// Check if the positional arguments is are all empty strings, or if
// there are any empty strings and the `sep` keyword argument is also
// an empty string.
if call.arguments.args.iter().all(is_empty_string)
|| (empty_separator && call.arguments.args.iter().any(is_empty_string))
{
let separator = call
// Require an empty `sep` argument.
let empty_separator = call
.arguments
.keywords
.find_keyword("sep")
.map_or(false, |keyword| is_empty_string(&keyword.value));
if !empty_separator {
return;
}
// Count the number of empty and non-empty arguments.
let empty_arguments = call
.arguments
.args
.iter()
.any(|keyword| {
keyword
.arg
.as_ref()
.is_some_and(|arg| arg.as_str() == "sep")
})
.then(|| {
let is_starred = call.arguments.args.iter().any(Expr::is_starred_expr);
if is_starred {
return Separator::Retain;
}
.filter(|arg| is_empty_string(arg))
.count();
if empty_arguments == 0 {
return;
}
let non_empty = call
.arguments
.args
.iter()
.filter(|arg| !is_empty_string(arg))
.count();
if non_empty > 1 {
return Separator::Retain;
}
// If removing the arguments would leave us with one or fewer, then we can remove the
// separator too.
let separator = if call.arguments.args.len() - empty_arguments > 1
|| call.arguments.args.iter().any(Expr::is_starred_expr)
{
Separator::Retain
} else {
Separator::Remove
};
Separator::Remove
});
let mut diagnostic = Diagnostic::new(PrintEmptyString { separator }, call.range());
let mut diagnostic = Diagnostic::new(
PrintEmptyString {
reason: if separator == Separator::Retain {
Reason::EmptyArgument
} else {
Reason::Both
},
},
call.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
@ -134,9 +195,9 @@ fn is_empty_string(expr: &Expr) -> bool {
matches!(
expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Str(s),
value: Constant::Str(value),
..
}) if s.is_empty()
}) if value.is_empty()
)
}
@ -148,18 +209,14 @@ enum Separator {
/// Generate a suggestion to remove the empty string positional argument and
/// the `sep` keyword argument, if it exists.
fn generate_suggestion(
call: &ast::ExprCall,
separator: Option<Separator>,
generator: Generator,
) -> String {
fn generate_suggestion(call: &ast::ExprCall, separator: Separator, generator: Generator) -> String {
let mut call = call.clone();
// Remove all empty string positional arguments.
call.arguments.args.retain(|arg| !is_empty_string(arg));
// Remove the `sep` keyword argument if it exists.
if separator == Some(Separator::Remove) {
if separator == Separator::Remove {
call.arguments.keywords.retain(|keyword| {
keyword
.arg

View file

@ -21,7 +21,7 @@ FURB105.py:3:1: FURB105 [*] Unnecessary empty string passed to `print`
5 5 | print("", end="bar")
6 6 | print("", sep=",", end="bar")
FURB105.py:4:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:4:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
3 | print("")
4 | print("", sep=",")
@ -62,7 +62,7 @@ FURB105.py:5:1: FURB105 [*] Unnecessary empty string passed to `print`
7 7 | print(sep="")
8 8 | print("", sep="")
FURB105.py:6:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:6:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
4 | print("", sep=",")
5 | print("", end="bar")
@ -83,7 +83,7 @@ FURB105.py:6:1: FURB105 [*] Unnecessary empty string passed to `print` with redu
8 8 | print("", sep="")
9 9 | print("", "", sep="")
FURB105.py:7:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:7:1: FURB105 [*] Unnecessary separator passed to `print`
|
5 | print("", end="bar")
6 | print("", sep=",", end="bar")
@ -92,7 +92,7 @@ FURB105.py:7:1: FURB105 [*] Unnecessary empty string passed to `print` with redu
8 | print("", sep="")
9 | print("", "", sep="")
|
= help: Remove empty string and separator
= help: Remove separator
Suggested fix
4 4 | print("", sep=",")
@ -104,7 +104,7 @@ FURB105.py:7:1: FURB105 [*] Unnecessary empty string passed to `print` with redu
9 9 | print("", "", sep="")
10 10 | print("", "", sep="", end="")
FURB105.py:8:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:8:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
6 | print("", sep=",", end="bar")
7 | print(sep="")
@ -125,7 +125,7 @@ FURB105.py:8:1: FURB105 [*] Unnecessary empty string passed to `print` with redu
10 10 | print("", "", sep="", end="")
11 11 | print("", "", sep="", end="bar")
FURB105.py:9:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:9:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
7 | print(sep="")
8 | print("", sep="")
@ -146,7 +146,7 @@ FURB105.py:9:1: FURB105 [*] Unnecessary empty string passed to `print` with redu
11 11 | print("", "", sep="", end="bar")
12 12 | print("", sep="", end="bar")
FURB105.py:10:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:10:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
8 | print("", sep="")
9 | print("", "", sep="")
@ -167,7 +167,7 @@ FURB105.py:10:1: FURB105 [*] Unnecessary empty string passed to `print` with red
12 12 | print("", sep="", end="bar")
13 13 | print(sep="", end="bar")
FURB105.py:11:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:11:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
9 | print("", "", sep="")
10 | print("", "", sep="", end="")
@ -188,7 +188,7 @@ FURB105.py:11:1: FURB105 [*] Unnecessary empty string passed to `print` with red
13 13 | print(sep="", end="bar")
14 14 | print("", "foo", sep="")
FURB105.py:12:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:12:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
10 | print("", "", sep="", end="")
11 | print("", "", sep="", end="bar")
@ -209,7 +209,7 @@ FURB105.py:12:1: FURB105 [*] Unnecessary empty string passed to `print` with red
14 14 | print("", "foo", sep="")
15 15 | print("foo", "", sep="")
FURB105.py:13:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:13:1: FURB105 [*] Unnecessary separator passed to `print`
|
11 | print("", "", sep="", end="bar")
12 | print("", sep="", end="bar")
@ -218,7 +218,7 @@ FURB105.py:13:1: FURB105 [*] Unnecessary empty string passed to `print` with red
14 | print("", "foo", sep="")
15 | print("foo", "", sep="")
|
= help: Remove empty string and separator
= help: Remove separator
Suggested fix
10 10 | print("", "", sep="", end="")
@ -230,7 +230,7 @@ FURB105.py:13:1: FURB105 [*] Unnecessary empty string passed to `print` with red
15 15 | print("foo", "", sep="")
16 16 | print("foo", "", "bar", sep="")
FURB105.py:14:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:14:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
12 | print("", sep="", end="bar")
13 | print(sep="", end="bar")
@ -251,7 +251,7 @@ FURB105.py:14:1: FURB105 [*] Unnecessary empty string passed to `print` with red
16 16 | print("foo", "", "bar", sep="")
17 17 | print("", *args)
FURB105.py:15:1: FURB105 [*] Unnecessary empty string passed to `print` with redundant separator
FURB105.py:15:1: FURB105 [*] Unnecessary empty string and separator passed to `print`
|
13 | print(sep="", end="bar")
14 | print("", "foo", sep="")
@ -300,6 +300,7 @@ FURB105.py:18:1: FURB105 [*] Unnecessary empty string passed to `print`
18 | print("", *args, sep="")
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB105
19 | print("", **kwargs)
20 | print(sep="\t")
|
= help: Remove empty string
@ -310,8 +311,8 @@ FURB105.py:18:1: FURB105 [*] Unnecessary empty string passed to `print`
18 |-print("", *args, sep="")
18 |+print(*args, sep="")
19 19 | print("", **kwargs)
20 20 |
21 21 | # OK.
20 20 | print(sep="\t")
21 21 |
FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
|
@ -319,8 +320,7 @@ FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
18 | print("", *args, sep="")
19 | print("", **kwargs)
| ^^^^^^^^^^^^^^^^^^^ FURB105
20 |
21 | # OK.
20 | print(sep="\t")
|
= help: Remove empty string
@ -330,8 +330,29 @@ FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
18 18 | print("", *args, sep="")
19 |-print("", **kwargs)
19 |+print(**kwargs)
20 20 |
21 21 | # OK.
22 22 |
20 20 | print(sep="\t")
21 21 |
22 22 | # OK.
FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
|
18 | print("", *args, sep="")
19 | print("", **kwargs)
20 | print(sep="\t")
| ^^^^^^^^^^^^^^^ FURB105
21 |
22 | # OK.
|
= help: Remove separator
Suggested fix
17 17 | print("", *args)
18 18 | print("", *args, sep="")
19 19 | print("", **kwargs)
20 |-print(sep="\t")
20 |+print()
21 21 |
22 22 | # OK.
23 23 |