-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 all 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,90 @@ | ||||||||||||||||||||||||||||||||||||||
use clippy_utils::diagnostics::span_lint_and_note; | ||||||||||||||||||||||||||||||||||||||
use clippy_utils::ty::is_type_diagnostic_item; | ||||||||||||||||||||||||||||||||||||||
use clippy_utils::{last_path_segment, SpanlessEq}; | ||||||||||||||||||||||||||||||||||||||
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. | ||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||
/// ### Example | ||||||||||||||||||||||||||||||||||||||
/// ```no_run | ||||||||||||||||||||||||||||||||||||||
/// # use std::collections::HashMap; | ||||||||||||||||||||||||||||||||||||||
/// let example = HashMap::from([(5, 1), (5, 2)]); | ||||||||||||||||||||||||||||||||||||||
/// ``` | ||||||||||||||||||||||||||||||||||||||
#[clippy::version = "1.79.0"] | ||||||||||||||||||||||||||||||||||||||
pub DUPLICATE_MAP_KEYS, | ||||||||||||||||||||||||||||||||||||||
suspicious, | ||||||||||||||||||||||||||||||||||||||
"`HashMap` with duplicate keys loses data" | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
declare_lint_pass!(DuplicateMapKeys => [DUPLICATE_MAP_KEYS]); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
impl<'tcx> LateLintPass<'tcx> for DuplicateMapKeys { | ||||||||||||||||||||||||||||||||||||||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'tcx>) { | ||||||||||||||||||||||||||||||||||||||
if has_hash_collision(cx, expr).is_some_and(|v| !v.is_empty()) { | ||||||||||||||||||||||||||||||||||||||
span_lint_and_note( | ||||||||||||||||||||||||||||||||||||||
cx, | ||||||||||||||||||||||||||||||||||||||
DUPLICATE_MAP_KEYS, | ||||||||||||||||||||||||||||||||||||||
expr.span, | ||||||||||||||||||||||||||||||||||||||
"this `HashMap` uses one key for multiple items 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<'a>( | ||||||||||||||||||||||||||||||||||||||
cx: &LateContext<'_>, | ||||||||||||||||||||||||||||||||||||||
expr: &'a rustc_hir::Expr<'_>, | ||||||||||||||||||||||||||||||||||||||
) -> Option<Vec<(rustc_hir::Expr<'a>, rustc_hir::Expr<'a>)>> { | ||||||||||||||||||||||||||||||||||||||
// 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 keys = Vec::new(); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
for arg in *args { | ||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||
if let ExprKind::Tup(args) = &arg.kind | ||||||||||||||||||||||||||||||||||||||
&& !args.is_empty() | ||||||||||||||||||||||||||||||||||||||
// && let ExprKind::Lit(lit) = args[0].kind | ||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||
keys.push(args[0]); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+65
to
+75
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
|
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Check if there are any duplicate keys | ||||||||||||||||||||||||||||||||||||||
let mut duplicate = Vec::new(); | ||||||||||||||||||||||||||||||||||||||
let mut spannless_eq = SpanlessEq::new(cx); | ||||||||||||||||||||||||||||||||||||||
for i in 0..keys.len() { | ||||||||||||||||||||||||||||||||||||||
for j in i + 1..keys.len() { | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+80
to
+81
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. This is a quadratic algorithm that will slow down considerably with larger sets (try a thousand elements). A better way would be to use a |
||||||||||||||||||||||||||||||||||||||
if spannless_eq.eq_expr(&keys[i], &keys[j]) { | ||||||||||||||||||||||||||||||||||||||
duplicate.push((keys[i], keys[j])); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
return Some(duplicate); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
None | ||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
#![warn(clippy::duplicate_map_keys)] | ||
#![allow(clippy::diverging_sub_expression)] | ||
use std::collections::HashMap; | ||
use std::hash::{Hash, Hasher}; | ||
|
||
fn main() -> Result<(), ()> { | ||
let _ = HashMap::from([(5, 1), (5, 2)]); // expect lint | ||
let _ = HashMap::from([((), 1), ((), 2)]); // expect lint | ||
let _ = HashMap::from([("a", 1), ("a", 2)]); // expect lint | ||
|
||
let _ = HashMap::from([(return Ok(()), 1), (return Err(()), 2)]); // expect no lint | ||
// is this what you meant? I'm not sure how to put return into there | ||
|
||
let _ = HashMap::from([(5, 1), (6, 2)]); // expect no lint | ||
let _ = HashMap::from([("a", 1), ("b", 2)]); // expect no lint | ||
|
||
let _ = HashMap::from([(Bad(true), 1), (Bad(false), 2)]); // expect no lint (false negative) | ||
|
||
Ok(()) | ||
} | ||
|
||
// Construction of false negative | ||
struct Bad(bool); | ||
|
||
// eq is used in the lint, but hash in the HashMap | ||
impl Eq for Bad {} | ||
|
||
impl PartialEq for Bad { | ||
fn eq(&self, other: &Self) -> bool { | ||
true | ||
} | ||
} | ||
|
||
impl Hash for Bad { | ||
fn hash<H: Hasher>(&self, state: &mut H) { | ||
// empty | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
error: this `HashMap` uses one key for multiple items and will lose data | ||
--> tests/ui/duplicate_map_keys.rs:7:13 | ||
| | ||
LL | let _ = HashMap::from([(5, 1), (5, 2)]); // expect lint | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
= note: consider using a different keys for all items | ||
= note: `-D clippy::duplicate-map-keys` implied by `-D warnings` | ||
= help: to override `-D warnings` add `#[allow(clippy::duplicate_map_keys)]` | ||
|
||
error: this `HashMap` uses one key for multiple items and will lose data | ||
--> tests/ui/duplicate_map_keys.rs:8:13 | ||
| | ||
LL | let _ = HashMap::from([((), 1), ((), 2)]); // expect lint | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
= note: consider using a different keys for all items | ||
|
||
error: this `HashMap` uses one key for multiple items and will lose data | ||
--> tests/ui/duplicate_map_keys.rs:9:13 | ||
| | ||
LL | let _ = HashMap::from([("a", 1), ("a", 2)]); // expect lint | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
= note: consider using a different keys for all items | ||
|
||
error: aborting due to 3 previous errors | ||
|
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.
Matching a 1-element slice would let you avoid the
args.len() == 1
check and index later.