diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF040.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF040.py new file mode 100644 index 0000000000000..5787f2d14dcbf --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF040.py @@ -0,0 +1,5 @@ +fruits = ["apples", "plums", "pear"] +fruits.filter(lambda fruit: fruit.startwith("p")) +assert len(fruits), 2 + +assert True, "always true" \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e9521d5f752d5..61e1bf43a6356 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1276,6 +1276,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::AssertWithPrintMessage) { ruff::rules::assert_with_print_message(checker, assert_stmt); } + if checker.enabled(Rule::InvalidAssertMessageLiteralArgument) { + ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt); + } } Stmt::With( with_stmt @ ast::StmtWith { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 20db1269bc6c7..5dc33a1c36216 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -978,6 +978,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "038") => (RuleGroup::Preview, rules::ruff::rules::RedundantBoolLiteral), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), (Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern), + (Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 4cec7109a8fae..aad87d1d336c6 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -66,6 +66,7 @@ mod tests { #[test_case(Rule::NoneNotAtEndOfUnion, Path::new("RUF036.pyi"))] #[test_case(Rule::RedundantBoolLiteral, Path::new("RUF038.py"))] #[test_case(Rule::RedundantBoolLiteral, Path::new("RUF038.pyi"))] + #[test_case(Rule::InvalidAssertMessageLiteralArgument, Path::new("RUF040.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs new file mode 100644 index 0000000000000..ddb72df5450cb --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs @@ -0,0 +1,59 @@ +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{Expr, StmtAssert}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for invalid use of literals in assert message argument. +/// +/// ## Why is this bad? +/// An assert message which is a non-string literal was likely intended +/// to be used in a comparison assertion, rather than as a message. +/// +/// ## Example +/// ```python +/// fruits = ["apples", "plums", "pears"] +/// fruits.filter(lambda fruit: fruit.startwith("p")) +/// assert len(fruits), 2 # True unless the list is empty +/// ``` +/// +/// Use instead: +/// ```python +/// fruits = ["apples", "plums", "pears"] +/// fruits.filter(lambda fruit: fruit.startwith("p")) +/// assert len(fruits) == 2 +/// ``` +#[violation] +pub struct InvalidAssertMessageLiteralArgument; + +impl Violation for InvalidAssertMessageLiteralArgument { + #[derive_message_formats] + fn message(&self) -> String { + "Non-string literal used as assert message".to_string() + } +} + +/// RUF040 +pub(crate) fn invalid_assert_message_literal_argument(checker: &mut Checker, stmt: &StmtAssert) { + let Some(message) = stmt.msg.as_deref() else { + return; + }; + + if !matches!( + message, + Expr::NumberLiteral(_) + | Expr::BooleanLiteral(_) + | Expr::NoneLiteral(_) + | Expr::EllipsisLiteral(_) + | Expr::BytesLiteral(_) + ) { + return; + } + + checker.diagnostics.push(Diagnostic::new( + InvalidAssertMessageLiteralArgument, + message.range(), + )); +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 1b789e7b0f4a0..95f79145d55b8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -9,6 +9,7 @@ pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use function_call_in_dataclass_default::*; pub(crate) use implicit_optional::*; pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*; +pub(crate) use invalid_assert_message_literal_argument::*; pub(crate) use invalid_formatter_suppression_comment::*; pub(crate) use invalid_index_type::*; pub(crate) use invalid_pyproject_toml::*; @@ -51,6 +52,7 @@ mod function_call_in_dataclass_default; mod helpers; mod implicit_optional; mod incorrectly_parenthesized_tuple_in_subscript; +mod invalid_assert_message_literal_argument; mod invalid_formatter_suppression_comment; mod invalid_index_type; mod invalid_pyproject_toml; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF040_RUF040.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF040_RUF040.py.snap new file mode 100644 index 0000000000000..bbac23424e93f --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF040_RUF040.py.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF040.py:3:21: RUF040 Non-string literal used as assert message + | +1 | fruits = ["apples", "plums", "pear"] +2 | fruits.filter(lambda fruit: fruit.startwith("p")) +3 | assert len(fruits), 2 + | ^ RUF040 +4 | +5 | assert True, "always true" + | diff --git a/ruff.schema.json b/ruff.schema.json index 1e57e8443517d..c70b5df565454 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3833,6 +3833,7 @@ "RUF038", "RUF039", "RUF04", + "RUF040", "RUF048", "RUF1", "RUF10",