Add new rule InEmptyCollection (#16480)

## Summary

Introducing a new rule based on discussions in #15732 and #15729 that
checks for unnecessary in with empty collections.

I called it in_empty_collection and gave the rule number RUF060.

Rule is in preview group.
This commit is contained in:
Marcus Näslund 2025-05-06 15:52:07 +02:00 committed by GitHub
parent f82b72882b
commit 76b6d53d8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 335 additions and 0 deletions

View file

@ -0,0 +1,37 @@
# Errors
1 in []
1 not in []
2 in list()
2 not in list()
_ in ()
_ not in ()
'x' in tuple()
'y' not in tuple()
'a' in set()
'a' not in set()
'b' in {}
'b' not in {}
1 in dict()
2 not in dict()
"a" in ""
b'c' in b""
"b" in f""
b"a" in bytearray()
b"a" in bytes()
1 in frozenset()
# OK
1 in [2]
1 in [1, 2, 3]
_ in ('a')
_ not in ('a')
'a' in set('a', 'b')
'a' not in set('b', 'c')
'b' in {1: 2}
'b' not in {3: 4}
"a" in "x"
b'c' in b"x"
"b" in f"x"
b"a" in bytearray([2])
b"a" in bytes("a", "utf-8")
1 in frozenset("c")

View file

@ -1504,6 +1504,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.enabled(Rule::NanComparison) {
pylint::rules::nan_comparison(checker, left, comparators);
}
if checker.enabled(Rule::InEmptyCollection) {
ruff::rules::in_empty_collection(checker, compare);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_compare(checker, compare);
}

View file

@ -1014,6 +1014,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
(Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip),
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

View file

@ -99,6 +99,7 @@ mod tests {
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))]
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_2.py"))]
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_3.py"))]
#[test_case(Rule::InEmptyCollection, Path::new("RUF060.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

View file

@ -0,0 +1,89 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for membership tests on empty collections (such as `list`, `tuple`, `set` or `dict`).
///
/// ## Why is this bad?
/// If the collection is always empty, the check is unnecessary, and can be removed.
///
/// ## Example
///
/// ```python
/// if 1 not in set():
/// print("got it!")
/// ```
///
/// Use instead:
///
/// ```python
/// print("got it!")
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct InEmptyCollection;
impl Violation for InEmptyCollection {
#[derive_message_formats]
fn message(&self) -> String {
"Unnecessary membership test on empty collection".to_string()
}
}
/// RUF060
pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) {
let [op] = &*compare.ops else {
return;
};
if !matches!(op, CmpOp::In | CmpOp::NotIn) {
return;
}
let [right] = &*compare.comparators else {
return;
};
let semantic = checker.semantic();
let collection_methods = [
"list",
"tuple",
"set",
"frozenset",
"dict",
"bytes",
"bytearray",
"str",
];
let is_empty_collection = match right {
Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(),
Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(),
Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::FString(s) => s
.value
.elements()
.all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())),
Expr::Call(ast::ExprCall {
func,
arguments,
range: _,
}) => {
arguments.is_empty()
&& collection_methods
.iter()
.any(|s| semantic.match_builtin_expr(func, s))
}
_ => false,
};
if is_empty_collection {
checker.report_diagnostic(Diagnostic::new(InEmptyCollection, compare.range()));
}
}

View file

@ -13,6 +13,7 @@ pub(crate) use function_call_in_dataclass_default::*;
pub(crate) use if_key_in_dict_del::*;
pub(crate) use implicit_classvar_in_dataclass::*;
pub(crate) use implicit_optional::*;
pub(crate) use in_empty_collection::*;
pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*;
pub(crate) use indented_form_feed::*;
pub(crate) use invalid_assert_message_literal_argument::*;
@ -73,6 +74,7 @@ mod helpers;
mod if_key_in_dict_del;
mod implicit_classvar_in_dataclass;
mod implicit_optional;
mod in_empty_collection;
mod incorrectly_parenthesized_tuple_in_subscript;
mod indented_form_feed;
mod invalid_assert_message_literal_argument;

View file

@ -0,0 +1,200 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF060.py:2:1: RUF060 Unnecessary membership test on empty collection
|
1 | # Errors
2 | 1 in []
| ^^^^^^^ RUF060
3 | 1 not in []
4 | 2 in list()
|
RUF060.py:3:1: RUF060 Unnecessary membership test on empty collection
|
1 | # Errors
2 | 1 in []
3 | 1 not in []
| ^^^^^^^^^^^ RUF060
4 | 2 in list()
5 | 2 not in list()
|
RUF060.py:4:1: RUF060 Unnecessary membership test on empty collection
|
2 | 1 in []
3 | 1 not in []
4 | 2 in list()
| ^^^^^^^^^^^ RUF060
5 | 2 not in list()
6 | _ in ()
|
RUF060.py:5:1: RUF060 Unnecessary membership test on empty collection
|
3 | 1 not in []
4 | 2 in list()
5 | 2 not in list()
| ^^^^^^^^^^^^^^^ RUF060
6 | _ in ()
7 | _ not in ()
|
RUF060.py:6:1: RUF060 Unnecessary membership test on empty collection
|
4 | 2 in list()
5 | 2 not in list()
6 | _ in ()
| ^^^^^^^ RUF060
7 | _ not in ()
8 | 'x' in tuple()
|
RUF060.py:7:1: RUF060 Unnecessary membership test on empty collection
|
5 | 2 not in list()
6 | _ in ()
7 | _ not in ()
| ^^^^^^^^^^^ RUF060
8 | 'x' in tuple()
9 | 'y' not in tuple()
|
RUF060.py:8:1: RUF060 Unnecessary membership test on empty collection
|
6 | _ in ()
7 | _ not in ()
8 | 'x' in tuple()
| ^^^^^^^^^^^^^^ RUF060
9 | 'y' not in tuple()
10 | 'a' in set()
|
RUF060.py:9:1: RUF060 Unnecessary membership test on empty collection
|
7 | _ not in ()
8 | 'x' in tuple()
9 | 'y' not in tuple()
| ^^^^^^^^^^^^^^^^^^ RUF060
10 | 'a' in set()
11 | 'a' not in set()
|
RUF060.py:10:1: RUF060 Unnecessary membership test on empty collection
|
8 | 'x' in tuple()
9 | 'y' not in tuple()
10 | 'a' in set()
| ^^^^^^^^^^^^ RUF060
11 | 'a' not in set()
12 | 'b' in {}
|
RUF060.py:11:1: RUF060 Unnecessary membership test on empty collection
|
9 | 'y' not in tuple()
10 | 'a' in set()
11 | 'a' not in set()
| ^^^^^^^^^^^^^^^^ RUF060
12 | 'b' in {}
13 | 'b' not in {}
|
RUF060.py:12:1: RUF060 Unnecessary membership test on empty collection
|
10 | 'a' in set()
11 | 'a' not in set()
12 | 'b' in {}
| ^^^^^^^^^ RUF060
13 | 'b' not in {}
14 | 1 in dict()
|
RUF060.py:13:1: RUF060 Unnecessary membership test on empty collection
|
11 | 'a' not in set()
12 | 'b' in {}
13 | 'b' not in {}
| ^^^^^^^^^^^^^ RUF060
14 | 1 in dict()
15 | 2 not in dict()
|
RUF060.py:14:1: RUF060 Unnecessary membership test on empty collection
|
12 | 'b' in {}
13 | 'b' not in {}
14 | 1 in dict()
| ^^^^^^^^^^^ RUF060
15 | 2 not in dict()
16 | "a" in ""
|
RUF060.py:15:1: RUF060 Unnecessary membership test on empty collection
|
13 | 'b' not in {}
14 | 1 in dict()
15 | 2 not in dict()
| ^^^^^^^^^^^^^^^ RUF060
16 | "a" in ""
17 | b'c' in b""
|
RUF060.py:16:1: RUF060 Unnecessary membership test on empty collection
|
14 | 1 in dict()
15 | 2 not in dict()
16 | "a" in ""
| ^^^^^^^^^ RUF060
17 | b'c' in b""
18 | "b" in f""
|
RUF060.py:17:1: RUF060 Unnecessary membership test on empty collection
|
15 | 2 not in dict()
16 | "a" in ""
17 | b'c' in b""
| ^^^^^^^^^^^ RUF060
18 | "b" in f""
19 | b"a" in bytearray()
|
RUF060.py:18:1: RUF060 Unnecessary membership test on empty collection
|
16 | "a" in ""
17 | b'c' in b""
18 | "b" in f""
| ^^^^^^^^^^ RUF060
19 | b"a" in bytearray()
20 | b"a" in bytes()
|
RUF060.py:19:1: RUF060 Unnecessary membership test on empty collection
|
17 | b'c' in b""
18 | "b" in f""
19 | b"a" in bytearray()
| ^^^^^^^^^^^^^^^^^^^ RUF060
20 | b"a" in bytes()
21 | 1 in frozenset()
|
RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection
|
18 | "b" in f""
19 | b"a" in bytearray()
20 | b"a" in bytes()
| ^^^^^^^^^^^^^^^ RUF060
21 | 1 in frozenset()
|
RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection
|
19 | b"a" in bytearray()
20 | b"a" in bytes()
21 | 1 in frozenset()
| ^^^^^^^^^^^^^^^^ RUF060
22 |
23 | # OK
|

2
ruff.schema.json generated
View file

@ -4035,6 +4035,8 @@
"RUF057",
"RUF058",
"RUF059",
"RUF06",
"RUF060",
"RUF1",
"RUF10",
"RUF100",