Skip to content

Commit

Permalink
[flake8-pyi] Add a fix for duplicate-literal-member (#14188)
Browse files Browse the repository at this point in the history
## Summary

Closes #14187.
  • Loading branch information
charliermarsh authored Nov 8, 2024
1 parent 2624249 commit 272d24b
Show file tree
Hide file tree
Showing 3 changed files with 380 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use std::collections::HashSet;

use rustc_hash::FxHashSet;

use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Expr, ExprContext};
use ruff_python_semantic::analyze::typing::traverse_literal;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};

use crate::checkers::ast::Checker;

Expand All @@ -27,31 +27,40 @@ use crate::checkers::ast::Checker;
/// foo: Literal["a", "b"]
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as safe; however, the fix will flatten nested
/// literals into a single top-level literal.
///
/// ## References
/// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)
#[violation]
pub struct DuplicateLiteralMember {
duplicate_name: String,
}

impl Violation for DuplicateLiteralMember {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;

impl AlwaysFixableViolation for DuplicateLiteralMember {
#[derive_message_formats]
fn message(&self) -> String {
format!("Duplicate literal member `{}`", self.duplicate_name)
}

fn fix_title(&self) -> String {
"Remove duplicates".to_string()
}
}

/// PYI062
pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr) {
let mut seen_nodes: HashSet<ComparableExpr<'_>, _> = FxHashSet::default();
let mut unique_nodes: Vec<&Expr> = Vec::new();
let mut diagnostics: Vec<Diagnostic> = Vec::new();

// Adds a member to `literal_exprs` if it is a `Literal` annotation
let mut check_for_duplicate_members = |expr: &'a Expr, _: &'a Expr| {
// If we've already seen this literal member, raise a violation.
if !seen_nodes.insert(expr.into()) {
if seen_nodes.insert(expr.into()) {
unique_nodes.push(expr);
} else {
diagnostics.push(Diagnostic::new(
DuplicateLiteralMember {
duplicate_name: checker.generator().expr(expr),
Expand All @@ -61,7 +70,36 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr
}
};

// Traverse the literal, collect all diagnostic members
// Traverse the literal, collect all diagnostic members.
traverse_literal(&mut check_for_duplicate_members, checker.semantic(), expr);

// If there's at least one diagnostic, create a fix to remove the duplicate members.
if !diagnostics.is_empty() {
if let Expr::Subscript(subscript) = expr {
let subscript = Expr::Subscript(ast::ExprSubscript {
slice: Box::new(if let [elt] = unique_nodes.as_slice() {
(*elt).clone()
} else {
Expr::Tuple(ast::ExprTuple {
elts: unique_nodes.into_iter().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: false,
})
}),
value: subscript.value.clone(),
range: TextRange::default(),
ctx: ExprContext::Load,
});
let fix = Fix::safe_edit(Edit::range_replacement(
checker.generator().expr(&subscript),
expr.range(),
));
for diagnostic in &mut diagnostics {
diagnostic.set_fix(fix.clone());
}
}
}

checker.diagnostics.append(&mut diagnostics);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI062.py:5:25: PYI062 Duplicate literal member `True`
PYI062.py:5:25: PYI062 [*] Duplicate literal member `True`
|
3 | import typing_extensions
4 |
Expand All @@ -10,8 +10,19 @@ PYI062.py:5:25: PYI062 Duplicate literal member `True`
6 |
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
= help: Remove duplicates

PYI062.py:5:31: PYI062 Duplicate literal member `False`
Safe fix
2 2 | import typing as t
3 3 | import typing_extensions
4 4 |
5 |-x: Literal[True, False, True, False] # PYI062 twice here
5 |+x: Literal[True, False] # PYI062 twice here
6 6 |
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
8 8 |

PYI062.py:5:31: PYI062 [*] Duplicate literal member `False`
|
3 | import typing_extensions
4 |
Expand All @@ -20,8 +31,19 @@ PYI062.py:5:31: PYI062 Duplicate literal member `False`
6 |
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
= help: Remove duplicates

Safe fix
2 2 | import typing as t
3 3 | import typing_extensions
4 4 |
5 |-x: Literal[True, False, True, False] # PYI062 twice here
5 |+x: Literal[True, False] # PYI062 twice here
6 6 |
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
8 8 |

PYI062.py:7:45: PYI062 Duplicate literal member `1`
PYI062.py:7:45: PYI062 [*] Duplicate literal member `1`
|
5 | x: Literal[True, False, True, False] # PYI062 twice here
6 |
Expand All @@ -30,8 +52,19 @@ PYI062.py:7:45: PYI062 Duplicate literal member `1`
8 |
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
= help: Remove duplicates

PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
Safe fix
4 4 |
5 5 | x: Literal[True, False, True, False] # PYI062 twice here
6 6 |
7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
7 |+y: Literal[1, print("hello"), 3, 4] # PYI062 on the last 1
8 8 |
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
10 10 |

PYI062.py:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}`
|
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
8 |
Expand All @@ -40,8 +73,19 @@ PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
10 |
11 | Literal[1, Literal[1]] # once
|
= help: Remove duplicates

Safe fix
6 6 |
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
8 8 |
9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
9 |+z: Literal[{1, 3, 5}, "foobar"] # PYI062 on the set literal
10 10 |
11 11 | Literal[1, Literal[1]] # once
12 12 | Literal[1, 2, Literal[1, 2]] # twice

PYI062.py:11:20: PYI062 Duplicate literal member `1`
PYI062.py:11:20: PYI062 [*] Duplicate literal member `1`
|
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
10 |
Expand All @@ -50,26 +94,59 @@ PYI062.py:11:20: PYI062 Duplicate literal member `1`
12 | Literal[1, 2, Literal[1, 2]] # twice
13 | Literal[1, Literal[1], Literal[1]] # twice
|
= help: Remove duplicates

PYI062.py:12:23: PYI062 Duplicate literal member `1`
Safe fix
8 8 |
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
10 10 |
11 |-Literal[1, Literal[1]] # once
11 |+Literal[1] # once
12 12 | Literal[1, 2, Literal[1, 2]] # twice
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once

PYI062.py:12:23: PYI062 [*] Duplicate literal member `1`
|
11 | Literal[1, Literal[1]] # once
12 | Literal[1, 2, Literal[1, 2]] # twice
| ^ PYI062
13 | Literal[1, Literal[1], Literal[1]] # twice
14 | Literal[1, Literal[2], Literal[2]] # once
|
= help: Remove duplicates

Safe fix
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
10 10 |
11 11 | Literal[1, Literal[1]] # once
12 |-Literal[1, 2, Literal[1, 2]] # twice
12 |+Literal[1, 2] # twice
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once

PYI062.py:12:26: PYI062 Duplicate literal member `2`
PYI062.py:12:26: PYI062 [*] Duplicate literal member `2`
|
11 | Literal[1, Literal[1]] # once
12 | Literal[1, 2, Literal[1, 2]] # twice
| ^ PYI062
13 | Literal[1, Literal[1], Literal[1]] # twice
14 | Literal[1, Literal[2], Literal[2]] # once
|
= help: Remove duplicates

Safe fix
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
10 10 |
11 11 | Literal[1, Literal[1]] # once
12 |-Literal[1, 2, Literal[1, 2]] # twice
12 |+Literal[1, 2] # twice
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once

PYI062.py:13:20: PYI062 Duplicate literal member `1`
PYI062.py:13:20: PYI062 [*] Duplicate literal member `1`
|
11 | Literal[1, Literal[1]] # once
12 | Literal[1, 2, Literal[1, 2]] # twice
Expand All @@ -78,8 +155,19 @@ PYI062.py:13:20: PYI062 Duplicate literal member `1`
14 | Literal[1, Literal[2], Literal[2]] # once
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
= help: Remove duplicates

PYI062.py:13:32: PYI062 Duplicate literal member `1`
Safe fix
10 10 |
11 11 | Literal[1, Literal[1]] # once
12 12 | Literal[1, 2, Literal[1, 2]] # twice
13 |-Literal[1, Literal[1], Literal[1]] # twice
13 |+Literal[1] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 16 | typing_extensions.Literal[1, 1, 1] # twice

PYI062.py:13:32: PYI062 [*] Duplicate literal member `1`
|
11 | Literal[1, Literal[1]] # once
12 | Literal[1, 2, Literal[1, 2]] # twice
Expand All @@ -88,8 +176,19 @@ PYI062.py:13:32: PYI062 Duplicate literal member `1`
14 | Literal[1, Literal[2], Literal[2]] # once
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
= help: Remove duplicates

Safe fix
10 10 |
11 11 | Literal[1, Literal[1]] # once
12 12 | Literal[1, 2, Literal[1, 2]] # twice
13 |-Literal[1, Literal[1], Literal[1]] # twice
13 |+Literal[1] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 16 | typing_extensions.Literal[1, 1, 1] # twice

PYI062.py:14:32: PYI062 Duplicate literal member `2`
PYI062.py:14:32: PYI062 [*] Duplicate literal member `2`
|
12 | Literal[1, 2, Literal[1, 2]] # twice
13 | Literal[1, Literal[1], Literal[1]] # twice
Expand All @@ -98,17 +197,39 @@ PYI062.py:14:32: PYI062 Duplicate literal member `2`
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 | typing_extensions.Literal[1, 1, 1] # twice
|
= help: Remove duplicates

PYI062.py:15:37: PYI062 Duplicate literal member `1`
Safe fix
11 11 | Literal[1, Literal[1]] # once
12 12 | Literal[1, 2, Literal[1, 2]] # twice
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 |-Literal[1, Literal[2], Literal[2]] # once
14 |+Literal[1, 2] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 16 | typing_extensions.Literal[1, 1, 1] # twice
17 17 |

PYI062.py:15:37: PYI062 [*] Duplicate literal member `1`
|
13 | Literal[1, Literal[1], Literal[1]] # twice
14 | Literal[1, Literal[2], Literal[2]] # once
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
| ^ PYI062
16 | typing_extensions.Literal[1, 1, 1] # twice
|
= help: Remove duplicates

Safe fix
12 12 | Literal[1, 2, Literal[1, 2]] # twice
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once
15 |+t.Literal[1, 2] # once
16 16 | typing_extensions.Literal[1, 1, 1] # twice
17 17 |
18 18 | # Ensure issue is only raised once, even on nested literals

PYI062.py:16:30: PYI062 Duplicate literal member `1`
PYI062.py:16:30: PYI062 [*] Duplicate literal member `1`
|
14 | Literal[1, Literal[2], Literal[2]] # once
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
Expand All @@ -117,8 +238,19 @@ PYI062.py:16:30: PYI062 Duplicate literal member `1`
17 |
18 | # Ensure issue is only raised once, even on nested literals
|
= help: Remove duplicates

PYI062.py:16:33: PYI062 Duplicate literal member `1`
Safe fix
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 |-typing_extensions.Literal[1, 1, 1] # twice
16 |+typing_extensions.Literal[1] # twice
17 17 |
18 18 | # Ensure issue is only raised once, even on nested literals
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062

PYI062.py:16:33: PYI062 [*] Duplicate literal member `1`
|
14 | Literal[1, Literal[2], Literal[2]] # once
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
Expand All @@ -127,12 +259,33 @@ PYI062.py:16:33: PYI062 Duplicate literal member `1`
17 |
18 | # Ensure issue is only raised once, even on nested literals
|
= help: Remove duplicates

Safe fix
13 13 | Literal[1, Literal[1], Literal[1]] # twice
14 14 | Literal[1, Literal[2], Literal[2]] # once
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
16 |-typing_extensions.Literal[1, 1, 1] # twice
16 |+typing_extensions.Literal[1] # twice
17 17 |
18 18 | # Ensure issue is only raised once, even on nested literals
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062

PYI062.py:19:46: PYI062 Duplicate literal member `True`
PYI062.py:19:46: PYI062 [*] Duplicate literal member `True`
|
18 | # Ensure issue is only raised once, even on nested literals
19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
| ^^^^ PYI062
20 |
21 | n: Literal["No", "duplicates", "here", 1, "1"]
|
= help: Remove duplicates

Safe fix
16 16 | typing_extensions.Literal[1, 1, 1] # twice
17 17 |
18 18 | # Ensure issue is only raised once, even on nested literals
19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
19 |+MyType = Literal["foo", True, False, "bar"] # PYI062
20 20 |
21 21 | n: Literal["No", "duplicates", "here", 1, "1"]
Loading

0 comments on commit 272d24b

Please sign in to comment.