[refurb] Add coverage of set and frozenset calls (FURB171) (#18035)

## Summary

Adds coverage of using set(...) in addition to `{...} in
SingleItemMembershipTest.

Fixes #15792
(and replaces the old PR #15793)

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Updated unit test and snapshot.

Steps to reproduce are in the issue linked above.

<!-- How was it tested? -->
This commit is contained in:
Marcus Näslund 2025-05-29 20:59:49 +02:00 committed by GitHub
parent 7df79cfb70
commit 9d3cad95bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 232 additions and 17 deletions

View file

@ -0,0 +1,53 @@
# Errors.
if 1 in set([1]):
print("Single-element set")
if 1 in set((1,)):
print("Single-element set")
if 1 in set({1}):
print("Single-element set")
if 1 in frozenset([1]):
print("Single-element set")
if 1 in frozenset((1,)):
print("Single-element set")
if 1 in frozenset({1}):
print("Single-element set")
if 1 in set(set([1])):
print('Recursive solution')
# Non-errors.
if 1 in set((1, 2)):
pass
if 1 in set([1, 2]):
pass
if 1 in set({1, 2}):
pass
if 1 in frozenset((1, 2)):
pass
if 1 in frozenset([1, 2]):
pass
if 1 in frozenset({1, 2}):
pass
if 1 in set(1,):
pass
if 1 in set(1,2):
pass
if 1 in set((x for x in range(2))):
pass

View file

@ -35,7 +35,8 @@ mod tests {
#[test_case(Rule::UnnecessaryFromFloat, Path::new("FURB164.py"))]
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171_0.py"))]
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171_1.py"))]
#[test_case(Rule::BitCount, Path::new("FURB161.py"))]
#[test_case(Rule::IntOnSlicedStr, Path::new("FURB166.py"))]
#[test_case(Rule::RegexFlagAlias, Path::new("FURB167.py"))]

View file

@ -1,6 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::generate_comparison;
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprStringLiteral};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@ -78,8 +79,8 @@ pub(crate) fn single_item_membership_test(
_ => return,
};
// Check if the right-hand side is a single-item object.
let Some(item) = single_item(right) else {
// Check if the right-hand side is a single-item object
let Some(item) = single_item(right, checker.semantic()) else {
return;
};
@ -115,7 +116,7 @@ pub(crate) fn single_item_membership_test(
/// Return the single item wrapped in `Some` if the expression contains a single
/// item, otherwise return `None`.
fn single_item(expr: &Expr) -> Option<&Expr> {
fn single_item<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> {
match expr {
Expr::List(ast::ExprList { elts, .. })
| Expr::Tuple(ast::ExprTuple { elts, .. })
@ -124,6 +125,19 @@ fn single_item(expr: &Expr) -> Option<&Expr> {
[item] => Some(item),
_ => None,
},
Expr::Call(ast::ExprCall {
func,
arguments,
range: _,
}) => {
if arguments.len() != 1 || !is_set_method(func, semantic) {
return None;
}
arguments
.find_positional(0)
.and_then(|arg| single_item(arg, semantic))
}
string_expr @ Expr::StringLiteral(ExprStringLiteral { value: string, .. })
if string.chars().count() == 1 =>
{
@ -133,6 +147,12 @@ fn single_item(expr: &Expr) -> Option<&Expr> {
}
}
fn is_set_method(func: &Expr, semantic: &SemanticModel) -> bool {
["set", "frozenset"]
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MembershipTest {
/// Ex) `1 in [1]`

View file

@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB171.py:3:4: FURB171 [*] Membership test against single-item container
FURB171_0.py:3:4: FURB171 [*] Membership test against single-item container
|
1 | # Errors.
2 |
@ -20,7 +20,7 @@ FURB171.py:3:4: FURB171 [*] Membership test against single-item container
5 5 |
6 6 | if 1 in [1]:
FURB171.py:6:4: FURB171 [*] Membership test against single-item container
FURB171_0.py:6:4: FURB171 [*] Membership test against single-item container
|
4 | print("Single-element tuple")
5 |
@ -40,7 +40,7 @@ FURB171.py:6:4: FURB171 [*] Membership test against single-item container
8 8 |
9 9 | if 1 in {1}:
FURB171.py:9:4: FURB171 [*] Membership test against single-item container
FURB171_0.py:9:4: FURB171 [*] Membership test against single-item container
|
7 | print("Single-element list")
8 |
@ -60,7 +60,7 @@ FURB171.py:9:4: FURB171 [*] Membership test against single-item container
11 11 |
12 12 | if "a" in "a":
FURB171.py:12:4: FURB171 [*] Membership test against single-item container
FURB171_0.py:12:4: FURB171 [*] Membership test against single-item container
|
10 | print("Single-element set")
11 |
@ -80,7 +80,7 @@ FURB171.py:12:4: FURB171 [*] Membership test against single-item container
14 14 |
15 15 | if 1 not in (1,):
FURB171.py:15:4: FURB171 [*] Membership test against single-item container
FURB171_0.py:15:4: FURB171 [*] Membership test against single-item container
|
13 | print("Single-element string")
14 |
@ -100,7 +100,7 @@ FURB171.py:15:4: FURB171 [*] Membership test against single-item container
17 17 |
18 18 | if not 1 in (1,):
FURB171.py:18:8: FURB171 [*] Membership test against single-item container
FURB171_0.py:18:8: FURB171 [*] Membership test against single-item container
|
16 | print("Check `not in` membership test")
17 |
@ -120,7 +120,7 @@ FURB171.py:18:8: FURB171 [*] Membership test against single-item container
20 20 |
21 21 | # Non-errors.
FURB171.py:52:5: FURB171 [*] Membership test against single-item container
FURB171_0.py:52:5: FURB171 [*] Membership test against single-item container
|
51 | # https://github.com/astral-sh/ruff/issues/10063
52 | _ = a in (
@ -147,7 +147,7 @@ FURB171.py:52:5: FURB171 [*] Membership test against single-item container
57 54 | _ = a in ( # Foo1
58 55 | ( # Foo2
FURB171.py:57:5: FURB171 [*] Membership test against single-item container
FURB171_0.py:57:5: FURB171 [*] Membership test against single-item container
|
55 | )
56 |
@ -199,7 +199,7 @@ FURB171.py:57:5: FURB171 [*] Membership test against single-item container
74 63 | foo = (
75 64 | lorem()
FURB171.py:77:28: FURB171 [*] Membership test against single-item container
FURB171_0.py:77:28: FURB171 [*] Membership test against single-item container
|
75 | lorem()
76 | .ipsum()
@ -228,7 +228,7 @@ FURB171.py:77:28: FURB171 [*] Membership test against single-item container
83 79 |
84 80 | foo = (
FURB171.py:87:28: FURB171 [*] Membership test against single-item container
FURB171_0.py:87:28: FURB171 [*] Membership test against single-item container
|
85 | lorem()
86 | .ipsum()
@ -262,7 +262,7 @@ FURB171.py:87:28: FURB171 [*] Membership test against single-item container
95 93 |
96 94 | foo = lorem() \
FURB171.py:98:24: FURB171 [*] Membership test against single-item container
FURB171_0.py:98:24: FURB171 [*] Membership test against single-item container
|
96 | foo = lorem() \
97 | .ipsum() \
@ -292,7 +292,7 @@ FURB171.py:98:24: FURB171 [*] Membership test against single-item container
104 100 | def _():
105 101 | if foo not \
FURB171.py:105:8: FURB171 [*] Membership test against single-item container
FURB171_0.py:105:8: FURB171 [*] Membership test against single-item container
|
104 | def _():
105 | if foo not \
@ -323,7 +323,7 @@ FURB171.py:105:8: FURB171 [*] Membership test against single-item container
112 107 | def _():
113 108 | if foo not \
FURB171.py:113:8: FURB171 [*] Membership test against single-item container
FURB171_0.py:113:8: FURB171 [*] Membership test against single-item container
|
112 | def _():
113 | if foo not \

View file

@ -0,0 +1,141 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB171_1.py:3:4: FURB171 [*] Membership test against single-item container
|
1 | # Errors.
2 |
3 | if 1 in set([1]):
| ^^^^^^^^^^^^^ FURB171
4 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
1 1 | # Errors.
2 2 |
3 |-if 1 in set([1]):
3 |+if 1 == 1:
4 4 | print("Single-element set")
5 5 |
6 6 | if 1 in set((1,)):
FURB171_1.py:6:4: FURB171 [*] Membership test against single-item container
|
4 | print("Single-element set")
5 |
6 | if 1 in set((1,)):
| ^^^^^^^^^^^^^^ FURB171
7 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
3 3 | if 1 in set([1]):
4 4 | print("Single-element set")
5 5 |
6 |-if 1 in set((1,)):
6 |+if 1 == 1:
7 7 | print("Single-element set")
8 8 |
9 9 | if 1 in set({1}):
FURB171_1.py:9:4: FURB171 [*] Membership test against single-item container
|
7 | print("Single-element set")
8 |
9 | if 1 in set({1}):
| ^^^^^^^^^^^^^ FURB171
10 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
6 6 | if 1 in set((1,)):
7 7 | print("Single-element set")
8 8 |
9 |-if 1 in set({1}):
9 |+if 1 == 1:
10 10 | print("Single-element set")
11 11 |
12 12 | if 1 in frozenset([1]):
FURB171_1.py:12:4: FURB171 [*] Membership test against single-item container
|
10 | print("Single-element set")
11 |
12 | if 1 in frozenset([1]):
| ^^^^^^^^^^^^^^^^^^^ FURB171
13 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
9 9 | if 1 in set({1}):
10 10 | print("Single-element set")
11 11 |
12 |-if 1 in frozenset([1]):
12 |+if 1 == 1:
13 13 | print("Single-element set")
14 14 |
15 15 | if 1 in frozenset((1,)):
FURB171_1.py:15:4: FURB171 [*] Membership test against single-item container
|
13 | print("Single-element set")
14 |
15 | if 1 in frozenset((1,)):
| ^^^^^^^^^^^^^^^^^^^^ FURB171
16 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
12 12 | if 1 in frozenset([1]):
13 13 | print("Single-element set")
14 14 |
15 |-if 1 in frozenset((1,)):
15 |+if 1 == 1:
16 16 | print("Single-element set")
17 17 |
18 18 | if 1 in frozenset({1}):
FURB171_1.py:18:4: FURB171 [*] Membership test against single-item container
|
16 | print("Single-element set")
17 |
18 | if 1 in frozenset({1}):
| ^^^^^^^^^^^^^^^^^^^ FURB171
19 | print("Single-element set")
|
= help: Convert to equality test
Safe fix
15 15 | if 1 in frozenset((1,)):
16 16 | print("Single-element set")
17 17 |
18 |-if 1 in frozenset({1}):
18 |+if 1 == 1:
19 19 | print("Single-element set")
20 20 |
21 21 | if 1 in set(set([1])):
FURB171_1.py:21:4: FURB171 [*] Membership test against single-item container
|
19 | print("Single-element set")
20 |
21 | if 1 in set(set([1])):
| ^^^^^^^^^^^^^^^^^^ FURB171
22 | print('Recursive solution')
|
= help: Convert to equality test
Safe fix
18 18 | if 1 in frozenset({1}):
19 19 | print("Single-element set")
20 20 |
21 |-if 1 in set(set([1])):
21 |+if 1 == 1:
22 22 | print('Recursive solution')
23 23 |
24 24 |