Skip to content
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] Recognize ClassVar from superclasses correctly (RUF045) #16299

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF045.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
from dataclasses import InitVar, KW_ONLY, MISSING, dataclass, field
from typing import ClassVar
from dataclasses import KW_ONLY, MISSING, InitVar, dataclass, field
from typing import Annotated, ClassVar, Final

from somewhere import A


class B:
class_var_outermost: ClassVar[int]
class_var_wrapped: Final[ClassVar[int]]
class_var_inlegally_wrapped: Final[list[ClassVar[int]]]
class_var_annotated: Annotated[ClassVar[int], 42]
class_var_invalid_annotated: Annotated[ClassVar[int]]
class_var.attribute: ClassVar[int]
class_var[subscript]: ClassVar[int]

if True:
class_var_nested: ClassVar[int]


@dataclass
class C:
class C(B):
# Errors
no_annotation = r"foo"
missing = MISSING
field = field()

class_var_invalid_annotated = 42
attribute = 42
subscript = 42
class_var_inlegally_wrapped = 42

# No errors
__slots__ = ("foo", "bar")
__radd__ = __add__
Expand All @@ -21,9 +41,18 @@ class C:
class_var_no_arguments: ClassVar = 42
class_var_with_arguments: ClassVar[int] = 42

class_var_outermost = 42
class_var_wrapped = 42
class_var_annotated = 42
class_var_nested = 42

init_var_no_arguments: InitVar = "lorem"
init_var_with_arguments: InitVar[str] = "ipsum"

kw_only: KW_ONLY
tu, ple, [unp, ack, ing] = (0, 1, 2, [3, 4, 5])
mul, [ti, ple] = (a, ssign), ment = {1: b"3", "2": 4}, [6j, 5]

@dataclass
class D(A):
class_var_unknown = 42
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::is_dunder;
use ruff_python_ast::{Expr, ExprName, Stmt, StmtAssign, StmtClassDef};
use ruff_python_ast::helpers::{is_dunder, map_subscript};
use ruff_python_ast::{Expr, ExprName, ExprSubscript, Stmt, StmtAssign, StmtClassDef};
use ruff_python_semantic::analyze::class::{
any_base_class, any_member_declaration, ClassMemberKind,
};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
Expand Down Expand Up @@ -67,7 +71,8 @@ impl Violation for ImplicitClassVarInDataclass {

/// RUF045
pub(crate) fn implicit_class_var_in_dataclass(checker: &mut Checker, class_def: &StmtClassDef) {
let dataclass_kind = dataclass_kind(class_def, checker.semantic());
let semantic = checker.semantic();
let dataclass_kind = dataclass_kind(class_def, semantic);

if !matches!(dataclass_kind, Some((DataclassKind::Stdlib, _))) {
return;
Expand Down Expand Up @@ -95,8 +100,96 @@ pub(crate) fn implicit_class_var_in_dataclass(checker: &mut Checker, class_def:
continue;
}

if might_have_class_var_annotation_in_superclass(id, class_def, semantic) {
continue;
}
Comment on lines +103 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems undesired to compute this in the loop. Is there a reason for not computing it before the loop or after the loop and dropping all diagnostics in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each iteration checks a different annotated assignment with a different id. It is indeed possible to refactor this, but if I were to do so I would prefer to wait until after #14688, which would add a few helpers for inspecting base classes.


let diagnostic = Diagnostic::new(ImplicitClassVarInDataclass, target.range());

checker.report_diagnostic(diagnostic);
}
}

/// Inspect each base class:
///
/// * If a base class is not inspectable, return true.
/// * If there is a member with the same `id` whose annotation has `ClassVar`, return true.
///
/// Otherwise, return false.
fn might_have_class_var_annotation_in_superclass(
id: &str,
class_def: &StmtClassDef,
semantic: &SemanticModel,
) -> bool {
if class_def.bases().is_empty() {
return false;
}

any_base_class(class_def, semantic, &mut |base| {
let Expr::Name(name) = map_subscript(base) else {
return false;
};

let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return true;
};
let Some(Stmt::ClassDef(base_class_def)) = binding.statement(semantic) else {
return true;
};

any_member_declaration(base_class_def, &mut |declaration| {
let ClassMemberKind::AnnAssign(ann_assign) = declaration.kind() else {
return false;
};

let Expr::Name(name) = &*ann_assign.target else {
return false;
};

if name.id != id {
return false;
}

annotation_contains_class_var(&ann_assign.annotation, semantic)
})
})
}

fn annotation_contains_class_var(annotation: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_typing() {
return false;
}

let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation else {
return false;
};

let Some(qualified_name) = semantic.resolve_qualified_name(value) else {
return false;
};

match qualified_name.segments() {
["typing" | "_typeshed" | "typing_extensions", "ClassVar"] => true,

["typing" | "_typeshed" | "typing_extensions", "Final"] => {
if matches!(&**slice, Expr::Tuple(_)) {
return false;
}

annotation_contains_class_var(slice, semantic)
}

["typing" | "_typeshed" | "typing_extensions", "Annotated"] => {
let Expr::Tuple(tuple) = &**slice else {
return false;
};
let Some(wrapped) = tuple.elts.first() else {
return false;
};

annotation_contains_class_var(wrapped, semantic)
}

_ => false,
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,76 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF045.py:8:5: RUF045 Assignment without annotation found in dataclass body
RUF045.py:23:5: RUF045 Assignment without annotation found in dataclass body
|
6 | class C:
7 | # Errors
8 | no_annotation = r"foo"
21 | class C(B):
22 | # Errors
23 | no_annotation = r"foo"
| ^^^^^^^^^^^^^ RUF045
9 | missing = MISSING
10 | field = field()
24 | missing = MISSING
25 | field = field()
|
= help: Use `ClassVar[...]`

RUF045.py:9:5: RUF045 Assignment without annotation found in dataclass body
RUF045.py:24:5: RUF045 Assignment without annotation found in dataclass body
|
7 | # Errors
8 | no_annotation = r"foo"
9 | missing = MISSING
22 | # Errors
23 | no_annotation = r"foo"
24 | missing = MISSING
| ^^^^^^^ RUF045
10 | field = field()
25 | field = field()
|
= help: Use `ClassVar[...]`

RUF045.py:10:5: RUF045 Assignment without annotation found in dataclass body
RUF045.py:25:5: RUF045 Assignment without annotation found in dataclass body
|
8 | no_annotation = r"foo"
9 | missing = MISSING
10 | field = field()
23 | no_annotation = r"foo"
24 | missing = MISSING
25 | field = field()
| ^^^^^ RUF045
11 |
12 | # No errors
26 |
27 | class_var_invalid_annotated = 42
|
= help: Use `ClassVar[...]`

RUF045.py:27:5: RUF045 Assignment without annotation found in dataclass body
|
25 | field = field()
26 |
27 | class_var_invalid_annotated = 42
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF045
28 | attribute = 42
29 | subscript = 42
|
= help: Use `ClassVar[...]`

RUF045.py:28:5: RUF045 Assignment without annotation found in dataclass body
|
27 | class_var_invalid_annotated = 42
28 | attribute = 42
| ^^^^^^^^^ RUF045
29 | subscript = 42
30 | class_var_inlegally_wrapped = 42
|
= help: Use `ClassVar[...]`

RUF045.py:29:5: RUF045 Assignment without annotation found in dataclass body
|
27 | class_var_invalid_annotated = 42
28 | attribute = 42
29 | subscript = 42
| ^^^^^^^^^ RUF045
30 | class_var_inlegally_wrapped = 42
|
= help: Use `ClassVar[...]`

RUF045.py:30:5: RUF045 Assignment without annotation found in dataclass body
|
28 | attribute = 42
29 | subscript = 42
30 | class_var_inlegally_wrapped = 42
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF045
31 |
32 | # No errors
|
= help: Use `ClassVar[...]`
Loading