Skip to content

Commit e3cd56a

Browse files
committed
map_entry improvements
Suggest using `or_insert_with` when possible
1 parent 1a2bfb0 commit e3cd56a

File tree

6 files changed

+325
-149
lines changed

6 files changed

+325
-149
lines changed

clippy_lints/src/entry.rs

+173-47
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clippy_utils::{
2+
can_move_expr_to_closure_no_visit,
23
diagnostics::span_lint_and_sugg,
34
is_expr_final_block_expr, is_expr_used_or_unified, match_def_path, paths, peel_hir_expr_while,
45
source::{reindent_multiline, snippet_indent, snippet_with_applicability, snippet_with_context},
@@ -7,7 +8,7 @@ use clippy_utils::{
78
use rustc_errors::Applicability;
89
use rustc_hir::{
910
intravisit::{walk_expr, ErasedMap, NestedVisitorMap, Visitor},
10-
Expr, ExprKind, Guard, Local, Stmt, StmtKind, UnOp,
11+
Block, Expr, ExprKind, Guard, HirId, Local, Stmt, StmtKind, UnOp,
1112
};
1213
use rustc_lint::{LateContext, LateLintPass};
1314
use rustc_session::{declare_lint_pass, declare_tool_lint};
@@ -78,13 +79,13 @@ impl<'tcx> LateLintPass<'tcx> for HashMapPass {
7879
let sugg = if let Some(else_expr) = else_expr {
7980
// if .. { .. } else { .. }
8081
let else_search = match find_insert_calls(cx, &contains_expr, else_expr) {
81-
Some(search) if !(then_search.insertions.is_empty() && search.insertions.is_empty()) => search,
82+
Some(search) if !(then_search.edits.is_empty() && search.edits.is_empty()) => search,
8283
_ => return,
8384
};
8485

85-
if then_search.insertions.is_empty() || else_search.insertions.is_empty() {
86+
if then_search.edits.is_empty() || else_search.edits.is_empty() {
8687
// if .. { insert } else { .. } or if .. { .. } else { then } of
87-
let (then_str, else_str, entry_kind) = if else_search.insertions.is_empty() {
88+
let (then_str, else_str, entry_kind) = if else_search.edits.is_empty() {
8889
if contains_expr.negated {
8990
(
9091
then_search.snippet_vacant(cx, then_expr.span, &mut app),
@@ -152,37 +153,48 @@ impl<'tcx> LateLintPass<'tcx> for HashMapPass {
152153
indent = indent_str,
153154
)
154155
}
155-
} else if then_search.insertions.is_empty() || !contains_expr.negated {
156+
} else if then_search.edits.is_empty() {
157+
// no insertions
156158
return;
157159
} else {
158160
// if .. { insert }
159-
match then_search.as_single_insertion() {
160-
Some(insertion) if !insertion.value.can_have_side_effects() => {
161-
format!(
162-
"{}.entry({}).or_insert({});",
163-
map_str,
164-
key_str,
165-
snippet_with_context(cx, insertion.value.span, insertion.call.span.ctxt(), "..", &mut app).0,
161+
if !then_search.allow_insert_closure {
162+
let (body_str, entry_kind) = if contains_expr.negated {
163+
(then_search.snippet_vacant(cx, then_expr.span, &mut app), "Vacant(e)")
164+
} else {
165+
(
166+
then_search.snippet_occupied(cx, then_expr.span, &mut app),
167+
"Occupied(mut e)",
166168
)
167-
},
168-
_ => {
169-
let (body_str, entry_kind) = if contains_expr.negated {
170-
(then_search.snippet_vacant(cx, then_expr.span, &mut app), "Vacant(e)")
169+
};
170+
format!(
171+
"if let {}::{} = {}.entry({}) {}",
172+
map_ty.entry_path(),
173+
entry_kind,
174+
map_str,
175+
key_str,
176+
body_str,
177+
)
178+
} else if let Some(insertion) = then_search.as_single_insertion() {
179+
let value_str = snippet_with_context(cx, insertion.value.span, then_expr.span.ctxt(), "..", &mut app).0;
180+
if contains_expr.negated {
181+
if insertion.value.can_have_side_effects() {
182+
format!("{}.entry({}).or_insert_with(|| {});", map_str, key_str, value_str)
171183
} else {
172-
(
173-
then_search.snippet_occupied(cx, then_expr.span, &mut app),
174-
"Occupied(mut e)",
175-
)
176-
};
177-
format!(
178-
"if let {}::{} = {}.entry({}) {}",
179-
map_ty.entry_path(),
180-
entry_kind,
181-
map_str,
182-
key_str,
183-
body_str,
184-
)
185-
},
184+
format!("{}.entry({}).or_insert({});", map_str, key_str, value_str)
185+
}
186+
} else {
187+
// Todo: if let Some(v) = map.get_mut(k)
188+
return;
189+
}
190+
} else {
191+
let block_str = then_search.snippet_closure(cx, then_expr.span, &mut app);
192+
if contains_expr.negated {
193+
format!("{}.entry({}).or_insert_with(|| {});", map_str, key_str, block_str)
194+
} else {
195+
// Todo: if let Some(v) = map.get_mut(k)
196+
return;
197+
}
186198
}
187199
};
188200

@@ -281,6 +293,19 @@ fn try_parse_insert(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<Inse
281293
}
282294
}
283295

296+
/// An edit that will need to be made to move the expression to use the entry api
297+
#[derive(Clone, Copy)]
298+
enum Edit<'tcx> {
299+
/// A semicolon that needs to be removed. Used to create a closure for `insert_with`.
300+
RemoveSemi(Span),
301+
/// An insertion into the map.
302+
Insertion(Insertion<'tcx>),
303+
}
304+
impl Edit<'tcx> {
305+
fn as_insertion(self) -> Option<Insertion<'tcx>> {
306+
if let Self::Insertion(i) = self { Some(i) } else { None }
307+
}
308+
}
284309
#[derive(Clone, Copy)]
285310
struct Insertion<'tcx> {
286311
call: &'tcx Expr<'tcx>,
@@ -303,24 +328,40 @@ struct InsertSearcher<'cx, 'i, 'tcx> {
303328
key: &'tcx Expr<'tcx>,
304329
/// The context of the top level block. All insert calls must be in the same context.
305330
ctxt: SyntaxContext,
331+
/// Whether this expression can be safely moved into a closure.
332+
allow_insert_closure: bool,
306333
/// Whether this expression can use the entry api.
307334
can_use_entry: bool,
335+
/// Whether this expression is the final expression in this code path. This may be a statement.
336+
in_tail_pos: bool,
308337
// A single insert expression has a slightly different suggestion.
309338
is_single_insert: bool,
310339
is_map_used: bool,
311-
insertions: &'i mut Vec<Insertion<'tcx>>,
340+
edits: &'i mut Vec<Edit<'tcx>>,
341+
loops: Vec<HirId>,
312342
}
313343
impl<'tcx> InsertSearcher<'_, '_, 'tcx> {
314344
/// Visit the expression as a branch in control flow. Multiple insert calls can be used, but
315345
/// only if they are on separate code paths. This will return whether the map was used in the
316346
/// given expression.
317347
fn visit_cond_arm(&mut self, e: &'tcx Expr<'_>) -> bool {
318348
let is_map_used = self.is_map_used;
349+
let in_tail_pos = self.in_tail_pos;
319350
self.visit_expr(e);
320351
let res = self.is_map_used;
321352
self.is_map_used = is_map_used;
353+
self.in_tail_pos = in_tail_pos;
322354
res
323355
}
356+
357+
/// Visits an expression which is not itself in a tail position, but other sibling expressions
358+
/// may be. e.g. if conditions
359+
fn visit_non_tail_expr(&mut self, e: &'tcx Expr<'_>) {
360+
let in_tail_pos = self.in_tail_pos;
361+
self.in_tail_pos = false;
362+
self.visit_expr(e);
363+
self.in_tail_pos = in_tail_pos;
364+
}
324365
}
325366
impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, '_, 'tcx> {
326367
type Map = ErasedMap<'tcx>;
@@ -330,17 +371,63 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, '_, 'tcx> {
330371

331372
fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) {
332373
match stmt.kind {
333-
StmtKind::Semi(e) | StmtKind::Expr(e) => self.visit_expr(e),
374+
StmtKind::Semi(e) => {
375+
self.visit_expr(e);
376+
377+
if self.in_tail_pos && self.allow_insert_closure {
378+
// The spans are used to slice the top level expression into multiple parts. This requires that
379+
// they all come from the same part of the source code.
380+
if stmt.span.ctxt() == self.ctxt && e.span.ctxt() == self.ctxt {
381+
self.edits
382+
.push(Edit::RemoveSemi(stmt.span.trim_start(e.span).unwrap_or(DUMMY_SP)));
383+
} else {
384+
self.allow_insert_closure = false;
385+
}
386+
}
387+
},
388+
StmtKind::Expr(e) => self.visit_expr(e),
334389
StmtKind::Local(Local { init: Some(e), .. }) => {
390+
self.allow_insert_closure &= !self.in_tail_pos;
391+
self.in_tail_pos = false;
335392
self.is_single_insert = false;
336393
self.visit_expr(e);
337394
},
338395
_ => {
396+
self.allow_insert_closure &= !self.in_tail_pos;
339397
self.is_single_insert = false;
340398
},
341399
}
342400
}
343401

402+
fn visit_block(&mut self, block: &'tcx Block<'_>) {
403+
// If the block is in a tail position, then the last expression (possibly a statement) is in the
404+
// tail position. The rest, however, are not.
405+
match (block.stmts, block.expr) {
406+
([], None) => {
407+
self.allow_insert_closure &= !self.in_tail_pos;
408+
},
409+
([], Some(expr)) => self.visit_expr(expr),
410+
(stmts, Some(expr)) => {
411+
let in_tail_pos = self.in_tail_pos;
412+
self.in_tail_pos = false;
413+
for stmt in stmts {
414+
self.visit_stmt(stmt);
415+
}
416+
self.in_tail_pos = in_tail_pos;
417+
self.visit_expr(expr);
418+
},
419+
([stmts @ .., stmt], None) => {
420+
let in_tail_pos = self.in_tail_pos;
421+
self.in_tail_pos = false;
422+
for stmt in stmts {
423+
self.visit_stmt(stmt);
424+
}
425+
self.in_tail_pos = in_tail_pos;
426+
self.visit_stmt(stmt);
427+
},
428+
}
429+
}
430+
344431
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
345432
if !self.can_use_entry {
346433
return;
@@ -357,15 +444,16 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, '_, 'tcx> {
357444
return;
358445
}
359446

360-
self.insertions.push(Insertion {
447+
self.edits.push(Edit::Insertion(Insertion {
361448
call: expr,
362449
value: insert_expr.value,
363-
});
450+
}));
364451
self.is_map_used = true;
452+
self.allow_insert_closure &= self.in_tail_pos;
365453

366454
// The value doesn't affect whether there is only a single insert expression.
367455
let is_single_insert = self.is_single_insert;
368-
self.visit_expr(insert_expr.value);
456+
self.visit_non_tail_expr(insert_expr.value);
369457
self.is_single_insert = is_single_insert;
370458
},
371459
_ if SpanlessEq::new(self.cx).eq_expr(self.map, expr) => {
@@ -374,39 +462,46 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, '_, 'tcx> {
374462
_ => match expr.kind {
375463
ExprKind::If(cond_expr, then_expr, Some(else_expr)) => {
376464
self.is_single_insert = false;
377-
self.visit_expr(cond_expr);
465+
self.visit_non_tail_expr(cond_expr);
378466
// Each branch may contain it's own insert expression.
379467
let mut is_map_used = self.visit_cond_arm(then_expr);
380468
is_map_used |= self.visit_cond_arm(else_expr);
381469
self.is_map_used = is_map_used;
382470
},
383471
ExprKind::Match(scrutinee_expr, arms, _) => {
384472
self.is_single_insert = false;
385-
self.visit_expr(scrutinee_expr);
473+
self.visit_non_tail_expr(scrutinee_expr);
386474
// Each branch may contain it's own insert expression.
387475
let mut is_map_used = self.is_map_used;
388476
for arm in arms {
389477
if let Some(Guard::If(guard) | Guard::IfLet(_, guard)) = arm.guard {
390-
self.visit_expr(guard)
478+
self.visit_non_tail_expr(guard)
391479
}
392480
is_map_used |= self.visit_cond_arm(arm.body);
393481
}
394482
self.is_map_used = is_map_used;
395483
},
396484
ExprKind::Loop(block, ..) => {
485+
self.loops.push(expr.hir_id);
486+
self.allow_insert_closure &= !self.in_tail_pos;
397487
// Don't allow insertions inside of a loop.
398-
let insertions_len = self.insertions.len();
488+
let edit_len = self.edits.len();
399489
self.visit_block(block);
400-
if self.insertions.len() != insertions_len {
490+
if self.edits.len() != edit_len {
401491
self.can_use_entry = false;
402492
}
493+
self.loops.pop();
403494
},
404495
ExprKind::Block(block, _) => self.visit_block(block),
405496
ExprKind::InlineAsm(_) | ExprKind::LlvmInlineAsm(_) => {
406497
self.can_use_entry = false;
407498
},
408499
_ => {
500+
self.allow_insert_closure &= !self.in_tail_pos;
501+
self.allow_insert_closure &= can_move_expr_to_closure_no_visit(self.cx, expr, &self.loops);
502+
// Sub expressions are no longer in the tail position.
409503
self.is_single_insert = false;
504+
self.in_tail_pos = false;
410505
walk_expr(self, expr);
411506
},
412507
},
@@ -415,18 +510,19 @@ impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, '_, 'tcx> {
415510
}
416511

417512
struct InsertSearchResults<'tcx> {
418-
insertions: Vec<Insertion<'tcx>>,
513+
edits: Vec<Edit<'tcx>>,
514+
allow_insert_closure: bool,
419515
is_single_insert: bool,
420516
}
421517
impl InsertSearchResults<'tcx> {
422518
fn as_single_insertion(&self) -> Option<Insertion<'tcx>> {
423-
self.is_single_insert.then(|| self.insertions[0])
519+
self.is_single_insert.then(|| self.edits[0].as_insertion().unwrap())
424520
}
425521

426522
fn snippet_occupied(&self, cx: &LateContext<'_>, mut span: Span, app: &mut Applicability) -> String {
427523
let ctxt = span.ctxt();
428524
let mut res = String::new();
429-
for insertion in self.insertions.iter() {
525+
for insertion in self.edits.iter().filter_map(|e| e.as_insertion()) {
430526
res.push_str(&snippet_with_applicability(
431527
cx,
432528
span.until(insertion.call.span),
@@ -451,7 +547,7 @@ impl InsertSearchResults<'tcx> {
451547
fn snippet_vacant(&self, cx: &LateContext<'_>, mut span: Span, app: &mut Applicability) -> String {
452548
let ctxt = span.ctxt();
453549
let mut res = String::new();
454-
for insertion in self.insertions.iter() {
550+
for insertion in self.edits.iter().filter_map(|e| e.as_insertion()) {
455551
res.push_str(&snippet_with_applicability(
456552
cx,
457553
span.until(insertion.call.span),
@@ -485,27 +581,57 @@ impl InsertSearchResults<'tcx> {
485581
res.push_str(&snippet_with_applicability(cx, span, "..", app));
486582
res
487583
}
584+
585+
fn snippet_closure(&self, cx: &LateContext<'_>, mut span: Span, app: &mut Applicability) -> String {
586+
let ctxt = span.ctxt();
587+
let mut res = String::new();
588+
for edit in &self.edits {
589+
match *edit {
590+
Edit::Insertion(insertion) => {
591+
res.push_str(&snippet_with_applicability(
592+
cx,
593+
span.until(insertion.call.span),
594+
"..",
595+
app,
596+
));
597+
res.push_str(&snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0);
598+
span = span.trim_start(insertion.call.span).unwrap_or(DUMMY_SP);
599+
},
600+
Edit::RemoveSemi(semi_span) => {
601+
res.push_str(&snippet_with_applicability(cx, span.until(semi_span), "..", app));
602+
span = span.trim_start(semi_span).unwrap_or(DUMMY_SP);
603+
},
604+
}
605+
}
606+
res.push_str(&snippet_with_applicability(cx, span, "..", app));
607+
res
608+
}
488609
}
489610
fn find_insert_calls(
490611
cx: &LateContext<'tcx>,
491612
contains_expr: &ContainsExpr<'tcx>,
492613
expr: &'tcx Expr<'_>,
493614
) -> Option<InsertSearchResults<'tcx>> {
494-
let mut insertions = Vec::new();
615+
let mut edits = Vec::new();
495616
let mut s = InsertSearcher {
496617
cx,
497618
map: contains_expr.map,
498619
key: contains_expr.key,
499620
ctxt: expr.span.ctxt(),
500-
insertions: &mut insertions,
621+
edits: &mut edits,
501622
is_map_used: false,
623+
allow_insert_closure: true,
502624
can_use_entry: true,
625+
in_tail_pos: true,
503626
is_single_insert: true,
627+
loops: Vec::new(),
504628
};
505629
s.visit_expr(expr);
630+
let allow_insert_closure = s.allow_insert_closure;
506631
let is_single_insert = s.is_single_insert;
507632
s.can_use_entry.then(|| InsertSearchResults {
508-
insertions,
633+
edits,
634+
allow_insert_closure,
509635
is_single_insert,
510636
})
511637
}

0 commit comments

Comments
 (0)