[ruff] Also report problems for attrs dataclasses in preview mode (RUF008, RUF009) (#14327)
Some checks are pending
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz (push) Blocked by required conditions
CI / Fuzz the parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
InSync 2024-11-14 22:13:49 +07:00 committed by GitHub
parent 9a3001b571
commit d8b1afbc6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 344 additions and 51 deletions

View file

@ -0,0 +1,47 @@
import typing
from typing import ClassVar, Sequence
import attr
from attr import s
from attrs import define, frozen
KNOWINGLY_MUTABLE_DEFAULT = []
@define
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: typing.ClassVar[list[int]] = []
@frozen
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
@attr.s
class C:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
@s
class D:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []

View file

@ -0,0 +1,75 @@
import datetime
import re
import typing
from collections import OrderedDict
from fractions import Fraction
from pathlib import Path
from typing import ClassVar, NamedTuple
import attr
from attrs import Factory, define, field, frozen
def default_function() -> list[int]:
return []
class ImmutableType(NamedTuple):
something: int = 8
@attr.s
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()
fine_path: Path = Path()
fine_date: datetime.date = datetime.date(2042, 1, 1)
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
fine_tuple: tuple[int] = tuple([1])
fine_regex: re.Pattern = re.compile(r".*")
fine_float: float = float("-inf")
fine_int: int = int(12)
fine_complex: complex = complex(1, 2)
fine_str: str = str("foo")
fine_bool: bool = bool("foo")
fine_fraction: Fraction = Fraction(1, 2)
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])
@define
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
fine_dataclass_function: list[int] = field(default_factory=list)
attrs_factory: dict[str, str] = Factory(OrderedDict)
class IntConversionDescriptor:
def __init__(self, *, default):
self._default = default
def __set_name__(self, owner, name):
self._name = "_" + name
def __get__(self, obj, type):
if obj is None:
return self._default
return getattr(obj, self._name, self._default)
def __set__(self, obj, value):
setattr(obj, self._name, int(value))
@frozen
class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

View file

@ -388,6 +388,11 @@ mod tests {
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
#[test_case(Rule::UnsafeMarkupUse, Path::new("RUF035.py"))]
#[test_case(
Rule::FunctionCallInDataclassDefaultArgument,
Path::new("RUF009_attrs.py")
)]
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008_attrs.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View file

@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{
is_class_var_annotation, is_dataclass, is_dataclass_field, is_descriptor_class,
dataclass_kind, is_class_var_annotation, is_dataclass_field, is_descriptor_class,
};
/// ## What it does
@ -74,7 +74,13 @@ pub(crate) fn function_call_in_dataclass_default(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
) {
if !is_dataclass(class_def, checker.semantic()) {
let semantic = checker.semantic();
let Some(dataclass_kind) = dataclass_kind(class_def, semantic) else {
return;
};
if dataclass_kind.is_attrs() && checker.settings.preview.is_disabled() {
return;
}
@ -87,26 +93,31 @@ pub(crate) fn function_call_in_dataclass_default(
.collect();
for statement in &class_def.body {
if let Stmt::AnnAssign(ast::StmtAnnAssign {
let Stmt::AnnAssign(ast::StmtAnnAssign {
annotation,
value: Some(expr),
..
}) = statement
else {
continue;
};
let Expr::Call(ast::ExprCall { func, .. }) = &**expr else {
continue;
};
if is_class_var_annotation(annotation, checker.semantic())
|| is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
|| is_dataclass_field(func, checker.semantic(), dataclass_kind)
|| is_descriptor_class(func, checker.semantic())
{
if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() {
if !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
&& !is_dataclass_field(func, checker.semantic())
&& !is_descriptor_class(func, checker.semantic())
{
checker.diagnostics.push(Diagnostic::new(
FunctionCallInDataclassDefaultArgument {
name: UnqualifiedName::from_expr(func).map(|name| name.to_string()),
},
expr.range(),
));
}
}
continue;
}
let kind = FunctionCallInDataclassDefaultArgument {
name: UnqualifiedName::from_expr(func).map(|name| name.to_string()),
};
let diagnostic = Diagnostic::new(kind, expr.range());
checker.diagnostics.push(diagnostic);
}
}

View file

@ -18,17 +18,42 @@ pub(super) fn is_special_attribute(value: &Expr) -> bool {
}
}
/// Returns `true` if the given [`Expr`] is a `dataclasses.field` call.
pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_module(Modules::DATACLASSES) {
return false;
}
/// Returns `true` if the given [`Expr`] is a stdlib `dataclasses.field` call.
fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"]))
}
/// Returns `true` if the given [`Expr`] is a call to `attr.ib()` or `attrs.field()`.
fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["attrs", "field" | "Factory"] | ["attr", "ib"]
)
})
}
/// Return `true` if `func` represents a `field()` call corresponding to the `dataclass_kind` variant passed in.
///
/// I.e., if `DataclassKind::Attrs` is passed in,
/// return `true` if `func` represents a call to `attr.ib()` or `attrs.field()`;
/// if `DataclassKind::Stdlib` is passed in,
/// return `true` if `func` represents a call to `dataclasse.field()`.
pub(super) fn is_dataclass_field(
func: &Expr,
semantic: &SemanticModel,
dataclass_kind: DataclassKind,
) -> bool {
match dataclass_kind {
DataclassKind::Attrs => is_attrs_field(func, semantic),
DataclassKind::Stdlib => is_stdlib_dataclass_field(func, semantic),
}
}
/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation.
pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_typing() {
@ -51,19 +76,49 @@ pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -
semantic.match_typing_expr(map_subscript(annotation), "Final")
}
/// Returns `true` if the given class is a dataclass.
pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
if !semantic.seen_module(Modules::DATACLASSES) {
return false;
/// Enumeration of various kinds of dataclasses recognised by Ruff
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum DataclassKind {
/// dataclasses created by the stdlib `dataclasses` module
Stdlib,
/// dataclasses created by the third-party `attrs` library
Attrs,
}
impl DataclassKind {
pub(super) const fn is_stdlib(self) -> bool {
matches!(self, DataclassKind::Stdlib)
}
class_def.decorator_list.iter().any(|decorator| {
semantic
.resolve_qualified_name(map_callable(&decorator.expression))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["dataclasses", "dataclass"])
})
})
pub(super) const fn is_attrs(self) -> bool {
matches!(self, DataclassKind::Attrs)
}
}
/// Return the kind of dataclass this class definition is (stdlib or `attrs`), or `None` if the class is not a dataclass.
pub(super) fn dataclass_kind(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
) -> Option<DataclassKind> {
if !(semantic.seen_module(Modules::DATACLASSES) || semantic.seen_module(Modules::ATTRS)) {
return None;
}
for decorator in &class_def.decorator_list {
let Some(qualified_name) =
semantic.resolve_qualified_name(map_callable(&decorator.expression))
else {
continue;
};
match qualified_name.segments() {
["attrs", "define" | "frozen"] | ["attr", "s"] => return Some(DataclassKind::Attrs),
["dataclasses", "dataclass"] => return Some(DataclassKind::Stdlib),
_ => continue,
}
}
None
}
/// Returns `true` if the given class has "default copy" semantics.

View file

@ -7,7 +7,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{
has_default_copy_semantics, is_class_var_annotation, is_dataclass, is_final_annotation,
dataclass_kind, has_default_copy_semantics, is_class_var_annotation, is_final_annotation,
is_special_attribute,
};
@ -65,8 +65,13 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt
&& !is_class_var_annotation(annotation, checker.semantic())
&& !is_final_annotation(annotation, checker.semantic())
&& !is_immutable_annotation(annotation, checker.semantic(), &[])
&& !is_dataclass(class_def, checker.semantic())
{
if let Some(dataclass_kind) = dataclass_kind(class_def, checker.semantic()) {
if dataclass_kind.is_stdlib() || checker.settings.preview.is_enabled() {
continue;
}
}
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
if has_default_copy_semantics(class_def, checker.semantic()) {
return;

View file

@ -6,7 +6,7 @@ use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
use crate::rules::ruff::rules::helpers::{dataclass_kind, is_class_var_annotation};
/// ## What it does
/// Checks for mutable default values in dataclass attributes.
@ -66,25 +66,33 @@ impl Violation for MutableDataclassDefault {
/// RUF008
pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_dataclass(class_def, checker.semantic()) {
let semantic = checker.semantic();
let Some(dataclass_kind) = dataclass_kind(class_def, semantic) else {
return;
};
if dataclass_kind.is_attrs() && checker.settings.preview.is_disabled() {
return;
}
for statement in &class_def.body {
if let Stmt::AnnAssign(ast::StmtAnnAssign {
let Stmt::AnnAssign(ast::StmtAnnAssign {
annotation,
value: Some(value),
..
}) = statement
else {
continue;
};
if is_mutable_expr(value, checker.semantic())
&& !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_annotation(annotation, checker.semantic(), &[])
{
if is_mutable_expr(value, checker.semantic())
&& !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_annotation(annotation, checker.semantic(), &[])
{
checker
.diagnostics
.push(Diagnostic::new(MutableDataclassDefault, value.range()));
}
let diagnostic = Diagnostic::new(MutableDataclassDefault, value.range());
checker.diagnostics.push(diagnostic);
}
}
}

View file

@ -10,7 +10,7 @@ use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, importer::ImportRequest};
use super::helpers::is_dataclass;
use super::helpers::{dataclass_kind, DataclassKind};
/// ## What it does
/// Checks for `__post_init__` dataclass methods with parameter defaults.
@ -91,7 +91,10 @@ pub(crate) fn post_init_default(checker: &mut Checker, function_def: &ast::StmtF
let current_scope = checker.semantic().current_scope();
match current_scope.kind {
ScopeKind::Class(class_def) => {
if !is_dataclass(class_def, checker.semantic()) {
if !matches!(
dataclass_kind(class_def, checker.semantic()),
Some(DataclassKind::Stdlib)
) {
return;
}
}

View file

@ -40,5 +40,3 @@ RUF009.py:45:34: RUF009 Do not perform function call `ImmutableType` in dataclas
46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|

View file

@ -0,0 +1,42 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF008_attrs.py:13:34: RUF008 Do not use mutable default values for dataclass attributes
|
11 | @define
12 | class A:
13 | mutable_default: list[int] = []
| ^^ RUF008
14 | immutable_annotation: typing.Sequence[int] = []
15 | without_annotation = []
|
RUF008_attrs.py:23:34: RUF008 Do not use mutable default values for dataclass attributes
|
21 | @frozen
22 | class B:
23 | mutable_default: list[int] = []
| ^^ RUF008
24 | immutable_annotation: Sequence[int] = []
25 | without_annotation = []
|
RUF008_attrs.py:33:34: RUF008 Do not use mutable default values for dataclass attributes
|
31 | @attr.s
32 | class C:
33 | mutable_default: list[int] = []
| ^^ RUF008
34 | immutable_annotation: Sequence[int] = []
35 | without_annotation = []
|
RUF008_attrs.py:42:34: RUF008 Do not use mutable default values for dataclass attributes
|
40 | @s
41 | class D:
42 | mutable_default: list[int] = []
| ^^ RUF008
43 | immutable_annotation: Sequence[int] = []
44 | without_annotation = []
|

View file

@ -0,0 +1,42 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF009_attrs.py:23:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
21 | @attr.s
22 | class A:
23 | hidden_mutable_default: list[int] = default_function()
| ^^^^^^^^^^^^^^^^^^ RUF009
24 | class_variable: typing.ClassVar[list[int]] = default_function()
25 | another_class_var: ClassVar[list[int]] = default_function()
|
RUF009_attrs.py:46:41: RUF009 Do not perform function call `default_function` in dataclass defaults
|
44 | @define
45 | class B:
46 | hidden_mutable_default: list[int] = default_function()
| ^^^^^^^^^^^^^^^^^^ RUF009
47 | another_dataclass: A = A()
48 | not_optimal: ImmutableType = ImmutableType(20)
|
RUF009_attrs.py:47:28: RUF009 Do not perform function call `A` in dataclass defaults
|
45 | class B:
46 | hidden_mutable_default: list[int] = default_function()
47 | another_dataclass: A = A()
| ^^^ RUF009
48 | not_optimal: ImmutableType = ImmutableType(20)
49 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
RUF009_attrs.py:48:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults
|
46 | hidden_mutable_default: list[int] = default_function()
47 | another_dataclass: A = A()
48 | not_optimal: ImmutableType = ImmutableType(20)
| ^^^^^^^^^^^^^^^^^ RUF009
49 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
50 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|