From 43dc99bdafc9c8616df4c66dafcda68c447f2baf Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Mon, 24 Apr 2023 21:31:13 -0700 Subject: [PATCH 01/20] RUF008/9 for non-dataclasses as well --- .../resources/test/fixtures/ruff/RUF010.py | 21 +++++ .../resources/test/fixtures/ruff/RUF011.py | 27 +++++++ crates/ruff/src/checkers/ast/mod.rs | 36 +++++++-- crates/ruff/src/codes.rs | 2 + crates/ruff/src/registry.rs | 2 + crates/ruff/src/rules/ruff/mod.rs | 13 ++- crates/ruff/src/rules/ruff/rules/mod.rs | 9 ++- ...rs => mutable_defaults_in_class_fields.rs} | 81 +++++++++++++++---- ..._rules__ruff__tests__RUF010_RUF010.py.snap | 42 ++++++++++ ..._rules__ruff__tests__RUF011_RUF011.py.snap | 48 +++++++++++ ruff.schema.json | 3 + 11 files changed, 259 insertions(+), 25 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF010.py create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF011.py rename crates/ruff/src/rules/ruff/rules/{mutable_defaults_in_dataclass_fields.rs => mutable_defaults_in_class_fields.rs} (69%) create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py new file mode 100644 index 0000000000000..2cf96991fd276 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -0,0 +1,21 @@ +import typing +from dataclasses import dataclass, field +from typing import Sequence + +KNOWINGLY_MUTABLE_DEFAULT = [] + + +class A: + mutable_default: list[int] = [] + immutable_annotation: typing.Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF010 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + + +class B: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF010 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF011.py b/crates/ruff/resources/test/fixtures/ruff/RUF011.py new file mode 100644 index 0000000000000..ce604222baa30 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF011.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from typing import NamedTuple + + +def default_function() -> list[int]: + return [] + + +class ImmutableType(NamedTuple): + something: int = 8 + + +class A: + hidden_mutable_default: list[int] = default_function() + + +DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) +DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) + + +class B: + hidden_mutable_default: list[int] = default_function() + another_dataclass: A = A() + not_optimal: ImmutableType = ImmutableType(20) + good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES + okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES + field: list[int] = field(default_vactory=list) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 536ab26abe383..82ed1da8b30b2 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -843,19 +843,43 @@ where if self.settings.rules.any_enabled(&[ Rule::MutableDataclassDefault, + Rule::MutableClassDefault, Rule::FunctionCallInDataclassDefaultArgument, - ]) && ruff::rules::is_dataclass(self, decorator_list) - { - if self.settings.rules.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_dataclass_default(self, body); + Rule::FunctionCallInClassDefaultArgument, + ]) { + let is_dataclass = ruff::rules::is_dataclass(self, decorator_list); + if is_dataclass && self.settings.rules.enabled(Rule::MutableDataclassDefault) { + ruff::rules::mutable_class_default(self, true, body); + } + + if is_dataclass + && self + .settings + .rules + .enabled(Rule::FunctionCallInDataclassDefaultArgument) + { + ruff::rules::function_call_in_class_defaults( + self, + body, + is_dataclass, + true, + ); + } + if self.settings.rules.enabled(Rule::MutableClassDefault) { + ruff::rules::mutable_class_default(self, false, body); } if self .settings .rules - .enabled(Rule::FunctionCallInDataclassDefaultArgument) + .enabled(Rule::FunctionCallInClassDefaultArgument) { - ruff::rules::function_call_in_dataclass_defaults(self, body); + ruff::rules::function_call_in_class_defaults( + self, + body, + is_dataclass, + false, + ); } } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 5a1f864cd3c70..72b222bf09f91 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -713,6 +713,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Ruff, "007") => Rule::PairwiseOverZipped, (Ruff, "008") => Rule::MutableDataclassDefault, (Ruff, "009") => Rule::FunctionCallInDataclassDefaultArgument, + (Ruff, "010") => Rule::MutableClassDefault, + (Ruff, "011") => Rule::FunctionCallInClassDefaultArgument, (Ruff, "100") => Rule::UnusedNOQA, // flake8-django diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 22eabf0636658..96e24cc0088db 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -657,6 +657,8 @@ ruff_macros::register_rules!( rules::ruff::rules::PairwiseOverZipped, rules::ruff::rules::MutableDataclassDefault, rules::ruff::rules::FunctionCallInDataclassDefaultArgument, + rules::ruff::rules::MutableClassDefault, + rules::ruff::rules::FunctionCallInClassDefaultArgument, // flake8-django rules::flake8_django::rules::DjangoNullableModelStringField, rules::flake8_django::rules::DjangoLocalsInRenderFunction, diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 0be811b724722..8a971d6f3513c 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -155,7 +155,18 @@ mod tests { #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"); "RUF008")] #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"); "RUF009")] - fn mutable_defaults(rule_code: Rule, path: &Path) -> Result<()> { + fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::Settings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::MutableClassDefault, Path::new("RUF010.py"); "RUF010")] + #[test_case(Rule::FunctionCallInClassDefaultArgument, Path::new("RUF011.py"); "RUF011")] + fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 6a063c18dc1f8..ea6aee7307771 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -1,7 +1,7 @@ mod ambiguous_unicode_character; mod asyncio_dangling_task; mod collection_literal_concatenation; -mod mutable_defaults_in_dataclass_fields; +mod mutable_defaults_in_class_fields; mod pairwise_over_zipped; mod unused_noqa; @@ -13,9 +13,10 @@ pub use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask}; pub use collection_literal_concatenation::{ collection_literal_concatenation, CollectionLiteralConcatenation, }; -pub use mutable_defaults_in_dataclass_fields::{ - function_call_in_dataclass_defaults, is_dataclass, mutable_dataclass_default, - FunctionCallInDataclassDefaultArgument, MutableDataclassDefault, +pub use mutable_defaults_in_class_fields::{ + function_call_in_class_defaults, is_dataclass, mutable_class_default, + FunctionCallInClassDefaultArgument, FunctionCallInDataclassDefaultArgument, + MutableClassDefault, MutableDataclassDefault, }; pub use pairwise_over_zipped::{pairwise_over_zipped, PairwiseOverZipped}; pub use unused_noqa::{UnusedCodes, UnusedNOQA}; 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_class_fields.rs similarity index 69% rename from crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs rename to crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index 8d1d9839ef248..0d4e9c1fd32a1 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -57,6 +57,17 @@ impl Violation for MutableDataclassDefault { } } +/// This rule is same as MutableDataclassDefault, but for any class. The same arguments apply. +#[violation] +pub struct MutableClassDefault; + +impl Violation for MutableClassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not use mutable default values for class attributes") + } +} + /// ## What it does /// Checks for function calls in dataclass defaults. /// @@ -129,6 +140,25 @@ impl Violation for FunctionCallInDataclassDefaultArgument { } } +/// Same as FunctionCallInDataclassDefaultArgument, but for any class. +/// Importantly, this error will be issued on calls to dataclasses.field +#[violation] +pub struct FunctionCallInClassDefaultArgument { + pub name: Option, +} + +impl Violation for FunctionCallInClassDefaultArgument { + #[derive_message_formats] + fn message(&self) -> String { + let FunctionCallInClassDefaultArgument { name } = self; + if let Some(name) = name { + format!("Do not perform function call `{name}` in non-dataclass attribute defaults") + } else { + format!("Do not perform function call in non-dataclass attribute defaults") + } + } +} + fn is_mutable_expr(expr: &Expr) -> bool { matches!( &expr.node, @@ -143,7 +173,7 @@ fn is_mutable_expr(expr: &Expr) -> bool { const ALLOWED_FUNCS: &[&[&str]] = &[&["dataclasses", "field"]]; -fn is_allowed_func(context: &Context, func: &Expr) -> bool { +fn is_allowed_dataclass_func(context: &Context, func: &Expr) -> bool { context.resolve_call_path(func).map_or(false, |call_path| { ALLOWED_FUNCS .iter() @@ -159,8 +189,13 @@ fn is_class_var_annotation(context: &Context, annotation: &Expr) -> bool { context.match_typing_expr(value, "ClassVar") } -/// RUF009 -pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { +/// RUF009/RUF011 +pub fn function_call_in_class_defaults( + checker: &mut Checker, + body: &[Stmt], + is_dataclass: bool, + emit_dataclass_error: bool, +) { for statement in body { if let StmtKind::AnnAssign { annotation, @@ -172,21 +207,39 @@ pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) continue; } if let ExprKind::Call { func, .. } = &expr.node { - if !is_allowed_func(&checker.ctx, func) { - checker.diagnostics.push(Diagnostic::new( - FunctionCallInDataclassDefaultArgument { - name: compose_call_path(func), - }, - Range::from(expr), - )); + if !is_dataclass || !is_allowed_dataclass_func(&checker.ctx, func) { + let diagnostic: Diagnostic = if emit_dataclass_error { + Diagnostic::new( + FunctionCallInDataclassDefaultArgument { + name: compose_call_path(func), + }, + Range::from(expr), + ) + } else { + Diagnostic::new( + FunctionCallInClassDefaultArgument { + name: compose_call_path(func), + }, + Range::from(expr), + ) + }; + checker.diagnostics.push(diagnostic); } } } } } -/// RUF008 -pub fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { +/// RUF008/RUF010 +pub fn mutable_class_default(checker: &mut Checker, emit_dataclass_error: bool, body: &[Stmt]) { + fn diagnostic(emit_dataclass_error: bool, value: &Expr) -> Diagnostic { + if emit_dataclass_error { + Diagnostic::new(MutableDataclassDefault, Range::from(value)) + } else { + Diagnostic::new(MutableClassDefault, Range::from(value)) + } + } + for statement in body { match &statement.node { StmtKind::AnnAssign { @@ -200,14 +253,14 @@ pub fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { { checker .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, Range::from(value))); + .push(diagnostic(emit_dataclass_error, value)); } } StmtKind::Assign { value, .. } => { if is_mutable_expr(value) { checker .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, Range::from(value))); + .push(diagnostic(emit_dataclass_error, value)); } } _ => (), diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap new file mode 100644 index 0000000000000..44ea3287aeeea --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF010.py:9:34: RUF010 Do not use mutable default values for class attributes + | + 9 | class A: +10 | mutable_default: list[int] = [] + | ^^ RUF010 +11 | immutable_annotation: typing.Sequence[int] = [] +12 | without_annotation = [] + | + +RUF010.py:11:26: RUF010 Do not use mutable default values for class attributes + | +11 | mutable_default: list[int] = [] +12 | immutable_annotation: typing.Sequence[int] = [] +13 | without_annotation = [] + | ^^ RUF010 +14 | ignored_via_comment: list[int] = [] # noqa: RUF010 +15 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + +RUF010.py:17:34: RUF010 Do not use mutable default values for class attributes + | +17 | class B: +18 | mutable_default: list[int] = [] + | ^^ RUF010 +19 | immutable_annotation: Sequence[int] = [] +20 | without_annotation = [] + | + +RUF010.py:19:26: RUF010 Do not use mutable default values for class attributes + | +19 | mutable_default: list[int] = [] +20 | immutable_annotation: Sequence[int] = [] +21 | without_annotation = [] + | ^^ RUF010 +22 | ignored_via_comment: list[int] = [] # noqa: RUF010 +23 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap new file mode 100644 index 0000000000000..95673fe38df85 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap @@ -0,0 +1,48 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF011.py:14:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults + | +14 | class A: +15 | hidden_mutable_default: list[int] = default_function() + | ^^^^^^^^^^^^^^^^^^ RUF011 + | + +RUF011.py:22:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults + | +22 | class B: +23 | hidden_mutable_default: list[int] = default_function() + | ^^^^^^^^^^^^^^^^^^ RUF011 +24 | another_dataclass: A = A() +25 | not_optimal: ImmutableType = ImmutableType(20) + | + +RUF011.py:23:28: RUF011 Do not perform function call `A` in non-dataclass attribute defaults + | +23 | class B: +24 | hidden_mutable_default: list[int] = default_function() +25 | another_dataclass: A = A() + | ^^^ RUF011 +26 | not_optimal: ImmutableType = ImmutableType(20) +27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES + | + +RUF011.py:24:34: RUF011 Do not perform function call `ImmutableType` in non-dataclass attribute defaults + | +24 | hidden_mutable_default: list[int] = default_function() +25 | another_dataclass: A = A() +26 | not_optimal: ImmutableType = ImmutableType(20) + | ^^^^^^^^^^^^^^^^^ RUF011 +27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +28 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES + | + +RUF011.py:27:24: RUF011 Do not perform function call `field` in non-dataclass attribute defaults + | +27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +28 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +29 | field: list[int] = field(default_vactory=list) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF011 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 2d126f00f9dd8..b17fe6f58f6e2 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2160,6 +2160,9 @@ "RUF007", "RUF008", "RUF009", + "RUF01", + "RUF010", + "RUF011", "RUF1", "RUF10", "RUF100", From 941d47a13f41e4c791366540e857a30d8dc4431f Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Tue, 25 Apr 2023 21:40:25 -0700 Subject: [PATCH 02/20] Update crates/ruff/src/checkers/ast/mod.rs Co-authored-by: Micha Reiser --- crates/ruff/src/checkers/ast/mod.rs | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 82ed1da8b30b2..cdc5fefca848b 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -848,22 +848,24 @@ where Rule::FunctionCallInClassDefaultArgument, ]) { let is_dataclass = ruff::rules::is_dataclass(self, decorator_list); - if is_dataclass && self.settings.rules.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_class_default(self, true, body); - } - - if is_dataclass - && self - .settings - .rules - .enabled(Rule::FunctionCallInDataclassDefaultArgument) - { - ruff::rules::function_call_in_class_defaults( - self, - body, - is_dataclass, - true, - ); + + if is_dataclass { + if self.settings.rules.enabled(Rule::MutableDataclassDefault) { + ruff::rules::mutable_class_default(self, true, body); + } + + if self + .settings + .rules + .enabled(Rule::FunctionCallInDataclassDefaultArgument) + { + ruff::rules::function_call_in_class_defaults( + self, + body, + is_dataclass, + true, + ); + } } if self.settings.rules.enabled(Rule::MutableClassDefault) { ruff::rules::mutable_class_default(self, false, body); From 87da6cb94b453bd466e3493abaa6571465b64cef Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Tue, 25 Apr 2023 21:40:31 -0700 Subject: [PATCH 03/20] Update crates/ruff/src/rules/ruff/mod.rs Co-authored-by: Micha Reiser --- crates/ruff/src/rules/ruff/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 8a971d6f3513c..b3812cbae21dc 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -164,6 +164,7 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + #[test_case(Rule::MutableClassDefault, Path::new("RUF010.py"); "RUF010")] #[test_case(Rule::FunctionCallInClassDefaultArgument, Path::new("RUF011.py"); "RUF011")] fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { From 86dfc750b5cab3736b7ac26d57a8a3269287f653 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Tue, 25 Apr 2023 21:42:28 -0700 Subject: [PATCH 04/20] fmt --- crates/ruff/src/checkers/ast/mod.rs | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index cdc5fefca848b..2395e6cf0efe4 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -848,24 +848,24 @@ where Rule::FunctionCallInClassDefaultArgument, ]) { let is_dataclass = ruff::rules::is_dataclass(self, decorator_list); - + if is_dataclass { - if self.settings.rules.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_class_default(self, true, body); - } - - if self - .settings - .rules - .enabled(Rule::FunctionCallInDataclassDefaultArgument) - { - ruff::rules::function_call_in_class_defaults( - self, - body, - is_dataclass, - true, - ); - } + if self.settings.rules.enabled(Rule::MutableDataclassDefault) { + ruff::rules::mutable_class_default(self, true, body); + } + + if self + .settings + .rules + .enabled(Rule::FunctionCallInDataclassDefaultArgument) + { + ruff::rules::function_call_in_class_defaults( + self, + body, + is_dataclass, + true, + ); + } } if self.settings.rules.enabled(Rule::MutableClassDefault) { ruff::rules::mutable_class_default(self, false, body); From 12aff2db5e331ea3b6ebbba33e16e961f48b32a4 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Thu, 27 Apr 2023 09:11:08 -0700 Subject: [PATCH 05/20] bump ruleset size --- crates/ruff/src/registry/rule_set.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/registry/rule_set.rs b/crates/ruff/src/registry/rule_set.rs index 8c358fa469837..4cb6df76b423a 100644 --- a/crates/ruff/src/registry/rule_set.rs +++ b/crates/ruff/src/registry/rule_set.rs @@ -7,10 +7,10 @@ use std::iter::FusedIterator; /// /// Uses a bitset where a bit of one signals that the Rule with that [u16] is in this set. #[derive(Clone, Default, CacheKey, PartialEq, Eq)] -pub struct RuleSet([u64; 9]); +pub struct RuleSet([u64; 10]); impl RuleSet { - const EMPTY: [u64; 9] = [0; 9]; + const EMPTY: [u64; 10] = [0; 10]; // 64 fits into a u16 without truncation #[allow(clippy::cast_possible_truncation)] From 03483ebc5f1fd22ec9c6f1958fdeffd8a3ae4b4b Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Fri, 28 Apr 2023 09:34:38 -0700 Subject: [PATCH 06/20] fixup --- .../resources/test/fixtures/ruff/RUF010.py | 1 - .../resources/test/fixtures/ruff/RUF011.py | 11 ++-- ..._rules__ruff__tests__RUF010_RUF010.py.snap | 44 +++++++-------- ..._rules__ruff__tests__RUF011_RUF011.py.snap | 54 ++++++++++--------- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 2cf96991fd276..430129a45bac9 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -1,5 +1,4 @@ import typing -from dataclasses import dataclass, field from typing import Sequence KNOWINGLY_MUTABLE_DEFAULT = [] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF011.py b/crates/ruff/resources/test/fixtures/ruff/RUF011.py index ce604222baa30..d6454b174c66d 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF011.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF011.py @@ -1,6 +1,7 @@ -from dataclasses import dataclass +import datetime +import typing from typing import NamedTuple - +from dataclasses import field def default_function() -> list[int]: return [] @@ -12,6 +13,8 @@ class ImmutableType(NamedTuple): class A: hidden_mutable_default: list[int] = default_function() + class_variable: typing.ClassVar[list[int]] = default_function() + fine_date: datetime.date = datetime.date(2042, 1, 1) DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) @@ -20,8 +23,8 @@ class A: class B: hidden_mutable_default: list[int] = default_function() - another_dataclass: A = A() + another_class: A = A() not_optimal: ImmutableType = ImmutableType(20) good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES - field: list[int] = field(default_vactory=list) + not_fine_field: list[int] = field(default_vactory=list) diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index 44ea3287aeeea..f1db232d9fc2f 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -1,42 +1,42 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF010.py:9:34: RUF010 Do not use mutable default values for class attributes +RUF010.py:8:34: RUF010 Do not use mutable default values for class attributes | - 9 | class A: -10 | mutable_default: list[int] = [] + 8 | class A: + 9 | mutable_default: list[int] = [] | ^^ RUF010 -11 | immutable_annotation: typing.Sequence[int] = [] -12 | without_annotation = [] +10 | immutable_annotation: typing.Sequence[int] = [] +11 | without_annotation = [] | -RUF010.py:11:26: RUF010 Do not use mutable default values for class attributes +RUF010.py:10:26: RUF010 Do not use mutable default values for class attributes | -11 | mutable_default: list[int] = [] -12 | immutable_annotation: typing.Sequence[int] = [] -13 | without_annotation = [] +10 | mutable_default: list[int] = [] +11 | immutable_annotation: typing.Sequence[int] = [] +12 | without_annotation = [] | ^^ RUF010 -14 | ignored_via_comment: list[int] = [] # noqa: RUF010 -15 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +13 | ignored_via_comment: list[int] = [] # noqa: RUF010 +14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF010.py:17:34: RUF010 Do not use mutable default values for class attributes +RUF010.py:16:34: RUF010 Do not use mutable default values for class attributes | -17 | class B: -18 | mutable_default: list[int] = [] +16 | class B: +17 | mutable_default: list[int] = [] | ^^ RUF010 -19 | immutable_annotation: Sequence[int] = [] -20 | without_annotation = [] +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] | -RUF010.py:19:26: RUF010 Do not use mutable default values for class attributes +RUF010.py:18:26: RUF010 Do not use mutable default values for class attributes | -19 | mutable_default: list[int] = [] -20 | immutable_annotation: Sequence[int] = [] -21 | without_annotation = [] +18 | mutable_default: list[int] = [] +19 | immutable_annotation: Sequence[int] = [] +20 | without_annotation = [] | ^^ RUF010 -22 | ignored_via_comment: list[int] = [] # noqa: RUF010 -23 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +21 | ignored_via_comment: list[int] = [] # noqa: RUF010 +22 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap index 95673fe38df85..3d1fad7ed199e 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap @@ -1,48 +1,50 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF011.py:14:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults +RUF011.py:15:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults | -14 | class A: -15 | hidden_mutable_default: list[int] = default_function() +15 | class A: +16 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF011 +17 | class_variable: typing.ClassVar[list[int]] = default_function() +18 | fine_date: datetime.date = datetime.date(2042, 1, 1) | -RUF011.py:22:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults +RUF011.py:25:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults | -22 | class B: -23 | hidden_mutable_default: list[int] = default_function() +25 | class B: +26 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF011 -24 | another_dataclass: A = A() -25 | not_optimal: ImmutableType = ImmutableType(20) +27 | another_class: A = A() +28 | not_optimal: ImmutableType = ImmutableType(20) | -RUF011.py:23:28: RUF011 Do not perform function call `A` in non-dataclass attribute defaults +RUF011.py:26:24: RUF011 Do not perform function call `A` in non-dataclass attribute defaults | -23 | class B: -24 | hidden_mutable_default: list[int] = default_function() -25 | another_dataclass: A = A() - | ^^^ RUF011 -26 | not_optimal: ImmutableType = ImmutableType(20) -27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +26 | class B: +27 | hidden_mutable_default: list[int] = default_function() +28 | another_class: A = A() + | ^^^ RUF011 +29 | not_optimal: ImmutableType = ImmutableType(20) +30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES | -RUF011.py:24:34: RUF011 Do not perform function call `ImmutableType` in non-dataclass attribute defaults +RUF011.py:27:34: RUF011 Do not perform function call `ImmutableType` in non-dataclass attribute defaults | -24 | hidden_mutable_default: list[int] = default_function() -25 | another_dataclass: A = A() -26 | not_optimal: ImmutableType = ImmutableType(20) +27 | hidden_mutable_default: list[int] = default_function() +28 | another_class: A = A() +29 | not_optimal: ImmutableType = ImmutableType(20) | ^^^^^^^^^^^^^^^^^ RUF011 -27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -28 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +31 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES | -RUF011.py:27:24: RUF011 Do not perform function call `field` in non-dataclass attribute defaults +RUF011.py:30:33: RUF011 Do not perform function call `field` in non-dataclass attribute defaults | -27 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -28 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES -29 | field: list[int] = field(default_vactory=list) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF011 +30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +31 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +32 | not_fine_field: list[int] = field(default_vactory=list) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF011 | From e1a2919462104075d8a38b0ee003d4c500742f83 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Fri, 28 Apr 2023 09:45:58 -0700 Subject: [PATCH 07/20] that is one opinionated linter --- .../src/rules/ruff/rules/mutable_defaults_in_class_fields.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index 8d02bf170eb6a..b119d7f92138e 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -221,8 +221,8 @@ pub fn function_call_in_class_defaults( continue; } if let ExprKind::Call { func, .. } = &expr.node { - if !is_immutable_func(&checker.ctx, func, &extend_immutable_calls) - && !(is_dataclass && is_allowed_dataclass_function(&checker.ctx, func)) + if !(is_immutable_func(&checker.ctx, func, &extend_immutable_calls) + || is_dataclass && is_allowed_dataclass_function(&checker.ctx, func)) { let diagnostic: Diagnostic = if emit_dataclass_error { Diagnostic::new( From 1937e6a28d93035209c7ebe27399944e576f9948 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Fri, 12 May 2023 08:59:04 -0700 Subject: [PATCH 08/20] removing RUF011 --- .../resources/test/fixtures/ruff/RUF008.py | 16 ++++++ crates/ruff/src/rules/ruff/mod.rs | 14 +----- .../rules/mutable_defaults_in_class_fields.rs | 49 ++++-------------- ..._rules__ruff__tests__RUF010_RUF010.py.snap | 42 ---------------- ..._rules__ruff__tests__RUF011_RUF011.py.snap | 50 ------------------- ruff.schema.json | 3 +- 6 files changed, 27 insertions(+), 147 deletions(-) delete mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap delete mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF008.py b/crates/ruff/resources/test/fixtures/ruff/RUF008.py index 3a40f7f094420..9a8fbbee5cb40 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF008.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF008.py @@ -25,3 +25,19 @@ class B: correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] + + +class A: + mutable_default: list[int] = [] + immutable_annotation: typing.Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF008 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + + +class B: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF008 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index b3812cbae21dc..c135e2177cc43 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -153,7 +153,7 @@ mod tests { Ok(()) } - #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"); "RUF008")] + #[test_case(Rule::MutableClassDefault, Path::new("RUF008.py"); "RUF008")] #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"); "RUF009")] fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); @@ -164,16 +164,4 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } - - #[test_case(Rule::MutableClassDefault, Path::new("RUF010.py"); "RUF010")] - #[test_case(Rule::FunctionCallInClassDefaultArgument, Path::new("RUF011.py"); "RUF011")] - fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let diagnostics = test_path( - Path::new("ruff").join(path).as_path(), - &settings::Settings::for_rule(rule_code), - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } } diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index ae3bf845f9dd7..731327856f473 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -12,11 +12,10 @@ use ruff_python_semantic::{ use crate::checkers::ast::Checker; /// ## What it does -/// Checks for mutable default values in dataclasses without the use of -/// `dataclasses.field`. +/// Checks for mutable default values in class attribute defaults. /// /// ## Why is it bad? -/// Mutable default values share state across all instances of the dataclass, +/// Mutable default values share state across all instances of the class, /// while not being obvious. This can lead to bugs when the attributes are /// changed in one instance, as those changes will unexpectedly affect all /// other instances. @@ -54,17 +53,6 @@ use crate::checkers::ast::Checker; /// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE /// ``` #[violation] -pub struct MutableDataclassDefault; - -impl Violation for MutableDataclassDefault { - #[derive_message_formats] - fn message(&self) -> String { - format!("Do not use mutable default values for dataclass attributes") - } -} - -/// This rule is same as MutableDataclassDefault, but for any class. The same arguments apply. -#[violation] pub struct MutableClassDefault; impl Violation for MutableClassDefault { @@ -159,25 +147,6 @@ impl Violation for FunctionCallInDataclassDefaultArgument { } } -/// Same as FunctionCallInDataclassDefaultArgument, but for any class. -/// Importantly, this error will be issued on calls to dataclasses.field -#[violation] -pub struct FunctionCallInClassDefaultArgument { - pub name: Option, -} - -impl Violation for FunctionCallInClassDefaultArgument { - #[derive_message_formats] - fn message(&self) -> String { - let FunctionCallInClassDefaultArgument { name } = self; - if let Some(name) = name { - format!("Do not perform function call `{name}` in non-dataclass attribute defaults") - } else { - format!("Do not perform function call in non-dataclass attribute defaults") - } - } -} - fn is_mutable_expr(expr: &Expr) -> bool { matches!( &expr.node, @@ -208,8 +177,8 @@ fn is_class_var_annotation(context: &Context, annotation: &Expr) -> bool { context.match_typing_expr(value, "ClassVar") } -/// RUF009/RUF011 -pub fn function_call_in_class_defaults( +/// RUF009 +pub fn function_call_in_dataclass_defaults( checker: &mut Checker, body: &[Stmt], is_dataclass: bool, @@ -259,13 +228,12 @@ pub fn function_call_in_class_defaults( } } -/// RUF008/RUF010 -pub fn mutable_class_default(checker: &mut Checker, emit_dataclass_error: bool, body: &[Stmt]) { +/// RUF008 +pub fn mutable_class_default(checker: &mut Checker, body: &[Stmt]) { fn diagnostic(emit_dataclass_error: bool, value: &Expr) -> Diagnostic { if emit_dataclass_error { Diagnostic::new(MutableDataclassDefault, value.range()) } else { - Diagnostic::new(MutableClassDefault, value.range()) } } @@ -282,14 +250,15 @@ pub fn mutable_class_default(checker: &mut Checker, emit_dataclass_error: bool, { checker .diagnostics - .push(diagnostic(emit_dataclass_error, value)); + .push( + Diagnostic::new(MutableClassDefault, value.range())); } } StmtKind::Assign { value, .. } => { if is_mutable_expr(value) { checker .diagnostics - .push(diagnostic(emit_dataclass_error, value)); + .push(Diagnostic::new(MutableClassDefault, value.range())); } } _ => (), diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap deleted file mode 100644 index f1db232d9fc2f..0000000000000 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/ruff/src/rules/ruff/mod.rs ---- -RUF010.py:8:34: RUF010 Do not use mutable default values for class attributes - | - 8 | class A: - 9 | mutable_default: list[int] = [] - | ^^ RUF010 -10 | immutable_annotation: typing.Sequence[int] = [] -11 | without_annotation = [] - | - -RUF010.py:10:26: RUF010 Do not use mutable default values for class attributes - | -10 | mutable_default: list[int] = [] -11 | immutable_annotation: typing.Sequence[int] = [] -12 | without_annotation = [] - | ^^ RUF010 -13 | ignored_via_comment: list[int] = [] # noqa: RUF010 -14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - -RUF010.py:16:34: RUF010 Do not use mutable default values for class attributes - | -16 | class B: -17 | mutable_default: list[int] = [] - | ^^ RUF010 -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] - | - -RUF010.py:18:26: RUF010 Do not use mutable default values for class attributes - | -18 | mutable_default: list[int] = [] -19 | immutable_annotation: Sequence[int] = [] -20 | without_annotation = [] - | ^^ RUF010 -21 | ignored_via_comment: list[int] = [] # noqa: RUF010 -22 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - - diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap deleted file mode 100644 index 3d1fad7ed199e..0000000000000 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/ruff/src/rules/ruff/mod.rs ---- -RUF011.py:15:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults - | -15 | class A: -16 | hidden_mutable_default: list[int] = default_function() - | ^^^^^^^^^^^^^^^^^^ RUF011 -17 | class_variable: typing.ClassVar[list[int]] = default_function() -18 | fine_date: datetime.date = datetime.date(2042, 1, 1) - | - -RUF011.py:25:41: RUF011 Do not perform function call `default_function` in non-dataclass attribute defaults - | -25 | class B: -26 | hidden_mutable_default: list[int] = default_function() - | ^^^^^^^^^^^^^^^^^^ RUF011 -27 | another_class: A = A() -28 | not_optimal: ImmutableType = ImmutableType(20) - | - -RUF011.py:26:24: RUF011 Do not perform function call `A` in non-dataclass attribute defaults - | -26 | class B: -27 | hidden_mutable_default: list[int] = default_function() -28 | another_class: A = A() - | ^^^ RUF011 -29 | not_optimal: ImmutableType = ImmutableType(20) -30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES - | - -RUF011.py:27:34: RUF011 Do not perform function call `ImmutableType` in non-dataclass attribute defaults - | -27 | hidden_mutable_default: list[int] = default_function() -28 | another_class: A = A() -29 | not_optimal: ImmutableType = ImmutableType(20) - | ^^^^^^^^^^^^^^^^^ RUF011 -30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -31 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES - | - -RUF011.py:30:33: RUF011 Do not perform function call `field` in non-dataclass attribute defaults - | -30 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -31 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES -32 | not_fine_field: list[int] = field(default_vactory=list) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF011 - | - - diff --git a/ruff.schema.json b/ruff.schema.json index 94bfcca876310..7f74995c565c6 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2177,7 +2177,6 @@ "RUF009", "RUF01", "RUF010", - "RUF011", "RUF1", "RUF10", "RUF100", @@ -2437,4 +2436,4 @@ "type": "string" } } -} \ No newline at end of file +} From 673939af54c5ae77beb0f92f23c7199cd9c0cd84 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Fri, 12 May 2023 09:09:40 -0700 Subject: [PATCH 09/20] Merge remote-tracking branch 'ruff/main' into only-mutable-fields # Conflicts: # crates/ruff/src/rules/ruff/rules/mod.rs # crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs --- .../resources/test/fixtures/ruff/RUF010.py | 20 ------- .../resources/test/fixtures/ruff/RUF011.py | 30 ----------- crates/ruff/src/checkers/ast/mod.rs | 51 ++++-------------- crates/ruff/src/codes.rs | 4 +- crates/ruff/src/registry.rs | 4 +- crates/ruff/src/rules/ruff/mod.rs | 2 +- .../rules/mutable_defaults_in_class_fields.rs | 53 +++---------------- ..._rules__ruff__tests__RUF008_RUF008.py.snap | 46 ++++++++++++++-- ruff.schema.json | 2 - 9 files changed, 63 insertions(+), 149 deletions(-) delete mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF010.py delete mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF011.py diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py deleted file mode 100644 index 430129a45bac9..0000000000000 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ /dev/null @@ -1,20 +0,0 @@ -import typing -from typing import Sequence - -KNOWINGLY_MUTABLE_DEFAULT = [] - - -class A: - mutable_default: list[int] = [] - immutable_annotation: typing.Sequence[int] = [] - without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF010 - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - - -class B: - mutable_default: list[int] = [] - immutable_annotation: Sequence[int] = [] - without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF010 - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF011.py b/crates/ruff/resources/test/fixtures/ruff/RUF011.py deleted file mode 100644 index d6454b174c66d..0000000000000 --- a/crates/ruff/resources/test/fixtures/ruff/RUF011.py +++ /dev/null @@ -1,30 +0,0 @@ -import datetime -import typing -from typing import NamedTuple -from dataclasses import field - -def default_function() -> list[int]: - return [] - - -class ImmutableType(NamedTuple): - something: int = 8 - - -class A: - hidden_mutable_default: list[int] = default_function() - class_variable: typing.ClassVar[list[int]] = default_function() - fine_date: datetime.date = datetime.date(2042, 1, 1) - - -DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) -DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) - - -class B: - hidden_mutable_default: list[int] = default_function() - another_class: A = A() - not_optimal: ImmutableType = ImmutableType(20) - good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES - okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES - not_fine_field: list[int] = field(default_vactory=list) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3459417f67ade..9ea2ff391832c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -760,48 +760,17 @@ where flake8_pie::rules::non_unique_enums(self, stmt, body); } - if self.settings.rules.any_enabled(&[ - Rule::MutableDataclassDefault, - Rule::MutableClassDefault, - Rule::FunctionCallInDataclassDefaultArgument, - Rule::FunctionCallInClassDefaultArgument, - ]) { - let is_dataclass = ruff::rules::is_dataclass(self, decorator_list); - - if is_dataclass { - if self.settings.rules.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_class_default(self, true, body); - } - - if self - .settings - .rules - .enabled(Rule::FunctionCallInDataclassDefaultArgument) - { - ruff::rules::function_call_in_class_defaults( - self, - body, - is_dataclass, - true, - ); - } - } - if self.settings.rules.enabled(Rule::MutableClassDefault) { - ruff::rules::mutable_class_default(self, false, body); - } + if self.settings.rules.enabled(Rule::MutableClassDefault) { + ruff::rules::mutable_class_default(self, body); + } - if self - .settings - .rules - .enabled(Rule::FunctionCallInClassDefaultArgument) - { - ruff::rules::function_call_in_class_defaults( - self, - body, - is_dataclass, - false, - ); - } + if self + .settings + .rules + .enabled(Rule::FunctionCallInDataclassDefaultArgument) + && ruff::rules::is_dataclass(self, decorator_list) + { + ruff::rules::function_call_in_dataclass_defaults(self, body); } if self.settings.rules.enabled(Rule::FStringDocstring) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index d8904d1c85242..f9749f9bf272e 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -725,10 +725,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Ruff, "005") => Rule::CollectionLiteralConcatenation, (Ruff, "006") => Rule::AsyncioDanglingTask, (Ruff, "007") => Rule::PairwiseOverZipped, - (Ruff, "008") => Rule::MutableDataclassDefault, + (Ruff, "008") => Rule::MutableClassDefault, (Ruff, "009") => Rule::FunctionCallInDataclassDefaultArgument, - (Ruff, "010") => Rule::MutableClassDefault, - (Ruff, "011") => Rule::FunctionCallInClassDefaultArgument, (Ruff, "100") => Rule::UnusedNOQA, // flake8-django diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index c4a85ef7ff4f3..5458b0ba29f11 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -661,10 +661,8 @@ ruff_macros::register_rules!( rules::ruff::rules::AsyncioDanglingTask, rules::ruff::rules::UnusedNOQA, rules::ruff::rules::PairwiseOverZipped, - rules::ruff::rules::MutableDataclassDefault, - rules::ruff::rules::FunctionCallInDataclassDefaultArgument, rules::ruff::rules::MutableClassDefault, - rules::ruff::rules::FunctionCallInClassDefaultArgument, + rules::ruff::rules::FunctionCallInDataclassDefaultArgument, // flake8-django rules::flake8_django::rules::DjangoNullableModelStringField, rules::flake8_django::rules::DjangoLocalsInRenderFunction, diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index c135e2177cc43..a40dbd4a06368 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -155,7 +155,7 @@ mod tests { #[test_case(Rule::MutableClassDefault, Path::new("RUF008.py"); "RUF008")] #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"); "RUF009")] - fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { + fn mutable_class_defaults(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index a1d8d1bdf95b0..151c2800ebf7d 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -178,16 +178,7 @@ fn is_class_var_annotation(context: &Context, annotation: &Expr) -> bool { } /// RUF009 -<<<<<<< HEAD:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs -pub fn function_call_in_dataclass_defaults( - checker: &mut Checker, - body: &[Stmt], - is_dataclass: bool, - emit_dataclass_error: bool, -) { -======= pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { ->>>>>>> ruff/main:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs let extend_immutable_calls: Vec = checker .settings .flake8_bugbear @@ -206,32 +197,16 @@ pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: & if is_class_var_annotation(&checker.ctx, annotation) { continue; } -<<<<<<< HEAD:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs - if let ExprKind::Call { func, .. } = &expr.node { - if !(is_immutable_func(&checker.ctx, func, &extend_immutable_calls) - || is_dataclass && is_allowed_dataclass_function(&checker.ctx, func)) -======= if let ExprKind::Call(ast::ExprCall { func, .. }) = &expr.node { if !is_immutable_func(&checker.ctx, func, &extend_immutable_calls) && !is_allowed_dataclass_function(&checker.ctx, func) ->>>>>>> ruff/main:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs { - let diagnostic: Diagnostic = if emit_dataclass_error { - Diagnostic::new( - FunctionCallInDataclassDefaultArgument { - name: compose_call_path(func), - }, - expr.range(), - ) - } else { - Diagnostic::new( - FunctionCallInClassDefaultArgument { - name: compose_call_path(func), - }, - expr.range(), - ) - }; - checker.diagnostics.push(diagnostic); + checker.diagnostics.push(Diagnostic::new( + FunctionCallInDataclassDefaultArgument { + name: compose_call_path(func), + }, + expr.range(), + )); } } } @@ -239,18 +214,7 @@ pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: & } /// RUF008 -<<<<<<< HEAD:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs -pub fn mutable_class_default(checker: &mut Checker, body: &[Stmt]) { - fn diagnostic(emit_dataclass_error: bool, value: &Expr) -> Diagnostic { - if emit_dataclass_error { - Diagnostic::new(MutableDataclassDefault, value.range()) - } else { - } - } - -======= -pub(crate) fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { ->>>>>>> ruff/main:crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt]) { for statement in body { match &statement.node { StmtKind::AnnAssign(ast::StmtAnnAssign { @@ -264,8 +228,7 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { { checker .diagnostics - .push( - Diagnostic::new(MutableClassDefault, value.range())); + .push(Diagnostic::new(MutableClassDefault, value.range())); } } StmtKind::Assign(ast::StmtAssign { 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 7d10aa9cadd6b..7abc6757127af 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:10:34: RUF008 Do not use mutable default values for class attributes | 10 | @dataclass() 11 | class A: @@ -11,7 +11,7 @@ RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attribut 14 | without_annotation = [] | -RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:12:26: RUF008 Do not use mutable default values for class attributes | 12 | mutable_default: list[int] = [] 13 | immutable_annotation: typing.Sequence[int] = [] @@ -21,7 +21,7 @@ RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attribut 16 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:21:34: RUF008 Do not use mutable default values for class attributes | 21 | @dataclass 22 | class B: @@ -31,7 +31,7 @@ RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attribut 25 | without_annotation = [] | -RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:23:26: RUF008 Do not use mutable default values for class attributes | 23 | mutable_default: list[int] = [] 24 | immutable_annotation: Sequence[int] = [] @@ -41,4 +41,42 @@ RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attribut 27 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | +RUF008.py:31:34: RUF008 Do not use mutable default values for class attributes + | +31 | class A: +32 | mutable_default: list[int] = [] + | ^^ RUF008 +33 | immutable_annotation: typing.Sequence[int] = [] +34 | without_annotation = [] + | + +RUF008.py:33:26: RUF008 Do not use mutable default values for class attributes + | +33 | mutable_default: list[int] = [] +34 | immutable_annotation: typing.Sequence[int] = [] +35 | without_annotation = [] + | ^^ RUF008 +36 | ignored_via_comment: list[int] = [] # noqa: RUF008 +37 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + +RUF008.py:39:34: RUF008 Do not use mutable default values for class attributes + | +39 | class B: +40 | mutable_default: list[int] = [] + | ^^ RUF008 +41 | immutable_annotation: Sequence[int] = [] +42 | without_annotation = [] + | + +RUF008.py:41:26: RUF008 Do not use mutable default values for class attributes + | +41 | mutable_default: list[int] = [] +42 | immutable_annotation: Sequence[int] = [] +43 | without_annotation = [] + | ^^ RUF008 +44 | ignored_via_comment: list[int] = [] # noqa: RUF008 +45 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + diff --git a/ruff.schema.json b/ruff.schema.json index 89e0ef00f18f6..fdf5bacd21156 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2169,8 +2169,6 @@ "RUF007", "RUF008", "RUF009", - "RUF01", - "RUF010", "RUF1", "RUF10", "RUF100", From 5c8ad65419dea7aa9b32ee4136a90e6257d14ce7 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Fri, 12 May 2023 09:13:11 -0700 Subject: [PATCH 10/20] fix EOL --- ruff.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index fdf5bacd21156..fa891b1c6ae57 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2428,4 +2428,4 @@ "type": "string" } } -} +} \ No newline at end of file From 4eaa1727e6e76e35aecbbb2f13dfdd0e64415434 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 28 May 2023 00:18:00 -0700 Subject: [PATCH 11/20] Split into new code. --- .../resources/test/fixtures/ruff/RUF008.py | 16 ----- .../resources/test/fixtures/ruff/RUF011.py | 20 ++++++ crates/ruff/src/checkers/ast/mod.rs | 24 ++++--- crates/ruff/src/codes.rs | 3 +- crates/ruff/src/registry.rs | 3 +- crates/ruff/src/rules/ruff/mod.rs | 13 +++- crates/ruff/src/rules/ruff/rules/mod.rs | 2 +- .../rules/mutable_defaults_in_class_fields.rs | 63 +++++++++++++++++-- ..._rules__ruff__tests__RUF008_RUF008.py.snap | 46 ++------------ ruff.schema.json | 1 + 10 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF011.py diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF008.py b/crates/ruff/resources/test/fixtures/ruff/RUF008.py index 9a8fbbee5cb40..3a40f7f094420 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF008.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF008.py @@ -25,19 +25,3 @@ class B: correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] - - -class A: - mutable_default: list[int] = [] - immutable_annotation: typing.Sequence[int] = [] - without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - - -class B: - mutable_default: list[int] = [] - immutable_annotation: Sequence[int] = [] - without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 - correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF011.py b/crates/ruff/resources/test/fixtures/ruff/RUF011.py new file mode 100644 index 0000000000000..f4b89c814d268 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF011.py @@ -0,0 +1,20 @@ +import typing +from typing import Sequence + +KNOWINGLY_MUTABLE_DEFAULT = [] + + +class A: + mutable_default: list[int] = [] + immutable_annotation: typing.Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF011 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + + +class B: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + ignored_via_comment: list[int] = [] # noqa: RUF011 + correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 22dd754362cac..b274b27f3daa7 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -748,17 +748,21 @@ where flake8_pie::rules::non_unique_enums(self, stmt, body); } - if self.settings.rules.enabled(Rule::MutableClassDefault) { - ruff::rules::mutable_class_default(self, body); - } + if self.any_enabled(&[ + Rule::MutableDataclassDefault, + Rule::FunctionCallInDataclassDefaultArgument, + Rule::MutableClassDefault, + ]) { + let is_dataclass = + ruff::rules::is_dataclass(&self.semantic_model, decorator_list); + if self.any_enabled(&[Rule::MutableDataclassDefault, Rule::MutableClassDefault]) + { + ruff::rules::mutable_class_default(self, body, is_dataclass); + } - if self - .settings - .rules - .enabled(Rule::FunctionCallInDataclassDefaultArgument) - && ruff::rules::is_dataclass(&self.semantic_model, decorator_list) - { - ruff::rules::function_call_in_dataclass_defaults(self, body); + if is_dataclass && self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { + ruff::rules::function_call_in_dataclass_defaults(self, body); + } } if self.enabled(Rule::FStringDocstring) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 41c9be7bc1a06..7c968bb86aac4 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -718,9 +718,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "005") => (RuleGroup::Unspecified, Rule::CollectionLiteralConcatenation), (Ruff, "006") => (RuleGroup::Unspecified, Rule::AsyncioDanglingTask), (Ruff, "007") => (RuleGroup::Unspecified, Rule::PairwiseOverZipped), - (Ruff, "008") => (RuleGroup::Unspecified, Rule::MutableClassDefault), + (Ruff, "008") => (RuleGroup::Unspecified, Rule::MutableDataclassDefault), (Ruff, "009") => (RuleGroup::Unspecified, Rule::FunctionCallInDataclassDefaultArgument), (Ruff, "010") => (RuleGroup::Unspecified, Rule::ExplicitFStringTypeConversion), + (Ruff, "011") => (RuleGroup::Unspecified, Rule::MutableClassDefault), (Ruff, "100") => (RuleGroup::Unspecified, Rule::UnusedNOQA), (Ruff, "200") => (RuleGroup::Unspecified, Rule::InvalidPyprojectToml), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index d3e317c9d0e40..d9f6f490a8531 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -643,10 +643,11 @@ ruff_macros::register_rules!( rules::ruff::rules::AsyncioDanglingTask, rules::ruff::rules::UnusedNOQA, rules::ruff::rules::PairwiseOverZipped, - rules::ruff::rules::MutableClassDefault, + rules::ruff::rules::MutableDataclassDefault, rules::ruff::rules::FunctionCallInDataclassDefaultArgument, rules::ruff::rules::ExplicitFStringTypeConversion, rules::ruff::rules::InvalidPyprojectToml, + rules::ruff::rules::MutableClassDefault, // flake8-django rules::flake8_django::rules::DjangoNullableModelStringField, rules::flake8_django::rules::DjangoLocalsInRenderFunction, diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 66bbf2abf6f72..0ce2b9afbc257 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -167,8 +167,19 @@ mod tests { Ok(()) } - #[test_case(Rule::MutableClassDefault, Path::new("RUF008.py"); "RUF008")] + #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"); "RUF008")] #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"); "RUF009")] + fn mutable_dataclass_defaults(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::Settings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Rule::MutableClassDefault, Path::new("RUF011.py"); "RUF010")] fn mutable_class_defaults(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/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 18f60bb1f986c..113083e920485 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -12,7 +12,7 @@ pub(crate) use explicit_f_string_type_conversion::{ pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; pub(crate) use mutable_defaults_in_class_fields::{ function_call_in_dataclass_defaults, is_dataclass, mutable_class_default, - FunctionCallInDataclassDefaultArgument, MutableClassDefault, + FunctionCallInDataclassDefaultArgument, MutableClassDefault, MutableDataclassDefault, }; pub(crate) use pairwise_over_zipped::{pairwise_over_zipped, PairwiseOverZipped}; pub(crate) use unused_noqa::{UnusedCodes, UnusedNOQA}; diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index 43c2f31243d0b..c59db36ad180b 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -12,10 +12,11 @@ use ruff_python_semantic::{ use crate::checkers::ast::Checker; /// ## What it does -/// Checks for mutable default values in class attribute defaults. +/// Checks for mutable default values in dataclasses without the use of +/// `dataclasses.field`. /// /// ## Why is this bad? -/// Mutable default values share state across all instances of the class, +/// Mutable default values share state across all instances of the dataclass, /// while not being obvious. This can lead to bugs when the attributes are /// changed in one instance, as those changes will unexpectedly affect all /// other instances. @@ -53,6 +54,49 @@ use crate::checkers::ast::Checker; /// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE /// ``` #[violation] +pub struct MutableDataclassDefault; + +impl Violation for MutableDataclassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not use mutable default values for dataclass attributes") + } +} + +/// ## What it does +/// Checks for mutable default values in class attributes not annotated with `ClassVar`. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the class, +/// while not being obvious. This can lead to bugs when the attributes are +/// changed in one instance, as those changes will unexpectedly affect all +/// other instances. +/// +/// ## Examples: +/// ```python +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from dataclasses import dataclass, field +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = field(default_factory=list) +/// ``` +/// +/// Alternatively, if you _want_ shared behaviour, make it more obvious +/// by assigning to a module-level variable: +/// ```python +/// I_KNOW_THIS_IS_SHARED_STATE = [1, 2, 3, 4] +/// +/// +/// class A: +/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE +#[violation] pub struct MutableClassDefault; impl Violation for MutableClassDefault { @@ -213,8 +257,15 @@ pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: & } } -/// RUF008 -pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt]) { +/// RUF008/RUF011 +pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt], is_dataclass: bool) { + fn diagnostic(is_dataclass: bool, value: &Expr) -> Diagnostic { + if is_dataclass { + Diagnostic::new(MutableDataclassDefault, value.range()) + } else { + Diagnostic::new(MutableClassDefault, value.range()) + } + } for statement in body { match statement { Stmt::AnnAssign(ast::StmtAnnAssign { @@ -228,14 +279,14 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt]) { { checker .diagnostics - .push(Diagnostic::new(MutableClassDefault, value.range())); + .push(diagnostic(is_dataclass,value)); } } Stmt::Assign(ast::StmtAssign { value, .. }) => { if is_mutable_expr(value) { checker .diagnostics - .push(Diagnostic::new(MutableClassDefault, value.range())); + .push(diagnostic(is_dataclass, 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 7abc6757127af..7d10aa9cadd6b 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF008.py:10:34: RUF008 Do not use mutable default values for class attributes +RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attributes | 10 | @dataclass() 11 | class A: @@ -11,7 +11,7 @@ RUF008.py:10:34: RUF008 Do not use mutable default values for class attributes 14 | without_annotation = [] | -RUF008.py:12:26: RUF008 Do not use mutable default values for class attributes +RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attributes | 12 | mutable_default: list[int] = [] 13 | immutable_annotation: typing.Sequence[int] = [] @@ -21,7 +21,7 @@ RUF008.py:12:26: RUF008 Do not use mutable default values for class attributes 16 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF008.py:21:34: RUF008 Do not use mutable default values for class attributes +RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attributes | 21 | @dataclass 22 | class B: @@ -31,7 +31,7 @@ RUF008.py:21:34: RUF008 Do not use mutable default values for class attributes 25 | without_annotation = [] | -RUF008.py:23:26: RUF008 Do not use mutable default values for class attributes +RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attributes | 23 | mutable_default: list[int] = [] 24 | immutable_annotation: Sequence[int] = [] @@ -41,42 +41,4 @@ RUF008.py:23:26: RUF008 Do not use mutable default values for class attributes 27 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF008.py:31:34: RUF008 Do not use mutable default values for class attributes - | -31 | class A: -32 | mutable_default: list[int] = [] - | ^^ RUF008 -33 | immutable_annotation: typing.Sequence[int] = [] -34 | without_annotation = [] - | - -RUF008.py:33:26: RUF008 Do not use mutable default values for class attributes - | -33 | mutable_default: list[int] = [] -34 | immutable_annotation: typing.Sequence[int] = [] -35 | without_annotation = [] - | ^^ RUF008 -36 | ignored_via_comment: list[int] = [] # noqa: RUF008 -37 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - -RUF008.py:39:34: RUF008 Do not use mutable default values for class attributes - | -39 | class B: -40 | mutable_default: list[int] = [] - | ^^ RUF008 -41 | immutable_annotation: Sequence[int] = [] -42 | without_annotation = [] - | - -RUF008.py:41:26: RUF008 Do not use mutable default values for class attributes - | -41 | mutable_default: list[int] = [] -42 | immutable_annotation: Sequence[int] = [] -43 | without_annotation = [] - | ^^ RUF008 -44 | ignored_via_comment: list[int] = [] # noqa: RUF008 -45 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - diff --git a/ruff.schema.json b/ruff.schema.json index 0a47edb5c9114..5c92003b641f9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2243,6 +2243,7 @@ "RUF009", "RUF01", "RUF010", + "RUF011", "RUF1", "RUF10", "RUF100", From fe8c1d4b7c0331d26c8a9cfd63640d857cc254ef Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 28 May 2023 00:18:19 -0700 Subject: [PATCH 12/20] Split into new code. --- ..._rules__ruff__tests__RUF011_RUF011.py.snap | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap new file mode 100644 index 0000000000000..467c6e0e74baf --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF011.py:8:34: RUF011 Do not use mutable default values for class attributes + | + 8 | class A: + 9 | mutable_default: list[int] = [] + | ^^ RUF011 +10 | immutable_annotation: typing.Sequence[int] = [] +11 | without_annotation = [] + | + +RUF011.py:10:26: RUF011 Do not use mutable default values for class attributes + | +10 | mutable_default: list[int] = [] +11 | immutable_annotation: typing.Sequence[int] = [] +12 | without_annotation = [] + | ^^ RUF011 +13 | ignored_via_comment: list[int] = [] # noqa: RUF011 +14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + +RUF011.py:16:34: RUF011 Do not use mutable default values for class attributes + | +16 | class B: +17 | mutable_default: list[int] = [] + | ^^ RUF011 +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] + | + +RUF011.py:18:26: RUF011 Do not use mutable default values for class attributes + | +18 | mutable_default: list[int] = [] +19 | immutable_annotation: Sequence[int] = [] +20 | without_annotation = [] + | ^^ RUF011 +21 | ignored_via_comment: list[int] = [] # noqa: RUF011 +22 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + + From 805d9cc38eff2937ee8c808cff851e1c8c53d241 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 28 May 2023 00:19:42 -0700 Subject: [PATCH 13/20] Split into new code. --- .../rules/ruff/rules/mutable_defaults_in_class_fields.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index c59db36ad180b..fe71030c68ac2 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -277,16 +277,12 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, body: &[Stmt], is_dat && !is_immutable_annotation(checker.semantic_model(), annotation) && is_mutable_expr(value) { - checker - .diagnostics - .push(diagnostic(is_dataclass,value)); + checker.diagnostics.push(diagnostic(is_dataclass, value)); } } Stmt::Assign(ast::StmtAssign { value, .. }) => { if is_mutable_expr(value) { - checker - .diagnostics - .push(diagnostic(is_dataclass, value)); + checker.diagnostics.push(diagnostic(is_dataclass, value)); } } _ => (), From 181126cae0ca83fb487d8f0c97e86337a48c8e74 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 28 May 2023 09:45:50 -0700 Subject: [PATCH 14/20] Copy over tests for ClassVar --- crates/ruff/resources/test/fixtures/ruff/RUF011.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF011.py b/crates/ruff/resources/test/fixtures/ruff/RUF011.py index f4b89c814d268..36be3454067c3 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF011.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF011.py @@ -1,5 +1,5 @@ import typing -from typing import Sequence +from typing import ClassVar, Sequence KNOWINGLY_MUTABLE_DEFAULT = [] @@ -10,6 +10,7 @@ class A: without_annotation = [] ignored_via_comment: list[int] = [] # noqa: RUF011 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + class_variable: typing.ClassVar[list[int]] = [] class B: @@ -18,3 +19,4 @@ class B: without_annotation = [] ignored_via_comment: list[int] = [] # noqa: RUF011 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + class_variable: ClassVar[list[int]] = [] From 50b6a4df1acadf0c32063af8d9b63908bdb67983 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 28 May 2023 13:57:30 -0700 Subject: [PATCH 15/20] fix cargo test --- ..._rules__ruff__tests__RUF011_RUF011.py.snap | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap index 467c6e0e74baf..eb59eb8c1e934 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF011_RUF011.py.snap @@ -20,23 +20,23 @@ RUF011.py:10:26: RUF011 Do not use mutable default values for class attributes 14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | -RUF011.py:16:34: RUF011 Do not use mutable default values for class attributes +RUF011.py:17:34: RUF011 Do not use mutable default values for class attributes | -16 | class B: -17 | mutable_default: list[int] = [] +17 | class B: +18 | mutable_default: list[int] = [] | ^^ RUF011 -18 | immutable_annotation: Sequence[int] = [] -19 | without_annotation = [] +19 | immutable_annotation: Sequence[int] = [] +20 | without_annotation = [] | -RUF011.py:18:26: RUF011 Do not use mutable default values for class attributes +RUF011.py:19:26: RUF011 Do not use mutable default values for class attributes | -18 | mutable_default: list[int] = [] -19 | immutable_annotation: Sequence[int] = [] -20 | without_annotation = [] +19 | mutable_default: list[int] = [] +20 | immutable_annotation: Sequence[int] = [] +21 | without_annotation = [] | ^^ RUF011 -21 | ignored_via_comment: list[int] = [] # noqa: RUF011 -22 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +22 | ignored_via_comment: list[int] = [] # noqa: RUF011 +23 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT | From 2d1ff5ebdd6db9d6fd6b03fef7c437990f63e12a Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 11 Jun 2023 11:49:32 -0700 Subject: [PATCH 16/20] ruff.schema.json --- ruff.schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index e23fad53f205c..5264fd83b16da 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2347,6 +2347,7 @@ "RUF01", "RUF010", "RUF011", + "RUF012", "RUF1", "RUF10", "RUF100", @@ -2634,4 +2635,4 @@ "type": "string" } } -} \ No newline at end of file +} From f7ae3bfc464800a917efb8ac7286f977e888f72c Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 11 Jun 2023 12:16:34 -0700 Subject: [PATCH 17/20] ruff.schema.json --- ruff.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.schema.json b/ruff.schema.json index 5264fd83b16da..1f967b76686bb 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2635,4 +2635,4 @@ "type": "string" } } -} +} \ No newline at end of file From d3d5a9e82cadc98f191d6f7cdab37b8851e964d0 Mon Sep 17 00:00:00 2001 From: Adam Pauls Date: Sun, 11 Jun 2023 12:16:53 -0700 Subject: [PATCH 18/20] .snap --- ..._rules__ruff__tests__RUF012_RUF012.py.snap | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap new file mode 100644 index 0000000000000..4f12483f56c2e --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF012.py:8:34: RUF012 Do not use mutable default values for class attributes + | + 7 | class A: + 8 | mutable_default: list[int] = [] + | ^^ RUF012 + 9 | immutable_annotation: typing.Sequence[int] = [] +10 | without_annotation = [] + | + +RUF012.py:10:26: RUF012 Do not use mutable default values for class attributes + | + 8 | mutable_default: list[int] = [] + 9 | immutable_annotation: typing.Sequence[int] = [] +10 | without_annotation = [] + | ^^ RUF012 +11 | ignored_via_comment: list[int] = [] # noqa: RUF012 +12 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + +RUF012.py:17:34: RUF012 Do not use mutable default values for class attributes + | +16 | class B: +17 | mutable_default: list[int] = [] + | ^^ RUF012 +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] + | + +RUF012.py:19:26: RUF012 Do not use mutable default values for class attributes + | +17 | mutable_default: list[int] = [] +18 | immutable_annotation: Sequence[int] = [] +19 | without_annotation = [] + | ^^ RUF012 +20 | ignored_via_comment: list[int] = [] # noqa: RUF012 +21 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT + | + + From 0c1d088af108ad879c4d9bdcbce3da4985dfc453 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 12:42:41 -0400 Subject: [PATCH 19/20] Tweak docs --- .../rules/mutable_defaults_in_class_fields.rs | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index 33a15a833cb85..effebb1ae1043 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -12,8 +12,7 @@ use ruff_python_semantic::{ use crate::checkers::ast::Checker; /// ## What it does -/// Checks for mutable default values in dataclasses without the use of -/// `dataclasses.field`. +/// Checks for mutable default values in dataclasses. /// /// ## Why is this bad? /// Mutable default values share state across all instances of the dataclass, @@ -21,11 +20,13 @@ use crate::checkers::ast::Checker; /// changed in one instance, as those changes will unexpectedly affect all /// other instances. /// +/// Instead of sharing mutable defaults, use the `field(default_factory=...)` +/// pattern. +/// /// ## Examples: /// ```python /// from dataclasses import dataclass /// -/// /// @dataclass /// class A: /// mutable_default: list[int] = [] @@ -35,24 +36,10 @@ use crate::checkers::ast::Checker; /// ```python /// from dataclasses import dataclass, field /// -/// /// @dataclass /// class A: /// mutable_default: list[int] = field(default_factory=list) /// ``` -/// -/// Alternatively, if you _want_ shared behaviour, make it more obvious -/// by assigning to a module-level variable: -/// ```python -/// from dataclasses import dataclass -/// -/// I_KNOW_THIS_IS_SHARED_STATE = [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE -/// ``` #[violation] pub struct MutableDataclassDefault; @@ -64,7 +51,7 @@ impl Violation for MutableDataclassDefault { } /// ## What it does -/// Checks for mutable default values in class attributes not annotated with `ClassVar`. +/// Checks for mutable default values in class attributes. /// /// ## Why is this bad? /// Mutable default values share state across all instances of the class, @@ -72,6 +59,9 @@ impl Violation for MutableDataclassDefault { /// changed in one instance, as those changes will unexpectedly affect all /// other instances. /// +/// When mutable value are intended, they should be annotated with +/// `typing.ClassVar`. +/// /// ## Examples: /// ```python /// class A: @@ -80,22 +70,11 @@ impl Violation for MutableDataclassDefault { /// /// Use instead: /// ```python -/// from dataclasses import dataclass, field +/// from typing import ClassVar /// -/// -/// @dataclass /// class A: -/// mutable_default: list[int] = field(default_factory=list) +/// mutable_default: ClassVar[list[int]] = [] /// ``` -/// -/// Alternatively, if you _want_ shared behaviour, make it more obvious -/// by assigning to a module-level variable: -/// ```python -/// I_KNOW_THIS_IS_SHARED_STATE = [1, 2, 3, 4] -/// -/// -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE #[violation] pub struct MutableClassDefault; From 987926bb2d23a3ca6b70de524a63140bc6364372 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jun 2023 12:47:11 -0400 Subject: [PATCH 20/20] Fix docs --- .../rules/ruff/rules/mutable_defaults_in_class_fields.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs index effebb1ae1043..ea1ab50cd18aa 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_class_fields.rs @@ -23,10 +23,11 @@ use crate::checkers::ast::Checker; /// Instead of sharing mutable defaults, use the `field(default_factory=...)` /// pattern. /// -/// ## Examples: +/// ## Examples /// ```python /// from dataclasses import dataclass /// +/// /// @dataclass /// class A: /// mutable_default: list[int] = [] @@ -36,6 +37,7 @@ use crate::checkers::ast::Checker; /// ```python /// from dataclasses import dataclass, field /// +/// /// @dataclass /// class A: /// mutable_default: list[int] = field(default_factory=list) @@ -62,7 +64,7 @@ impl Violation for MutableDataclassDefault { /// When mutable value are intended, they should be annotated with /// `typing.ClassVar`. /// -/// ## Examples: +/// ## Examples /// ```python /// class A: /// mutable_default: list[int] = [] @@ -72,6 +74,7 @@ impl Violation for MutableDataclassDefault { /// ```python /// from typing import ClassVar /// +/// /// class A: /// mutable_default: ClassVar[list[int]] = [] /// ``` @@ -95,7 +98,7 @@ impl Violation for MutableClassDefault { /// ## Options /// - `flake8-bugbear.extend-immutable-calls` /// -/// ## Examples: +/// ## Examples /// ```python /// from dataclasses import dataclass ///