Skip to content

Commit aa2180f

Browse files
authored
fix: map_entry suggest wrongly when key is not Copy (rust-lang#14314)
Closes rust-lang#13306 Closes rust-lang#9925 Closes rust-lang#9470 Closes rust-lang#9305 Clippy gives wrong suggestions when the key is not `Copy`. As suggested in rust-lang#9925, in such cases Clippy will simply warn but no fix. changelog: [`map_entry`]: fix wrong suggestions when key is not `Copy`
2 parents 62f34f2 + 18616dc commit aa2180f

File tree

3 files changed

+167
-15
lines changed

3 files changed

+167
-15
lines changed

clippy_lints/src/entry.rs

Lines changed: 32 additions & 15 deletions
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

@@ -354,6 +354,8 @@ struct InsertSearcher<'cx, 'tcx> {
354354
key: &'tcx Expr<'tcx>,
355355
/// The context of the top level block. All insert calls must be in the same context.
356356
ctxt: SyntaxContext,
357+
/// The spanless equality utility used to compare expressions.
358+
spanless_eq: SpanlessEq<'cx, 'tcx>,
357359
/// Whether this expression can be safely moved into a closure.
358360
allow_insert_closure: bool,
359361
/// Whether this expression can use the entry api.
@@ -364,6 +366,8 @@ struct InsertSearcher<'cx, 'tcx> {
364366
is_single_insert: bool,
365367
/// If the visitor has seen the map being used.
366368
is_map_used: bool,
369+
/// If the visitor has seen the key being used.
370+
is_key_used: bool,
367371
/// The locations where changes need to be made for the suggestion.
368372
edits: Vec<Edit<'tcx>>,
369373
/// A stack of loops the visitor is currently in.
@@ -479,11 +483,11 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
479483
}
480484

481485
match try_parse_insert(self.cx, expr) {
482-
Some(insert_expr) if SpanlessEq::new(self.cx).eq_expr(self.map, insert_expr.map) => {
486+
Some(insert_expr) if self.spanless_eq.eq_expr(self.map, insert_expr.map) => {
483487
self.visit_insert_expr_arguments(&insert_expr);
484488
// Multiple inserts, inserts with a different key, and inserts from a macro can't use the entry api.
485489
if self.is_map_used
486-
|| !SpanlessEq::new(self.cx).eq_expr(self.key, insert_expr.key)
490+
|| !self.spanless_eq.eq_expr(self.key, insert_expr.key)
487491
|| expr.span.ctxt() != self.ctxt
488492
{
489493
self.can_use_entry = false;
@@ -502,9 +506,12 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
502506
self.visit_non_tail_expr(insert_expr.value);
503507
self.is_single_insert = is_single_insert;
504508
},
505-
_ if is_any_expr_in_map_used(self.cx, self.map, expr) => {
509+
_ if is_any_expr_in_map_used(self.cx, &mut self.spanless_eq, self.map, expr) => {
506510
self.is_map_used = true;
507511
},
512+
_ if self.spanless_eq.eq_expr(self.key, expr) => {
513+
self.is_key_used = true;
514+
},
508515
_ => match expr.kind {
509516
ExprKind::If(cond_expr, then_expr, Some(else_expr)) => {
510517
self.is_single_insert = false;
@@ -568,9 +575,14 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
568575
/// Check if the given expression is used for each sub-expression in the given map.
569576
/// For example, in map `a.b.c.my_map`, The expression `a.b.c.my_map`, `a.b.c`, `a.b`, and `a` are
570577
/// all checked.
571-
fn is_any_expr_in_map_used<'tcx>(cx: &LateContext<'tcx>, map: &'tcx Expr<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
578+
fn is_any_expr_in_map_used<'tcx>(
579+
cx: &LateContext<'tcx>,
580+
spanless_eq: &mut SpanlessEq<'_, 'tcx>,
581+
map: &'tcx Expr<'tcx>,
582+
expr: &'tcx Expr<'tcx>,
583+
) -> bool {
572584
for_each_expr(cx, map, |e| {
573-
if SpanlessEq::new(cx).eq_expr(e, expr) {
585+
if spanless_eq.eq_expr(e, expr) {
574586
return ControlFlow::Break(());
575587
}
576588
ControlFlow::Continue(())
@@ -582,6 +594,7 @@ struct InsertSearchResults<'tcx> {
582594
edits: Vec<Edit<'tcx>>,
583595
allow_insert_closure: bool,
584596
is_single_insert: bool,
597+
is_key_used_and_no_copy: bool,
585598
}
586599
impl<'tcx> InsertSearchResults<'tcx> {
587600
fn as_single_insertion(&self) -> Option<Insertion<'tcx>> {
@@ -694,22 +707,26 @@ fn find_insert_calls<'tcx>(
694707
map: contains_expr.map,
695708
key: contains_expr.key,
696709
ctxt: expr.span.ctxt(),
710+
spanless_eq: SpanlessEq::new(cx),
697711
allow_insert_closure: true,
698712
can_use_entry: true,
699713
in_tail_pos: true,
700714
is_single_insert: true,
701715
is_map_used: false,
716+
is_key_used: false,
702717
edits: Vec::new(),
703718
loops: Vec::new(),
704719
locals: HirIdSet::default(),
705720
};
706721
s.visit_expr(expr);
707722
let allow_insert_closure = s.allow_insert_closure;
708723
let is_single_insert = s.is_single_insert;
724+
let is_key_used_and_no_copy = s.is_key_used && !is_copy(cx, cx.typeck_results().expr_ty(contains_expr.key));
709725
let edits = s.edits;
710726
s.can_use_entry.then_some(InsertSearchResults {
711727
edits,
712728
allow_insert_closure,
713729
is_single_insert,
730+
is_key_used_and_no_copy,
714731
})
715732
}

tests/ui/entry_unfixable.rs

Lines changed: 94 additions & 0 deletions
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

Lines changed: 41 additions & 0 deletions
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)