[flake8-bugbear] Add fix for duplicate-value (B033) (#9510)

## Summary

Adds autofix for
[B033](https://docs.astral.sh/ruff/rules/duplicate-value/)

## Test Plan

`cargo test`
This commit is contained in:
Steve C
2024-01-14 18:20:16 -05:00
committed by GitHub
parent 953d48b7f4
commit 0c0d3db1b5
6 changed files with 342 additions and 12 deletions

View File

@@ -2,10 +2,28 @@
# Errors.
###
incorrect_set = {"value1", 23, 5, "value1"}
incorrect_set = {1, 1, 2}
incorrect_set_multiline = {
"value1",
23,
5,
"value1",
# B033
}
incorrect_set = {1, 1}
incorrect_set = {1, 1,}
incorrect_set = {0, 1, 1,}
incorrect_set = {0, 1, 1}
incorrect_set = {
0,
1,
1,
}
###
# Non-errors.
###
correct_set = {"value1", 23, 5}
correct_set = {5, "5"}
correct_set = {5}
correct_set = {}

View File

@@ -980,9 +980,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_pie::rules::unnecessary_spread(checker, dict);
}
}
Expr::Set(ast::ExprSet { elts, range: _ }) => {
Expr::Set(set) => {
if checker.enabled(Rule::DuplicateValue) {
flake8_bugbear::rules::duplicate_value(checker, elts);
flake8_bugbear::rules::duplicate_value(checker, set);
}
}
Expr::Yield(_) => {

View File

@@ -12,6 +12,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
@@ -69,6 +70,24 @@ mod tests {
Ok(())
}
#[test_case(Rule::DuplicateValue, Path::new("B033.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";

View File

@@ -1,9 +1,11 @@
use ruff_python_ast::Expr;
use anyhow::{Context, Result};
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -31,28 +33,82 @@ pub struct DuplicateValue {
}
impl Violation for DuplicateValue {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateValue { value } = self;
format!("Sets should not contain duplicate item `{value}`")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Remove duplicate item"))
}
}
/// B033
pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec<Expr>) {
pub(crate) fn duplicate_value(checker: &mut Checker, set: &ast::ExprSet) {
let mut seen_values: FxHashSet<ComparableExpr> = FxHashSet::default();
for elt in elts {
for (index, elt) in set.elts.iter().enumerate() {
if elt.is_literal_expr() {
let comparable_value: ComparableExpr = elt.into();
if !seen_values.insert(comparable_value) {
checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
DuplicateValue {
value: checker.generator().expr(elt),
},
elt.range(),
));
);
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_member(set, index, checker.locator().contents()).map(Fix::safe_edit)
});
}
checker.diagnostics.push(diagnostic);
}
};
}
}
/// Remove the member at the given index from the [`ast::ExprSet`].
fn remove_member(set: &ast::ExprSet, index: usize, source: &str) -> Result<Edit> {
if index < set.elts.len() - 1 {
// Case 1: the expression is _not_ the last node, so delete from the start of the
// expression to the end of the subsequent comma.
// Ex) Delete `"a"` in `{"a", "b", "c"}`.
let mut tokenizer = SimpleTokenizer::starts_at(set.elts[index].end(), source);
// Find the trailing comma.
tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
// Find the next non-whitespace token.
let next = tokenizer
.find(|token| {
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
})
.context("Unable to find next token")?;
Ok(Edit::deletion(set.elts[index].start(), next.start()))
} else if index > 0 {
// Case 2: the expression is the last node, but not the _only_ node, so delete from the
// start of the previous comma to the end of the expression.
// Ex) Delete `"c"` in `{"a", "b", "c"}`.
let mut tokenizer = SimpleTokenizer::starts_at(set.elts[index - 1].end(), source);
// Find the trailing comma.
let comma = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
Ok(Edit::deletion(comma.start(), set.elts[index].end()))
} else {
// Case 3: expression is the only node, so delete it.
// Ex) Delete `"a"` in `{"a"}`.
Ok(Edit::range_deletion(set.elts[index].range()))
}
}

View File

@@ -7,17 +7,85 @@ B033.py:4:35: B033 Sets should not contain duplicate item `"value1"`
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
| ^^^^^^^^ B033
5 | incorrect_set = {1, 1}
5 | incorrect_set = {1, 1, 2}
6 | incorrect_set_multiline = {
|
= help: Remove duplicate item
B033.py:5:21: B033 Sets should not contain duplicate item `1`
|
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
5 | incorrect_set = {1, 1}
5 | incorrect_set = {1, 1, 2}
| ^ B033
6 |
7 | ###
6 | incorrect_set_multiline = {
7 | "value1",
|
= help: Remove duplicate item
B033.py:10:5: B033 Sets should not contain duplicate item `"value1"`
|
8 | 23,
9 | 5,
10 | "value1",
| ^^^^^^^^ B033
11 | # B033
12 | }
|
= help: Remove duplicate item
B033.py:13:21: B033 Sets should not contain duplicate item `1`
|
11 | # B033
12 | }
13 | incorrect_set = {1, 1}
| ^ B033
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
|
= help: Remove duplicate item
B033.py:14:21: B033 Sets should not contain duplicate item `1`
|
12 | }
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
| ^ B033
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
|
= help: Remove duplicate item
B033.py:15:24: B033 Sets should not contain duplicate item `1`
|
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
| ^ B033
16 | incorrect_set = {0, 1, 1}
17 | incorrect_set = {
|
= help: Remove duplicate item
B033.py:16:24: B033 Sets should not contain duplicate item `1`
|
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
| ^ B033
17 | incorrect_set = {
18 | 0,
|
= help: Remove duplicate item
B033.py:20:5: B033 Sets should not contain duplicate item `1`
|
18 | 0,
19 | 1,
20 | 1,
| ^ B033
21 | }
|
= help: Remove duplicate item

View File

@@ -0,0 +1,169 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B033.py:4:35: B033 [*] Sets should not contain duplicate item `"value1"`
|
2 | # Errors.
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
| ^^^^^^^^ B033
5 | incorrect_set = {1, 1, 2}
6 | incorrect_set_multiline = {
|
= help: Remove duplicate item
Safe fix
1 1 | ###
2 2 | # Errors.
3 3 | ###
4 |-incorrect_set = {"value1", 23, 5, "value1"}
4 |+incorrect_set = {"value1", 23, 5}
5 5 | incorrect_set = {1, 1, 2}
6 6 | incorrect_set_multiline = {
7 7 | "value1",
B033.py:5:21: B033 [*] Sets should not contain duplicate item `1`
|
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
5 | incorrect_set = {1, 1, 2}
| ^ B033
6 | incorrect_set_multiline = {
7 | "value1",
|
= help: Remove duplicate item
Safe fix
2 2 | # Errors.
3 3 | ###
4 4 | incorrect_set = {"value1", 23, 5, "value1"}
5 |-incorrect_set = {1, 1, 2}
5 |+incorrect_set = {1, 2}
6 6 | incorrect_set_multiline = {
7 7 | "value1",
8 8 | 23,
B033.py:10:5: B033 [*] Sets should not contain duplicate item `"value1"`
|
8 | 23,
9 | 5,
10 | "value1",
| ^^^^^^^^ B033
11 | # B033
12 | }
|
= help: Remove duplicate item
Safe fix
7 7 | "value1",
8 8 | 23,
9 9 | 5,
10 |- "value1",
11 10 | # B033
12 11 | }
13 12 | incorrect_set = {1, 1}
B033.py:13:21: B033 [*] Sets should not contain duplicate item `1`
|
11 | # B033
12 | }
13 | incorrect_set = {1, 1}
| ^ B033
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
|
= help: Remove duplicate item
Safe fix
10 10 | "value1",
11 11 | # B033
12 12 | }
13 |-incorrect_set = {1, 1}
13 |+incorrect_set = {1}
14 14 | incorrect_set = {1, 1,}
15 15 | incorrect_set = {0, 1, 1,}
16 16 | incorrect_set = {0, 1, 1}
B033.py:14:21: B033 [*] Sets should not contain duplicate item `1`
|
12 | }
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
| ^ B033
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
|
= help: Remove duplicate item
Safe fix
11 11 | # B033
12 12 | }
13 13 | incorrect_set = {1, 1}
14 |-incorrect_set = {1, 1,}
14 |+incorrect_set = {1,}
15 15 | incorrect_set = {0, 1, 1,}
16 16 | incorrect_set = {0, 1, 1}
17 17 | incorrect_set = {
B033.py:15:24: B033 [*] Sets should not contain duplicate item `1`
|
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
| ^ B033
16 | incorrect_set = {0, 1, 1}
17 | incorrect_set = {
|
= help: Remove duplicate item
Safe fix
12 12 | }
13 13 | incorrect_set = {1, 1}
14 14 | incorrect_set = {1, 1,}
15 |-incorrect_set = {0, 1, 1,}
15 |+incorrect_set = {0, 1,}
16 16 | incorrect_set = {0, 1, 1}
17 17 | incorrect_set = {
18 18 | 0,
B033.py:16:24: B033 [*] Sets should not contain duplicate item `1`
|
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
| ^ B033
17 | incorrect_set = {
18 | 0,
|
= help: Remove duplicate item
Safe fix
13 13 | incorrect_set = {1, 1}
14 14 | incorrect_set = {1, 1,}
15 15 | incorrect_set = {0, 1, 1,}
16 |-incorrect_set = {0, 1, 1}
16 |+incorrect_set = {0, 1}
17 17 | incorrect_set = {
18 18 | 0,
19 19 | 1,
B033.py:20:5: B033 [*] Sets should not contain duplicate item `1`
|
18 | 0,
19 | 1,
20 | 1,
| ^ B033
21 | }
|
= help: Remove duplicate item
Safe fix
17 17 | incorrect_set = {
18 18 | 0,
19 19 | 1,
20 |- 1,
21 20 | }
22 21 |
23 22 | ###