]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/entry.rs
:arrow_up: rust-analyzer
[rust.git] / src / tools / clippy / clippy_lints / src / entry.rs
1 use clippy_utils::higher;
2 use clippy_utils::{
3     can_move_expr_to_closure_no_visit,
4     diagnostics::span_lint_and_sugg,
5     is_expr_final_block_expr, is_expr_used_or_unified, match_def_path, paths, peel_hir_expr_while,
6     source::{reindent_multiline, snippet_indent, snippet_with_applicability, snippet_with_context},
7     SpanlessEq,
8 };
9 use core::fmt::Write;
10 use rustc_errors::Applicability;
11 use rustc_hir::{
12     hir_id::HirIdSet,
13     intravisit::{walk_expr, Visitor},
14     Block, Expr, ExprKind, Guard, HirId, Let, Pat, Stmt, StmtKind, UnOp,
15 };
16 use rustc_lint::{LateContext, LateLintPass};
17 use rustc_session::{declare_lint_pass, declare_tool_lint};
18 use rustc_span::{Span, SyntaxContext, DUMMY_SP};
19
20 declare_clippy_lint! {
21     /// ### What it does
22     /// Checks for uses of `contains_key` + `insert` on `HashMap`
23     /// or `BTreeMap`.
24     ///
25     /// ### Why is this bad?
26     /// Using `entry` is more efficient.
27     ///
28     /// ### Known problems
29     /// The suggestion may have type inference errors in some cases. e.g.
30     /// ```rust
31     /// let mut map = std::collections::HashMap::new();
32     /// let _ = if !map.contains_key(&0) {
33     ///     map.insert(0, 0)
34     /// } else {
35     ///     None
36     /// };
37     /// ```
38     ///
39     /// ### Example
40     /// ```rust
41     /// # use std::collections::HashMap;
42     /// # let mut map = HashMap::new();
43     /// # let k = 1;
44     /// # let v = 1;
45     /// if !map.contains_key(&k) {
46     ///     map.insert(k, v);
47     /// }
48     /// ```
49     /// Use instead:
50     /// ```rust
51     /// # use std::collections::HashMap;
52     /// # let mut map = HashMap::new();
53     /// # let k = 1;
54     /// # let v = 1;
55     /// map.entry(k).or_insert(v);
56     /// ```
57     #[clippy::version = "pre 1.29.0"]
58     pub MAP_ENTRY,
59     perf,
60     "use of `contains_key` followed by `insert` on a `HashMap` or `BTreeMap`"
61 }
62
63 declare_lint_pass!(HashMapPass => [MAP_ENTRY]);
64
65 impl<'tcx> LateLintPass<'tcx> for HashMapPass {
66     #[expect(clippy::too_many_lines)]
67     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
68         let (cond_expr, then_expr, else_expr) = match higher::If::hir(expr) {
69             Some(higher::If { cond, then, r#else }) => (cond, then, r#else),
70             _ => return,
71         };
72
73         let (map_ty, contains_expr) = match try_parse_contains(cx, cond_expr) {
74             Some(x) => x,
75             None => return,
76         };
77
78         let then_search = match find_insert_calls(cx, &contains_expr, then_expr) {
79             Some(x) => x,
80             None => return,
81         };
82
83         let mut app = Applicability::MachineApplicable;
84         let map_str = snippet_with_context(cx, contains_expr.map.span, contains_expr.call_ctxt, "..", &mut app).0;
85         let key_str = snippet_with_context(cx, contains_expr.key.span, contains_expr.call_ctxt, "..", &mut app).0;
86         let sugg = if let Some(else_expr) = else_expr {
87             let else_search = match find_insert_calls(cx, &contains_expr, else_expr) {
88                 Some(search) => search,
89                 None => return,
90             };
91
92             if then_search.edits.is_empty() && else_search.edits.is_empty() {
93                 // No insertions
94                 return;
95             } else if then_search.edits.is_empty() || else_search.edits.is_empty() {
96                 // if .. { insert } else { .. } or if .. { .. } else { insert }
97                 let ((then_str, entry_kind), else_str) = match (else_search.edits.is_empty(), contains_expr.negated) {
98                     (true, true) => (
99                         then_search.snippet_vacant(cx, then_expr.span, &mut app),
100                         snippet_with_applicability(cx, else_expr.span, "{ .. }", &mut app),
101                     ),
102                     (true, false) => (
103                         then_search.snippet_occupied(cx, then_expr.span, &mut app),
104                         snippet_with_applicability(cx, else_expr.span, "{ .. }", &mut app),
105                     ),
106                     (false, true) => (
107                         else_search.snippet_occupied(cx, else_expr.span, &mut app),
108                         snippet_with_applicability(cx, then_expr.span, "{ .. }", &mut app),
109                     ),
110                     (false, false) => (
111                         else_search.snippet_vacant(cx, else_expr.span, &mut app),
112                         snippet_with_applicability(cx, then_expr.span, "{ .. }", &mut app),
113                     ),
114                 };
115                 format!(
116                     "if let {}::{entry_kind} = {map_str}.entry({key_str}) {then_str} else {else_str}",
117                     map_ty.entry_path(),
118                 )
119             } else {
120                 // if .. { insert } else { insert }
121                 let ((then_str, then_entry), (else_str, else_entry)) = if contains_expr.negated {
122                     (
123                         then_search.snippet_vacant(cx, then_expr.span, &mut app),
124                         else_search.snippet_occupied(cx, else_expr.span, &mut app),
125                     )
126                 } else {
127                     (
128                         then_search.snippet_occupied(cx, then_expr.span, &mut app),
129                         else_search.snippet_vacant(cx, else_expr.span, &mut app),
130                     )
131                 };
132                 let indent_str = snippet_indent(cx, expr.span);
133                 let indent_str = indent_str.as_deref().unwrap_or("");
134                 format!(
135                     "match {map_str}.entry({key_str}) {{\n{indent_str}    {entry}::{then_entry} => {}\n\
136                         {indent_str}    {entry}::{else_entry} => {}\n{indent_str}}}",
137                     reindent_multiline(then_str.into(), true, Some(4 + indent_str.len())),
138                     reindent_multiline(else_str.into(), true, Some(4 + indent_str.len())),
139                     entry = map_ty.entry_path(),
140                 )
141             }
142         } else {
143             if then_search.edits.is_empty() {
144                 // no insertions
145                 return;
146             }
147
148             // if .. { insert }
149             if !then_search.allow_insert_closure {
150                 let (body_str, entry_kind) = if contains_expr.negated {
151                     then_search.snippet_vacant(cx, then_expr.span, &mut app)
152                 } else {
153                     then_search.snippet_occupied(cx, then_expr.span, &mut app)
154                 };
155                 format!(
156                     "if let {}::{entry_kind} = {map_str}.entry({key_str}) {body_str}",
157                     map_ty.entry_path(),
158                 )
159             } else if let Some(insertion) = then_search.as_single_insertion() {
160                 let value_str = snippet_with_context(cx, insertion.value.span, then_expr.span.ctxt(), "..", &mut app).0;
161                 if contains_expr.negated {
162                     if insertion.value.can_have_side_effects() {
163                         format!("{map_str}.entry({key_str}).or_insert_with(|| {value_str});")
164                     } else {
165                         format!("{map_str}.entry({key_str}).or_insert({value_str});")
166                     }
167                 } else {
168                     // TODO: suggest using `if let Some(v) = map.get_mut(k) { .. }` here.
169                     // This would need to be a different lint.
170                     return;
171                 }
172             } else {
173                 let block_str = then_search.snippet_closure(cx, then_expr.span, &mut app);
174                 if contains_expr.negated {
175                     format!("{map_str}.entry({key_str}).or_insert_with(|| {block_str});")
176                 } else {
177                     // TODO: suggest using `if let Some(v) = map.get_mut(k) { .. }` here.
178                     // This would need to be a different lint.
179                     return;
180                 }
181             }
182         };
183
184         span_lint_and_sugg(
185             cx,
186             MAP_ENTRY,
187             expr.span,
188             &format!("usage of `contains_key` followed by `insert` on a `{}`", map_ty.name()),
189             "try this",
190             sugg,
191             app,
192         );
193     }
194 }
195
196 #[derive(Clone, Copy)]
197 enum MapType {
198     Hash,
199     BTree,
200 }
201 impl MapType {
202     fn name(self) -> &'static str {
203         match self {
204             Self::Hash => "HashMap",
205             Self::BTree => "BTreeMap",
206         }
207     }
208     fn entry_path(self) -> &'static str {
209         match self {
210             Self::Hash => "std::collections::hash_map::Entry",
211             Self::BTree => "std::collections::btree_map::Entry",
212         }
213     }
214 }
215
216 struct ContainsExpr<'tcx> {
217     negated: bool,
218     map: &'tcx Expr<'tcx>,
219     key: &'tcx Expr<'tcx>,
220     call_ctxt: SyntaxContext,
221 }
222 fn try_parse_contains<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<(MapType, ContainsExpr<'tcx>)> {
223     let mut negated = false;
224     let expr = peel_hir_expr_while(expr, |e| match e.kind {
225         ExprKind::Unary(UnOp::Not, e) => {
226             negated = !negated;
227             Some(e)
228         },
229         _ => None,
230     });
231     match expr.kind {
232         ExprKind::MethodCall(
233             _,
234             map,
235             [
236                 Expr {
237                     kind: ExprKind::AddrOf(_, _, key),
238                     span: key_span,
239                     ..
240                 },
241             ],
242             _,
243         ) if key_span.ctxt() == expr.span.ctxt() => {
244             let id = cx.typeck_results().type_dependent_def_id(expr.hir_id)?;
245             let expr = ContainsExpr {
246                 negated,
247                 map,
248                 key,
249                 call_ctxt: expr.span.ctxt(),
250             };
251             if match_def_path(cx, id, &paths::BTREEMAP_CONTAINS_KEY) {
252                 Some((MapType::BTree, expr))
253             } else if match_def_path(cx, id, &paths::HASHMAP_CONTAINS_KEY) {
254                 Some((MapType::Hash, expr))
255             } else {
256                 None
257             }
258         },
259         _ => None,
260     }
261 }
262
263 struct InsertExpr<'tcx> {
264     map: &'tcx Expr<'tcx>,
265     key: &'tcx Expr<'tcx>,
266     value: &'tcx Expr<'tcx>,
267 }
268 fn try_parse_insert<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<InsertExpr<'tcx>> {
269     if let ExprKind::MethodCall(_, map, [key, value], _) = expr.kind {
270         let id = cx.typeck_results().type_dependent_def_id(expr.hir_id)?;
271         if match_def_path(cx, id, &paths::BTREEMAP_INSERT) || match_def_path(cx, id, &paths::HASHMAP_INSERT) {
272             Some(InsertExpr { map, key, value })
273         } else {
274             None
275         }
276     } else {
277         None
278     }
279 }
280
281 /// An edit that will need to be made to move the expression to use the entry api
282 #[derive(Clone, Copy)]
283 enum Edit<'tcx> {
284     /// A semicolon that needs to be removed. Used to create a closure for `insert_with`.
285     RemoveSemi(Span),
286     /// An insertion into the map.
287     Insertion(Insertion<'tcx>),
288 }
289 impl<'tcx> Edit<'tcx> {
290     fn as_insertion(self) -> Option<Insertion<'tcx>> {
291         if let Self::Insertion(i) = self { Some(i) } else { None }
292     }
293 }
294 #[derive(Clone, Copy)]
295 struct Insertion<'tcx> {
296     call: &'tcx Expr<'tcx>,
297     value: &'tcx Expr<'tcx>,
298 }
299
300 /// This visitor needs to do a multiple things:
301 /// * Find all usages of the map. An insertion can only be made before any other usages of the map.
302 /// * Determine if there's an insertion using the same key. There's no need for the entry api
303 ///   otherwise.
304 /// * Determine if the final statement executed is an insertion. This is needed to use
305 ///   `or_insert_with`.
306 /// * Determine if there's any sub-expression that can't be placed in a closure.
307 /// * Determine if there's only a single insert statement. `or_insert` can be used in this case.
308 #[expect(clippy::struct_excessive_bools)]
309 struct InsertSearcher<'cx, 'tcx> {
310     cx: &'cx LateContext<'tcx>,
311     /// The map expression used in the contains call.
312     map: &'tcx Expr<'tcx>,
313     /// The key expression used in the contains call.
314     key: &'tcx Expr<'tcx>,
315     /// The context of the top level block. All insert calls must be in the same context.
316     ctxt: SyntaxContext,
317     /// Whether this expression can be safely moved into a closure.
318     allow_insert_closure: bool,
319     /// Whether this expression can use the entry api.
320     can_use_entry: bool,
321     /// Whether this expression is the final expression in this code path. This may be a statement.
322     in_tail_pos: bool,
323     // Is this expression a single insert. A slightly better suggestion can be made in this case.
324     is_single_insert: bool,
325     /// If the visitor has seen the map being used.
326     is_map_used: bool,
327     /// The locations where changes need to be made for the suggestion.
328     edits: Vec<Edit<'tcx>>,
329     /// A stack of loops the visitor is currently in.
330     loops: Vec<HirId>,
331     /// Local variables created in the expression. These don't need to be captured.
332     locals: HirIdSet,
333 }
334 impl<'tcx> InsertSearcher<'_, 'tcx> {
335     /// Visit the expression as a branch in control flow. Multiple insert calls can be used, but
336     /// only if they are on separate code paths. This will return whether the map was used in the
337     /// given expression.
338     fn visit_cond_arm(&mut self, e: &'tcx Expr<'_>) -> bool {
339         let is_map_used = self.is_map_used;
340         let in_tail_pos = self.in_tail_pos;
341         self.visit_expr(e);
342         let res = self.is_map_used;
343         self.is_map_used = is_map_used;
344         self.in_tail_pos = in_tail_pos;
345         res
346     }
347
348     /// Visits an expression which is not itself in a tail position, but other sibling expressions
349     /// may be. e.g. if conditions
350     fn visit_non_tail_expr(&mut self, e: &'tcx Expr<'_>) {
351         let in_tail_pos = self.in_tail_pos;
352         self.in_tail_pos = false;
353         self.visit_expr(e);
354         self.in_tail_pos = in_tail_pos;
355     }
356 }
357 impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
358     fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) {
359         match stmt.kind {
360             StmtKind::Semi(e) => {
361                 self.visit_expr(e);
362
363                 if self.in_tail_pos && self.allow_insert_closure {
364                     // The spans are used to slice the top level expression into multiple parts. This requires that
365                     // they all come from the same part of the source code.
366                     if stmt.span.ctxt() == self.ctxt && e.span.ctxt() == self.ctxt {
367                         self.edits
368                             .push(Edit::RemoveSemi(stmt.span.trim_start(e.span).unwrap_or(DUMMY_SP)));
369                     } else {
370                         self.allow_insert_closure = false;
371                     }
372                 }
373             },
374             StmtKind::Expr(e) => self.visit_expr(e),
375             StmtKind::Local(l) => {
376                 self.visit_pat(l.pat);
377                 if let Some(e) = l.init {
378                     self.allow_insert_closure &= !self.in_tail_pos;
379                     self.in_tail_pos = false;
380                     self.is_single_insert = false;
381                     self.visit_expr(e);
382                 }
383             },
384             StmtKind::Item(_) => {
385                 self.allow_insert_closure &= !self.in_tail_pos;
386                 self.is_single_insert = false;
387             },
388         }
389     }
390
391     fn visit_block(&mut self, block: &'tcx Block<'_>) {
392         // If the block is in a tail position, then the last expression (possibly a statement) is in the
393         // tail position. The rest, however, are not.
394         match (block.stmts, block.expr) {
395             ([], None) => {
396                 self.allow_insert_closure &= !self.in_tail_pos;
397             },
398             ([], Some(expr)) => self.visit_expr(expr),
399             (stmts, Some(expr)) => {
400                 let in_tail_pos = self.in_tail_pos;
401                 self.in_tail_pos = false;
402                 for stmt in stmts {
403                     self.visit_stmt(stmt);
404                 }
405                 self.in_tail_pos = in_tail_pos;
406                 self.visit_expr(expr);
407             },
408             ([stmts @ .., stmt], None) => {
409                 let in_tail_pos = self.in_tail_pos;
410                 self.in_tail_pos = false;
411                 for stmt in stmts {
412                     self.visit_stmt(stmt);
413                 }
414                 self.in_tail_pos = in_tail_pos;
415                 self.visit_stmt(stmt);
416             },
417         }
418     }
419
420     fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
421         if !self.can_use_entry {
422             return;
423         }
424
425         match try_parse_insert(self.cx, expr) {
426             Some(insert_expr) if SpanlessEq::new(self.cx).eq_expr(self.map, insert_expr.map) => {
427                 // Multiple inserts, inserts with a different key, and inserts from a macro can't use the entry api.
428                 if self.is_map_used
429                     || !SpanlessEq::new(self.cx).eq_expr(self.key, insert_expr.key)
430                     || expr.span.ctxt() != self.ctxt
431                 {
432                     self.can_use_entry = false;
433                     return;
434                 }
435
436                 self.edits.push(Edit::Insertion(Insertion {
437                     call: expr,
438                     value: insert_expr.value,
439                 }));
440                 self.is_map_used = true;
441                 self.allow_insert_closure &= self.in_tail_pos;
442
443                 // The value doesn't affect whether there is only a single insert expression.
444                 let is_single_insert = self.is_single_insert;
445                 self.visit_non_tail_expr(insert_expr.value);
446                 self.is_single_insert = is_single_insert;
447             },
448             _ if SpanlessEq::new(self.cx).eq_expr(self.map, expr) => {
449                 self.is_map_used = true;
450             },
451             _ => match expr.kind {
452                 ExprKind::If(cond_expr, then_expr, Some(else_expr)) => {
453                     self.is_single_insert = false;
454                     self.visit_non_tail_expr(cond_expr);
455                     // Each branch may contain it's own insert expression.
456                     let mut is_map_used = self.visit_cond_arm(then_expr);
457                     is_map_used |= self.visit_cond_arm(else_expr);
458                     self.is_map_used = is_map_used;
459                 },
460                 ExprKind::Match(scrutinee_expr, arms, _) => {
461                     self.is_single_insert = false;
462                     self.visit_non_tail_expr(scrutinee_expr);
463                     // Each branch may contain it's own insert expression.
464                     let mut is_map_used = self.is_map_used;
465                     for arm in arms {
466                         self.visit_pat(arm.pat);
467                         if let Some(Guard::If(guard) | Guard::IfLet(&Let { init: guard, .. })) = arm.guard {
468                             self.visit_non_tail_expr(guard);
469                         }
470                         is_map_used |= self.visit_cond_arm(arm.body);
471                     }
472                     self.is_map_used = is_map_used;
473                 },
474                 ExprKind::Loop(block, ..) => {
475                     self.loops.push(expr.hir_id);
476                     self.is_single_insert = false;
477                     self.allow_insert_closure &= !self.in_tail_pos;
478                     // Don't allow insertions inside of a loop.
479                     let edit_len = self.edits.len();
480                     self.visit_block(block);
481                     if self.edits.len() != edit_len {
482                         self.can_use_entry = false;
483                     }
484                     self.loops.pop();
485                 },
486                 ExprKind::Block(block, _) => self.visit_block(block),
487                 ExprKind::InlineAsm(_) => {
488                     self.can_use_entry = false;
489                 },
490                 _ => {
491                     self.allow_insert_closure &= !self.in_tail_pos;
492                     self.allow_insert_closure &=
493                         can_move_expr_to_closure_no_visit(self.cx, expr, &self.loops, &self.locals);
494                     // Sub expressions are no longer in the tail position.
495                     self.is_single_insert = false;
496                     self.in_tail_pos = false;
497                     walk_expr(self, expr);
498                 },
499             },
500         }
501     }
502
503     fn visit_pat(&mut self, p: &'tcx Pat<'tcx>) {
504         p.each_binding_or_first(&mut |_, id, _, _| {
505             self.locals.insert(id);
506         });
507     }
508 }
509
510 struct InsertSearchResults<'tcx> {
511     edits: Vec<Edit<'tcx>>,
512     allow_insert_closure: bool,
513     is_single_insert: bool,
514 }
515 impl<'tcx> InsertSearchResults<'tcx> {
516     fn as_single_insertion(&self) -> Option<Insertion<'tcx>> {
517         self.is_single_insert.then(|| self.edits[0].as_insertion().unwrap())
518     }
519
520     fn snippet(
521         &self,
522         cx: &LateContext<'_>,
523         mut span: Span,
524         app: &mut Applicability,
525         write_wrapped: impl Fn(&mut String, Insertion<'_>, SyntaxContext, &mut Applicability),
526     ) -> String {
527         let ctxt = span.ctxt();
528         let mut res = String::new();
529         for insertion in self.edits.iter().filter_map(|e| e.as_insertion()) {
530             res.push_str(&snippet_with_applicability(
531                 cx,
532                 span.until(insertion.call.span),
533                 "..",
534                 app,
535             ));
536             if is_expr_used_or_unified(cx.tcx, insertion.call) {
537                 write_wrapped(&mut res, insertion, ctxt, app);
538             } else {
539                 let _ = write!(
540                     res,
541                     "e.insert({})",
542                     snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0
543                 );
544             }
545             span = span.trim_start(insertion.call.span).unwrap_or(DUMMY_SP);
546         }
547         res.push_str(&snippet_with_applicability(cx, span, "..", app));
548         res
549     }
550
551     fn snippet_occupied(&self, cx: &LateContext<'_>, span: Span, app: &mut Applicability) -> (String, &'static str) {
552         (
553             self.snippet(cx, span, app, |res, insertion, ctxt, app| {
554                 // Insertion into a map would return `Some(&mut value)`, but the entry returns `&mut value`
555                 let _ = write!(
556                     res,
557                     "Some(e.insert({}))",
558                     snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0
559                 );
560             }),
561             "Occupied(mut e)",
562         )
563     }
564
565     fn snippet_vacant(&self, cx: &LateContext<'_>, span: Span, app: &mut Applicability) -> (String, &'static str) {
566         (
567             self.snippet(cx, span, app, |res, insertion, ctxt, app| {
568                 // Insertion into a map would return `None`, but the entry returns a mutable reference.
569                 let _ = if is_expr_final_block_expr(cx.tcx, insertion.call) {
570                     write!(
571                         res,
572                         "e.insert({});\n{}None",
573                         snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0,
574                         snippet_indent(cx, insertion.call.span).as_deref().unwrap_or(""),
575                     )
576                 } else {
577                     write!(
578                         res,
579                         "{{ e.insert({}); None }}",
580                         snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0,
581                     )
582                 };
583             }),
584             "Vacant(e)",
585         )
586     }
587
588     fn snippet_closure(&self, cx: &LateContext<'_>, mut span: Span, app: &mut Applicability) -> String {
589         let ctxt = span.ctxt();
590         let mut res = String::new();
591         for edit in &self.edits {
592             match *edit {
593                 Edit::Insertion(insertion) => {
594                     // Cut out the value from `map.insert(key, value)`
595                     res.push_str(&snippet_with_applicability(
596                         cx,
597                         span.until(insertion.call.span),
598                         "..",
599                         app,
600                     ));
601                     res.push_str(&snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0);
602                     span = span.trim_start(insertion.call.span).unwrap_or(DUMMY_SP);
603                 },
604                 Edit::RemoveSemi(semi_span) => {
605                     // Cut out the semicolon. This allows the value to be returned from the closure.
606                     res.push_str(&snippet_with_applicability(cx, span.until(semi_span), "..", app));
607                     span = span.trim_start(semi_span).unwrap_or(DUMMY_SP);
608                 },
609             }
610         }
611         res.push_str(&snippet_with_applicability(cx, span, "..", app));
612         res
613     }
614 }
615
616 fn find_insert_calls<'tcx>(
617     cx: &LateContext<'tcx>,
618     contains_expr: &ContainsExpr<'tcx>,
619     expr: &'tcx Expr<'_>,
620 ) -> Option<InsertSearchResults<'tcx>> {
621     let mut s = InsertSearcher {
622         cx,
623         map: contains_expr.map,
624         key: contains_expr.key,
625         ctxt: expr.span.ctxt(),
626         edits: Vec::new(),
627         is_map_used: false,
628         allow_insert_closure: true,
629         can_use_entry: true,
630         in_tail_pos: true,
631         is_single_insert: true,
632         loops: Vec::new(),
633         locals: HirIdSet::default(),
634     };
635     s.visit_expr(expr);
636     let allow_insert_closure = s.allow_insert_closure;
637     let is_single_insert = s.is_single_insert;
638     let edits = s.edits;
639     s.can_use_entry.then_some(InsertSearchResults {
640         edits,
641         allow_insert_closure,
642         is_single_insert,
643     })
644 }