mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00
## Summary Addresses #11974 to add a `RUF` rule to replace `print` expressions in `assert` statements with the inner message. An autofix is available, but is considered unsafe as it changes behaviour of the execution, notably: - removal of the printout in `stdout`, and - `AssertionError` instance containing a different message. While the detection of the condition is a straightforward matter, deciding how to resolve the print arguments into a string literal can be a relatively subjective matter. The implementation of this PR chooses to be as tolerant as possible, and will attempt to reformat any number of `print` arguments containing single or concatenated strings or variables into either a string literal, or a f-string if any variables or placeholders are detected. ## Test Plan `cargo test`. ## Examples For ease of discussion, this is the diff for the tests: ```diff # Standard Case # Expects: # - single StringLiteral -assert True, print("This print is not intentional.") +assert True, "This print is not intentional." # Concatenated string literals # Expects: # - single StringLiteral -assert True, print("This print" " is not intentional.") +assert True, "This print is not intentional." # Positional arguments, string literals # Expects: # - single StringLiteral concatenated with " " -assert True, print("This print", "is not intentional") +assert True, "This print is not intentional" # Concatenated string literals combined with Positional arguments # Expects: # - single stringliteral concatenated with " " only between `print` and `is` -assert True, print("This " "print", "is not intentional.") +assert True, "This print is not intentional." # Positional arguments, string literals with a variable # Expects: # - single FString concatenated with " " -assert True, print("This", print.__name__, "is not intentional.") +assert True, f"This {print.__name__} is not intentional." # Mixed brackets string literals # Expects: # - single StringLiteral concatenated with " " -assert True, print("This print", 'is not intentional', """and should be removed""") +assert True, "This print is not intentional and should be removed" # Mixed brackets with other brackets inside # Expects: # - single StringLiteral concatenated with " " and escaped brackets -assert True, print("This print", 'is not "intentional"', """and "should" be 'removed'""") +assert True, "This print is not \"intentional\" and \"should\" be 'removed'" # Positional arguments, string literals with a separator # Expects: # - single StringLiteral concatenated with "|" -assert True, print("This print", "is not intentional", sep="|") +assert True, "This print|is not intentional" # Positional arguments, string literals with None as separator # Expects: # - single StringLiteral concatenated with " " -assert True, print("This print", "is not intentional", sep=None) +assert True, "This print is not intentional" # Positional arguments, string literals with variable as separator, needs f-string # Expects: # - single FString concatenated with "{U00A0}" -assert True, print("This print", "is not intentional", sep=U00A0) +assert True, f"This print{U00A0}is not intentional" # Unnecessary f-string # Expects: # - single StringLiteral -assert True, print(f"This f-string is just a literal.") +assert True, "This f-string is just a literal." # Positional arguments, string literals and f-strings # Expects: # - single FString concatenated with " " -assert True, print("This print", f"is not {'intentional':s}") +assert True, f"This print is not {'intentional':s}" # Positional arguments, string literals and f-strings with a separator # Expects: # - single FString concatenated with "|" -assert True, print("This print", f"is not {'intentional':s}", sep="|") +assert True, f"This print|is not {'intentional':s}" # A single f-string # Expects: # - single FString -assert True, print(f"This print is not {'intentional':s}") +assert True, f"This print is not {'intentional':s}" # A single f-string with a redundant separator # Expects: # - single FString -assert True, print(f"This print is not {'intentional':s}", sep="|") +assert True, f"This print is not {'intentional':s}" # Complex f-string with variable as separator # Expects: # - single FString concatenated with "{U00A0}", all placeholders preserved condition = "True is True" maintainer = "John Doe" -assert True, print("Unreachable due to", condition, f", ask {maintainer} for advice", sep=U00A0) +assert True, f"Unreachable due to{U00A0}{condition}{U00A0}, ask {maintainer} for advice" # Empty print # Expects: # - `msg` entirely removed from assertion -assert True, print() +assert True # Empty print with separator # Expects: # - `msg` entirely removed from assertion -assert True, print(sep=" ") +assert True # Custom print function that actually returns a string # Expects: @@ -100,4 +100,4 @@ # Use of `builtins.print` # Expects: # - single StringLiteral -assert True, builtins.print("This print should be removed.") +assert True, "This print should be removed." ``` ## Known Issues The current implementation resolves all arguments and separators of the `print` expression into a single string, be it `StringLiteralValue::single` or a `FStringValue::single`. This: - potentially joins together strings well beyond the ideal character limit for each line, and - does not preserve multi-line strings in their original format, in favour of a single line `"...\n...\n..."` format. These are purely formatting issues only occurring in unusual scenarios. Additionally, the autofix will tolerate `print` calls that were previously invalid: ```python assert True, print("this", "should not be allowed", sep=42) ``` This will be transformed into ```python assert True, f"this{42}should not be allowed" ``` which some could argue is an alteration of behaviour. --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
0c8b5eb17a
commit
c3f61a012e
8 changed files with 810 additions and 5 deletions
108
crates/ruff_linter/resources/test/fixtures/ruff/RUF030.py
vendored
Normal file
108
crates/ruff_linter/resources/test/fixtures/ruff/RUF030.py
vendored
Normal file
|
@ -0,0 +1,108 @@
|
|||
U00A0 = "\u00a0"
|
||||
|
||||
# Standard Case
|
||||
# Expects:
|
||||
# - single StringLiteral
|
||||
assert True, print("This print is not intentional.")
|
||||
|
||||
# Concatenated string literals
|
||||
# Expects:
|
||||
# - single StringLiteral
|
||||
assert True, print("This print" " is not intentional.")
|
||||
|
||||
# Positional arguments, string literals
|
||||
# Expects:
|
||||
# - single StringLiteral concatenated with " "
|
||||
assert True, print("This print", "is not intentional")
|
||||
|
||||
# Concatenated string literals combined with Positional arguments
|
||||
# Expects:
|
||||
# - single stringliteral concatenated with " " only between `print` and `is`
|
||||
assert True, print("This " "print", "is not intentional.")
|
||||
|
||||
# Positional arguments, string literals with a variable
|
||||
# Expects:
|
||||
# - single FString concatenated with " "
|
||||
assert True, print("This", print.__name__, "is not intentional.")
|
||||
|
||||
# Mixed brackets string literals
|
||||
# Expects:
|
||||
# - single StringLiteral concatenated with " "
|
||||
assert True, print("This print", 'is not intentional', """and should be removed""")
|
||||
|
||||
# Mixed brackets with other brackets inside
|
||||
# Expects:
|
||||
# - single StringLiteral concatenated with " " and escaped brackets
|
||||
assert True, print("This print", 'is not "intentional"', """and "should" be 'removed'""")
|
||||
|
||||
# Positional arguments, string literals with a separator
|
||||
# Expects:
|
||||
# - single StringLiteral concatenated with "|"
|
||||
assert True, print("This print", "is not intentional", sep="|")
|
||||
|
||||
# Positional arguments, string literals with None as separator
|
||||
# Expects:
|
||||
# - single StringLiteral concatenated with " "
|
||||
assert True, print("This print", "is not intentional", sep=None)
|
||||
|
||||
# Positional arguments, string literals with variable as separator, needs f-string
|
||||
# Expects:
|
||||
# - single FString concatenated with "{U00A0}"
|
||||
assert True, print("This print", "is not intentional", sep=U00A0)
|
||||
|
||||
# Unnecessary f-string
|
||||
# Expects:
|
||||
# - single StringLiteral
|
||||
assert True, print(f"This f-string is just a literal.")
|
||||
|
||||
# Positional arguments, string literals and f-strings
|
||||
# Expects:
|
||||
# - single FString concatenated with " "
|
||||
assert True, print("This print", f"is not {'intentional':s}")
|
||||
|
||||
# Positional arguments, string literals and f-strings with a separator
|
||||
# Expects:
|
||||
# - single FString concatenated with "|"
|
||||
assert True, print("This print", f"is not {'intentional':s}", sep="|")
|
||||
|
||||
# A single f-string
|
||||
# Expects:
|
||||
# - single FString
|
||||
assert True, print(f"This print is not {'intentional':s}")
|
||||
|
||||
# A single f-string with a redundant separator
|
||||
# Expects:
|
||||
# - single FString
|
||||
assert True, print(f"This print is not {'intentional':s}", sep="|")
|
||||
|
||||
# Complex f-string with variable as separator
|
||||
# Expects:
|
||||
# - single FString concatenated with "{U00A0}", all placeholders preserved
|
||||
condition = "True is True"
|
||||
maintainer = "John Doe"
|
||||
assert True, print("Unreachable due to", condition, f", ask {maintainer} for advice", sep=U00A0)
|
||||
|
||||
# Empty print
|
||||
# Expects:
|
||||
# - `msg` entirely removed from assertion
|
||||
assert True, print()
|
||||
|
||||
# Empty print with separator
|
||||
# Expects:
|
||||
# - `msg` entirely removed from assertion
|
||||
assert True, print(sep=" ")
|
||||
|
||||
# Custom print function that actually returns a string
|
||||
# Expects:
|
||||
# - no violation as the function is not a built-in print
|
||||
def print(s: str):
|
||||
return "This is my assertion error message: " + s
|
||||
|
||||
assert True, print("this print shall not be removed.")
|
||||
|
||||
import builtins
|
||||
|
||||
# Use of `builtins.print`
|
||||
# Expects:
|
||||
# - single StringLiteral
|
||||
assert True, builtins.print("This print should be removed.")
|
|
@ -1232,11 +1232,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
}
|
||||
}
|
||||
}
|
||||
Stmt::Assert(ast::StmtAssert {
|
||||
Stmt::Assert(
|
||||
assert_stmt @ ast::StmtAssert {
|
||||
test,
|
||||
msg,
|
||||
range: _,
|
||||
}) => {
|
||||
},
|
||||
) => {
|
||||
if !checker.semantic.in_type_checking_block() {
|
||||
if checker.enabled(Rule::Assert) {
|
||||
checker
|
||||
|
@ -1267,6 +1269,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
if checker.enabled(Rule::InvalidMockAccess) {
|
||||
pygrep_hooks::rules::non_existent_mock_method(checker, test);
|
||||
}
|
||||
if checker.enabled(Rule::AssertWithPrintMessage) {
|
||||
ruff::rules::assert_with_print_message(checker, assert_stmt);
|
||||
}
|
||||
}
|
||||
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
|
|
|
@ -977,6 +977,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
|
||||
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
|
||||
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
|
||||
(Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
|
|
@ -54,6 +54,7 @@ mod tests {
|
|||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
|
||||
#[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))]
|
||||
#[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))]
|
||||
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `assert expression, print(message)`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The return value of the second expression is used as the contents of the
|
||||
/// `AssertionError` raised by the `assert` statement. Using a `print` expression
|
||||
/// in this context will output the message to `stdout`, before raising an
|
||||
/// empty `AssertionError(None)`.
|
||||
///
|
||||
/// Instead, remove the `print` and pass the message directly as the second
|
||||
/// expression, allowing `stderr` to capture the message in a well-formatted context.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// assert False, print("This is a message")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// assert False, "This is a message"
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as changing the second expression
|
||||
/// will result in a different `AssertionError` message being raised, as well as
|
||||
/// a change in `stdout` output.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
|
||||
#[violation]
|
||||
pub struct AssertWithPrintMessage;
|
||||
|
||||
impl AlwaysFixableViolation for AssertWithPrintMessage {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`print()` expression in `assert` statement is likely unintentional")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove `print`".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF030
|
||||
///
|
||||
/// Checks if the `msg` argument to an `assert` statement is a `print` call, and if so,
|
||||
/// replace the message with the arguments to the `print` call.
|
||||
pub(crate) fn assert_with_print_message(checker: &mut Checker, stmt: &ast::StmtAssert) {
|
||||
if let Some(Expr::Call(call)) = stmt.msg.as_deref() {
|
||||
// We have to check that the print call is a call to the built-in `print` function
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if semantic.match_builtin_expr(&call.func, "print") {
|
||||
// This is the confirmed rule condition
|
||||
let mut diagnostic = Diagnostic::new(AssertWithPrintMessage, call.range());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
checker.generator().stmt(&Stmt::Assert(ast::StmtAssert {
|
||||
test: stmt.test.clone(),
|
||||
msg: print_arguments::to_expr(&call.arguments).map(Box::new),
|
||||
range: TextRange::default(),
|
||||
})),
|
||||
// We have to replace the entire statement,
|
||||
// as the `print` could be empty and thus `call.range()`
|
||||
// will cease to exist.
|
||||
stmt.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the arguments from a `print` call and converts them to some kind of string
|
||||
/// expression.
|
||||
///
|
||||
/// Three cases are handled:
|
||||
/// - if there are no arguments, return `None` so that `diagnostic` can remove `msg` from `assert`;
|
||||
/// - if all of `print` arguments including `sep` are string literals, return a `Expr::StringLiteral`;
|
||||
/// - otherwise, return a `Expr::FString`.
|
||||
mod print_arguments {
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast::{
|
||||
Arguments, ConversionFlag, Expr, ExprFString, ExprStringLiteral, FString, FStringElement,
|
||||
FStringElements, FStringExpressionElement, FStringFlags, FStringLiteralElement,
|
||||
FStringValue, StringLiteral, StringLiteralFlags, StringLiteralValue,
|
||||
};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
/// Converts an expression to a list of `FStringElement`s.
|
||||
///
|
||||
/// Three cases are handled:
|
||||
/// - if the expression is a string literal, each part of the string will be converted to a
|
||||
/// `FStringLiteralElement`.
|
||||
/// - if the expression is an f-string, the elements will be returned as-is.
|
||||
/// - otherwise, the expression will be wrapped in a `FStringExpressionElement`.
|
||||
fn expr_to_fstring_elements(expr: &Expr) -> Vec<FStringElement> {
|
||||
match expr {
|
||||
// If the expression is a string literal, convert each part to a `FStringLiteralElement`.
|
||||
Expr::StringLiteral(string) => string
|
||||
.value
|
||||
.iter()
|
||||
.map(|part| {
|
||||
FStringElement::Literal(FStringLiteralElement {
|
||||
value: part.value.clone(),
|
||||
range: TextRange::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
||||
// If the expression is an f-string, return the elements.
|
||||
Expr::FString(fstring) => fstring.value.elements().cloned().collect(),
|
||||
|
||||
// Otherwise, return the expression as a single `FStringExpressionElement` wrapping
|
||||
// the expression.
|
||||
expr => vec![FStringElement::Expression(FStringExpressionElement {
|
||||
expression: Box::new(expr.clone()),
|
||||
debug_text: None,
|
||||
conversion: ConversionFlag::None,
|
||||
format_spec: None,
|
||||
range: TextRange::default(),
|
||||
})],
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a list of `FStringElement`s to a list of `StringLiteral`s.
|
||||
///
|
||||
/// If any of the elements are not string literals, `None` is returned.
|
||||
///
|
||||
/// This is useful (in combination with [`expr_to_fstring_elements`]) for
|
||||
/// checking if the `sep` and `args` arguments to `print` are all string
|
||||
/// literals.
|
||||
fn fstring_elements_to_string_literals<'a>(
|
||||
mut elements: impl ExactSizeIterator<Item = &'a FStringElement>,
|
||||
) -> Option<Vec<StringLiteral>> {
|
||||
elements.try_fold(Vec::with_capacity(elements.len()), |mut acc, element| {
|
||||
if let FStringElement::Literal(literal) = element {
|
||||
acc.push(StringLiteral {
|
||||
value: literal.value.clone(),
|
||||
flags: StringLiteralFlags::default(),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
Some(acc)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the `sep` and `args` arguments to a [`Expr::StringLiteral`].
|
||||
///
|
||||
/// This function will return [`None`] if any of the arguments are not string literals,
|
||||
/// or if there are no arguments at all.
|
||||
fn args_to_string_literal_expr<'a>(
|
||||
args: impl ExactSizeIterator<Item = &'a Vec<FStringElement>>,
|
||||
sep: impl ExactSizeIterator<Item = &'a FStringElement>,
|
||||
) -> Option<Expr> {
|
||||
// If there are no arguments, short-circuit and return `None`
|
||||
if args.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Attempt to convert the `sep` and `args` arguments to string literals.
|
||||
// We need to maintain `args` as a Vec of Vecs, as the first Vec represents
|
||||
// the arguments to the `print` call, and the inner Vecs represent the elements
|
||||
// of a concatenated string literal. (e.g. "text", "text" "text") The `sep` will
|
||||
// be inserted only between the outer Vecs.
|
||||
let (Some(sep), Some(args)) = (
|
||||
fstring_elements_to_string_literals(sep),
|
||||
args.map(|arg| fstring_elements_to_string_literals(arg.iter()))
|
||||
.collect::<Option<Vec<_>>>(),
|
||||
) else {
|
||||
// If any of the arguments are not string literals, return None
|
||||
return None;
|
||||
};
|
||||
|
||||
// Put the `sep` into a single Rust `String`
|
||||
let sep_string = sep
|
||||
.into_iter()
|
||||
.map(|string_literal| string_literal.value)
|
||||
.join("");
|
||||
|
||||
// Join the `args` with the `sep`
|
||||
let combined_string = args
|
||||
.into_iter()
|
||||
.map(|string_literals| {
|
||||
string_literals
|
||||
.into_iter()
|
||||
.map(|string_literal| string_literal.value)
|
||||
.join("")
|
||||
})
|
||||
.join(&sep_string);
|
||||
|
||||
Some(Expr::StringLiteral(ExprStringLiteral {
|
||||
range: TextRange::default(),
|
||||
value: StringLiteralValue::single(StringLiteral {
|
||||
value: combined_string.into(),
|
||||
flags: StringLiteralFlags::default(),
|
||||
range: TextRange::default(),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Converts the `sep` and `args` arguments to a [`Expr::FString`].
|
||||
///
|
||||
/// This function will only return [`None`] if there are no arguments at all.
|
||||
///
|
||||
/// ## Note
|
||||
/// This function will always return an f-string, even if all arguments are string literals.
|
||||
/// This can produce unnecessary f-strings.
|
||||
///
|
||||
/// Also note that the iterator arguments of this function are consumed,
|
||||
/// as opposed to the references taken by [`args_to_string_literal_expr`].
|
||||
fn args_to_fstring_expr(
|
||||
mut args: impl ExactSizeIterator<Item = Vec<FStringElement>>,
|
||||
sep: impl ExactSizeIterator<Item = FStringElement>,
|
||||
) -> Option<Expr> {
|
||||
// If there are no arguments, short-circuit and return `None`
|
||||
let first_arg = args.next()?;
|
||||
let sep = sep.collect::<Vec<_>>();
|
||||
|
||||
let fstring_elements = args.fold(first_arg, |mut elements, arg| {
|
||||
elements.extend(sep.clone());
|
||||
elements.extend(arg);
|
||||
elements
|
||||
});
|
||||
|
||||
Some(Expr::FString(ExprFString {
|
||||
value: FStringValue::single(FString {
|
||||
elements: FStringElements::from(fstring_elements),
|
||||
flags: FStringFlags::default(),
|
||||
range: TextRange::default(),
|
||||
}),
|
||||
range: TextRange::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Attempts to convert the `print` arguments to a suitable string expression.
|
||||
///
|
||||
/// If the `sep` argument is provided, it will be used as the separator between
|
||||
/// arguments. Otherwise, a space will be used.
|
||||
///
|
||||
/// `end` and `file` keyword arguments are ignored, as they don't affect the
|
||||
/// output of the `print` statement.
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// - [`Some`]<[`Expr::StringLiteral`]> if all arguments including `sep` are string literals.
|
||||
/// - [`Some`]<[`Expr::FString`]> if any of the arguments are not string literals.
|
||||
/// - [`None`] if the `print` contains no positional arguments at all.
|
||||
pub(super) fn to_expr(arguments: &Arguments) -> Option<Expr> {
|
||||
// Convert the `sep` argument into `FStringElement`s
|
||||
let sep = arguments
|
||||
.find_keyword("sep")
|
||||
.and_then(
|
||||
// If the `sep` argument is `None`, treat this as default behavior.
|
||||
|keyword| {
|
||||
if let Expr::NoneLiteral(_) = keyword.value {
|
||||
None
|
||||
} else {
|
||||
Some(&keyword.value)
|
||||
}
|
||||
},
|
||||
)
|
||||
.map(expr_to_fstring_elements)
|
||||
.unwrap_or_else(|| {
|
||||
vec![FStringElement::Literal(FStringLiteralElement {
|
||||
range: TextRange::default(),
|
||||
value: " ".into(),
|
||||
})]
|
||||
});
|
||||
|
||||
let args = arguments
|
||||
.args
|
||||
.iter()
|
||||
.map(expr_to_fstring_elements)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Attempt to convert the `sep` and `args` arguments to a string literal,
|
||||
// falling back to an f-string if the arguments are not all string literals.
|
||||
args_to_string_literal_expr(args.iter(), sep.iter())
|
||||
.or_else(|| args_to_fstring_expr(args.into_iter(), sep.into_iter()))
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub(crate) use ambiguous_unicode_character::*;
|
||||
pub(crate) use assert_with_print_message::*;
|
||||
pub(crate) use assignment_in_assert::*;
|
||||
pub(crate) use asyncio_dangling_task::*;
|
||||
pub(crate) use collection_literal_concatenation::*;
|
||||
|
@ -30,6 +31,7 @@ pub(crate) use unused_async::*;
|
|||
pub(crate) use unused_noqa::*;
|
||||
|
||||
mod ambiguous_unicode_character;
|
||||
mod assert_with_print_message;
|
||||
mod assignment_in_assert;
|
||||
mod asyncio_dangling_task;
|
||||
mod collection_literal_concatenation;
|
||||
|
|
|
@ -0,0 +1,396 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF030.py:6:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
4 | # Expects:
|
||||
5 | # - single StringLiteral
|
||||
6 | assert True, print("This print is not intentional.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
7 |
|
||||
8 | # Concatenated string literals
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
3 3 | # Standard Case
|
||||
4 4 | # Expects:
|
||||
5 5 | # - single StringLiteral
|
||||
6 |-assert True, print("This print is not intentional.")
|
||||
6 |+assert True, "This print is not intentional."
|
||||
7 7 |
|
||||
8 8 | # Concatenated string literals
|
||||
9 9 | # Expects:
|
||||
|
||||
RUF030.py:11:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
9 | # Expects:
|
||||
10 | # - single StringLiteral
|
||||
11 | assert True, print("This print" " is not intentional.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
12 |
|
||||
13 | # Positional arguments, string literals
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | # Concatenated string literals
|
||||
9 9 | # Expects:
|
||||
10 10 | # - single StringLiteral
|
||||
11 |-assert True, print("This print" " is not intentional.")
|
||||
11 |+assert True, "This print is not intentional."
|
||||
12 12 |
|
||||
13 13 | # Positional arguments, string literals
|
||||
14 14 | # Expects:
|
||||
|
||||
RUF030.py:16:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
14 | # Expects:
|
||||
15 | # - single StringLiteral concatenated with " "
|
||||
16 | assert True, print("This print", "is not intentional")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
17 |
|
||||
18 | # Concatenated string literals combined with Positional arguments
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
13 13 | # Positional arguments, string literals
|
||||
14 14 | # Expects:
|
||||
15 15 | # - single StringLiteral concatenated with " "
|
||||
16 |-assert True, print("This print", "is not intentional")
|
||||
16 |+assert True, "This print is not intentional"
|
||||
17 17 |
|
||||
18 18 | # Concatenated string literals combined with Positional arguments
|
||||
19 19 | # Expects:
|
||||
|
||||
RUF030.py:21:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
19 | # Expects:
|
||||
20 | # - single stringliteral concatenated with " " only between `print` and `is`
|
||||
21 | assert True, print("This " "print", "is not intentional.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
22 |
|
||||
23 | # Positional arguments, string literals with a variable
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
18 18 | # Concatenated string literals combined with Positional arguments
|
||||
19 19 | # Expects:
|
||||
20 20 | # - single stringliteral concatenated with " " only between `print` and `is`
|
||||
21 |-assert True, print("This " "print", "is not intentional.")
|
||||
21 |+assert True, "This print is not intentional."
|
||||
22 22 |
|
||||
23 23 | # Positional arguments, string literals with a variable
|
||||
24 24 | # Expects:
|
||||
|
||||
RUF030.py:26:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
24 | # Expects:
|
||||
25 | # - single FString concatenated with " "
|
||||
26 | assert True, print("This", print.__name__, "is not intentional.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
27 |
|
||||
28 | # Mixed brackets string literals
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
23 23 | # Positional arguments, string literals with a variable
|
||||
24 24 | # Expects:
|
||||
25 25 | # - single FString concatenated with " "
|
||||
26 |-assert True, print("This", print.__name__, "is not intentional.")
|
||||
26 |+assert True, f"This {print.__name__} is not intentional."
|
||||
27 27 |
|
||||
28 28 | # Mixed brackets string literals
|
||||
29 29 | # Expects:
|
||||
|
||||
RUF030.py:31:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
29 | # Expects:
|
||||
30 | # - single StringLiteral concatenated with " "
|
||||
31 | assert True, print("This print", 'is not intentional', """and should be removed""")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
32 |
|
||||
33 | # Mixed brackets with other brackets inside
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
28 28 | # Mixed brackets string literals
|
||||
29 29 | # Expects:
|
||||
30 30 | # - single StringLiteral concatenated with " "
|
||||
31 |-assert True, print("This print", 'is not intentional', """and should be removed""")
|
||||
31 |+assert True, "This print is not intentional and should be removed"
|
||||
32 32 |
|
||||
33 33 | # Mixed brackets with other brackets inside
|
||||
34 34 | # Expects:
|
||||
|
||||
RUF030.py:36:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
34 | # Expects:
|
||||
35 | # - single StringLiteral concatenated with " " and escaped brackets
|
||||
36 | assert True, print("This print", 'is not "intentional"', """and "should" be 'removed'""")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
37 |
|
||||
38 | # Positional arguments, string literals with a separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
33 33 | # Mixed brackets with other brackets inside
|
||||
34 34 | # Expects:
|
||||
35 35 | # - single StringLiteral concatenated with " " and escaped brackets
|
||||
36 |-assert True, print("This print", 'is not "intentional"', """and "should" be 'removed'""")
|
||||
36 |+assert True, "This print is not \"intentional\" and \"should\" be 'removed'"
|
||||
37 37 |
|
||||
38 38 | # Positional arguments, string literals with a separator
|
||||
39 39 | # Expects:
|
||||
|
||||
RUF030.py:41:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
39 | # Expects:
|
||||
40 | # - single StringLiteral concatenated with "|"
|
||||
41 | assert True, print("This print", "is not intentional", sep="|")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
42 |
|
||||
43 | # Positional arguments, string literals with None as separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
38 38 | # Positional arguments, string literals with a separator
|
||||
39 39 | # Expects:
|
||||
40 40 | # - single StringLiteral concatenated with "|"
|
||||
41 |-assert True, print("This print", "is not intentional", sep="|")
|
||||
41 |+assert True, "This print|is not intentional"
|
||||
42 42 |
|
||||
43 43 | # Positional arguments, string literals with None as separator
|
||||
44 44 | # Expects:
|
||||
|
||||
RUF030.py:46:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
44 | # Expects:
|
||||
45 | # - single StringLiteral concatenated with " "
|
||||
46 | assert True, print("This print", "is not intentional", sep=None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
47 |
|
||||
48 | # Positional arguments, string literals with variable as separator, needs f-string
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
43 43 | # Positional arguments, string literals with None as separator
|
||||
44 44 | # Expects:
|
||||
45 45 | # - single StringLiteral concatenated with " "
|
||||
46 |-assert True, print("This print", "is not intentional", sep=None)
|
||||
46 |+assert True, "This print is not intentional"
|
||||
47 47 |
|
||||
48 48 | # Positional arguments, string literals with variable as separator, needs f-string
|
||||
49 49 | # Expects:
|
||||
|
||||
RUF030.py:51:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
49 | # Expects:
|
||||
50 | # - single FString concatenated with "{U00A0}"
|
||||
51 | assert True, print("This print", "is not intentional", sep=U00A0)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
52 |
|
||||
53 | # Unnecessary f-string
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
48 48 | # Positional arguments, string literals with variable as separator, needs f-string
|
||||
49 49 | # Expects:
|
||||
50 50 | # - single FString concatenated with "{U00A0}"
|
||||
51 |-assert True, print("This print", "is not intentional", sep=U00A0)
|
||||
51 |+assert True, f"This print{U00A0}is not intentional"
|
||||
52 52 |
|
||||
53 53 | # Unnecessary f-string
|
||||
54 54 | # Expects:
|
||||
|
||||
RUF030.py:56:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
54 | # Expects:
|
||||
55 | # - single StringLiteral
|
||||
56 | assert True, print(f"This f-string is just a literal.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
57 |
|
||||
58 | # Positional arguments, string literals and f-strings
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
53 53 | # Unnecessary f-string
|
||||
54 54 | # Expects:
|
||||
55 55 | # - single StringLiteral
|
||||
56 |-assert True, print(f"This f-string is just a literal.")
|
||||
56 |+assert True, "This f-string is just a literal."
|
||||
57 57 |
|
||||
58 58 | # Positional arguments, string literals and f-strings
|
||||
59 59 | # Expects:
|
||||
|
||||
RUF030.py:61:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
59 | # Expects:
|
||||
60 | # - single FString concatenated with " "
|
||||
61 | assert True, print("This print", f"is not {'intentional':s}")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
62 |
|
||||
63 | # Positional arguments, string literals and f-strings with a separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
58 58 | # Positional arguments, string literals and f-strings
|
||||
59 59 | # Expects:
|
||||
60 60 | # - single FString concatenated with " "
|
||||
61 |-assert True, print("This print", f"is not {'intentional':s}")
|
||||
61 |+assert True, f"This print is not {'intentional':s}"
|
||||
62 62 |
|
||||
63 63 | # Positional arguments, string literals and f-strings with a separator
|
||||
64 64 | # Expects:
|
||||
|
||||
RUF030.py:66:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
64 | # Expects:
|
||||
65 | # - single FString concatenated with "|"
|
||||
66 | assert True, print("This print", f"is not {'intentional':s}", sep="|")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
67 |
|
||||
68 | # A single f-string
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
63 63 | # Positional arguments, string literals and f-strings with a separator
|
||||
64 64 | # Expects:
|
||||
65 65 | # - single FString concatenated with "|"
|
||||
66 |-assert True, print("This print", f"is not {'intentional':s}", sep="|")
|
||||
66 |+assert True, f"This print|is not {'intentional':s}"
|
||||
67 67 |
|
||||
68 68 | # A single f-string
|
||||
69 69 | # Expects:
|
||||
|
||||
RUF030.py:71:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
69 | # Expects:
|
||||
70 | # - single FString
|
||||
71 | assert True, print(f"This print is not {'intentional':s}")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
72 |
|
||||
73 | # A single f-string with a redundant separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
68 68 | # A single f-string
|
||||
69 69 | # Expects:
|
||||
70 70 | # - single FString
|
||||
71 |-assert True, print(f"This print is not {'intentional':s}")
|
||||
71 |+assert True, f"This print is not {'intentional':s}"
|
||||
72 72 |
|
||||
73 73 | # A single f-string with a redundant separator
|
||||
74 74 | # Expects:
|
||||
|
||||
RUF030.py:76:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
74 | # Expects:
|
||||
75 | # - single FString
|
||||
76 | assert True, print(f"This print is not {'intentional':s}", sep="|")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
77 |
|
||||
78 | # Complex f-string with variable as separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
73 73 | # A single f-string with a redundant separator
|
||||
74 74 | # Expects:
|
||||
75 75 | # - single FString
|
||||
76 |-assert True, print(f"This print is not {'intentional':s}", sep="|")
|
||||
76 |+assert True, f"This print is not {'intentional':s}"
|
||||
77 77 |
|
||||
78 78 | # Complex f-string with variable as separator
|
||||
79 79 | # Expects:
|
||||
|
||||
RUF030.py:83:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
81 | condition = "True is True"
|
||||
82 | maintainer = "John Doe"
|
||||
83 | assert True, print("Unreachable due to", condition, f", ask {maintainer} for advice", sep=U00A0)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
84 |
|
||||
85 | # Empty print
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
80 80 | # - single FString concatenated with "{U00A0}", all placeholders preserved
|
||||
81 81 | condition = "True is True"
|
||||
82 82 | maintainer = "John Doe"
|
||||
83 |-assert True, print("Unreachable due to", condition, f", ask {maintainer} for advice", sep=U00A0)
|
||||
83 |+assert True, f"Unreachable due to{U00A0}{condition}{U00A0}, ask {maintainer} for advice"
|
||||
84 84 |
|
||||
85 85 | # Empty print
|
||||
86 86 | # Expects:
|
||||
|
||||
RUF030.py:88:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
86 | # Expects:
|
||||
87 | # - `msg` entirely removed from assertion
|
||||
88 | assert True, print()
|
||||
| ^^^^^^^ RUF030
|
||||
89 |
|
||||
90 | # Empty print with separator
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
85 85 | # Empty print
|
||||
86 86 | # Expects:
|
||||
87 87 | # - `msg` entirely removed from assertion
|
||||
88 |-assert True, print()
|
||||
88 |+assert True
|
||||
89 89 |
|
||||
90 90 | # Empty print with separator
|
||||
91 91 | # Expects:
|
||||
|
||||
RUF030.py:93:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
91 | # Expects:
|
||||
92 | # - `msg` entirely removed from assertion
|
||||
93 | assert True, print(sep=" ")
|
||||
| ^^^^^^^^^^^^^^ RUF030
|
||||
94 |
|
||||
95 | # Custom print function that actually returns a string
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
90 90 | # Empty print with separator
|
||||
91 91 | # Expects:
|
||||
92 92 | # - `msg` entirely removed from assertion
|
||||
93 |-assert True, print(sep=" ")
|
||||
93 |+assert True
|
||||
94 94 |
|
||||
95 95 | # Custom print function that actually returns a string
|
||||
96 96 | # Expects:
|
||||
|
||||
RUF030.py:108:14: RUF030 [*] `print()` expression in `assert` statement is likely unintentional
|
||||
|
|
||||
106 | # Expects:
|
||||
107 | # - single StringLiteral
|
||||
108 | assert True, builtins.print("This print should be removed.")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF030
|
||||
|
|
||||
= help: Remove `print`
|
||||
|
||||
ℹ Unsafe fix
|
||||
105 105 | # Use of `builtins.print`
|
||||
106 106 | # Expects:
|
||||
107 107 | # - single StringLiteral
|
||||
108 |-assert True, builtins.print("This print should be removed.")
|
||||
108 |+assert True, "This print should be removed."
|
2
ruff.schema.json
generated
2
ruff.schema.json
generated
|
@ -3641,6 +3641,8 @@
|
|||
"RUF027",
|
||||
"RUF028",
|
||||
"RUF029",
|
||||
"RUF03",
|
||||
"RUF030",
|
||||
"RUF1",
|
||||
"RUF10",
|
||||
"RUF100",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue