Skip to content

Commit cb201bc

Browse files
authored
PIE804: Prevent keyword arguments duplication (#8450)
1 parent 6c0068e commit cb201bc

File tree

3 files changed

+142
-44
lines changed

3 files changed

+142
-44
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE804.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
foo(**{"": True})
2020
foo(**{f"buzz__{bar}": True})
2121
abc(**{"for": 3})
22-
2322
foo(**{},)
23+
24+
# Duplicated key names won't be fixed, to avoid syntax errors.
25+
abc(**{'a': b}, **{'a': c}) # PIE804
26+
abc(a=1, **{'a': c}, **{'b': c}) # PIE804

crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs

+71-24
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
use std::hash::BuildHasherDefault;
2+
13
use itertools::Itertools;
2-
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
3-
use ruff_python_ast::{self as ast, Expr};
4+
use rustc_hash::FxHashSet;
45

6+
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
57
use ruff_macros::{derive_message_formats, violation};
6-
use ruff_text_size::Ranged;
7-
8+
use ruff_python_ast::{self as ast, Expr};
89
use ruff_python_stdlib::identifiers::is_identifier;
10+
use ruff_text_size::Ranged;
911

1012
use crate::checkers::ast::Checker;
1113
use crate::fix::edits::{remove_argument, Parentheses};
@@ -41,36 +43,39 @@ use crate::fix::edits::{remove_argument, Parentheses};
4143
#[violation]
4244
pub struct UnnecessaryDictKwargs;
4345

44-
impl AlwaysFixableViolation for UnnecessaryDictKwargs {
46+
impl Violation for UnnecessaryDictKwargs {
47+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
48+
4549
#[derive_message_formats]
4650
fn message(&self) -> String {
4751
format!("Unnecessary `dict` kwargs")
4852
}
4953

50-
fn fix_title(&self) -> String {
51-
format!("Remove unnecessary kwargs")
54+
fn fix_title(&self) -> Option<String> {
55+
Some(format!("Remove unnecessary kwargs"))
5256
}
5357
}
5458

5559
/// PIE804
5660
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
57-
for kw in &call.arguments.keywords {
58-
// keyword is a spread operator (indicated by None)
59-
if kw.arg.is_some() {
61+
let mut duplicate_keywords = None;
62+
for keyword in &call.arguments.keywords {
63+
// keyword is a spread operator (indicated by None).
64+
if keyword.arg.is_some() {
6065
continue;
6166
}
6267

63-
let Expr::Dict(ast::ExprDict { keys, values, .. }) = &kw.value else {
68+
let Expr::Dict(ast::ExprDict { keys, values, .. }) = &keyword.value else {
6469
continue;
6570
};
6671

6772
// Ex) `foo(**{**bar})`
6873
if matches!(keys.as_slice(), [None]) {
69-
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
74+
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range());
7075

7176
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
7277
format!("**{}", checker.locator().slice(values[0].range())),
73-
kw.range(),
78+
keyword.range(),
7479
)));
7580

7681
checker.diagnostics.push(diagnostic);
@@ -87,35 +92,77 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCal
8792
continue;
8893
}
8994

90-
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
95+
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range());
9196

9297
if values.is_empty() {
9398
diagnostic.try_set_fix(|| {
9499
remove_argument(
95-
kw,
100+
keyword,
96101
&call.arguments,
97102
Parentheses::Preserve,
98103
checker.locator().contents(),
99104
)
100105
.map(Fix::safe_edit)
101106
});
102107
} else {
103-
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
104-
kwargs
108+
// Compute the set of duplicate keywords (lazily).
109+
if duplicate_keywords.is_none() {
110+
duplicate_keywords = Some(duplicates(call));
111+
}
112+
113+
// Avoid fixing if doing so could introduce a duplicate keyword argument.
114+
if let Some(duplicate_keywords) = duplicate_keywords.as_ref() {
115+
if kwargs
105116
.iter()
106-
.zip(values.iter())
107-
.map(|(kwarg, value)| {
108-
format!("{}={}", kwarg, checker.locator().slice(value.range()))
109-
})
110-
.join(", "),
111-
kw.range(),
112-
)));
117+
.all(|kwarg| !duplicate_keywords.contains(kwarg))
118+
{
119+
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
120+
kwargs
121+
.iter()
122+
.zip(values.iter())
123+
.map(|(kwarg, value)| {
124+
format!("{}={}", kwarg, checker.locator().slice(value.range()))
125+
})
126+
.join(", "),
127+
keyword.range(),
128+
)));
129+
}
130+
}
113131
}
114132

115133
checker.diagnostics.push(diagnostic);
116134
}
117135
}
118136

137+
/// Determine the set of keywords that appear in multiple positions (either directly, as in
138+
/// `func(x=1)`, or indirectly, as in `func(**{"x": 1})`).
139+
fn duplicates(call: &ast::ExprCall) -> FxHashSet<&str> {
140+
let mut seen = FxHashSet::with_capacity_and_hasher(
141+
call.arguments.keywords.len(),
142+
BuildHasherDefault::default(),
143+
);
144+
let mut duplicates = FxHashSet::with_capacity_and_hasher(
145+
call.arguments.keywords.len(),
146+
BuildHasherDefault::default(),
147+
);
148+
for keyword in &call.arguments.keywords {
149+
if let Some(name) = &keyword.arg {
150+
if !seen.insert(name.as_str()) {
151+
duplicates.insert(name.as_str());
152+
}
153+
} else if let Expr::Dict(ast::ExprDict { keys, .. }) = &keyword.value {
154+
for key in keys {
155+
if let Some(name) = key.as_ref().and_then(as_kwarg) {
156+
if !seen.insert(name) {
157+
duplicates.insert(name);
158+
}
159+
}
160+
}
161+
}
162+
}
163+
duplicates
164+
}
165+
119166
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
120167
fn as_kwarg(key: &Expr) -> Option<&str> {
121168
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {

crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snap

+67-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
source: crates/ruff_linter/src/rules/flake8_pie/mod.rs
33
---
4-
PIE804.py:1:1: PIE804 [*] Unnecessary `dict` kwargs
4+
PIE804.py:1:5: PIE804 [*] Unnecessary `dict` kwargs
55
|
66
1 | foo(**{"bar": True}) # PIE804
7-
| ^^^^^^^^^^^^^^^^^^^^ PIE804
7+
| ^^^^^^^^^^^^^^^ PIE804
88
2 |
99
3 | foo(**{"r2d2": True}) # PIE804
1010
|
@@ -17,12 +17,12 @@ PIE804.py:1:1: PIE804 [*] Unnecessary `dict` kwargs
1717
3 3 | foo(**{"r2d2": True}) # PIE804
1818
4 4 |
1919

20-
PIE804.py:3:1: PIE804 [*] Unnecessary `dict` kwargs
20+
PIE804.py:3:5: PIE804 [*] Unnecessary `dict` kwargs
2121
|
2222
1 | foo(**{"bar": True}) # PIE804
2323
2 |
2424
3 | foo(**{"r2d2": True}) # PIE804
25-
| ^^^^^^^^^^^^^^^^^^^^^ PIE804
25+
| ^^^^^^^^^^^^^^^^ PIE804
2626
4 |
2727
5 | Foo.objects.create(**{"bar": True}) # PIE804
2828
|
@@ -37,12 +37,12 @@ PIE804.py:3:1: PIE804 [*] Unnecessary `dict` kwargs
3737
5 5 | Foo.objects.create(**{"bar": True}) # PIE804
3838
6 6 |
3939

40-
PIE804.py:5:1: PIE804 [*] Unnecessary `dict` kwargs
40+
PIE804.py:5:20: PIE804 [*] Unnecessary `dict` kwargs
4141
|
4242
3 | foo(**{"r2d2": True}) # PIE804
4343
4 |
4444
5 | Foo.objects.create(**{"bar": True}) # PIE804
45-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
45+
| ^^^^^^^^^^^^^^^ PIE804
4646
6 |
4747
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
4848
|
@@ -58,12 +58,12 @@ PIE804.py:5:1: PIE804 [*] Unnecessary `dict` kwargs
5858
7 7 | Foo.objects.create(**{"_id": some_id}) # PIE804
5959
8 8 |
6060

61-
PIE804.py:7:1: PIE804 [*] Unnecessary `dict` kwargs
61+
PIE804.py:7:20: PIE804 [*] Unnecessary `dict` kwargs
6262
|
6363
5 | Foo.objects.create(**{"bar": True}) # PIE804
6464
6 |
6565
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
66-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
66+
| ^^^^^^^^^^^^^^^^^^ PIE804
6767
8 |
6868
9 | Foo.objects.create(**{**bar}) # PIE804
6969
|
@@ -79,12 +79,12 @@ PIE804.py:7:1: PIE804 [*] Unnecessary `dict` kwargs
7979
9 9 | Foo.objects.create(**{**bar}) # PIE804
8080
10 10 |
8181

82-
PIE804.py:9:1: PIE804 [*] Unnecessary `dict` kwargs
82+
PIE804.py:9:20: PIE804 [*] Unnecessary `dict` kwargs
8383
|
8484
7 | Foo.objects.create(**{"_id": some_id}) # PIE804
8585
8 |
8686
9 | Foo.objects.create(**{**bar}) # PIE804
87-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE804
87+
| ^^^^^^^^^ PIE804
8888
10 |
8989
11 | foo(**{})
9090
|
@@ -100,12 +100,12 @@ PIE804.py:9:1: PIE804 [*] Unnecessary `dict` kwargs
100100
11 11 | foo(**{})
101101
12 12 |
102102

103-
PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
103+
PIE804.py:11:5: PIE804 [*] Unnecessary `dict` kwargs
104104
|
105105
9 | Foo.objects.create(**{**bar}) # PIE804
106106
10 |
107107
11 | foo(**{})
108-
| ^^^^^^^^^ PIE804
108+
| ^^^^ PIE804
109109
12 |
110110
13 | foo(**{**data, "foo": "buzz"})
111111
|
@@ -121,20 +121,68 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
121121
13 13 | foo(**{**data, "foo": "buzz"})
122122
14 14 | foo(**buzz)
123123

124-
PIE804.py:23:1: PIE804 [*] Unnecessary `dict` kwargs
124+
PIE804.py:22:5: PIE804 [*] Unnecessary `dict` kwargs
125125
|
126+
20 | foo(**{f"buzz__{bar}": True})
126127
21 | abc(**{"for": 3})
127-
22 |
128-
23 | foo(**{},)
129-
| ^^^^^^^^^^ PIE804
128+
22 | foo(**{},)
129+
| ^^^^ PIE804
130+
23 |
131+
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
130132
|
131133
= help: Remove unnecessary kwargs
132134

133135
Safe fix
136+
19 19 | foo(**{"": True})
134137
20 20 | foo(**{f"buzz__{bar}": True})
135138
21 21 | abc(**{"for": 3})
136-
22 22 |
137-
23 |-foo(**{},)
138-
23 |+foo()
139+
22 |-foo(**{},)
140+
22 |+foo()
141+
23 23 |
142+
24 24 | # Duplicated key names won't be fixed, to avoid syntax errors.
143+
25 25 | abc(**{'a': b}, **{'a': c}) # PIE804
144+
145+
PIE804.py:25:5: PIE804 Unnecessary `dict` kwargs
146+
|
147+
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
148+
25 | abc(**{'a': b}, **{'a': c}) # PIE804
149+
| ^^^^^^^^^^ PIE804
150+
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
151+
|
152+
= help: Remove unnecessary kwargs
153+
154+
PIE804.py:25:17: PIE804 Unnecessary `dict` kwargs
155+
|
156+
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
157+
25 | abc(**{'a': b}, **{'a': c}) # PIE804
158+
| ^^^^^^^^^^ PIE804
159+
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
160+
|
161+
= help: Remove unnecessary kwargs
162+
163+
PIE804.py:26:10: PIE804 Unnecessary `dict` kwargs
164+
|
165+
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
166+
25 | abc(**{'a': b}, **{'a': c}) # PIE804
167+
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
168+
| ^^^^^^^^^^ PIE804
169+
|
170+
= help: Remove unnecessary kwargs
171+
172+
PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs
173+
|
174+
24 | # Duplicated key names won't be fixed, to avoid syntax errors.
175+
25 | abc(**{'a': b}, **{'a': c}) # PIE804
176+
26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804
177+
| ^^^^^^^^^^ PIE804
178+
|
179+
= help: Remove unnecessary kwargs
180+
181+
Safe fix
182+
23 23 |
183+
24 24 | # Duplicated key names won't be fixed, to avoid syntax errors.
184+
25 25 | abc(**{'a': b}, **{'a': c}) # PIE804
185+
26 |-abc(a=1, **{'a': c}, **{'b': c}) # PIE804
186+
26 |+abc(a=1, **{'a': c}, b=c) # PIE804
139187

140188

0 commit comments

Comments
 (0)