[ruff] Add assert-with-print-expression rule (#11974) (#11981)

## 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:
Denny Wong 2024-06-23 17:54:55 +01:00 committed by GitHub
parent 0c8b5eb17a
commit c3f61a012e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 810 additions and 5 deletions

View 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.")

View file

@ -1232,11 +1232,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
} }
} }
} }
Stmt::Assert(ast::StmtAssert { Stmt::Assert(
test, assert_stmt @ ast::StmtAssert {
msg, test,
range: _, msg,
}) => { range: _,
},
) => {
if !checker.semantic.in_type_checking_block() { if !checker.semantic.in_type_checking_block() {
if checker.enabled(Rule::Assert) { if checker.enabled(Rule::Assert) {
checker checker
@ -1267,6 +1269,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidMockAccess) { if checker.enabled(Rule::InvalidMockAccess) {
pygrep_hooks::rules::non_existent_mock_method(checker, test); 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, .. }) => { Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::TooManyNestedBlocks) { if checker.enabled(Rule::TooManyNestedBlocks) {

View file

@ -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, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment), (Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync), (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, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA), (Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml), (Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View file

@ -54,6 +54,7 @@ mod tests {
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))] #[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
#[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))] #[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))]
#[test_case(Rule::UnusedAsync, Path::new("RUF029.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"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View file

@ -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()))
}
}

View file

@ -1,4 +1,5 @@
pub(crate) use ambiguous_unicode_character::*; pub(crate) use ambiguous_unicode_character::*;
pub(crate) use assert_with_print_message::*;
pub(crate) use assignment_in_assert::*; pub(crate) use assignment_in_assert::*;
pub(crate) use asyncio_dangling_task::*; pub(crate) use asyncio_dangling_task::*;
pub(crate) use collection_literal_concatenation::*; pub(crate) use collection_literal_concatenation::*;
@ -30,6 +31,7 @@ pub(crate) use unused_async::*;
pub(crate) use unused_noqa::*; pub(crate) use unused_noqa::*;
mod ambiguous_unicode_character; mod ambiguous_unicode_character;
mod assert_with_print_message;
mod assignment_in_assert; mod assignment_in_assert;
mod asyncio_dangling_task; mod asyncio_dangling_task;
mod collection_literal_concatenation; mod collection_literal_concatenation;

View file

@ -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
View file

@ -3641,6 +3641,8 @@
"RUF027", "RUF027",
"RUF028", "RUF028",
"RUF029", "RUF029",
"RUF03",
"RUF030",
"RUF1", "RUF1",
"RUF10", "RUF10",
"RUF100", "RUF100",