From b1e354bd9940a7de3859cea29c7dae8a58eb6b10 Mon Sep 17 00:00:00 2001 From: Ruchir <61278001+Ruchir28@users.noreply.github.com> Date: Tue, 18 Nov 2025 02:22:24 +0530 Subject: [PATCH] [`ruff`] Avoid false positive on `ClassVar` reassignment (`RUF012`) (#21478) ## Summary Fixes #21389 Avoid RUF012 false positives when reassigning a ClassVar ## Test Plan Added the new reassignment scenario to `crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py`. --------- Co-authored-by: Brent Westbrook --- .../resources/test/fixtures/ruff/RUF012.py | 6 ++++++ .../rules/ruff/rules/mutable_class_default.rs | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py index eb27b31726..c6a27523da 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF012.py @@ -132,3 +132,9 @@ class AWithQuotes: final_variable: 'Final[list[int]]' = [] class_variable_without_subscript: 'ClassVar' = [] final_variable_without_subscript: 'Final' = [] + + +# Reassignment of a ClassVar should not trigger RUF012 +class P: + class_variable: ClassVar[list] = [10, 20, 30, 40, 50] + class_variable = [*class_variable[0::1], *class_variable[2::3]] diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs index 6dd1e1df59..991008f662 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs @@ -1,6 +1,7 @@ -use ruff_python_ast::{self as ast, Stmt}; +use rustc_hash::FxHashSet; use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Stmt}; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use ruff_text_size::Ranged; @@ -96,6 +97,9 @@ impl Violation for MutableClassDefault { /// RUF012 pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) { + // Collect any `ClassVar`s we find in case they get reassigned later. + let mut class_var_targets = FxHashSet::default(); + for statement in &class_def.body { match statement { Stmt::AnnAssign(ast::StmtAnnAssign { @@ -104,6 +108,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas value: Some(value), .. }) => { + if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + if is_class_var_annotation(annotation, checker.semantic()) { + class_var_targets.insert(id); + } + } + if !is_special_attribute(target) && is_mutable_expr(value, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic()) @@ -123,8 +133,12 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas } } Stmt::Assign(ast::StmtAssign { value, targets, .. }) => { - if !targets.iter().all(is_special_attribute) - && is_mutable_expr(value, checker.semantic()) + if !targets.iter().all(|target| { + is_special_attribute(target) + || target + .as_name_expr() + .is_some_and(|name| class_var_targets.contains(&name.id)) + }) && is_mutable_expr(value, checker.semantic()) { // Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation. if has_default_copy_semantics(class_def, checker.semantic()) {