diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF030.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF030.py new file mode 100644 index 00000000000000..0f4e9a7610b814 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF030.py @@ -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.") diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 552fb66844b6b9..fdb27a664ccf71 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1232,11 +1232,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - Stmt::Assert(ast::StmtAssert { - test, - msg, - range: _, - }) => { + 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) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 89a0c7e5c7e895..14bd9e6848cce7 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -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), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 7bce548d85dd1a..c9708eb8482531 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -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()); diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs new file mode 100644 index 00000000000000..cf5b80f0048463 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -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 { + 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, + ) -> Option> { + 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>, + sep: impl ExactSizeIterator, + ) -> Option { + // 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::>>(), + ) 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>, + sep: impl ExactSizeIterator, + ) -> Option { + // If there are no arguments, short-circuit and return `None` + let first_arg = args.next()?; + let sep = sep.collect::>(); + + 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 { + // 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::>(); + + // 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())) + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index d93f4a781c477f..399aa8584abbd0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -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; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap new file mode 100644 index 00000000000000..aeea27858e9880 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap @@ -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." diff --git a/ruff.schema.json b/ruff.schema.json index 349889ac705cda..15779c1222558c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3641,6 +3641,8 @@ "RUF027", "RUF028", "RUF029", + "RUF03", + "RUF030", "RUF1", "RUF10", "RUF100",