diff --git a/crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py b/crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py new file mode 100644 index 0000000000..2dcc56cffd --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/named_expr_without_context.py @@ -0,0 +1,19 @@ +# Errors +(a := 42) +if True: + (b := 1) + + +class Foo: + (c := 1) + + +# OK +if a := 42: + print("Success") + +a = 0 +while (a := a + 1) < 10: + print("Correct") + +a = (b := 1) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3a41d785ab..15f6a24085 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1895,6 +1895,9 @@ where if self.settings.rules.enabled(Rule::InvalidMockAccess) { pygrep_hooks::rules::uncalled_mock_method(self, value); } + if self.settings.rules.enabled(Rule::NamedExprWithoutContext) { + pylint::rules::named_expr_without_context(self, value); + } if self.settings.rules.enabled(Rule::AsyncioDanglingTask) { if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| { self.ctx.resolve_call_path(expr) diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 7d2ddb5160..6295daafd8 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -185,6 +185,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R5501") => (RuleGroup::Unspecified, Rule::CollapsibleElseIf), (Pylint, "W0120") => (RuleGroup::Unspecified, Rule::UselessElseOnLoop), (Pylint, "W0129") => (RuleGroup::Unspecified, Rule::AssertOnStringLiteral), + (Pylint, "W0131") => (RuleGroup::Unspecified, Rule::NamedExprWithoutContext), (Pylint, "W0406") => (RuleGroup::Unspecified, Rule::ImportSelf), (Pylint, "W0602") => (RuleGroup::Unspecified, Rule::GlobalVariableNotAssigned), (Pylint, "W0603") => (RuleGroup::Unspecified, Rule::GlobalStatement), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index d5c0cc0715..367d4c588d 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -161,6 +161,7 @@ ruff_macros::register_rules!( rules::pylint::rules::NestedMinMax, rules::pylint::rules::DuplicateValue, rules::pylint::rules::DuplicateBases, + rules::pylint::rules::NamedExprWithoutContext, // flake8-async rules::flake8_async::rules::BlockingHttpCallInAsyncFunction, rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction, diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 2a7d12296d..87bd4ce8e8 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -58,6 +58,7 @@ mod tests { #[test_case(Rule::LoggingTooFewArgs, Path::new("logging_too_few_args.py"); "PLE1206")] #[test_case(Rule::LoggingTooManyArgs, Path::new("logging_too_many_args.py"); "PLE1205")] #[test_case(Rule::MagicValueComparison, Path::new("magic_value_comparison.py"); "PLR2004")] + #[test_case(Rule::NamedExprWithoutContext, Path::new("named_expr_without_context.py"); "PLW0131")] #[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"); "PLE0117")] #[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"); "PLR0206")] #[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"); "PLW2901")] diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 58c8480f0a..a75c21d076 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -27,6 +27,7 @@ pub(crate) use load_before_global_declaration::{ pub(crate) use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs}; pub(crate) use magic_value_comparison::{magic_value_comparison, MagicValueComparison}; pub(crate) use manual_import_from::{manual_from_import, ManualFromImport}; +pub(crate) use named_expr_without_context::{named_expr_without_context, NamedExprWithoutContext}; pub(crate) use nested_min_max::{nested_min_max, NestedMinMax}; pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding; pub(crate) use property_with_parameters::{property_with_parameters, PropertyWithParameters}; @@ -73,6 +74,7 @@ mod load_before_global_declaration; mod logging; mod magic_value_comparison; mod manual_import_from; +mod named_expr_without_context; mod nested_min_max; mod nonlocal_without_binding; mod property_with_parameters; diff --git a/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs new file mode 100644 index 0000000000..3576d36ea3 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs @@ -0,0 +1,44 @@ +use rustpython_parser::ast::{self, Expr}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for usages of named expressions (e.g., `a := 42`) that can be +/// replaced by regular assignment statements (e.g., `a = 42`). +/// +/// ## Why is this bad? +/// While a top-level named expression is syntactically and semantically valid, +/// it's less clear than a regular assignment statement. Named expressions are +/// intended to be used in comprehensions and generator expressions, where +/// assignment statements are not allowed. +/// +/// ## Example +/// ```python +/// (a := 42) +/// ``` +/// +/// Use instead: +/// ```python +/// a = 42 +/// ``` +#[violation] +pub struct NamedExprWithoutContext; + +impl Violation for NamedExprWithoutContext { + #[derive_message_formats] + fn message(&self) -> String { + format!("Named expression used without context") + } +} + +/// PLW0131 +pub(crate) fn named_expr_without_context(checker: &mut Checker, value: &Expr) { + if let Expr::NamedExpr(ast::ExprNamedExpr { range, .. }) = value { + checker + .diagnostics + .push(Diagnostic::new(NamedExprWithoutContext, *range)); + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap new file mode 100644 index 0000000000..141525c796 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +named_expr_without_context.py:2:2: PLW0131 Named expression used without context + | +2 | # Errors +3 | (a := 42) + | ^^^^^^^ PLW0131 +4 | if True: +5 | (b := 1) + | + +named_expr_without_context.py:4:6: PLW0131 Named expression used without context + | +4 | (a := 42) +5 | if True: +6 | (b := 1) + | ^^^^^^ PLW0131 + | + +named_expr_without_context.py:8:6: PLW0131 Named expression used without context + | +8 | class Foo: +9 | (c := 1) + | ^^^^^^ PLW0131 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 6066f3896c..9b05164ff0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2078,6 +2078,7 @@ "PLW0129", "PLW013", "PLW0130", + "PLW0131", "PLW04", "PLW040", "PLW0406",