diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF018.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF018.py new file mode 100644 index 00000000000000..f7d4e24c6ea3b2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF018.py @@ -0,0 +1,7 @@ +# RUF018 +assert (x := 0) == 0 +assert x, (y := "error") + +# OK +if z := 0: + pass diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 948bbb6f648af9..707d8e762bf987 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1413,6 +1413,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { pylint::rules::repeated_equality_comparison(checker, bool_op); } } + Expr::NamedExpr(..) => { + if checker.enabled(Rule::AssignmentInAssert) { + ruff::rules::assignment_in_assert(checker, expr); + } + } _ => {} }; } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 968a46f221a57f..f0b9a808040516 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -865,6 +865,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType), #[allow(deprecated)] (Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation), + (Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert), (Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Unspecified, 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 cdda430bdefaff..60ac081d0bcd68 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -42,6 +42,7 @@ mod tests { )] #[test_case(Rule::QuadraticListSummation, Path::new("RUF017_1.py"))] #[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))] + #[test_case(Rule::AssignmentInAssert, Path::new("RUF018.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/assignment_in_assert.rs b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs new file mode 100644 index 00000000000000..05761fe9967df5 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs @@ -0,0 +1,53 @@ +use ruff_python_ast::Expr; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for named assignment expressions (e.g., `x := 0`) in `assert` +/// statements. +/// +/// ## Why is this bad? +/// Named assignment expressions (also known as "walrus operators") are used to +/// assign a value to a variable as part of a larger expression. +/// +/// Named assignments are syntactically valid in `assert` statements. However, +/// when the Python interpreter is run under the `-O` flag, `assert` statements +/// are not executed. In this case, the named assignment will also be ignored, +/// which may result in unexpected behavior (e.g., undefined variable +/// accesses). +/// +/// ## Examples +/// ```python +/// assert (x := 0) == 0 +/// ``` +/// +/// Use instead: +/// ```python +/// x = 0 +/// assert x == 0 +/// ``` +/// +/// ## References +/// - [Python documentation: `-O`](https://docs.python.org/3/using/cmdline.html#cmdoption-O) +#[violation] +pub struct AssignmentInAssert; + +impl Violation for AssignmentInAssert { + #[derive_message_formats] + fn message(&self) -> String { + format!("Avoid assignment expressions in `assert` statements") + } +} + +/// RUF018 +pub(crate) fn assignment_in_assert(checker: &mut Checker, value: &Expr) { + if checker.semantic().current_statement().is_assert_stmt() { + checker + .diagnostics + .push(Diagnostic::new(AssignmentInAssert, value.range())); + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 8ffa324791c305..910c6e6379900e 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 assignment_in_assert::*; pub(crate) use asyncio_dangling_task::*; pub(crate) use collection_literal_concatenation::*; pub(crate) use explicit_f_string_type_conversion::*; @@ -16,6 +17,7 @@ pub(crate) use unreachable::*; pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; +mod assignment_in_assert; mod asyncio_dangling_task; mod collection_literal_concatenation; mod confusables; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF018_RUF018.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF018_RUF018.py.snap new file mode 100644 index 00000000000000..d4ceedb6246cff --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF018_RUF018.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF018.py:2:9: RUF018 Avoid assignment expressions in `assert` statements + | +1 | # RUF018 +2 | assert (x := 0) == 0 + | ^^^^^^ RUF018 +3 | assert x, (y := "error") + | + +RUF018.py:3:12: RUF018 Avoid assignment expressions in `assert` statements + | +1 | # RUF018 +2 | assert (x := 0) == 0 +3 | assert x, (y := "error") + | ^^^^^^^^^^^^ RUF018 +4 | +5 | # OK + | + + diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index d480ec3e73c570..629b2c913bc23f 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -1047,6 +1047,7 @@ mod tests { Rule::TooManyPublicMethods, Rule::UndocumentedWarn, Rule::UnnecessaryEnumerate, + Rule::AssignmentInAssert, ]; #[allow(clippy::needless_pass_by_value)] diff --git a/ruff.schema.json b/ruff.schema.json index 0149b046ee7753..cd341eb9f1efbe 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3145,6 +3145,7 @@ "RUF015", "RUF016", "RUF017", + "RUF018", "RUF1", "RUF10", "RUF100",