[ruff] Add extra cases to RUF069

[ruff] Add extra cases to RUF069

Now it handles the following cases:
- __all__: list[str] = ["a", "a"]
- __all__: typing.Any = ("a", "a")
- __all__.extend(["a", "a"])
- __all__ += ["a", "a"]

It still does not track mutable __all__ such as:
__all__ = ["a"]
__all__ += ["a"]

It will be a false negative.
This commit is contained in:
Leandro Braga 2025-12-21 10:54:52 -03:00
parent 42f1cb523b
commit c1ea670b81
5 changed files with 278 additions and 118 deletions

View file

@ -1,3 +1,17 @@
import typing
class A: ...
class B: ...
__all__ = "A" + "B"
__all__: list[str] = ["A", "B"]
__all__: list[str] = ["A", "B", "A"]
__all__: typing.Any = ("A", "B")
__all__: typing.Any = ("A", "B", "B")
__all__ = ["A", "B"]
__all__ = ["A", "A", "B"]
__all__ = ["A", "B", "A"]
@ -7,8 +21,9 @@ __all__ = [
"A",
"B",
# Comment
"B"
"B",
]
class A: ...
class B: ...
__all__ += ["B", "B"]
__all__ += ["A", "B"]
__all__.extend(["B", "B"])
__all__.extend(["A", "B"])

View file

@ -1245,6 +1245,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_extend_call(checker, call);
}
if checker.is_rule_enabled(Rule::DuplicateEntryInDunderAll) {
ruff::rules::duplicate_entry_in_dunder_all_extend_call(checker, call);
}
if checker.is_rule_enabled(Rule::DefaultFactoryKwarg) {
ruff::rules::default_factory_kwarg(checker, call);
}

View file

@ -966,6 +966,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign);
}
if checker.is_rule_enabled(Rule::DuplicateEntryInDunderAll) {
ruff::rules::duplicate_entry_in_dunder_all_aug_assign(checker, aug_assign);
}
}
Stmt::If(
if_ @ ast::StmtIf {
@ -1435,7 +1438,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::sort_dunder_all_assign(checker, assign);
}
if checker.is_rule_enabled(Rule::DuplicateEntryInDunderAll) {
ruff::rules::duplicate_entry_in_dunder_all(checker, assign);
ruff::rules::duplicate_entry_in_dunder_all_assign(checker, assign);
}
if checker.source_type.is_stub() {
if checker.any_rule_enabled(&[
@ -1528,6 +1531,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt);
}
if checker.is_rule_enabled(Rule::DuplicateEntryInDunderAll) {
ruff::rules::duplicate_entry_in_dunder_all_ann_assign(checker, assign_stmt);
}
if checker.source_type.is_stub() {
if let Some(value) = value {
if checker.is_rule_enabled(Rule::AssignmentDefaultInStub) {

View file

@ -50,14 +50,66 @@ impl Violation for DuplicateEntryInDunderAll {
}
}
/// RUF069
/// This routine checks whether `__all__` contains duplicated entries, and emits
/// a violation if it does.
pub(crate) fn duplicate_entry_in_dunder_all(
/// Apply RUF069 to `StmtAssign` AST node. For example: `__all__ = ["a", "b", "a"]`.
pub(crate) fn duplicate_entry_in_dunder_all_assign(
checker: &Checker,
ast::StmtAssign { value, targets, .. }: &ast::StmtAssign,
) {
let [target] = targets.as_slice() else { return };
if let [expr] = targets.as_slice() {
duplicate_entry_in_dunder_all(checker, expr, value);
}
}
/// Apply RUF069 to `StmtAugAssign` AST node. For example: `__all__ += ["a", "b", "a"]`.
pub(crate) fn duplicate_entry_in_dunder_all_aug_assign(
checker: &Checker,
node: &ast::StmtAugAssign,
) {
if node.op.is_add() {
duplicate_entry_in_dunder_all(checker, &node.target, &node.value);
}
}
/// Apply RUF069 to `__all__.extend()`.
pub(crate) fn duplicate_entry_in_dunder_all_extend_call(
checker: &Checker,
ast::ExprCall {
func,
arguments: ast::Arguments { args, keywords, .. },
..
}: &ast::ExprCall,
) {
let ([value_passed], []) = (&**args, &**keywords) else {
return;
};
let ast::Expr::Attribute(ast::ExprAttribute {
ref value,
ref attr,
..
}) = **func
else {
return;
};
if attr == "extend" {
duplicate_entry_in_dunder_all(checker, value, value_passed);
}
}
/// Apply RUF069 to a `StmtAnnAssign` AST node.
/// For example: `__all__: list[str] = ["a", "b", "a"]`.
pub(crate) fn duplicate_entry_in_dunder_all_ann_assign(
checker: &Checker,
node: &ast::StmtAnnAssign,
) {
if let Some(value) = &node.value {
duplicate_entry_in_dunder_all(checker, &node.target, value);
}
}
/// RUF069
/// This routine checks whether `__all__` contains duplicated entries, and emits
/// a violation if it does.
fn duplicate_entry_in_dunder_all(checker: &Checker, target: &ast::Expr, value: &ast::Expr) {
let ast::Expr::Name(ast::ExprName { id, .. }) = target else {
return;
};
@ -71,7 +123,7 @@ pub(crate) fn duplicate_entry_in_dunder_all(
return;
}
let elts = match value.as_ref() {
let elts = match value {
ast::Expr::List(ast::ExprList { elts, .. }) => elts,
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => elts,
_ => return,

View file

@ -2,115 +2,199 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:2:17
|
1 | __all__ = ["A", "B"]
2 | __all__ = ["A", "A", "B"]
| ^^^
3 | __all__ = ["A", "B", "A"]
4 | __all__ = ["A", "A", "B", "B"]
|
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
- __all__ = ["A", "A", "B"]
2 + __all__ = ["A", "B"]
3 | __all__ = ["A", "B", "A"]
4 | __all__ = ["A", "A", "B", "B"]
5 | __all__ = [
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:3:22
|
1 | __all__ = ["A", "B"]
2 | __all__ = ["A", "A", "B"]
3 | __all__ = ["A", "B", "A"]
| ^^^
4 | __all__ = ["A", "A", "B", "B"]
5 | __all__ = [
|
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
2 | __all__ = ["A", "A", "B"]
- __all__ = ["A", "B", "A"]
3 + __all__ = ["A", "B"]
4 | __all__ = ["A", "A", "B", "B"]
5 | __all__ = [
6 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:4:17
|
2 | __all__ = ["A", "A", "B"]
3 | __all__ = ["A", "B", "A"]
4 | __all__ = ["A", "A", "B", "B"]
| ^^^
5 | __all__ = [
6 | "A",
|
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
2 | __all__ = ["A", "A", "B"]
3 | __all__ = ["A", "B", "A"]
- __all__ = ["A", "A", "B", "B"]
4 + __all__ = ["A", "B", "B"]
5 | __all__ = [
6 | "A",
7 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:4:27
|
2 | __all__ = ["A", "A", "B"]
3 | __all__ = ["A", "B", "A"]
4 | __all__ = ["A", "A", "B", "B"]
| ^^^
5 | __all__ = [
6 | "A",
|
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
2 | __all__ = ["A", "A", "B"]
3 | __all__ = ["A", "B", "A"]
- __all__ = ["A", "A", "B", "B"]
4 + __all__ = ["A", "A", "B"]
5 | __all__ = [
6 | "A",
7 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:7:5
|
5 | __all__ = [
6 | "A",
7 | "A",
| ^^^
8 | "B",
9 | # Comment
|
help: Remove duplicate entries from `__all__`
4 | __all__ = ["A", "A", "B", "B"]
5 | __all__ = [
6 | "A",
- "A",
7 | "B",
8 | # Comment
9 | "B"
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:10:5
--> RUF069.py:12:33
|
8 | "B",
9 | # Comment
10 | "B"
10 | __all__ = "A" + "B"
11 | __all__: list[str] = ["A", "B"]
12 | __all__: list[str] = ["A", "B", "A"]
| ^^^
13 | __all__: typing.Any = ("A", "B")
14 | __all__: typing.Any = ("A", "B", "B")
|
help: Remove duplicate entries from `__all__`
9 |
10 | __all__ = "A" + "B"
11 | __all__: list[str] = ["A", "B"]
- __all__: list[str] = ["A", "B", "A"]
12 + __all__: list[str] = ["A", "B"]
13 | __all__: typing.Any = ("A", "B")
14 | __all__: typing.Any = ("A", "B", "B")
15 | __all__ = ["A", "B"]
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:14:34
|
12 | __all__: list[str] = ["A", "B", "A"]
13 | __all__: typing.Any = ("A", "B")
14 | __all__: typing.Any = ("A", "B", "B")
| ^^^
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
|
help: Remove duplicate entries from `__all__`
11 | __all__: list[str] = ["A", "B"]
12 | __all__: list[str] = ["A", "B", "A"]
13 | __all__: typing.Any = ("A", "B")
- __all__: typing.Any = ("A", "B", "B")
14 + __all__: typing.Any = ("A", "B")
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:16:17
|
14 | __all__: typing.Any = ("A", "B", "B")
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
| ^^^
17 | __all__ = ["A", "B", "A"]
18 | __all__ = ["A", "A", "B", "B"]
|
help: Remove duplicate entries from `__all__`
13 | __all__: typing.Any = ("A", "B")
14 | __all__: typing.Any = ("A", "B", "B")
15 | __all__ = ["A", "B"]
- __all__ = ["A", "A", "B"]
16 + __all__ = ["A", "B"]
17 | __all__ = ["A", "B", "A"]
18 | __all__ = ["A", "A", "B", "B"]
19 | __all__ = [
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:17:22
|
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
| ^^^
18 | __all__ = ["A", "A", "B", "B"]
19 | __all__ = [
|
help: Remove duplicate entries from `__all__`
14 | __all__: typing.Any = ("A", "B", "B")
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
- __all__ = ["A", "B", "A"]
17 + __all__ = ["A", "B"]
18 | __all__ = ["A", "A", "B", "B"]
19 | __all__ = [
20 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:18:17
|
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
18 | __all__ = ["A", "A", "B", "B"]
| ^^^
19 | __all__ = [
20 | "A",
|
help: Remove duplicate entries from `__all__`
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
- __all__ = ["A", "A", "B", "B"]
18 + __all__ = ["A", "B", "B"]
19 | __all__ = [
20 | "A",
21 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:18:27
|
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
18 | __all__ = ["A", "A", "B", "B"]
| ^^^
19 | __all__ = [
20 | "A",
|
help: Remove duplicate entries from `__all__`
15 | __all__ = ["A", "B"]
16 | __all__ = ["A", "A", "B"]
17 | __all__ = ["A", "B", "A"]
- __all__ = ["A", "A", "B", "B"]
18 + __all__ = ["A", "A", "B"]
19 | __all__ = [
20 | "A",
21 | "A",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:21:5
|
19 | __all__ = [
20 | "A",
21 | "A",
| ^^^
11 | ]
22 | "B",
23 | # Comment
|
help: Remove duplicate entries from `__all__`
7 | "A",
8 | "B",
9 | # Comment
- "B"
10 | ]
11 |
12 | class A: ...
18 | __all__ = ["A", "A", "B", "B"]
19 | __all__ = [
20 | "A",
- "A",
21 | "B",
22 | # Comment
23 | "B",
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:24:5
|
22 | "B",
23 | # Comment
24 | "B",
| ^^^
25 | ]
26 | __all__ += ["B", "B"]
|
help: Remove duplicate entries from `__all__`
20 | "A",
21 | "A",
22 | "B",
- # Comment
- "B",
23 + # Comment,
24 | ]
25 | __all__ += ["B", "B"]
26 | __all__ += ["A", "B"]
note: This is an unsafe fix and may change runtime behavior
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:26:18
|
24 | "B",
25 | ]
26 | __all__ += ["B", "B"]
| ^^^
27 | __all__ += ["A", "B"]
28 | __all__.extend(["B", "B"])
|
help: Remove duplicate entries from `__all__`
23 | # Comment
24 | "B",
25 | ]
- __all__ += ["B", "B"]
26 + __all__ += ["B"]
27 | __all__ += ["A", "B"]
28 | __all__.extend(["B", "B"])
29 | __all__.extend(["A", "B"])
RUF069 [*] `__all__` contains duplicate entries
--> RUF069.py:28:22
|
26 | __all__ += ["B", "B"]
27 | __all__ += ["A", "B"]
28 | __all__.extend(["B", "B"])
| ^^^
29 | __all__.extend(["A", "B"])
|
help: Remove duplicate entries from `__all__`
25 | ]
26 | __all__ += ["B", "B"]
27 | __all__ += ["A", "B"]
- __all__.extend(["B", "B"])
28 + __all__.extend(["B"])
29 | __all__.extend(["A", "B"])