Skip to content

Commit 272d24b

Browse files
[flake8-pyi] Add a fix for duplicate-literal-member (#14188)
## Summary Closes #14187.
1 parent 2624249 commit 272d24b

File tree

3 files changed

+380
-36
lines changed

3 files changed

+380
-36
lines changed

crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs

+46-8
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ use std::collections::HashSet;
22

33
use rustc_hash::FxHashSet;
44

5-
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
5+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
66
use ruff_macros::{derive_message_formats, violation};
77
use ruff_python_ast::comparable::ComparableExpr;
8-
use ruff_python_ast::Expr;
8+
use ruff_python_ast::{self as ast, Expr, ExprContext};
99
use ruff_python_semantic::analyze::typing::traverse_literal;
10-
use ruff_text_size::Ranged;
10+
use ruff_text_size::{Ranged, TextRange};
1111

1212
use crate::checkers::ast::Checker;
1313

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

37-
impl Violation for DuplicateLiteralMember {
38-
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
39-
41+
impl AlwaysFixableViolation for DuplicateLiteralMember {
4042
#[derive_message_formats]
4143
fn message(&self) -> String {
4244
format!("Duplicate literal member `{}`", self.duplicate_name)
4345
}
46+
47+
fn fix_title(&self) -> String {
48+
"Remove duplicates".to_string()
49+
}
4450
}
4551

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

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

64-
// Traverse the literal, collect all diagnostic members
73+
// Traverse the literal, collect all diagnostic members.
6574
traverse_literal(&mut check_for_duplicate_members, checker.semantic(), expr);
75+
76+
// If there's at least one diagnostic, create a fix to remove the duplicate members.
77+
if !diagnostics.is_empty() {
78+
if let Expr::Subscript(subscript) = expr {
79+
let subscript = Expr::Subscript(ast::ExprSubscript {
80+
slice: Box::new(if let [elt] = unique_nodes.as_slice() {
81+
(*elt).clone()
82+
} else {
83+
Expr::Tuple(ast::ExprTuple {
84+
elts: unique_nodes.into_iter().cloned().collect(),
85+
range: TextRange::default(),
86+
ctx: ExprContext::Load,
87+
parenthesized: false,
88+
})
89+
}),
90+
value: subscript.value.clone(),
91+
range: TextRange::default(),
92+
ctx: ExprContext::Load,
93+
});
94+
let fix = Fix::safe_edit(Edit::range_replacement(
95+
checker.generator().expr(&subscript),
96+
expr.range(),
97+
));
98+
for diagnostic in &mut diagnostics {
99+
diagnostic.set_fix(fix.clone());
100+
}
101+
}
102+
}
103+
66104
checker.diagnostics.append(&mut diagnostics);
67105
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
33
---
4-
PYI062.py:5:25: PYI062 Duplicate literal member `True`
4+
PYI062.py:5:25: PYI062 [*] Duplicate literal member `True`
55
|
66
3 | import typing_extensions
77
4 |
@@ -10,8 +10,19 @@ PYI062.py:5:25: PYI062 Duplicate literal member `True`
1010
6 |
1111
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
1212
|
13+
= help: Remove duplicates
1314

14-
PYI062.py:5:31: PYI062 Duplicate literal member `False`
15+
Safe fix
16+
2 2 | import typing as t
17+
3 3 | import typing_extensions
18+
4 4 |
19+
5 |-x: Literal[True, False, True, False] # PYI062 twice here
20+
5 |+x: Literal[True, False] # PYI062 twice here
21+
6 6 |
22+
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
23+
8 8 |
24+
25+
PYI062.py:5:31: PYI062 [*] Duplicate literal member `False`
1526
|
1627
3 | import typing_extensions
1728
4 |
@@ -20,8 +31,19 @@ PYI062.py:5:31: PYI062 Duplicate literal member `False`
2031
6 |
2132
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
2233
|
34+
= help: Remove duplicates
35+
36+
Safe fix
37+
2 2 | import typing as t
38+
3 3 | import typing_extensions
39+
4 4 |
40+
5 |-x: Literal[True, False, True, False] # PYI062 twice here
41+
5 |+x: Literal[True, False] # PYI062 twice here
42+
6 6 |
43+
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
44+
8 8 |
2345

24-
PYI062.py:7:45: PYI062 Duplicate literal member `1`
46+
PYI062.py:7:45: PYI062 [*] Duplicate literal member `1`
2547
|
2648
5 | x: Literal[True, False, True, False] # PYI062 twice here
2749
6 |
@@ -30,8 +52,19 @@ PYI062.py:7:45: PYI062 Duplicate literal member `1`
3052
8 |
3153
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
3254
|
55+
= help: Remove duplicates
3356

34-
PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
57+
Safe fix
58+
4 4 |
59+
5 5 | x: Literal[True, False, True, False] # PYI062 twice here
60+
6 6 |
61+
7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
62+
7 |+y: Literal[1, print("hello"), 3, 4] # PYI062 on the last 1
63+
8 8 |
64+
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
65+
10 10 |
66+
67+
PYI062.py:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}`
3568
|
3669
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
3770
8 |
@@ -40,8 +73,19 @@ PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
4073
10 |
4174
11 | Literal[1, Literal[1]] # once
4275
|
76+
= help: Remove duplicates
77+
78+
Safe fix
79+
6 6 |
80+
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
81+
8 8 |
82+
9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
83+
9 |+z: Literal[{1, 3, 5}, "foobar"] # PYI062 on the set literal
84+
10 10 |
85+
11 11 | Literal[1, Literal[1]] # once
86+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
4387

44-
PYI062.py:11:20: PYI062 Duplicate literal member `1`
88+
PYI062.py:11:20: PYI062 [*] Duplicate literal member `1`
4589
|
4690
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
4791
10 |
@@ -50,26 +94,59 @@ PYI062.py:11:20: PYI062 Duplicate literal member `1`
5094
12 | Literal[1, 2, Literal[1, 2]] # twice
5195
13 | Literal[1, Literal[1], Literal[1]] # twice
5296
|
97+
= help: Remove duplicates
5398

54-
PYI062.py:12:23: PYI062 Duplicate literal member `1`
99+
Safe fix
100+
8 8 |
101+
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
102+
10 10 |
103+
11 |-Literal[1, Literal[1]] # once
104+
11 |+Literal[1] # once
105+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
106+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
107+
14 14 | Literal[1, Literal[2], Literal[2]] # once
108+
109+
PYI062.py:12:23: PYI062 [*] Duplicate literal member `1`
55110
|
56111
11 | Literal[1, Literal[1]] # once
57112
12 | Literal[1, 2, Literal[1, 2]] # twice
58113
| ^ PYI062
59114
13 | Literal[1, Literal[1], Literal[1]] # twice
60115
14 | Literal[1, Literal[2], Literal[2]] # once
61116
|
117+
= help: Remove duplicates
118+
119+
Safe fix
120+
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
121+
10 10 |
122+
11 11 | Literal[1, Literal[1]] # once
123+
12 |-Literal[1, 2, Literal[1, 2]] # twice
124+
12 |+Literal[1, 2] # twice
125+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
126+
14 14 | Literal[1, Literal[2], Literal[2]] # once
127+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
62128

63-
PYI062.py:12:26: PYI062 Duplicate literal member `2`
129+
PYI062.py:12:26: PYI062 [*] Duplicate literal member `2`
64130
|
65131
11 | Literal[1, Literal[1]] # once
66132
12 | Literal[1, 2, Literal[1, 2]] # twice
67133
| ^ PYI062
68134
13 | Literal[1, Literal[1], Literal[1]] # twice
69135
14 | Literal[1, Literal[2], Literal[2]] # once
70136
|
137+
= help: Remove duplicates
138+
139+
Safe fix
140+
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
141+
10 10 |
142+
11 11 | Literal[1, Literal[1]] # once
143+
12 |-Literal[1, 2, Literal[1, 2]] # twice
144+
12 |+Literal[1, 2] # twice
145+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
146+
14 14 | Literal[1, Literal[2], Literal[2]] # once
147+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
71148

72-
PYI062.py:13:20: PYI062 Duplicate literal member `1`
149+
PYI062.py:13:20: PYI062 [*] Duplicate literal member `1`
73150
|
74151
11 | Literal[1, Literal[1]] # once
75152
12 | Literal[1, 2, Literal[1, 2]] # twice
@@ -78,8 +155,19 @@ PYI062.py:13:20: PYI062 Duplicate literal member `1`
78155
14 | Literal[1, Literal[2], Literal[2]] # once
79156
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
80157
|
158+
= help: Remove duplicates
81159

82-
PYI062.py:13:32: PYI062 Duplicate literal member `1`
160+
Safe fix
161+
10 10 |
162+
11 11 | Literal[1, Literal[1]] # once
163+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
164+
13 |-Literal[1, Literal[1], Literal[1]] # twice
165+
13 |+Literal[1] # twice
166+
14 14 | Literal[1, Literal[2], Literal[2]] # once
167+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
168+
16 16 | typing_extensions.Literal[1, 1, 1] # twice
169+
170+
PYI062.py:13:32: PYI062 [*] Duplicate literal member `1`
83171
|
84172
11 | Literal[1, Literal[1]] # once
85173
12 | Literal[1, 2, Literal[1, 2]] # twice
@@ -88,8 +176,19 @@ PYI062.py:13:32: PYI062 Duplicate literal member `1`
88176
14 | Literal[1, Literal[2], Literal[2]] # once
89177
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
90178
|
179+
= help: Remove duplicates
180+
181+
Safe fix
182+
10 10 |
183+
11 11 | Literal[1, Literal[1]] # once
184+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
185+
13 |-Literal[1, Literal[1], Literal[1]] # twice
186+
13 |+Literal[1] # twice
187+
14 14 | Literal[1, Literal[2], Literal[2]] # once
188+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
189+
16 16 | typing_extensions.Literal[1, 1, 1] # twice
91190

92-
PYI062.py:14:32: PYI062 Duplicate literal member `2`
191+
PYI062.py:14:32: PYI062 [*] Duplicate literal member `2`
93192
|
94193
12 | Literal[1, 2, Literal[1, 2]] # twice
95194
13 | Literal[1, Literal[1], Literal[1]] # twice
@@ -98,17 +197,39 @@ PYI062.py:14:32: PYI062 Duplicate literal member `2`
98197
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
99198
16 | typing_extensions.Literal[1, 1, 1] # twice
100199
|
200+
= help: Remove duplicates
101201

102-
PYI062.py:15:37: PYI062 Duplicate literal member `1`
202+
Safe fix
203+
11 11 | Literal[1, Literal[1]] # once
204+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
205+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
206+
14 |-Literal[1, Literal[2], Literal[2]] # once
207+
14 |+Literal[1, 2] # once
208+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
209+
16 16 | typing_extensions.Literal[1, 1, 1] # twice
210+
17 17 |
211+
212+
PYI062.py:15:37: PYI062 [*] Duplicate literal member `1`
103213
|
104214
13 | Literal[1, Literal[1], Literal[1]] # twice
105215
14 | Literal[1, Literal[2], Literal[2]] # once
106216
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
107217
| ^ PYI062
108218
16 | typing_extensions.Literal[1, 1, 1] # twice
109219
|
220+
= help: Remove duplicates
221+
222+
Safe fix
223+
12 12 | Literal[1, 2, Literal[1, 2]] # twice
224+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
225+
14 14 | Literal[1, Literal[2], Literal[2]] # once
226+
15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once
227+
15 |+t.Literal[1, 2] # once
228+
16 16 | typing_extensions.Literal[1, 1, 1] # twice
229+
17 17 |
230+
18 18 | # Ensure issue is only raised once, even on nested literals
110231

111-
PYI062.py:16:30: PYI062 Duplicate literal member `1`
232+
PYI062.py:16:30: PYI062 [*] Duplicate literal member `1`
112233
|
113234
14 | Literal[1, Literal[2], Literal[2]] # once
114235
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
@@ -117,8 +238,19 @@ PYI062.py:16:30: PYI062 Duplicate literal member `1`
117238
17 |
118239
18 | # Ensure issue is only raised once, even on nested literals
119240
|
241+
= help: Remove duplicates
120242

121-
PYI062.py:16:33: PYI062 Duplicate literal member `1`
243+
Safe fix
244+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
245+
14 14 | Literal[1, Literal[2], Literal[2]] # once
246+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
247+
16 |-typing_extensions.Literal[1, 1, 1] # twice
248+
16 |+typing_extensions.Literal[1] # twice
249+
17 17 |
250+
18 18 | # Ensure issue is only raised once, even on nested literals
251+
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
252+
253+
PYI062.py:16:33: PYI062 [*] Duplicate literal member `1`
122254
|
123255
14 | Literal[1, Literal[2], Literal[2]] # once
124256
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
@@ -127,12 +259,33 @@ PYI062.py:16:33: PYI062 Duplicate literal member `1`
127259
17 |
128260
18 | # Ensure issue is only raised once, even on nested literals
129261
|
262+
= help: Remove duplicates
263+
264+
Safe fix
265+
13 13 | Literal[1, Literal[1], Literal[1]] # twice
266+
14 14 | Literal[1, Literal[2], Literal[2]] # once
267+
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
268+
16 |-typing_extensions.Literal[1, 1, 1] # twice
269+
16 |+typing_extensions.Literal[1] # twice
270+
17 17 |
271+
18 18 | # Ensure issue is only raised once, even on nested literals
272+
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
130273

131-
PYI062.py:19:46: PYI062 Duplicate literal member `True`
274+
PYI062.py:19:46: PYI062 [*] Duplicate literal member `True`
132275
|
133276
18 | # Ensure issue is only raised once, even on nested literals
134277
19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
135278
| ^^^^ PYI062
136279
20 |
137280
21 | n: Literal["No", "duplicates", "here", 1, "1"]
138281
|
282+
= help: Remove duplicates
283+
284+
Safe fix
285+
16 16 | typing_extensions.Literal[1, 1, 1] # twice
286+
17 17 |
287+
18 18 | # Ensure issue is only raised once, even on nested literals
288+
19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
289+
19 |+MyType = Literal["foo", True, False, "bar"] # PYI062
290+
20 20 |
291+
21 21 | n: Literal["No", "duplicates", "here", 1, "1"]

0 commit comments

Comments
 (0)