mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-17 19:27:11 +00:00
Enhance RUF012 rule to handle ClassVar reassignment
This commit is contained in:
parent
665f68036c
commit
16afe0a5bc
2 changed files with 40 additions and 8 deletions
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
|
||||
|
|
@ -96,7 +97,20 @@ impl Violation for MutableClassDefault {
|
|||
|
||||
/// RUF012
|
||||
pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) {
|
||||
let mut class_var_targets: FxHashSet<String> = FxHashSet::default();
|
||||
|
||||
for statement in &class_def.body {
|
||||
if let Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
annotation, target, ..
|
||||
}) = statement
|
||||
{
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if is_class_var_annotation(annotation, checker.semantic()) {
|
||||
class_var_targets.insert(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match statement {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
annotation,
|
||||
|
|
@ -123,15 +137,27 @@ 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())
|
||||
{
|
||||
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
|
||||
if has_default_copy_semantics(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
if is_mutable_expr(value, checker.semantic()) {
|
||||
let has_mutable_non_class_var_target = targets.iter().any(|target| {
|
||||
if is_special_attribute(target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(MutableClassDefault, value.range());
|
||||
match target {
|
||||
ast::Expr::Name(ast::ExprName { id, .. }) => {
|
||||
!class_var_targets.contains(id.as_str())
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
|
||||
if has_mutable_non_class_var_target {
|
||||
if has_default_copy_semantics(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(MutableClassDefault, value.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue