-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6149177
commit ff74a2b
Showing
9 changed files
with
934 additions
and
1 deletion.
There are no files selected for viewing
126 changes: 126 additions & 0 deletions
126
crates/ruff_linter/resources/test/fixtures/ruff/RUF051.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
d = {} | ||
l = [] | ||
|
||
|
||
### Errors | ||
|
||
if k in d: # Bare name | ||
del d[k] | ||
|
||
if '' in d: # String | ||
del d[""] # Different quotes | ||
|
||
if b"" in d: # Bytes | ||
del d[ # Multiline slice | ||
b'''''' # Triple quotes | ||
] | ||
|
||
if 0 in d: del d[0] # Single-line statement | ||
|
||
if 3j in d: # Complex | ||
del d[3j] | ||
|
||
if 0.1234 in d: # Float | ||
del d[.1_2_3_4] # Number separators and shorthand syntax | ||
|
||
if True in d: # True | ||
del d[True] | ||
|
||
if False in d: # False | ||
del d[False] | ||
|
||
if None in d: # None | ||
del d[ | ||
# Comment in the middle | ||
None | ||
] | ||
|
||
if ... in d: # Ellipsis | ||
del d[ | ||
# Comment in the middle, indented | ||
...] | ||
|
||
if "a" "bc" in d: # String concatenation | ||
del d['abc'] | ||
|
||
if r"\foo" in d: # Raw string | ||
del d['\\foo'] | ||
|
||
if b'yt' b'es' in d: # Bytes concatenation | ||
del d[rb"""ytes"""] # Raw bytes | ||
|
||
|
||
### Safely fixable | ||
|
||
if k in d: | ||
del d[k] | ||
|
||
if '' in d: | ||
del d[""] | ||
|
||
if b"" in d: | ||
del d[ | ||
b'''''' | ||
] | ||
|
||
if 0 in d: del d[0] | ||
|
||
if 3j in d: | ||
del d[3j] | ||
|
||
if 0.1234 in d: | ||
del d[.1_2_3_4] | ||
|
||
if True in d: | ||
del d[True] | ||
|
||
if False in d: | ||
del d[False] | ||
|
||
if None in d: | ||
del d[ | ||
None | ||
] | ||
|
||
if ... in d: | ||
del d[ | ||
...] | ||
|
||
if "a" "bc" in d: | ||
del d['abc'] | ||
|
||
if r"\foo" in d: | ||
del d['\\foo'] | ||
|
||
if b'yt' b'es' in d: | ||
del d[rb"""ytes"""] # This should not make the fix unsafe | ||
|
||
|
||
### No errors | ||
|
||
if k in l: # Not a dict | ||
del l[k] | ||
|
||
if d.__contains__(k): # Explicit dunder call | ||
del d[k] | ||
|
||
if a.k in d: # Attribute | ||
del d[a.k] | ||
|
||
if (a, b) in d: # Tuple | ||
del d[a, b] | ||
|
||
if 2 in d: # Different key value (int) | ||
del d[3] | ||
|
||
if 2_4j in d: # Different key value (complex) | ||
del d[3.6] # Different key value (float) | ||
|
||
if 0.1 + 0.2 in d: # Complex expression | ||
del d[0.3] | ||
|
||
if f"0" in d: # f-string | ||
del d[f"0"] | ||
|
||
if k in a.d: # Attribute dict | ||
del a.d[k] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; | ||
use ruff_macros::{derive_message_formats, ViolationMetadata}; | ||
use ruff_python_ast::comparable::ComparableExpr; | ||
use ruff_python_ast::{ | ||
CmpOp, Expr, ExprCompare, ExprName, ExprSubscript, Stmt, StmtDelete, StmtIf, | ||
}; | ||
use ruff_python_semantic::analyze::typing; | ||
use ruff_python_semantic::SemanticModel; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
// Real type: Expr::Name; | ||
type Key = Expr; | ||
type Dict = Expr; | ||
|
||
/// ## What it does | ||
/// Checks for `if key in dictionary: del dictionary[key]`. | ||
/// | ||
/// ## Why is this bad? | ||
/// When removing a key from a dictionary, it is unnecessary to check for its existence. | ||
/// `.pop(..., None)` is simpler and has the same semantic. | ||
/// | ||
/// ## Example | ||
/// | ||
/// ```python | ||
/// if key in dictionary: | ||
/// del dictionary[key] | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// | ||
/// ```python | ||
/// dictionary.pop(key, None) | ||
/// ``` | ||
#[derive(ViolationMetadata)] | ||
pub(crate) struct IfKeyInDictDel; | ||
|
||
impl AlwaysFixableViolation for IfKeyInDictDel { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
"Use `pop` instead of `key in dict` followed by `delete dict[key]`".to_string() | ||
} | ||
|
||
fn fix_title(&self) -> String { | ||
"Replace with `.pop(..., None)`".to_string() | ||
} | ||
} | ||
|
||
/// RUF051 | ||
pub(crate) fn if_key_in_dict_del(checker: &mut Checker, stmt: &StmtIf) { | ||
let Some((test_dict, test_key)) = extract_dict_and_key_from_test(&stmt.test) else { | ||
return; | ||
}; | ||
let Some((del_dict, del_key)) = extract_dict_and_key_from_del(&stmt.body) else { | ||
return; | ||
}; | ||
|
||
if !is_same_key(test_key, del_key) || !is_same_dict(test_dict, del_dict) { | ||
return; | ||
} | ||
|
||
if !is_known_to_be_of_type_dict(checker.semantic(), test_dict) { | ||
return; | ||
} | ||
|
||
let diagnostic = Diagnostic::new(IfKeyInDictDel, stmt.range); | ||
let Some(fix) = replace_with_dict_pop_fix(checker, stmt, test_dict, test_key) else { | ||
// This is only reached when the `if` body has no statement, | ||
// which is impossible as we have already checked for this above. | ||
return; | ||
}; | ||
|
||
checker.diagnostics.push(diagnostic.with_fix(fix)); | ||
} | ||
|
||
fn extract_dict_and_key_from_test(test: &Expr) -> Option<(&Dict, &Key)> { | ||
let Expr::Compare(ExprCompare { | ||
left, | ||
ops, | ||
comparators, | ||
.. | ||
}) = test | ||
else { | ||
return None; | ||
}; | ||
|
||
if !matches!(ops.as_ref(), [CmpOp::In]) { | ||
return None; | ||
} | ||
|
||
let [right] = comparators.as_ref() else { | ||
return None; | ||
}; | ||
|
||
dict_and_key_verified(right, left.as_ref()) | ||
} | ||
|
||
fn extract_dict_and_key_from_del(body: &[Stmt]) -> Option<(&Dict, &Key)> { | ||
let [Stmt::Delete(StmtDelete { targets, .. })] = body else { | ||
return None; | ||
}; | ||
let [Expr::Subscript(ExprSubscript { value, slice, .. })] = &targets[..] else { | ||
return None; | ||
}; | ||
|
||
dict_and_key_verified(value.as_ref(), slice.as_ref()) | ||
} | ||
|
||
fn dict_and_key_verified<'d, 'k>(dict: &'d Dict, key: &'k Key) -> Option<(&'d Dict, &'k Key)> { | ||
if !key.is_name_expr() && !key.is_literal_expr() { | ||
return None; | ||
} | ||
|
||
if !dict.is_name_expr() { | ||
return None; | ||
} | ||
|
||
Some((dict, key)) | ||
} | ||
|
||
fn is_same_key(test: &Expr, del: &Expr) -> bool { | ||
match (test, del) { | ||
(Expr::Name(..), Expr::Name(..)) | ||
| (Expr::NoneLiteral(..), Expr::NoneLiteral(..)) | ||
| (Expr::EllipsisLiteral(..), Expr::EllipsisLiteral(..)) | ||
| (Expr::BooleanLiteral(..), Expr::BooleanLiteral(..)) | ||
| (Expr::NumberLiteral(..), Expr::NumberLiteral(..)) | ||
| (Expr::BytesLiteral(..), Expr::BytesLiteral(..)) | ||
| (Expr::StringLiteral(..), Expr::StringLiteral(..)) => { | ||
ComparableExpr::from(test) == ComparableExpr::from(del) | ||
} | ||
|
||
_ => false, | ||
} | ||
} | ||
|
||
fn is_same_dict(test: &Expr, del: &Expr) -> bool { | ||
match (test, del) { | ||
(Expr::Name(ExprName { id: test, .. }), Expr::Name(ExprName { id: del, .. })) => { | ||
test.as_str() == del.as_str() | ||
} | ||
|
||
_ => false, | ||
} | ||
} | ||
|
||
fn is_known_to_be_of_type_dict(semantic: &SemanticModel, dict: &Expr) -> bool { | ||
dict.as_name_expr().is_some_and(|name| { | ||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { | ||
return false; | ||
}; | ||
typing::is_dict(binding, semantic) | ||
}) | ||
} | ||
|
||
fn replace_with_dict_pop_fix( | ||
checker: &Checker, | ||
stmt: &StmtIf, | ||
dict: &Dict, | ||
key: &Key, | ||
) -> Option<Fix> { | ||
let locator = checker.locator(); | ||
let dict_expr = locator.slice(dict); | ||
let key_expr = locator.slice(key); | ||
|
||
let replacement = format!("{dict_expr}.pop({key_expr}, None)"); | ||
let edit = Edit::range_replacement(replacement, stmt.range); | ||
|
||
let test_expr = &stmt.test; | ||
let del_stmt = stmt.body.first()?; | ||
let test_to_del = TextRange::new(test_expr.end(), del_stmt.start()); | ||
|
||
let comment_ranges = checker.comment_ranges(); | ||
let applicability = if comment_ranges.has_comments(&test_to_del, checker.source()) { | ||
Applicability::Unsafe | ||
} else { | ||
Applicability::Safe | ||
}; | ||
|
||
Some(Fix::applicable_edit(edit, applicability)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.