From c1ea670b81a8df3b2938e4e15d4692ae9433e83a Mon Sep 17 00:00:00 2001 From: Leandro Braga <18340809+leandrobbraga@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:54:52 -0300 Subject: [PATCH] [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. --- .../resources/test/fixtures/ruff/RUF069.py | 23 +- .../src/checkers/ast/analyze/expression.rs | 3 + .../src/checkers/ast/analyze/statement.rs | 8 +- .../rules/ruff/rules/duplicate_all_entry.rs | 64 +++- ..._rules__ruff__tests__RUF069_RUF069.py.snap | 298 +++++++++++------- 5 files changed, 278 insertions(+), 118 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF069.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF069.py index c2f1e4f1fe..c7e0060c5e 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF069.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF069.py @@ -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"]) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 0957aee346..547272698c 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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); } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index ddb2efd05e..a5220c8428 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -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) { diff --git a/crates/ruff_linter/src/rules/ruff/rules/duplicate_all_entry.rs b/crates/ruff_linter/src/rules/ruff/rules/duplicate_all_entry.rs index fc8c2e5885..56cac2ce13 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/duplicate_all_entry.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/duplicate_all_entry.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF069_RUF069.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF069_RUF069.py.snap index 75e616c225..74d5680fca 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF069_RUF069.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF069_RUF069.py.snap @@ -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"])