Treat sep arguments with effects as unsafe removals (#13165)

## Summary

Closes https://github.com/astral-sh/ruff/issues/13126.
This commit is contained in:
Charlie Marsh 2024-08-30 12:17:47 -04:00 committed by GitHub
parent f8656ff35e
commit 34dafb67a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 108 additions and 48 deletions

View file

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

View file

@ -1,7 +1,9 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -85,11 +87,15 @@ pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range()); let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range());
diagnostic.set_fix(Fix::safe_edit(Edit::replacement( diagnostic.set_fix(
generate_suggestion(call, Separator::Remove, checker.generator()), EmptyStringFix::from_call(
call.start(), call,
call.end(), Separator::Remove,
))); checker.semantic(),
checker.generator(),
)
.into_fix(),
);
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
@ -109,11 +115,15 @@ pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
call.range(), call.range(),
); );
diagnostic.set_fix(Fix::safe_edit(Edit::replacement( diagnostic.set_fix(
generate_suggestion(call, Separator::Remove, checker.generator()), EmptyStringFix::from_call(
call.start(), call,
call.end(), Separator::Remove,
))); checker.semantic(),
checker.generator(),
)
.into_fix(),
);
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
@ -172,11 +182,10 @@ pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
call.range(), call.range(),
); );
diagnostic.set_fix(Fix::safe_edit(Edit::replacement( diagnostic.set_fix(
generate_suggestion(call, separator, checker.generator()), EmptyStringFix::from_call(call, separator, checker.semantic(), checker.generator())
call.start(), .into_fix(),
call.end(), );
)));
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
@ -200,10 +209,21 @@ enum Separator {
Retain, Retain,
} }
/// Generate a suggestion to remove the empty string positional argument and #[derive(Debug, Clone)]
/// the `sep` keyword argument, if it exists. struct EmptyStringFix(Fix);
fn generate_suggestion(call: &ast::ExprCall, separator: Separator, generator: Generator) -> String {
impl EmptyStringFix {
/// Generate a suggestion to remove the empty string positional argument and
/// the `sep` keyword argument, if it exists.
fn from_call(
call: &ast::ExprCall,
separator: Separator,
semantic: &SemanticModel,
generator: Generator,
) -> Self {
let range = call.range();
let mut call = call.clone(); let mut call = call.clone();
let mut applicability = Applicability::Safe;
// Remove all empty string positional arguments. // Remove all empty string positional arguments.
call.arguments.args = call call.arguments.args = call
@ -222,15 +242,35 @@ fn generate_suggestion(call: &ast::ExprCall, separator: Separator, generator: Ge
.keywords .keywords
.iter() .iter()
.filter(|keyword| { .filter(|keyword| {
keyword let Some(arg) = keyword.arg.as_ref() else {
.arg return true;
.as_ref() };
.map_or(true, |arg| arg.as_str() != "sep")
if arg.as_str() != "sep" {
return true;
}
if contains_effect(&keyword.value, |id| semantic.has_builtin_binding(id)) {
applicability = Applicability::Unsafe;
}
false
}) })
.cloned() .cloned()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_boxed_slice(); .into_boxed_slice();
} }
generator.expr(&call.into()) let contents = generator.expr(&call.into());
Self(Fix::applicable_edit(
Edit::range_replacement(contents, range),
applicability,
))
}
/// Return the [`Fix`] contained in this [`EmptyStringFix`].
fn into_fix(self) -> Fix {
self.0
}
} }

View file

@ -312,7 +312,7 @@ FURB105.py:18:1: FURB105 [*] Unnecessary empty string passed to `print`
18 |+print(*args, sep="") 18 |+print(*args, sep="")
19 19 | print("", **kwargs) 19 19 | print("", **kwargs)
20 20 | print(sep="\t") 20 20 | print(sep="\t")
21 21 | 21 21 | print(sep=print(1))
FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print` FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
| |
@ -321,6 +321,7 @@ FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
19 | print("", **kwargs) 19 | print("", **kwargs)
| ^^^^^^^^^^^^^^^^^^^ FURB105 | ^^^^^^^^^^^^^^^^^^^ FURB105
20 | print(sep="\t") 20 | print(sep="\t")
21 | print(sep=print(1))
| |
= help: Remove empty string = help: Remove empty string
@ -331,8 +332,8 @@ FURB105.py:19:1: FURB105 [*] Unnecessary empty string passed to `print`
19 |-print("", **kwargs) 19 |-print("", **kwargs)
19 |+print(**kwargs) 19 |+print(**kwargs)
20 20 | print(sep="\t") 20 20 | print(sep="\t")
21 21 | 21 21 | print(sep=print(1))
22 22 | # OK. 22 22 |
FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print` FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
| |
@ -340,8 +341,7 @@ FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
19 | print("", **kwargs) 19 | print("", **kwargs)
20 | print(sep="\t") 20 | print(sep="\t")
| ^^^^^^^^^^^^^^^ FURB105 | ^^^^^^^^^^^^^^^ FURB105
21 | 21 | print(sep=print(1))
22 | # OK.
| |
= help: Remove separator = help: Remove separator
@ -351,8 +351,27 @@ FURB105.py:20:1: FURB105 [*] Unnecessary separator passed to `print`
19 19 | print("", **kwargs) 19 19 | print("", **kwargs)
20 |-print(sep="\t") 20 |-print(sep="\t")
20 |+print() 20 |+print()
21 21 | 21 21 | print(sep=print(1))
22 22 | # OK. 22 22 |
23 23 | 23 23 | # OK.
FURB105.py:21:1: FURB105 [*] Unnecessary separator passed to `print`
|
19 | print("", **kwargs)
20 | print(sep="\t")
21 | print(sep=print(1))
| ^^^^^^^^^^^^^^^^^^^ FURB105
22 |
23 | # OK.
|
= help: Remove separator
Unsafe fix
18 18 | print("", *args, sep="")
19 19 | print("", **kwargs)
20 20 | print(sep="\t")
21 |-print(sep=print(1))
21 |+print()
22 22 |
23 23 | # OK.
24 24 |