Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify key in dct and dct[key] to dct.get(key) #7895

Merged
merged 7 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF019.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
d = {}
# RUF019
if "k" in d and d["k"]:
pass

k = "k"
if k in d and d[k]:
pass

if (k) in d and d[k]:
pass

if k in d and d[(k)]:
pass

# OK
v = "k" in d and d["k"]

if f() in d and d[f()]:
pass
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::RepeatedEqualityComparison) {
pylint::rules::repeated_equality_comparison(checker, bool_op);
}
if checker.enabled(Rule::UnnecessaryKeyCheck) {
ruff::rules::unnecessary_key_check(checker, expr);
}
}
Expr::NamedExpr(..) => {
if checker.enabled(Rule::AssignmentInAssert) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
#[allow(deprecated)]
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),

Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ mod tests {
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_1.py"))]
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))]
#[test_case(Rule::AssignmentInAssert, Path::new("RUF018.py"))]
#[test_case(Rule::UnnecessaryKeyCheck, Path::new("RUF019.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) use mutable_dataclass_default::*;
pub(crate) use pairwise_over_zipped::*;
pub(crate) use static_key_dict_comprehension::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
#[cfg(feature = "unreachable-code")]
pub(crate) use unreachable::*;
pub(crate) use unused_noqa::*;
Expand All @@ -32,6 +33,7 @@ mod mutable_dataclass_default;
mod pairwise_over_zipped;
mod static_key_dict_comprehension;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
#[cfg(feature = "unreachable-code")]
pub(crate) mod unreachable;
mod unused_noqa;
Expand Down
131 changes: 131 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};

use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_text_size::Ranged;

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

/// ## What it does
/// Checks for unnecessary key checks prior to accessing a dictionary.
///
/// ## Why is this bad?
/// When working with dictionaries, the `get` can be used to access a value
/// without having to check if the dictionary contains the relevant key,
/// returning `None` if the key is not present.
///
/// ## Examples
/// ```python
/// if "key" in dct and dct["key"]:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// if dct.get("key"):
/// ...
/// ```
#[violation]
pub struct UnnecessaryKeyCheck;

impl AlwaysFixableViolation for UnnecessaryKeyCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary key check before dictionary access")
}

fn fix_title(&self) -> String {
format!("Replace with `dict.get`")
}
}

/// RUF019
pub(crate) fn unnecessary_key_check(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().in_boolean_test() {
return;
}

let Expr::BoolOp(ast::ExprBoolOp {
op: BoolOp::And,
values,
..
}) = expr
else {
return;
};

let [left, right] = values.as_slice() else {
return;
};

// Left should be, e.g., `key in dct`.
let Expr::Compare(ast::ExprCompare {
left: key_left,
ops,
comparators,
..
}) = left
else {
return;
};

if !matches!(ops.as_slice(), [CmpOp::In]) {
return;
}

let [obj_left] = comparators.as_slice() else {
return;
};

// Right should be, e.g., `dct[key]`.
let Expr::Subscript(ast::ExprSubscript {
value: obj_right,
slice: key_right,
..
}) = right
else {
return;
};

if ComparableExpr::from(obj_left) != ComparableExpr::from(obj_right)
|| ComparableExpr::from(key_left) != ComparableExpr::from(key_right)
{
return;
}

if contains_effect(obj_left, |id| checker.semantic().is_builtin(id))
|| contains_effect(key_left, |id| checker.semantic().is_builtin(id))
{
return;
}

let mut diagnostic = Diagnostic::new(UnnecessaryKeyCheck, expr.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!(
"{}.get({})",
checker.locator().slice(
parenthesized_range(
obj_right.into(),
right.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(obj_right.range())
),
checker.locator().slice(
parenthesized_range(
key_right.into(),
right.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(key_right.range())
),
),
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF019.py:3:4: RUF019 [*] Unnecessary key check before dictionary access
|
1 | d = {}
2 | # RUF019
3 | if "k" in d and d["k"]:
| ^^^^^^^^^^^^^^^^^^^ RUF019
Comment on lines +8 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future improvements could be to also support other checks like:

if "k" in d and d["k"] == value:
	...

which could be replaced with:

if d.get("k") == value:
	...

Instead of ==, there could be !=, in, not in, etc.

4 | pass
|
= help: Replace with `dict.get`

ℹ Fix
1 1 | d = {}
2 2 | # RUF019
3 |-if "k" in d and d["k"]:
3 |+if d.get("k"):
4 4 | pass
5 5 |
6 6 | k = "k"

RUF019.py:7:4: RUF019 [*] Unnecessary key check before dictionary access
|
6 | k = "k"
7 | if k in d and d[k]:
| ^^^^^^^^^^^^^^^ RUF019
8 | pass
|
= help: Replace with `dict.get`

ℹ Fix
4 4 | pass
5 5 |
6 6 | k = "k"
7 |-if k in d and d[k]:
7 |+if d.get(k):
8 8 | pass
9 9 |
10 10 | if (k) in d and d[k]:

RUF019.py:10:4: RUF019 [*] Unnecessary key check before dictionary access
|
8 | pass
9 |
10 | if (k) in d and d[k]:
| ^^^^^^^^^^^^^^^^^ RUF019
11 | pass
|
= help: Replace with `dict.get`

ℹ Fix
7 7 | if k in d and d[k]:
8 8 | pass
9 9 |
10 |-if (k) in d and d[k]:
10 |+if d.get(k):
11 11 | pass
12 12 |
13 13 | if k in d and d[(k)]:

RUF019.py:13:4: RUF019 [*] Unnecessary key check before dictionary access
|
11 | pass
12 |
13 | if k in d and d[(k)]:
| ^^^^^^^^^^^^^^^^^ RUF019
14 | pass
|
= help: Replace with `dict.get`

ℹ Fix
10 10 | if (k) in d and d[k]:
11 11 | pass
12 12 |
13 |-if k in d and d[(k)]:
13 |+if d.get((k)):
14 14 | pass
15 15 |
16 16 | # OK


1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading