From b2d9f599376c83171ab0945d74f49c06394cefe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20N=C3=A4slund?= Date: Mon, 12 May 2025 22:17:13 +0200 Subject: [PATCH] [`ruff`] Implement a recursive check for `RUF060` (#17976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The existing implementation of RUF060 (InEmptyCollection) is not recursive, meaning that although set([]) results in an empty collection, the existing code fails it because set is taking an argument. The updated implementation allows set and frozenset to take empty collection as positional argument (which results in empty set/frozenset). ## Test Plan Added test cases for recursive cases + updated snapshot (see RUF060.py). --------- Co-authored-by: Marcus Näslund --- .../resources/test/fixtures/ruff/RUF060.py | 7 ++++ .../rules/ruff/rules/in_empty_collection.rs | 27 +++++++++++---- ..._rules__ruff__tests__RUF060_RUF060.py.snap | 34 +++++++++++++++++-- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py index 52ea18de39..c3b96c6b7b 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py @@ -19,6 +19,9 @@ b'c' in b"" b"a" in bytearray() b"a" in bytes() 1 in frozenset() +1 in set(set()) +2 in frozenset([]) +'' in set("") # OK 1 in [2] @@ -35,3 +38,7 @@ b'c' in b"x" b"a" in bytearray([2]) b"a" in bytes("a", "utf-8") 1 in frozenset("c") +1 in set(set((1,2))) +1 in set(set([1])) +'' in {""} +frozenset() in {frozenset()} diff --git a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs index 142b968b1b..9132508e8c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{self as ast, CmpOp, Expr}; +use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -48,6 +49,14 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) }; let semantic = checker.semantic(); + + if is_empty(right, semantic) { + checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range())); + } +} + +fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool { + let set_methods = ["set", "frozenset"]; let collection_methods = [ "list", "tuple", @@ -59,7 +68,7 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) "str", ]; - let is_empty_collection = match right { + match expr { Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(), Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(), Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(), @@ -75,15 +84,19 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) arguments, range: _, }) => { - arguments.is_empty() - && collection_methods + if arguments.is_empty() { + collection_methods .iter() .any(|s| semantic.match_builtin_expr(func, s)) + } else if let Some(arg) = arguments.find_positional(0) { + set_methods + .iter() + .any(|s| semantic.match_builtin_expr(func, s)) + && is_empty(arg, semantic) + } else { + false + } } _ => false, - }; - - if is_empty_collection { - checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range())); } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap index a269459cd7..ac92e967a9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap @@ -187,6 +187,7 @@ RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection 20 | b"a" in bytes() | ^^^^^^^^^^^^^^^ RUF060 21 | 1 in frozenset() +22 | 1 in set(set()) | RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection @@ -195,6 +196,35 @@ RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection 20 | b"a" in bytes() 21 | 1 in frozenset() | ^^^^^^^^^^^^^^^^ RUF060 -22 | -23 | # OK +22 | 1 in set(set()) +23 | 2 in frozenset([]) + | + +RUF060.py:22:1: RUF060 Unnecessary membership test on empty collection + | +20 | b"a" in bytes() +21 | 1 in frozenset() +22 | 1 in set(set()) + | ^^^^^^^^^^^^^^^ RUF060 +23 | 2 in frozenset([]) +24 | '' in set("") + | + +RUF060.py:23:1: RUF060 Unnecessary membership test on empty collection + | +21 | 1 in frozenset() +22 | 1 in set(set()) +23 | 2 in frozenset([]) + | ^^^^^^^^^^^^^^^^^^ RUF060 +24 | '' in set("") + | + +RUF060.py:24:1: RUF060 Unnecessary membership test on empty collection + | +22 | 1 in set(set()) +23 | 2 in frozenset([]) +24 | '' in set("") + | ^^^^^^^^^^^^^ RUF060 +25 | +26 | # OK |