-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New lint: [duplicate_map_keys
]
#12575
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
Changes from 4 commits
7a903bd
cff98f2
434a92e
efb8e4d
6c13a90
ca6d9fb
cdd8618
27af9c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,91 @@ | ||||||||||||
use clippy_utils::diagnostics::span_lint_and_note; | ||||||||||||
use clippy_utils::last_path_segment; | ||||||||||||
use clippy_utils::ty::is_type_diagnostic_item; | ||||||||||||
use rustc_hir::ExprKind; | ||||||||||||
use rustc_lint::{LateContext, LateLintPass}; | ||||||||||||
use rustc_session::declare_lint_pass; | ||||||||||||
use rustc_span::symbol::sym; | ||||||||||||
|
||||||||||||
declare_clippy_lint! { | ||||||||||||
/// ### What it does | ||||||||||||
/// When two items are inserted into a `HashMap` with the same key, | ||||||||||||
/// the second item will overwrite the first item. | ||||||||||||
/// | ||||||||||||
/// ### Why is this bad? | ||||||||||||
/// This can lead to data loss. | ||||||||||||
/// | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
/// ### Example | ||||||||||||
/// ```no_run | ||||||||||||
/// # use std::collections::HashMap; | ||||||||||||
/// let example = HashMap::from([(5, 1), (5, 2)]); | ||||||||||||
/// ``` | ||||||||||||
#[clippy::version = "1.79.0"] | ||||||||||||
pub HASH_COLLISION, | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Hash collision" to me means different keys with the same hash, but this lint specifically looks for same keys. Should this be renamed from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I should have caught that. |
||||||||||||
suspicious, | ||||||||||||
"`HashMap` with two identical keys loses data" | ||||||||||||
} | ||||||||||||
|
||||||||||||
declare_lint_pass!(HashCollision => [HASH_COLLISION]); | ||||||||||||
|
||||||||||||
impl<'tcx> LateLintPass<'tcx> for HashCollision { | ||||||||||||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'tcx>) { | ||||||||||||
if has_hash_collision(cx, expr) { | ||||||||||||
span_lint_and_note( | ||||||||||||
cx, | ||||||||||||
HASH_COLLISION, | ||||||||||||
expr.span, | ||||||||||||
"this `HashMap` has a hash collision and will lose data", | ||||||||||||
None, | ||||||||||||
"consider using a different keys for all items", | ||||||||||||
); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
// TODO: Also check for different sources of hash collisions | ||||||||||||
// TODO: Maybe other types of hash maps should be checked as well? | ||||||||||||
fn has_hash_collision(cx: &LateContext<'_>, expr: &rustc_hir::Expr<'_>) -> bool { | ||||||||||||
// If the expression is a call to `HashMap::from`, check if the keys are the same | ||||||||||||
if let ExprKind::Call(func, args) = &expr.kind | ||||||||||||
// First check for HashMap::from | ||||||||||||
&& let ty = cx.typeck_results().expr_ty(expr) | ||||||||||||
&& is_type_diagnostic_item(cx, ty, sym::HashMap) | ||||||||||||
&& let ExprKind::Path(func_path) = func.kind | ||||||||||||
&& last_path_segment(&func_path).ident.name == sym::from | ||||||||||||
// There should be exactly one argument to HashMap::from | ||||||||||||
&& args.len() == 1 | ||||||||||||
// which should be an array | ||||||||||||
&& let ExprKind::Array(args) = &args[0].kind | ||||||||||||
// Then check the keys | ||||||||||||
{ | ||||||||||||
// Put all keys in a vector | ||||||||||||
let mut literals = Vec::new(); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use Also, why not return a (possibly empty) set of duplicate key spans instead of a plain bool? That way we could better pinpoint the error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for point out |
||||||||||||
|
||||||||||||
for arg in *args { | ||||||||||||
// | ||||||||||||
if let ExprKind::Tup(args) = &arg.kind | ||||||||||||
&& !args.is_empty() | ||||||||||||
&& let ExprKind::Lit(lit) = args[0].kind | ||||||||||||
{ | ||||||||||||
literals.push(lit.node.clone()); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
// Debug: it gets here, but doesn't trigger the lint | ||||||||||||
|
||||||||||||
// Check if there are any duplicate keys | ||||||||||||
let mut duplicate = false; | ||||||||||||
for i in 0..literals.len() { | ||||||||||||
for j in i + 1..literals.len() { | ||||||||||||
if literals[i] == literals[j] { | ||||||||||||
duplicate = true; | ||||||||||||
break; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
if duplicate { | ||||||||||||
break; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
return duplicate; | ||||||||||||
} | ||||||||||||
false | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#![warn(clippy::hash_collision)] | ||
use std::collections::HashMap; | ||
|
||
fn main() { | ||
let example = HashMap::from([(5, 1), (5, 2)]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to see more tests. For example one with a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've now written and tested all tests you recommended, but I didn't fully understand what you meant with "key expr containg a let _ = HashMap::from([(return Ok(()), 1), (return Err(()), 2)]); // expect no lint is what I did, but everything should already be caught by |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
error: this `HashMap` has a hash collision and will lose data | ||
--> tests/ui/hash_collision.rs:5:19 | ||
| | ||
LL | let example = HashMap::from([(5, 1), (5, 2)]); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
= note: consider using a different keys for all items | ||
= note: `-D clippy::hash-collision` implied by `-D warnings` | ||
= help: to override `-D warnings` add `#[allow(clippy::hash_collision)]` | ||
|
||
error: aborting due to 1 previous error | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.