mirror of https://github.com/astral-sh/ruff
implement PLR6201 with autofix (#7973)
## Summary Implements [R6201](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/use-set-for-membership.html) along with the autofix! See #970 ## Test Plan `cargo test`, and manually
This commit is contained in:
parent
cb6d74c27b
commit
5da0f9111e
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Errors
|
||||||
|
1 in [1, 2, 3]
|
||||||
|
1 in (1, 2, 3)
|
||||||
|
1 in (
|
||||||
|
1, 2, 3
|
||||||
|
)
|
||||||
|
|
||||||
|
# OK
|
||||||
|
fruits = ["cherry", "grapes"]
|
||||||
|
"cherry" in fruits
|
||||||
|
|
@ -1197,6 +1197,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::ComparisonWithItself) {
|
if checker.enabled(Rule::ComparisonWithItself) {
|
||||||
pylint::rules::comparison_with_itself(checker, left, ops, comparators);
|
pylint::rules::comparison_with_itself(checker, left, ops, comparators);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::LiteralMembership) {
|
||||||
|
pylint::rules::literal_membership(checker, compare);
|
||||||
|
}
|
||||||
if checker.enabled(Rule::ComparisonOfConstant) {
|
if checker.enabled(Rule::ComparisonOfConstant) {
|
||||||
pylint::rules::comparison_of_constant(checker, left, ops, comparators);
|
pylint::rules::comparison_of_constant(checker, left, ops, comparators);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
|
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
|
||||||
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
|
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
|
||||||
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
|
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
|
||||||
|
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
||||||
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
|
(Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop),
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ mod tests {
|
||||||
#[test_case(Rule::BadDunderMethodName, Path::new("bad_dunder_method_name.py"))]
|
#[test_case(Rule::BadDunderMethodName, Path::new("bad_dunder_method_name.py"))]
|
||||||
#[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))]
|
#[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))]
|
||||||
#[test_case(Rule::MisplacedBareRaise, Path::new("misplaced_bare_raise.py"))]
|
#[test_case(Rule::MisplacedBareRaise, Path::new("misplaced_bare_raise.py"))]
|
||||||
|
#[test_case(Rule::LiteralMembership, Path::new("literal_membership.py"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
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 `list` and `tuple` literals.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// When testing for membership in a static sequence, prefer a `set` literal
|
||||||
|
/// over a `list` or `tuple`, as Python optimizes `set` membership tests.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// 1 in [1, 2, 3]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// 1 in {1, 2, 3}
|
||||||
|
/// ```
|
||||||
|
/// ## References
|
||||||
|
/// - [What’s New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations)
|
||||||
|
#[violation]
|
||||||
|
pub struct LiteralMembership;
|
||||||
|
|
||||||
|
impl AlwaysFixableViolation for LiteralMembership {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
format!("Use a `set` literal when testing for membership")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_title(&self) -> String {
|
||||||
|
format!("Convert to `set`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PLR6201
|
||||||
|
pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompare) {
|
||||||
|
let [op] = compare.ops.as_slice() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !matches!(op, CmpOp::In | CmpOp::NotIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [right] = compare.comparators.as_slice() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !matches!(right, Expr::List(_) | Expr::Tuple(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(LiteralMembership, right.range());
|
||||||
|
|
||||||
|
let literal = checker.locator().slice(right);
|
||||||
|
let set = format!("{{{}}}", &literal[1..literal.len() - 1]);
|
||||||
|
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(set, right.range())));
|
||||||
|
|
||||||
|
checker.diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ pub(crate) use invalid_envvar_value::*;
|
||||||
pub(crate) use invalid_str_return::*;
|
pub(crate) use invalid_str_return::*;
|
||||||
pub(crate) use invalid_string_characters::*;
|
pub(crate) use invalid_string_characters::*;
|
||||||
pub(crate) use iteration_over_set::*;
|
pub(crate) use iteration_over_set::*;
|
||||||
|
pub(crate) use literal_membership::*;
|
||||||
pub(crate) use load_before_global_declaration::*;
|
pub(crate) use load_before_global_declaration::*;
|
||||||
pub(crate) use logging::*;
|
pub(crate) use logging::*;
|
||||||
pub(crate) use magic_value_comparison::*;
|
pub(crate) use magic_value_comparison::*;
|
||||||
|
|
@ -86,6 +87,7 @@ mod invalid_envvar_value;
|
||||||
mod invalid_str_return;
|
mod invalid_str_return;
|
||||||
mod invalid_string_characters;
|
mod invalid_string_characters;
|
||||||
mod iteration_over_set;
|
mod iteration_over_set;
|
||||||
|
mod literal_membership;
|
||||||
mod load_before_global_declaration;
|
mod load_before_global_declaration;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod magic_value_comparison;
|
mod magic_value_comparison;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||||
|
---
|
||||||
|
literal_membership.py:2:6: PLR6201 [*] Use a `set` literal when testing for membership
|
||||||
|
|
|
||||||
|
1 | # Errors
|
||||||
|
2 | 1 in [1, 2, 3]
|
||||||
|
| ^^^^^^^^^ PLR6201
|
||||||
|
3 | 1 in (1, 2, 3)
|
||||||
|
4 | 1 in (
|
||||||
|
|
|
||||||
|
= help: Convert to `set`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
1 1 | # Errors
|
||||||
|
2 |-1 in [1, 2, 3]
|
||||||
|
2 |+1 in {1, 2, 3}
|
||||||
|
3 3 | 1 in (1, 2, 3)
|
||||||
|
4 4 | 1 in (
|
||||||
|
5 5 | 1, 2, 3
|
||||||
|
|
||||||
|
literal_membership.py:3:6: PLR6201 [*] Use a `set` literal when testing for membership
|
||||||
|
|
|
||||||
|
1 | # Errors
|
||||||
|
2 | 1 in [1, 2, 3]
|
||||||
|
3 | 1 in (1, 2, 3)
|
||||||
|
| ^^^^^^^^^ PLR6201
|
||||||
|
4 | 1 in (
|
||||||
|
5 | 1, 2, 3
|
||||||
|
|
|
||||||
|
= help: Convert to `set`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
1 1 | # Errors
|
||||||
|
2 2 | 1 in [1, 2, 3]
|
||||||
|
3 |-1 in (1, 2, 3)
|
||||||
|
3 |+1 in {1, 2, 3}
|
||||||
|
4 4 | 1 in (
|
||||||
|
5 5 | 1, 2, 3
|
||||||
|
6 6 | )
|
||||||
|
|
||||||
|
literal_membership.py:4:6: PLR6201 [*] Use a `set` literal when testing for membership
|
||||||
|
|
|
||||||
|
2 | 1 in [1, 2, 3]
|
||||||
|
3 | 1 in (1, 2, 3)
|
||||||
|
4 | 1 in (
|
||||||
|
| ______^
|
||||||
|
5 | | 1, 2, 3
|
||||||
|
6 | | )
|
||||||
|
| |_^ PLR6201
|
||||||
|
7 |
|
||||||
|
8 | # OK
|
||||||
|
|
|
||||||
|
= help: Convert to `set`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
1 1 | # Errors
|
||||||
|
2 2 | 1 in [1, 2, 3]
|
||||||
|
3 3 | 1 in (1, 2, 3)
|
||||||
|
4 |-1 in (
|
||||||
|
4 |+1 in {
|
||||||
|
5 5 | 1, 2, 3
|
||||||
|
6 |-)
|
||||||
|
6 |+}
|
||||||
|
7 7 |
|
||||||
|
8 8 | # OK
|
||||||
|
9 9 | fruits = ["cherry", "grapes"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2968,6 +2968,9 @@
|
||||||
"PLR550",
|
"PLR550",
|
||||||
"PLR5501",
|
"PLR5501",
|
||||||
"PLR6",
|
"PLR6",
|
||||||
|
"PLR62",
|
||||||
|
"PLR620",
|
||||||
|
"PLR6201",
|
||||||
"PLR63",
|
"PLR63",
|
||||||
"PLR630",
|
"PLR630",
|
||||||
"PLR6301",
|
"PLR6301",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue