Skip to content

Commit e442304

Browse files
authored
[ruff] Validate arguments before offering a fix (RUF056) (#18631)
## Summary Fixes #18628 by avoiding a fix if there are "unknown" arguments, including any keyword arguments and more than the expected 2 positional arguments. I'm a bit on the fence here because it also seems reasonable to avoid a diagnostic at all. Especially in the final test case I added (`not my_dict.get(default=False)`), the hint suggesting to remove `default=False` seems pretty misleading. At the same time, I guess the diagnostic at least calls attention to the call site, which could help to fix the missing argument bug too. As I commented on the issue, I double-checked that keyword arguments are invalid as far back as Python 3.8, even though the positional-only marker was only added to the [docs](https://docs.python.org/3.11/library/stdtypes.html#dict.get) in 3.12 (link is to 3.11, showing its absence). ## Test Plan New tests derived from the bug report ## Stabilization This was planned to be stabilized in 0.12, and the bug is less severe than some others, but if there's nobody opposed, I will plan **not to stabilize** this one for now.
1 parent 6d56ee8 commit e442304

File tree

3 files changed

+162
-134
lines changed

3 files changed

+162
-134
lines changed

crates/ruff_linter/resources/test/fixtures/ruff/RUF056.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,39 @@ def inner():
149149
value = not my_dict.get("key", 0.0) # [RUF056]
150150
value = not my_dict.get("key", "") # [RUF056]
151151

152-
# testing dict.get call using kwargs
153-
value = not my_dict.get(key="key", default=False) # [RUF056]
154-
value = not my_dict.get(default=[], key="key") # [RUF056]
155-
156152
# testing invalid dict.get call with inline comment
157153
value = not my_dict.get("key", # comment1
158154
[] # comment2
159155
) # [RUF056]
160156

161-
# testing invalid dict.get call with kwargs and inline comment
162-
value = not my_dict.get(key="key", # comment1
163-
default=False # comment2
164-
) # [RUF056]
165-
value = not my_dict.get(default=[], # comment1
166-
key="key" # comment2
167-
) # [RUF056]
168-
169-
# testing invalid dict.get calls
170-
value = not my_dict.get(key="key", other="something", default=False)
171-
value = not my_dict.get(default=False, other="something", key="test")
157+
# regression tests for https://github.com/astral-sh/ruff/issues/18628
158+
# we should avoid fixes when there are "unknown" arguments present, including
159+
# extra positional arguments, either of the positional-only arguments passed as
160+
# a keyword, or completely unknown keywords.
161+
162+
# extra positional
163+
not my_dict.get("key", False, "?!")
164+
165+
# `default` is positional-only, so these are invalid
166+
not my_dict.get("key", default=False)
167+
not my_dict.get(key="key", default=False)
168+
not my_dict.get(default=[], key="key")
169+
not my_dict.get(default=False)
170+
not my_dict.get(key="key", other="something", default=False)
171+
not my_dict.get(default=False, other="something", key="test")
172+
173+
# comments don't really matter here because of the kwargs but include them for
174+
# completeness
175+
not my_dict.get(
176+
key="key", # comment1
177+
default=False, # comment2
178+
) # comment 3
179+
not my_dict.get(
180+
default=[], # comment1
181+
key="key", # comment2
182+
) # comment 3
183+
184+
# the fix is arguably okay here because the same `takes no keyword arguments`
185+
# TypeError is raised at runtime before and after the fix, but we still bail
186+
# out for having an unrecognized number of arguments
187+
not my_dict.get("key", False, foo=...)

crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
55

66
use crate::checkers::ast::Checker;
77
use crate::fix::edits::{Parentheses, remove_argument};
8-
use crate::{AlwaysFixableViolation, Applicability, Fix};
8+
use crate::{Applicability, Fix, FixAvailability, Violation};
99

1010
/// ## What it does
1111
/// Checks for `dict.get(key, falsy_value)` calls in boolean test positions.
@@ -28,21 +28,34 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
2828
/// ```
2929
///
3030
/// ## Fix safety
31-
/// This rule's fix is marked as safe, unless the `dict.get()` call contains comments between arguments.
31+
///
32+
/// This rule's fix is marked as safe, unless the `dict.get()` call contains comments between
33+
/// arguments that will be deleted.
34+
///
35+
/// ## Fix availability
36+
///
37+
/// This rule's fix is unavailable in cases where invalid arguments are provided to `dict.get`. As
38+
/// shown in the [documentation], `dict.get` takes two positional-only arguments, so invalid cases
39+
/// are identified by the presence of more than two arguments or any keyword arguments.
40+
///
41+
/// [documentation]: https://docs.python.org/3.13/library/stdtypes.html#dict.get
3242
#[derive(ViolationMetadata)]
3343
pub(crate) struct FalsyDictGetFallback;
3444

35-
impl AlwaysFixableViolation for FalsyDictGetFallback {
45+
impl Violation for FalsyDictGetFallback {
46+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
47+
3648
#[derive_message_formats]
3749
fn message(&self) -> String {
3850
"Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.".to_string()
3951
}
4052

41-
fn fix_title(&self) -> String {
42-
"Remove falsy fallback from `dict.get()`".to_string()
53+
fn fix_title(&self) -> Option<String> {
54+
Some("Remove falsy fallback from `dict.get()`".to_string())
4355
}
4456
}
4557

58+
/// RUF056
4659
pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) {
4760
let semantic = checker.semantic();
4861

@@ -89,6 +102,16 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) {
89102

90103
let mut diagnostic = checker.report_diagnostic(FalsyDictGetFallback, fallback_arg.range());
91104

105+
// All arguments to `dict.get` are positional-only.
106+
if !call.arguments.keywords.is_empty() {
107+
return;
108+
}
109+
110+
// And there are only two of them, at most.
111+
if call.arguments.args.len() > 2 {
112+
return;
113+
}
114+
92115
let comment_ranges = checker.comment_ranges();
93116

94117
// Determine applicability based on the presence of comments

crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap

Lines changed: 103 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ RUF056.py:149:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in
323323
149 |+value = not my_dict.get("key") # [RUF056]
324324
150 150 | value = not my_dict.get("key", "") # [RUF056]
325325
151 151 |
326-
152 152 | # testing dict.get call using kwargs
326+
152 152 | # testing invalid dict.get call with inline comment
327327

328328
RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
329329
|
@@ -332,7 +332,7 @@ RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in
332332
150 | value = not my_dict.get("key", "") # [RUF056]
333333
| ^^ RUF056
334334
151 |
335-
152 | # testing dict.get call using kwargs
335+
152 | # testing invalid dict.get call with inline comment
336336
|
337337
= help: Remove falsy fallback from `dict.get()`
338338

@@ -343,142 +343,131 @@ RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in
343343
150 |-value = not my_dict.get("key", "") # [RUF056]
344344
150 |+value = not my_dict.get("key") # [RUF056]
345345
151 151 |
346-
152 152 | # testing dict.get call using kwargs
347-
153 153 | value = not my_dict.get(key="key", default=False) # [RUF056]
346+
152 152 | # testing invalid dict.get call with inline comment
347+
153 153 | value = not my_dict.get("key", # comment1
348348

349-
RUF056.py:153:36: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
349+
RUF056.py:154:22: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
350350
|
351-
152 | # testing dict.get call using kwargs
352-
153 | value = not my_dict.get(key="key", default=False) # [RUF056]
353-
| ^^^^^^^^^^^^^ RUF056
354-
154 | value = not my_dict.get(default=[], key="key") # [RUF056]
351+
152 | # testing invalid dict.get call with inline comment
352+
153 | value = not my_dict.get("key", # comment1
353+
154 | [] # comment2
354+
| ^^ RUF056
355+
155 | ) # [RUF056]
355356
|
356357
= help: Remove falsy fallback from `dict.get()`
357358

358-
Safe fix
359+
Unsafe fix
359360
150 150 | value = not my_dict.get("key", "") # [RUF056]
360361
151 151 |
361-
152 152 | # testing dict.get call using kwargs
362-
153 |-value = not my_dict.get(key="key", default=False) # [RUF056]
363-
153 |+value = not my_dict.get(key="key") # [RUF056]
364-
154 154 | value = not my_dict.get(default=[], key="key") # [RUF056]
365-
155 155 |
366-
156 156 | # testing invalid dict.get call with inline comment
367-
368-
RUF056.py:154:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
369-
|
370-
152 | # testing dict.get call using kwargs
371-
153 | value = not my_dict.get(key="key", default=False) # [RUF056]
372-
154 | value = not my_dict.get(default=[], key="key") # [RUF056]
373-
| ^^^^^^^^^^ RUF056
374-
155 |
375-
156 | # testing invalid dict.get call with inline comment
362+
152 152 | # testing invalid dict.get call with inline comment
363+
153 |-value = not my_dict.get("key", # comment1
364+
154 |- [] # comment2
365+
153 |+value = not my_dict.get("key" # comment2
366+
155 154 | ) # [RUF056]
367+
156 155 |
368+
157 156 | # regression tests for https://github.com/astral-sh/ruff/issues/18628
369+
370+
RUF056.py:163:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
371+
|
372+
162 | # extra positional
373+
163 | not my_dict.get("key", False, "?!")
374+
| ^^^^^ RUF056
375+
164 |
376+
165 | # `default` is positional-only, so these are invalid
376377
|
377378
= help: Remove falsy fallback from `dict.get()`
378379

379-
Safe fix
380-
151 151 |
381-
152 152 | # testing dict.get call using kwargs
382-
153 153 | value = not my_dict.get(key="key", default=False) # [RUF056]
383-
154 |-value = not my_dict.get(default=[], key="key") # [RUF056]
384-
154 |+value = not my_dict.get(key="key") # [RUF056]
385-
155 155 |
386-
156 156 | # testing invalid dict.get call with inline comment
387-
157 157 | value = not my_dict.get("key", # comment1
388-
389-
RUF056.py:158:22: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
390-
|
391-
156 | # testing invalid dict.get call with inline comment
392-
157 | value = not my_dict.get("key", # comment1
393-
158 | [] # comment2
394-
| ^^ RUF056
395-
159 | ) # [RUF056]
380+
RUF056.py:166:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
381+
|
382+
165 | # `default` is positional-only, so these are invalid
383+
166 | not my_dict.get("key", default=False)
384+
| ^^^^^^^^^^^^^ RUF056
385+
167 | not my_dict.get(key="key", default=False)
386+
168 | not my_dict.get(default=[], key="key")
396387
|
397388
= help: Remove falsy fallback from `dict.get()`
398389

399-
Unsafe fix
400-
154 154 | value = not my_dict.get(default=[], key="key") # [RUF056]
401-
155 155 |
402-
156 156 | # testing invalid dict.get call with inline comment
403-
157 |-value = not my_dict.get("key", # comment1
404-
158 |- [] # comment2
405-
157 |+value = not my_dict.get("key" # comment2
406-
159 158 | ) # [RUF056]
407-
160 159 |
408-
161 160 | # testing invalid dict.get call with kwargs and inline comment
409-
410-
RUF056.py:163:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
411-
|
412-
161 | # testing invalid dict.get call with kwargs and inline comment
413-
162 | value = not my_dict.get(key="key", # comment1
414-
163 | default=False # comment2
415-
| ^^^^^^^^^^^^^ RUF056
416-
164 | ) # [RUF056]
417-
165 | value = not my_dict.get(default=[], # comment1
390+
RUF056.py:167:28: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
391+
|
392+
165 | # `default` is positional-only, so these are invalid
393+
166 | not my_dict.get("key", default=False)
394+
167 | not my_dict.get(key="key", default=False)
395+
| ^^^^^^^^^^^^^ RUF056
396+
168 | not my_dict.get(default=[], key="key")
397+
169 | not my_dict.get(default=False)
418398
|
419399
= help: Remove falsy fallback from `dict.get()`
420400

421-
Unsafe fix
422-
159 159 | ) # [RUF056]
423-
160 160 |
424-
161 161 | # testing invalid dict.get call with kwargs and inline comment
425-
162 |-value = not my_dict.get(key="key", # comment1
426-
163 |- default=False # comment2
427-
162 |+value = not my_dict.get(key="key" # comment2
428-
164 163 | ) # [RUF056]
429-
165 164 | value = not my_dict.get(default=[], # comment1
430-
166 165 | key="key" # comment2
431-
432-
RUF056.py:165:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
433-
|
434-
163 | default=False # comment2
435-
164 | ) # [RUF056]
436-
165 | value = not my_dict.get(default=[], # comment1
437-
| ^^^^^^^^^^ RUF056
438-
166 | key="key" # comment2
439-
167 | ) # [RUF056]
401+
RUF056.py:168:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
402+
|
403+
166 | not my_dict.get("key", default=False)
404+
167 | not my_dict.get(key="key", default=False)
405+
168 | not my_dict.get(default=[], key="key")
406+
| ^^^^^^^^^^ RUF056
407+
169 | not my_dict.get(default=False)
408+
170 | not my_dict.get(key="key", other="something", default=False)
440409
|
441410
= help: Remove falsy fallback from `dict.get()`
442411

443-
Unsafe fix
444-
162 162 | value = not my_dict.get(key="key", # comment1
445-
163 163 | default=False # comment2
446-
164 164 | ) # [RUF056]
447-
165 |-value = not my_dict.get(default=[], # comment1
448-
165 |+value = not my_dict.get(# comment1
449-
166 166 | key="key" # comment2
450-
167 167 | ) # [RUF056]
451-
168 168 |
452-
453-
RUF056.py:170:55: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
454-
|
455-
169 | # testing invalid dict.get calls
456-
170 | value = not my_dict.get(key="key", other="something", default=False)
457-
| ^^^^^^^^^^^^^ RUF056
458-
171 | value = not my_dict.get(default=False, other="something", key="test")
412+
RUF056.py:169:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
413+
|
414+
167 | not my_dict.get(key="key", default=False)
415+
168 | not my_dict.get(default=[], key="key")
416+
169 | not my_dict.get(default=False)
417+
| ^^^^^^^^^^^^^ RUF056
418+
170 | not my_dict.get(key="key", other="something", default=False)
419+
171 | not my_dict.get(default=False, other="something", key="test")
459420
|
460421
= help: Remove falsy fallback from `dict.get()`
461422

462-
Safe fix
463-
167 167 | ) # [RUF056]
464-
168 168 |
465-
169 169 | # testing invalid dict.get calls
466-
170 |-value = not my_dict.get(key="key", other="something", default=False)
467-
170 |+value = not my_dict.get(key="key", other="something")
468-
171 171 | value = not my_dict.get(default=False, other="something", key="test")
423+
RUF056.py:170:47: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
424+
|
425+
168 | not my_dict.get(default=[], key="key")
426+
169 | not my_dict.get(default=False)
427+
170 | not my_dict.get(key="key", other="something", default=False)
428+
| ^^^^^^^^^^^^^ RUF056
429+
171 | not my_dict.get(default=False, other="something", key="test")
430+
|
431+
= help: Remove falsy fallback from `dict.get()`
469432

470-
RUF056.py:171:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
433+
RUF056.py:171:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
471434
|
472-
169 | # testing invalid dict.get calls
473-
170 | value = not my_dict.get(key="key", other="something", default=False)
474-
171 | value = not my_dict.get(default=False, other="something", key="test")
475-
| ^^^^^^^^^^^^^ RUF056
435+
169 | not my_dict.get(default=False)
436+
170 | not my_dict.get(key="key", other="something", default=False)
437+
171 | not my_dict.get(default=False, other="something", key="test")
438+
| ^^^^^^^^^^^^^ RUF056
439+
172 |
440+
173 | # comments don't really matter here because of the kwargs but include them for
476441
|
477442
= help: Remove falsy fallback from `dict.get()`
478443

479-
Safe fix
480-
168 168 |
481-
169 169 | # testing invalid dict.get calls
482-
170 170 | value = not my_dict.get(key="key", other="something", default=False)
483-
171 |-value = not my_dict.get(default=False, other="something", key="test")
484-
171 |+value = not my_dict.get(other="something", key="test")
444+
RUF056.py:177:5: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
445+
|
446+
175 | not my_dict.get(
447+
176 | key="key", # comment1
448+
177 | default=False, # comment2
449+
| ^^^^^^^^^^^^^ RUF056
450+
178 | ) # comment 3
451+
179 | not my_dict.get(
452+
|
453+
= help: Remove falsy fallback from `dict.get()`
454+
455+
RUF056.py:180:5: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
456+
|
457+
178 | ) # comment 3
458+
179 | not my_dict.get(
459+
180 | default=[], # comment1
460+
| ^^^^^^^^^^ RUF056
461+
181 | key="key", # comment2
462+
182 | ) # comment 3
463+
|
464+
= help: Remove falsy fallback from `dict.get()`
465+
466+
RUF056.py:187:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.
467+
|
468+
185 | # TypeError is raised at runtime before and after the fix, but we still bail
469+
186 | # out for having an unrecognized number of arguments
470+
187 | not my_dict.get("key", False, foo=...)
471+
| ^^^^^ RUF056
472+
|
473+
= help: Remove falsy fallback from `dict.get()`

0 commit comments

Comments
 (0)