Skip to content

Commit b2d9f59

Browse files
naslundxMarcus Näslund
andauthored
[ruff] Implement a recursive check for RUF060 (#17976)
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> 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 <marcus.naslund@kognity.com>
1 parent d7ef014 commit b2d9f59

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
b"a" in bytearray()
2020
b"a" in bytes()
2121
1 in frozenset()
22+
1 in set(set())
23+
2 in frozenset([])
24+
'' in set("")
2225

2326
# OK
2427
1 in [2]
@@ -35,3 +38,7 @@
3538
b"a" in bytearray([2])
3639
b"a" in bytes("a", "utf-8")
3740
1 in frozenset("c")
41+
1 in set(set((1,2)))
42+
1 in set(set([1]))
43+
'' in {""}
44+
frozenset() in {frozenset()}

crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Violation};
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
33
use ruff_python_ast::{self as ast, CmpOp, Expr};
4+
use ruff_python_semantic::SemanticModel;
45
use ruff_text_size::Ranged;
56

67
use crate::checkers::ast::Checker;
@@ -48,6 +49,14 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
4849
};
4950

5051
let semantic = checker.semantic();
52+
53+
if is_empty(right, semantic) {
54+
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
55+
}
56+
}
57+
58+
fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
59+
let set_methods = ["set", "frozenset"];
5160
let collection_methods = [
5261
"list",
5362
"tuple",
@@ -59,7 +68,7 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
5968
"str",
6069
];
6170

62-
let is_empty_collection = match right {
71+
match expr {
6372
Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(),
6473
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(),
6574
Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(),
@@ -75,15 +84,19 @@ pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare)
7584
arguments,
7685
range: _,
7786
}) => {
78-
arguments.is_empty()
79-
&& collection_methods
87+
if arguments.is_empty() {
88+
collection_methods
8089
.iter()
8190
.any(|s| semantic.match_builtin_expr(func, s))
91+
} else if let Some(arg) = arguments.find_positional(0) {
92+
set_methods
93+
.iter()
94+
.any(|s| semantic.match_builtin_expr(func, s))
95+
&& is_empty(arg, semantic)
96+
} else {
97+
false
98+
}
8299
}
83100
_ => false,
84-
};
85-
86-
if is_empty_collection {
87-
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
88101
}
89102
}

crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection
187187
20 | b"a" in bytes()
188188
| ^^^^^^^^^^^^^^^ RUF060
189189
21 | 1 in frozenset()
190+
22 | 1 in set(set())
190191
|
191192

192193
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
195196
20 | b"a" in bytes()
196197
21 | 1 in frozenset()
197198
| ^^^^^^^^^^^^^^^^ RUF060
198-
22 |
199-
23 | # OK
199+
22 | 1 in set(set())
200+
23 | 2 in frozenset([])
201+
|
202+
203+
RUF060.py:22:1: RUF060 Unnecessary membership test on empty collection
204+
|
205+
20 | b"a" in bytes()
206+
21 | 1 in frozenset()
207+
22 | 1 in set(set())
208+
| ^^^^^^^^^^^^^^^ RUF060
209+
23 | 2 in frozenset([])
210+
24 | '' in set("")
211+
|
212+
213+
RUF060.py:23:1: RUF060 Unnecessary membership test on empty collection
214+
|
215+
21 | 1 in frozenset()
216+
22 | 1 in set(set())
217+
23 | 2 in frozenset([])
218+
| ^^^^^^^^^^^^^^^^^^ RUF060
219+
24 | '' in set("")
220+
|
221+
222+
RUF060.py:24:1: RUF060 Unnecessary membership test on empty collection
223+
|
224+
22 | 1 in set(set())
225+
23 | 2 in frozenset([])
226+
24 | '' in set("")
227+
| ^^^^^^^^^^^^^ RUF060
228+
25 |
229+
26 | # OK
200230
|

0 commit comments

Comments
 (0)