diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF008.py b/crates/ruff/resources/test/fixtures/ruff/RUF008.py index 4504714ae5..3a40f7f094 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF008.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF008.py @@ -1,6 +1,6 @@ import typing from dataclasses import dataclass, field -from typing import Sequence +from typing import ClassVar, Sequence KNOWINGLY_MUTABLE_DEFAULT = [] @@ -13,6 +13,7 @@ class A: ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) + class_variable: typing.ClassVar[list[int]] = [] @dataclass @@ -23,3 +24,4 @@ class B: ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) + class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF009.py b/crates/ruff/resources/test/fixtures/ruff/RUF009.py index cfe4e6b871..9a8a9e6ee2 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF009.py @@ -1,5 +1,6 @@ +import typing from dataclasses import dataclass -from typing import NamedTuple +from typing import ClassVar, NamedTuple def default_function() -> list[int]: @@ -13,6 +14,8 @@ class ImmutableType(NamedTuple): @dataclass() class A: hidden_mutable_default: list[int] = default_function() + class_variable: typing.ClassVar[list[int]] = default_function() + another_class_var: ClassVar[list[int]] = default_function() DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs index cd6e9e05eb..8d1d9839ef 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs @@ -151,13 +151,26 @@ fn is_allowed_func(context: &Context, func: &Expr) -> bool { }) } +/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. +fn is_class_var_annotation(context: &Context, annotation: &Expr) -> bool { + let ExprKind::Subscript { value, .. } = &annotation.node else { + return false; + }; + context.match_typing_expr(value, "ClassVar") +} + /// RUF009 pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { for statement in body { if let StmtKind::AnnAssign { - value: Some(expr), .. + annotation, + value: Some(expr), + .. } = &statement.node { + if is_class_var_annotation(&checker.ctx, annotation) { + continue; + } if let ExprKind::Call { func, .. } = &expr.node { if !is_allowed_func(&checker.ctx, func) { checker.diagnostics.push(Diagnostic::new( @@ -181,7 +194,10 @@ pub fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { value: Some(value), .. } => { - if !is_immutable_annotation(&checker.ctx, annotation) && is_mutable_expr(value) { + if !is_class_var_annotation(&checker.ctx, annotation) + && !is_immutable_annotation(&checker.ctx, annotation) + && is_mutable_expr(value) + { checker .diagnostics .push(Diagnostic::new(MutableDataclassDefault, Range::from(value))); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap index 653d496831..7d10aa9cad 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap @@ -21,24 +21,24 @@ RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attribut 16 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF008.py:20:34: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attributes | -20 | @dataclass -21 | class B: -22 | mutable_default: list[int] = [] +21 | @dataclass +22 | class B: +23 | mutable_default: list[int] = [] | ^^ RUF008 -23 | immutable_annotation: Sequence[int] = [] -24 | without_annotation = [] +24 | immutable_annotation: Sequence[int] = [] +25 | without_annotation = [] | -RUF008.py:22:26: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attributes | -22 | mutable_default: list[int] = [] -23 | immutable_annotation: Sequence[int] = [] -24 | without_annotation = [] +23 | mutable_default: list[int] = [] +24 | immutable_annotation: Sequence[int] = [] +25 | without_annotation = [] | ^^ RUF008 -25 | ignored_via_comment: list[int] = [] # noqa: RUF008 -26 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +26 | ignored_via_comment: list[int] = [] # noqa: RUF008 +27 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap index 25e81cdeb0..4f95c1be16 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap @@ -1,42 +1,44 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF009.py:15:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:16:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -15 | @dataclass() -16 | class A: -17 | hidden_mutable_default: list[int] = default_function() +16 | @dataclass() +17 | class A: +18 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 +19 | class_variable: typing.ClassVar[list[int]] = default_function() +20 | another_class_var: ClassVar[list[int]] = default_function() | -RUF009.py:24:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:27:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -24 | @dataclass -25 | class B: -26 | hidden_mutable_default: list[int] = default_function() +27 | @dataclass +28 | class B: +29 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -27 | another_dataclass: A = A() -28 | not_optimal: ImmutableType = ImmutableType(20) +30 | another_dataclass: A = A() +31 | not_optimal: ImmutableType = ImmutableType(20) | -RUF009.py:25:28: RUF009 Do not perform function call `A` in dataclass defaults +RUF009.py:28:28: RUF009 Do not perform function call `A` in dataclass defaults | -25 | class B: -26 | hidden_mutable_default: list[int] = default_function() -27 | another_dataclass: A = A() +28 | class B: +29 | hidden_mutable_default: list[int] = default_function() +30 | another_dataclass: A = A() | ^^^ RUF009 -28 | not_optimal: ImmutableType = ImmutableType(20) -29 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +31 | not_optimal: ImmutableType = ImmutableType(20) +32 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES | -RUF009.py:26:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults +RUF009.py:29:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults | -26 | hidden_mutable_default: list[int] = default_function() -27 | another_dataclass: A = A() -28 | not_optimal: ImmutableType = ImmutableType(20) +29 | hidden_mutable_default: list[int] = default_function() +30 | another_dataclass: A = A() +31 | not_optimal: ImmutableType = ImmutableType(20) | ^^^^^^^^^^^^^^^^^ RUF009 -29 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -30 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +32 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +33 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES |