-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ruff] Detect mutable defaults in field calls (RUF008)
#23046
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ruff] Detect mutable defaults in field calls (RUF008)
#23046
Conversation
RUF008 previously only caught bare mutable defaults like `[]` or `{}`,
but missed mutable defaults wrapped in `field(default=...)`,
`attrs.field(default=...)`, `attr.ib(default=...)`, and
`attr.attrib(default=...)` calls.
When the assigned value is a recognized dataclass field call, extract the
`default` keyword argument and check that for mutability instead.
Add positive cases for `field(default=[])`, `attrs.field(default={})`,
`attr.ib(default=[])`, `attr.attrib(default=set())`, and `dict()`.
Add negative cases for `factory=list`, immutable defaults, ClassVar
annotations combined with field defaults, and non-mutable variables.
|
Hi @ntBre would you review my first PR? |
|
ntBre
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! This is looking good.
The main thing we need to do is make this a preview change. See my inline comment for a link to other example preview functions. Other than that, I just a couple of nits!
| /// If the default value is intended to be mutable, it must be annotated with | ||
| /// `typing.ClassVar`; otherwise, a `ValueError` will be raised. | ||
| /// | ||
| /// This rule also detects mutable defaults passed via the `default` keyword |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to make this a preview change and also mention that here:
| /// This rule also detects mutable defaults passed via the `default` keyword | |
| /// In [preview](https://docs.astral.sh/ruff/preview/) this rule also detects mutable defaults passed via the `default` keyword |
crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs
Outdated
Show resolved
Hide resolved
| } else { | ||
| Some(value.as_ref()) | ||
| } | ||
| } else { | ||
| Some(value.as_ref()) | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might be slightly nicer as a match to avoid these dangling elses:
let value_to_check = match &**value {
Expr::Call(ast::ExprCall {
func, arguments, ..
}) if is_dataclass_field(func, checker.semantic(), dataclass_kind) => {
arguments.find_argument_value("default", 0)
}
value => Some(value),
};I would probably just shadow value instead of using value_to_check too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated this and pushed!
Would you review once more? this would be good to merge 😉
crates/ruff_linter/resources/test/fixtures/ruff/RUF008_attrs.py
Outdated
Show resolved
Hide resolved
|
Oh, and thank you for handling |
…ult.rs Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
…ult.rs Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
ntBre
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still has not been gated behind a preview check, and my suggestion to add comments to the test cases was for every new test case, not that single line.
Please make sure you've addressed all of the feedback before pinging us for an additional review.
| # Mutable defaults wrapped in field() calls | ||
| @dataclass | ||
| class C: | ||
| mutable_default: list[int] = field(default=[]) # okay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is not correct. This line emits a diagnostic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you review my last commit if that looks good to you?
field(default=...) calls (RUF008)ruff] Detect mutable defaults in field calls (RUF008)
|
Thank you for merging this! |
The field(default=...) detection from astral-sh#23046 is behind a preview gate, so class D diagnostics should only appear in the preview snapshot. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The field(default=...) detection from astral-sh#23046 is behind a preview gate, so class D diagnostics should only appear in the preview snapshot. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Resolves #16495.
RUF008 previously only caught bare mutable defaults like
mutable_default: list[int] = []in dataclass attributes, but missed mutable defaults wrapped infield(default=...)calls. For example, the following was not flagged:This PR modifies
mutable_dataclass_default()to look inside recognized dataclass field calls (dataclasses.field(),attrs.field(),attr.ib(),attr.attrib()) and check thedefaultkeyword argument for mutability.The approach mirrors how RUF009 already uses
is_dataclass_field()to identify field calls — RUF008 now reuses the same helper to extract and inspect thedefaultkeyword argument. No duplicate diagnostics are introduced since RUF009 already skips field calls entirely.Test Plan
cargo nextest run -p ruff_linterandcargo clippy.Added test cases covering:
field(default=[]),attrs.field(default={}),attr.ib(default=[]),attr.attrib(default=set()),field(default=dict())factory=list,default=(),default="hello",default=1,default=KNOWINGLY_MUTABLE_DEFAULT,ClassVarannotation withfield(default=[])