Skip to content

Commit ab15c34

Browse files
committed
fix: map_entry suggest wrongly when key is not Copy
1 parent 4a9b8c6 commit ab15c34

File tree

3 files changed

+154
-10
lines changed

3 files changed

+154
-10
lines changed

clippy_lints/src/entry.rs

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use clippy_utils::diagnostics::span_lint_and_sugg;
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
22
use clippy_utils::source::{reindent_multiline, snippet_indent, snippet_with_applicability, snippet_with_context};
3+
use clippy_utils::ty::is_copy;
34
use clippy_utils::visitors::for_each_expr;
45
use clippy_utils::{
56
SpanlessEq, can_move_expr_to_closure_no_visit, higher, is_expr_final_block_expr, is_expr_used_or_unified,
@@ -84,14 +85,21 @@ impl<'tcx> LateLintPass<'tcx> for HashMapPass {
8485
return;
8586
};
8687

88+
let lint_msg = format!("usage of `contains_key` followed by `insert` on a `{}`", map_ty.name());
8789
let mut app = Applicability::MachineApplicable;
8890
let map_str = snippet_with_context(cx, contains_expr.map.span, contains_expr.call_ctxt, "..", &mut app).0;
8991
let key_str = snippet_with_context(cx, contains_expr.key.span, contains_expr.call_ctxt, "..", &mut app).0;
92+
9093
let sugg = if let Some(else_expr) = else_expr {
9194
let Some(else_search) = find_insert_calls(cx, &contains_expr, else_expr) else {
9295
return;
9396
};
9497

98+
if then_search.is_key_used_and_no_copy || else_search.is_key_used_and_no_copy {
99+
span_lint(cx, MAP_ENTRY, expr.span, lint_msg);
100+
return;
101+
}
102+
95103
if then_search.edits.is_empty() && else_search.edits.is_empty() {
96104
// No insertions
97105
return;
@@ -184,15 +192,7 @@ impl<'tcx> LateLintPass<'tcx> for HashMapPass {
184192
}
185193
};
186194

187-
span_lint_and_sugg(
188-
cx,
189-
MAP_ENTRY,
190-
expr.span,
191-
format!("usage of `contains_key` followed by `insert` on a `{}`", map_ty.name()),
192-
"try",
193-
sugg,
194-
app,
195-
);
195+
span_lint_and_sugg(cx, MAP_ENTRY, expr.span, lint_msg, "try", sugg, app);
196196
}
197197
}
198198

@@ -364,6 +364,8 @@ struct InsertSearcher<'cx, 'tcx> {
364364
is_single_insert: bool,
365365
/// If the visitor has seen the map being used.
366366
is_map_used: bool,
367+
/// If the visitor has seen the key being used.
368+
is_key_used: bool,
367369
/// The locations where changes need to be made for the suggestion.
368370
edits: Vec<Edit<'tcx>>,
369371
/// A stack of loops the visitor is currently in.
@@ -505,6 +507,9 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
505507
_ if is_any_expr_in_map_used(self.cx, self.map, expr) => {
506508
self.is_map_used = true;
507509
},
510+
_ if SpanlessEq::new(self.cx).eq_expr(self.key, expr) => {
511+
self.is_key_used = true;
512+
},
508513
_ => match expr.kind {
509514
ExprKind::If(cond_expr, then_expr, Some(else_expr)) => {
510515
self.is_single_insert = false;
@@ -582,6 +587,7 @@ struct InsertSearchResults<'tcx> {
582587
edits: Vec<Edit<'tcx>>,
583588
allow_insert_closure: bool,
584589
is_single_insert: bool,
590+
is_key_used_and_no_copy: bool,
585591
}
586592
impl<'tcx> InsertSearchResults<'tcx> {
587593
fn as_single_insertion(&self) -> Option<Insertion<'tcx>> {
@@ -699,17 +705,20 @@ fn find_insert_calls<'tcx>(
699705
in_tail_pos: true,
700706
is_single_insert: true,
701707
is_map_used: false,
708+
is_key_used: false,
702709
edits: Vec::new(),
703710
loops: Vec::new(),
704711
locals: HirIdSet::default(),
705712
};
706713
s.visit_expr(expr);
707714
let allow_insert_closure = s.allow_insert_closure;
708715
let is_single_insert = s.is_single_insert;
716+
let is_key_used_and_no_copy = s.is_key_used && !is_copy(cx, cx.typeck_results().expr_ty(contains_expr.key));
709717
let edits = s.edits;
710718
s.can_use_entry.then_some(InsertSearchResults {
711719
edits,
712720
allow_insert_closure,
713721
is_single_insert,
722+
is_key_used_and_no_copy,
714723
})
715724
}

tests/ui/entry_unfixable.rs

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#![allow(unused, clippy::needless_pass_by_value, clippy::collapsible_if)]
2+
#![warn(clippy::map_entry)]
3+
//@no-rustfix
4+
5+
use std::collections::HashMap;
6+
use std::hash::Hash;
7+
8+
macro_rules! m {
9+
($e:expr) => {{ $e }};
10+
}
11+
12+
macro_rules! insert {
13+
($map:expr, $key:expr, $val:expr) => {
14+
$map.insert($key, $val)
15+
};
16+
}
17+
18+
mod issue13306 {
19+
use std::collections::HashMap;
20+
21+
struct Env {
22+
enclosing: Option<Box<Env>>,
23+
values: HashMap<String, usize>,
24+
}
25+
26+
impl Env {
27+
fn assign(&mut self, name: String, value: usize) -> bool {
28+
if !self.values.contains_key(&name) {
29+
//~^ map_entry
30+
self.values.insert(name, value);
31+
true
32+
} else if let Some(enclosing) = &mut self.enclosing {
33+
enclosing.assign(name, value)
34+
} else {
35+
false
36+
}
37+
}
38+
}
39+
}
40+
41+
fn issue9925(mut hm: HashMap<String, bool>) {
42+
let key = "hello".to_string();
43+
if hm.contains_key(&key) {
44+
//~^ map_entry
45+
let bval = hm.get_mut(&key).unwrap();
46+
*bval = false;
47+
} else {
48+
hm.insert(key, true);
49+
}
50+
}
51+
52+
mod issue9470 {
53+
use std::collections::HashMap;
54+
use std::sync::Mutex;
55+
56+
struct Interner(i32);
57+
58+
impl Interner {
59+
const fn new() -> Self {
60+
Interner(0)
61+
}
62+
63+
fn resolve(&self, name: String) -> Option<String> {
64+
todo!()
65+
}
66+
}
67+
68+
static INTERNER: Mutex<Interner> = Mutex::new(Interner::new());
69+
70+
struct VM {
71+
stack: Vec<i32>,
72+
globals: HashMap<String, i32>,
73+
}
74+
75+
impl VM {
76+
fn stack_top(&self) -> &i32 {
77+
self.stack.last().unwrap()
78+
}
79+
80+
fn resolve(&mut self, name: String, value: i32) -> Result<(), String> {
81+
if self.globals.contains_key(&name) {
82+
//~^ map_entry
83+
self.globals.insert(name, value);
84+
} else {
85+
let interner = INTERNER.lock().unwrap();
86+
return Err(interner.resolve(name).unwrap().to_owned());
87+
}
88+
89+
Ok(())
90+
}
91+
}
92+
}
93+
94+
fn main() {}

tests/ui/entry_unfixable.stderr

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
error: usage of `contains_key` followed by `insert` on a `HashMap`
2+
--> tests/ui/entry_unfixable.rs:28:13
3+
|
4+
LL | / if !self.values.contains_key(&name) {
5+
LL | |
6+
LL | | self.values.insert(name, value);
7+
LL | | true
8+
... |
9+
LL | | false
10+
LL | | }
11+
| |_____________^
12+
|
13+
= note: `-D clippy::map-entry` implied by `-D warnings`
14+
= help: to override `-D warnings` add `#[allow(clippy::map_entry)]`
15+
16+
error: usage of `contains_key` followed by `insert` on a `HashMap`
17+
--> tests/ui/entry_unfixable.rs:43:5
18+
|
19+
LL | / if hm.contains_key(&key) {
20+
LL | |
21+
LL | | let bval = hm.get_mut(&key).unwrap();
22+
LL | | *bval = false;
23+
LL | | } else {
24+
LL | | hm.insert(key, true);
25+
LL | | }
26+
| |_____^
27+
28+
error: usage of `contains_key` followed by `insert` on a `HashMap`
29+
--> tests/ui/entry_unfixable.rs:81:13
30+
|
31+
LL | / if self.globals.contains_key(&name) {
32+
LL | |
33+
LL | | self.globals.insert(name, value);
34+
LL | | } else {
35+
LL | | let interner = INTERNER.lock().unwrap();
36+
LL | | return Err(interner.resolve(name).unwrap().to_owned());
37+
LL | | }
38+
| |_____________^
39+
40+
error: aborting due to 3 previous errors
41+

0 commit comments

Comments
 (0)