mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:28 +00:00
Simplify key in dct and dct[key]
to dct.get(key)
(#7895)
## Summary Close #5933 ## Test Plan `cargo test`
This commit is contained in:
parent
66179af4f1
commit
6f9c317aa5
8 changed files with 241 additions and 0 deletions
20
crates/ruff_linter/resources/test/fixtures/ruff/RUF019.py
vendored
Normal file
20
crates/ruff_linter/resources/test/fixtures/ruff/RUF019.py
vendored
Normal file
|
@ -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
|
|
@ -1412,6 +1412,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::RepeatedEqualityComparison) {
|
if checker.enabled(Rule::RepeatedEqualityComparison) {
|
||||||
pylint::rules::repeated_equality_comparison(checker, bool_op);
|
pylint::rules::repeated_equality_comparison(checker, bool_op);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::UnnecessaryKeyCheck) {
|
||||||
|
ruff::rules::unnecessary_key_check(checker, expr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expr::NamedExpr(..) => {
|
Expr::NamedExpr(..) => {
|
||||||
if checker.enabled(Rule::AssignmentInAssert) {
|
if checker.enabled(Rule::AssignmentInAssert) {
|
||||||
|
|
|
@ -866,6 +866,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||||
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
|
(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, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
|
||||||
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ mod tests {
|
||||||
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_1.py"))]
|
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_1.py"))]
|
||||||
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))]
|
#[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))]
|
||||||
#[test_case(Rule::AssignmentInAssert, Path::new("RUF018.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<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub(crate) use mutable_dataclass_default::*;
|
||||||
pub(crate) use pairwise_over_zipped::*;
|
pub(crate) use pairwise_over_zipped::*;
|
||||||
pub(crate) use static_key_dict_comprehension::*;
|
pub(crate) use static_key_dict_comprehension::*;
|
||||||
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
||||||
|
pub(crate) use unnecessary_key_check::*;
|
||||||
#[cfg(feature = "unreachable-code")]
|
#[cfg(feature = "unreachable-code")]
|
||||||
pub(crate) use unreachable::*;
|
pub(crate) use unreachable::*;
|
||||||
pub(crate) use unused_noqa::*;
|
pub(crate) use unused_noqa::*;
|
||||||
|
@ -32,6 +33,7 @@ mod mutable_dataclass_default;
|
||||||
mod pairwise_over_zipped;
|
mod pairwise_over_zipped;
|
||||||
mod static_key_dict_comprehension;
|
mod static_key_dict_comprehension;
|
||||||
mod unnecessary_iterable_allocation_for_first_element;
|
mod unnecessary_iterable_allocation_for_first_element;
|
||||||
|
mod unnecessary_key_check;
|
||||||
#[cfg(feature = "unreachable-code")]
|
#[cfg(feature = "unreachable-code")]
|
||||||
pub(crate) mod unreachable;
|
pub(crate) mod unreachable;
|
||||||
mod unused_noqa;
|
mod unused_noqa;
|
||||||
|
|
131
crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs
Normal file
131
crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
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
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3185,6 +3185,7 @@
|
||||||
"RUF016",
|
"RUF016",
|
||||||
"RUF017",
|
"RUF017",
|
||||||
"RUF018",
|
"RUF018",
|
||||||
|
"RUF019",
|
||||||
"RUF1",
|
"RUF1",
|
||||||
"RUF10",
|
"RUF10",
|
||||||
"RUF100",
|
"RUF100",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue