lints: Add map_lookup_then_insert lint (#62681)

## Objective

Sometimes we can do a hash lookup twice, which is slow and unidiomatic.
We should use the Entry API instead.

## Solution

Add another `dylint` lint called `map_lookup_then_insert` that catches
those cases.

This works with HashMap, BTreeMap, HashSet, BTreeSet and
entry-compatible ADTs.

## What it catches

See the examples in the `tooling/lints/ui/map_lookup_then_insert.rs`.

It's mostly things of the form

```rust
if let Some(value) = map.get(&key) {
    *value
} else {
    map.insert(key, 1);
    1
}
```

which should be 

```rust
*map.entry(key).or_insert(1)
```

The two key expressions are matched up to borrows/derefs,
value-preserving
conversions (`clone`, `to_owned`, `to_string` on string types, `to_vec`
on
slices, `String::from` on `&str` — each resolved by definition, not by
name),
and immutable local aliases. So looking up `&key` and inserting
`key.clone()`
is caught too. When a conversion had to be peeled, the diagnostic
carries a
note that an entry-API rewrite would perform that copy on hits as well
as
misses.

## What it deliberately does not catch

Forms Clippy already catches.

## Findings

A `--workspace` run finds 31 warnings at 30 unique sites in `zed`:
the 16 the plain key matching finds, plus 15 more that require the
conversion/alias matching (e.g. `tiles_by_key.get(key)` followed by
`tiles_by_key.insert(key.clone(), tile)` in `metal_atlas.rs`).

Will post those fixes in a follow up PR.

## Testing

UI tests cover every `Entry` method positively and negatively, BTreeMap
mirrors, set cases, and the Clippy-deferral boundary. The
key-normalization logic is covered by nine positive cases (cloned keys,
UFCS forms, `to_owned`/`to_string`/`to_vec`/`String::from`, deref
layers, local aliases, sets) and six negative cases (inherent methods
merely named `clone`/`to_owned`, mutated aliases, a clone of a different
key, `to_string` on a non-string key, and side-effecting initializers).

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


Release Notes:

- N/A
This commit is contained in:
Miguel Raz Guzmán Macedo authored and GitHub committed 2026-08-28 20:01:28 +00:00
1 parent 47df871ecc
commit 81299967df
5 files changed
+1387

No files matched your search

+1
View File
@@ -25,6 +25,7 @@ picks them up automatically when run from that directory.
- `shared_string_from_str_literal` — `SharedString::new/from` etc where `SharedString::from_static` should be used instead.
- `async_block_without_await` — `async { … }` blocks whose body contains no `.await` expression.
- `entity_update_in_render` — `Entity::update`/`WeakEntity::update` mutating an entity inside `Render::render`.
- `map_lookup_then_insert` — map value lookups that branch into an insertion for the same key, on any map with an `entry` method (`HashMap`, `BTreeMap`, `FxHashMap`, `hashbrown`, `indexmap`, ...), and the same pattern on `HashSet`/`BTreeSet` (where a bare `insert` already searches only once). Keys are matched up to borrows/derefs, value-preserving conversions (`clone`, `to_owned`, `to_string` on string types, `to_vec` on slices, `String::from` on `&str` — each resolved by definition, not name), and immutable local aliases, so looking up `&key` and inserting `key.clone()` is caught.
- `notify_in_render` — `Context::notify()` called inside `Render::render`.
- `owned_string_into_shared` — `String::from(<lit>).into()` / `<lit>.to_string().into()` / `<lit>.to_owned().into()` whose target is `SharedString`, `Arc<str>`, `Rc<str>`, or `Cow<'_, str>`.
- `blocking_io_on_foreground` - Catch blocking IO calls that are called on the main thread (but not on closures or background threads)
+4
View File
@@ -28,12 +28,14 @@ use rustc_span::Span;
mod blocking_io_on_foreground;
mod entity_update_in_render;
mod map_lookup_then_insert;
mod notify_in_render;
mod owned_string_into_shared;
mod render_helpers;
use blocking_io_on_foreground::BLOCKING_IO_ON_FOREGROUND;
use entity_update_in_render::ENTITY_UPDATE_IN_RENDER;
use map_lookup_then_insert::MAP_LOOKUP_THEN_INSERT;
use notify_in_render::NOTIFY_IN_RENDER;
use owned_string_into_shared::OWNED_STRING_INTO_SHARED;
@@ -54,6 +56,7 @@ pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint
ASYNC_BLOCK_WITHOUT_AWAIT,
BLOCKING_IO_ON_FOREGROUND,
ENTITY_UPDATE_IN_RENDER,
MAP_LOOKUP_THEN_INSERT,
NOTIFY_IN_RENDER,
OWNED_STRING_INTO_SHARED,
]);
@@ -61,6 +64,7 @@ pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint
lint_store.register_late_pass(|_| Box::new(AsyncBlockWithoutAwait));
lint_store.register_late_pass(|_| Box::new(blocking_io_on_foreground::BlockingIoOnForeground));
lint_store.register_late_pass(|_| Box::new(entity_update_in_render::EntityUpdateInRender));
lint_store.register_late_pass(|_| Box::new(map_lookup_then_insert::MapLookupThenInsert));
lint_store.register_late_pass(|_| Box::new(notify_in_render::NotifyInRender));
lint_store.register_late_pass(|_| Box::new(owned_string_into_shared::OwnedStringIntoShared));
}
+450
View File
@@ -0,0 +1,450 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::visitors::{Visitable, for_each_expr_without_closures};
use clippy_utils::{SpanlessEq, expr_or_init, sym};
use rustc_hir::def_id::DefId;
use rustc_hir::{Expr, ExprKind, LangItem, LetStmt, MatchSource, UnOp};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::Ty;
use rustc_span::Symbol;
use std::ops::ControlFlow;
rustc_session::declare_lint! {
/// ### What it does
///
/// Flags control flow that branches on map value lookup methods and then
/// inserts into the same map with the same key.
///
/// The lint applies to any map type with an inherent `entry` method: std's
/// `HashMap` and `BTreeMap`, hasher aliases such as `FxHashMap`, and
/// third-party maps such as `hashbrown::HashMap` or `indexmap::IndexMap`.
///
/// The lint covers direct `match`, `if let`, `is_some`/`is_none`, and
/// `let ... else` forms using `get`, `get_mut`, or `get_key_value`.
///
/// Keys are compared after stripping borrows and derefs, peeling
/// conversions that yield an owned key equal to the original (`clone`,
/// `to_owned`, `to_string` on string types, `to_vec` on slices, and
/// `String::from` on `&str`), and resolving immutable local aliases to
/// their initializers. So looking up `&key` and inserting `key.clone()`
/// is recognized as the same key. Each peeled conversion is resolved by
/// definition, not by name, so an unrelated user method that happens to
/// be called `clone` does not count.
///
/// When such a conversion is peeled, the diagnostic carries a note: the
/// entry API takes the key by value up front, so a conversion the
/// original code performs only on the miss path would run on every call.
///
/// The same pattern on `HashSet` and `BTreeSet` is also flagged. Sets have
/// no stable entry API, and need none: `insert` alone searches once, skips
/// duplicates, and returns `false` when the value was already present.
///
/// Forms that Clippy already rewrites are excluded. `contains_key` or
/// `contains` followed by `insert` is `map_entry` or
/// `set_contains_or_insert`, and `get(..).is_some()`/`is_none()` on the
/// std collections is `unnecessary_get_then_check`, whose `contains_key`/
/// `contains` suggestion feeds the former two.
///
/// ### Why is this bad?
///
/// The lookup searches the map once and `insert` searches it again. The
/// map's entry API can reuse the result of one search.
pub MAP_LOOKUP_THEN_INSERT,
Warn,
"a map value lookup is followed by insertion for the same key"
}
pub(crate) struct MapLookupThenInsert;
rustc_session::impl_lint_pass!(MapLookupThenInsert => [MAP_LOOKUP_THEN_INSERT]);
impl<'tcx> LateLintPass<'tcx> for MapLookupThenInsert {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
if expr.span.from_expansion() {
return;
}
let repeated_lookup = match &expr.kind {
ExprKind::If(condition, then_expression, else_expression) => {
lookup_from_condition(cx, condition).and_then(|lookup| {
find_matching_insert(cx, lookup, *then_expression)
.or_else(|| {
else_expression
.and_then(|expression| find_matching_insert(cx, lookup, expression))
})
.map(|key_converted| (lookup, key_converted))
})
}
ExprKind::Match(scrutinee, arms, MatchSource::Normal) => parse_lookup(cx, scrutinee)
.and_then(|lookup| {
arms.iter()
.find_map(|arm| find_matching_insert(cx, lookup, arm.body))
.map(|key_converted| (lookup, key_converted))
}),
_ => None,
};
if let Some((lookup, key_converted)) = repeated_lookup {
emit_lint(cx, expr.span, lookup, key_converted);
}
}
fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx LetStmt<'tcx>) {
if local.span.from_expansion() {
return;
}
let Some(init) = local.init else {
return;
};
let Some(else_block) = local.els else {
return;
};
let Some(lookup) = parse_lookup(cx, init) else {
return;
};
if let Some(key_converted) = find_matching_insert(cx, lookup, else_block) {
emit_lint(cx, local.span, lookup, key_converted);
}
}
}
#[derive(Clone, Copy)]
struct Lookup<'tcx> {
map: &'tcx Expr<'tcx>,
key: &'tcx Expr<'tcx>,
}
fn lookup_from_condition<'tcx>(
cx: &LateContext<'tcx>,
condition: &'tcx Expr<'tcx>,
) -> Option<Lookup<'tcx>> {
match &condition.kind {
ExprKind::Let(let_expression) => parse_lookup(cx, let_expression.init),
ExprKind::MethodCall(segment, receiver, [], _)
if matches!(segment.ident.name.as_str(), "is_some" | "is_none")
&& !clippy_covers_get_then_check(cx, receiver) =>
{
parse_lookup(cx, receiver)
}
_ => None,
}
}
fn parse_lookup<'tcx>(
cx: &LateContext<'tcx>,
expression: &'tcx Expr<'tcx>,
) -> Option<Lookup<'tcx>> {
let expression = match &expression.kind {
ExprKind::MethodCall(segment, receiver, [], _)
if matches!(segment.ident.name.as_str(), "copied" | "cloned") =>
{
receiver
}
_ => expression,
};
let ExprKind::MethodCall(segment, map, [key], _) = &expression.kind else {
return None;
};
if !matches!(
segment.ident.name.as_str(),
"get" | "get_mut" | "get_key_value"
) {
return None;
}
let map_type = cx.typeck_results().expr_ty_adjusted(map).peel_refs();
let map_definition = map_type.ty_adt_def()?;
if !is_std_set(cx, map_definition.did()) && !has_inherent_entry_method(cx, map_definition.did())
{
return None;
}
// The purity check runs on the peeled key so that borrows and
// value-preserving conversions do not count as side effects.
let mut key_converted = false;
let peeled_key = peel_key_expr(cx, key, &mut key_converted);
if map.can_have_side_effects() || peeled_key.can_have_side_effects() {
return None;
}
Some(Lookup { map, key })
}
/// Searches `node` for an insertion into `lookup`'s map under `lookup`'s key.
/// Returns `Some(key_converted)` when one is found, where `key_converted`
/// records whether the match required peeling an owned-key conversion.
fn find_matching_insert<'tcx>(
cx: &LateContext<'tcx>,
lookup: Lookup<'tcx>,
node: impl Visitable<'tcx>,
) -> Option<bool> {
for_each_expr_without_closures(node, |expression| {
if let Some(insert) = parse_insert(expression)
&& SpanlessEq::new(cx).eq_expr(lookup.map, insert.map)
&& let Some(key_converted) = keys_match(cx, lookup.key, insert.key)
{
ControlFlow::Break(key_converted)
} else {
ControlFlow::Continue(())
}
})
}
fn parse_insert<'tcx>(expression: &'tcx Expr<'tcx>) -> Option<Lookup<'tcx>> {
// Maps insert `(key, value)`; sets insert only the value.
let ExprKind::MethodCall(segment, map, [key] | [key, _], _) = &expression.kind else {
return None;
};
if segment.ident.name.as_str() != "insert" {
return None;
}
Some(Lookup { map, key })
}
/// Checks whether the two key expressions denote the same key value. Returns
/// `Some(key_converted)` on a match, where `key_converted` records whether an
/// owned-key conversion had to be peeled to see it.
///
/// Two forms are recognized:
/// * the keys are equal after peeling borrows, derefs, and value-preserving
/// conversions, e.g. looking up `&entry.key` and inserting
/// `entry.key.clone()`;
/// * after additionally resolving immutable local aliases to their
/// initializers, e.g. `let owned = key.clone();` before a lookup of `&key`
/// and an insertion of `owned`. Both resolved forms must be free of side
/// effects: two structurally equal side-effecting initializers (say, two
/// separate `next_key()` calls) are two different key values.
fn keys_match<'tcx>(
cx: &LateContext<'tcx>,
lookup_key: &'tcx Expr<'tcx>,
insert_key: &'tcx Expr<'tcx>,
) -> Option<bool> {
let mut key_converted = false;
let lookup_key = peel_key_expr(cx, lookup_key, &mut key_converted);
let insert_key = peel_key_expr(cx, insert_key, &mut key_converted);
if SpanlessEq::new(cx).eq_expr(lookup_key, insert_key) {
return Some(key_converted);
}
let lookup_key = resolve_local_aliases(cx, lookup_key, &mut key_converted);
let insert_key = resolve_local_aliases(cx, insert_key, &mut key_converted);
if !lookup_key.can_have_side_effects()
&& !insert_key.can_have_side_effects()
&& SpanlessEq::new(cx).eq_expr(lookup_key, insert_key)
{
return Some(key_converted);
}
None
}
/// Follows immutable local bindings back to their initializers, peeling each
/// initializer down to its underlying key expression. `expr_or_init` only
/// resolves bindings that cannot be reassigned, so the binding's value is the
/// initializer's value wherever the binding is referenced.
fn resolve_local_aliases<'tcx>(
cx: &LateContext<'tcx>,
mut expression: &'tcx Expr<'tcx>,
key_converted: &mut bool,
) -> &'tcx Expr<'tcx> {
loop {
let resolved = expr_or_init(cx, expression);
if resolved.hir_id == expression.hir_id {
return expression;
}
expression = peel_key_expr(cx, resolved, key_converted);
}
}
/// Strips layers that do not change which slot of the map the key selects:
/// borrows, derefs, and owned-key conversions of the underlying expression.
fn peel_key_expr<'tcx>(
cx: &LateContext<'tcx>,
mut expression: &'tcx Expr<'tcx>,
key_converted: &mut bool,
) -> &'tcx Expr<'tcx> {
loop {
match &expression.kind {
ExprKind::AddrOf(_, _, inner) | ExprKind::Unary(UnOp::Deref, inner) => {
expression = inner;
}
_ => match peel_value_preserving_conversion(cx, expression) {
Some(inner) => {
*key_converted = true;
expression = inner;
}
None => return expression,
},
}
}
}
/// If `expression` is a conversion that yields an owned key equal to its
/// input — under the same `Eq`/`Hash`/`Ord` contract the map lookup itself
/// relies on — returns the input.
///
/// Each conversion is resolved by definition, not by name, so a user-defined
/// method that merely happens to be called `clone` or `to_owned` does not
/// count. For the `ToOwned` family, the `Borrow` contract (the owned and
/// borrowed forms hash and compare identically) is the same contract that
/// makes the map lookup itself well-defined. For `Clone`, the original code
/// already inserts the clone under the probed key, so the lint assumes
/// nothing the code did not.
fn peel_value_preserving_conversion<'tcx>(
cx: &LateContext<'tcx>,
expression: &'tcx Expr<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
match &expression.kind {
ExprKind::MethodCall(segment, receiver, [], _) => {
let method = cx
.typeck_results()
.type_dependent_def_id(expression.hir_id)?;
let preserves_value = match segment.ident.name.as_str() {
"clone" => is_assoc_of_diag_trait(cx, method, sym::Clone),
"to_owned" => is_assoc_of_diag_trait(cx, method, sym::ToOwned),
// `ToString` runs an arbitrary `Display` impl; only the
// string types are guaranteed to format as themselves.
"to_string" => {
is_assoc_of_diag_trait(cx, method, sym::ToString) && {
let receiver_type =
cx.typeck_results().expr_ty_adjusted(receiver).peel_refs();
receiver_type.is_str() || is_string(cx, receiver_type)
}
}
"to_vec" => cx.tcx.impl_of_assoc(method).is_some_and(|impl_id| {
cx.tcx.type_of(impl_id).instantiate_identity().is_slice()
}),
_ => false,
};
preserves_value.then_some(receiver)
}
// UFCS forms: `Clone::clone(&key)`, `ToOwned::to_owned(&key)`, and
// `String::from(key)` where `key` is a `&str`.
ExprKind::Call(callee, [argument]) => {
let ExprKind::Path(qpath) = &callee.kind else {
return None;
};
let function = cx.qpath_res(qpath, callee.hir_id).opt_def_id()?;
let preserves_value = is_assoc_of_diag_trait(cx, function, sym::Clone)
|| is_assoc_of_diag_trait(cx, function, sym::ToOwned)
|| (is_assoc_of_diag_trait(cx, function, sym::From)
&& is_string(cx, cx.typeck_results().expr_ty(expression))
&& cx.typeck_results().expr_ty(argument).peel_refs().is_str());
preserves_value.then_some(argument)
}
_ => None,
}
}
/// Whether `item` is an associated item of the trait named by the diagnostic
/// item `trait_name`. Qualified paths can resolve to a trait impl's item
/// rather than the trait's, so impl items are first mapped back to the trait
/// item they implement.
fn is_assoc_of_diag_trait(cx: &LateContext<'_>, item: DefId, trait_name: Symbol) -> bool {
let item = cx
.tcx
.opt_associated_item(item)
.and_then(|associated| associated.trait_item_def_id())
.unwrap_or(item);
cx.tcx
.trait_of_assoc(item)
.is_some_and(|trait_id| cx.tcx.is_diagnostic_item(trait_name, trait_id))
}
fn is_string(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
ty.ty_adt_def()
.is_some_and(|definition| cx.tcx.is_lang_item(definition.did(), LangItem::String))
}
/// Clippy already rewrites `get(..).is_some()`/`is_none()` on the std maps
/// and sets: `unnecessary_get_then_check` turns the condition into
/// `contains_key`/`contains`, which `map_entry`/`set_contains_or_insert` then
/// turn into the final form. Defer to that chain instead of duplicating it.
/// Conditions using `get_mut` or `get_key_value`, and any `get` on a non-std
/// map, are not covered by Clippy and stay ours.
fn clippy_covers_get_then_check(cx: &LateContext<'_>, receiver: &Expr<'_>) -> bool {
let ExprKind::MethodCall(segment, map, [_], _) = &receiver.kind else {
return false;
};
if segment.ident.name.as_str() != "get" {
return false;
}
cx.typeck_results()
.expr_ty_adjusted(map)
.peel_refs()
.ty_adt_def()
.is_some_and(|definition| {
matches!(
cx.tcx.get_diagnostic_name(definition.did()),
Some(sym::HashMap | sym::BTreeMap | sym::HashSet | sym::BTreeSet)
)
})
}
/// Sets are gated by diagnostic name rather than by an `entry` method: their
/// entry API is unstable (`hash_set_entry`), and the suggested rewrite (plain
/// `insert`) relies on std's no-op-when-present insertion semantics.
fn is_std_set(cx: &LateContext<'_>, definition: DefId) -> bool {
matches!(
cx.tcx.get_diagnostic_name(definition),
Some(sym::HashSet | sym::BTreeSet)
)
}
/// A lookup-then-insert pattern is only worth reporting when the map offers an
/// entry API to rewrite it with. Rather than hard-coding `HashMap` and
/// `BTreeMap`, accept any type whose inherent impls provide an `entry` method;
/// this also covers maps like `hashbrown::HashMap` and `indexmap::IndexMap`.
/// Hasher aliases such as `FxHashMap` need no special case because they
/// resolve to the underlying map type.
fn has_inherent_entry_method(cx: &LateContext<'_>, map_definition: DefId) -> bool {
let entry = Symbol::intern("entry");
cx.tcx.inherent_impls(map_definition).iter().any(|&imp| {
cx.tcx
.associated_items(imp)
.filter_by_name_unhygienic(entry)
.next()
.is_some()
})
}
fn emit_lint(
cx: &LateContext<'_>,
span: rustc_span::Span,
lookup: Lookup<'_>,
key_converted: bool,
) {
let is_set = cx
.typeck_results()
.expr_ty_adjusted(lookup.map)
.peel_refs()
.ty_adt_def()
.is_some_and(|definition| is_std_set(cx, definition.did()));
let (message, help) = if is_set {
(
"this set lookup and insertion search for the same value twice",
"`insert` alone searches once, skips duplicates, and returns `false` \
when the value was already present",
)
} else {
(
"this map lookup and insertion search for the same key twice",
"to search the map only once, use the entry API: \
https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html",
)
};
span_lint_and_then(cx, MAP_LOOKUP_THEN_INSERT, span, message, |diagnostic| {
diagnostic.help(help);
if key_converted {
diagnostic.note(
"the inserted key is an owned copy of the looked-up key; a rewrite that \
passes the key by value up front would make that copy on hits as well \
as misses",
);
}
});
}
+573
View File
@@ -0,0 +1,573 @@
// Tests for the `map_lookup_then_insert` lint.
#![allow(unused)]
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
fn main() {}
// ------------ SHOULD FIRE -----------
fn hash_map_if_let(map: &mut HashMap<String, usize>, key: String) -> usize {
if let Some(value) = map.get(&key) {
*value
} else {
map.insert(key, 1);
1
}
}
fn hash_map_match(map: &mut HashMap<String, usize>, key: String) -> usize {
match map.get(&key).copied() {
Some(value) => value,
None => {
map.insert(key, 1);
1
}
}
}
// Fix: `map.entry(key).and_modify(|value| *value += 1).or_insert(1)`.
fn hash_map_get_mut(map: &mut HashMap<String, usize>, key: String) {
match map.get_mut(&key) {
Some(value) => *value += 1,
None => {
map.insert(key, 1);
}
}
}
fn hash_map_get_key_value(map: &mut HashMap<String, usize>, key: String) -> usize {
match map.get_key_value(&key) {
Some((_, value)) => *value,
None => {
map.insert(key, 1);
1
}
}
}
fn btree_map_match(map: &mut BTreeMap<String, usize>, key: String) -> usize {
match map.get(&key) {
Some(value) => *value,
None => {
map.insert(key, 1);
1
}
}
}
fn btree_map_if_let(map: &mut BTreeMap<String, usize>, key: String) -> usize {
if let Some(value) = map.get(&key) {
*value
} else {
map.insert(key, 1);
1
}
}
// Fix: `map.entry(key).and_modify(|value| *value += 1).or_insert(1)`.
fn btree_map_get_mut(map: &mut BTreeMap<String, usize>, key: String) {
match map.get_mut(&key) {
Some(value) => *value += 1,
None => {
map.insert(key, 1);
}
}
}
fn btree_map_get_key_value(map: &mut BTreeMap<String, usize>, key: String) -> usize {
match map.get_key_value(&key) {
Some((_, value)) => *value,
None => {
map.insert(key, 1);
1
}
}
}
fn btree_map_let_else(map: &mut BTreeMap<String, usize>, key: String) -> usize {
let Some(value) = map.get(&key) else {
map.insert(key, 1);
return 1;
};
*value
}
// `btree_map::Entry` has no `insert_entry`; the fix here is
// `map.entry(key).and_modify(|value| *value = 2)`.
fn btree_map_overwrite_when_present(map: &mut BTreeMap<String, usize>, key: String) {
if let Some(_current) = map.get(&key) {
map.insert(key, 2);
}
}
// Sets repeat the search too. They need no entry API: the fix is plain
// `set.insert(value)`, which returns `false` when the value was present.
fn hash_set_if_let(set: &mut HashSet<String>, value: String) -> bool {
if let Some(existing) = set.get(&value) {
existing.is_empty()
} else {
set.insert(value)
}
}
fn btree_set_match(set: &mut BTreeSet<String>, value: String) -> bool {
match set.get(&value) {
Some(_) => false,
None => set.insert(value),
}
}
fn hash_map_let_else(map: &mut HashMap<String, usize>, key: String) -> usize {
let Some(value) = map.get(&key) else {
map.insert(key, 1);
return 1;
};
*value
}
fn expensive() -> usize {
42
}
// Fix: `map.entry(key).or_insert_with(expensive)`.
fn fix_with_or_insert_with(map: &mut HashMap<String, usize>, key: String) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(key, expensive());
}
}
}
// Fix: `map.entry(key).or_insert_with_key(|key| key.len())`.
fn fix_with_or_insert_with_key(map: &mut HashMap<String, usize>, key: String) {
match map.get(&key) {
Some(_) => {}
None => {
let value = key.len();
map.insert(key, value);
}
}
}
// Fix: `map.entry(key).or_default()`.
fn fix_with_or_default(map: &mut HashMap<String, usize>, key: String) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(key, usize::default());
}
}
}
// Fix: `map.entry(key).insert_entry(2)`, keeping the occupancy check.
fn fix_with_insert_entry(map: &mut HashMap<String, usize>, key: String) {
if let Some(_current) = map.get(&key) {
map.insert(key, 2);
}
}
// Hasher aliases like `FxHashMap` resolve to `HashMap`, so they are covered.
type FxLikeMap = HashMap<
String,
usize,
std::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>,
>;
fn custom_hasher_alias(map: &mut FxLikeMap, key: String) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(key, 1);
}
}
}
// A third-party map (`hashbrown`, `indexmap`, ...) that offers an inherent
// `entry` method is covered.
struct EntryMap;
impl EntryMap {
fn get(&self, _key: &str) -> Option<&usize> {
None
}
fn insert(&mut self, _key: String, _value: usize) {}
fn entry(&mut self, _key: String) -> std::collections::hash_map::Entry<'_, String, usize> {
unimplemented!()
}
}
// Clippy's `unnecessary_get_then_check` only covers the std collections, so
// the `get(..).is_none()` form on a third-party map is still ours to catch.
fn entry_compatible_custom_map(map: &mut EntryMap, key: String) {
if map.get(&key).is_none() {
map.insert(key, 1);
}
}
// --- Keys that differ only by a value-preserving conversion ---
// The shape that motivated conversion peeling: code in the wild looks up
// `&key` and inserts `key.clone()`, which a textual comparison misses.
fn insert_clones_the_key(map: &mut HashMap<String, Vec<usize>>, key: String, value: usize) {
if let Some(values) = map.get_mut(&key) {
values.push(value);
} else {
map.insert(key.clone(), vec![value]);
}
}
fn insert_clones_the_key_ufcs(map: &mut HashMap<String, usize>, key: String) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(Clone::clone(&key), 1);
}
}
}
fn insert_key_to_owned(map: &mut HashMap<String, usize>, key: &str) {
match map.get(key) {
Some(_) => {}
None => {
map.insert(key.to_owned(), 1);
}
}
}
fn insert_key_to_string(map: &mut HashMap<String, usize>, key: &str) {
match map.get(key) {
Some(_) => {}
None => {
map.insert(key.to_string(), 1);
}
}
}
fn insert_key_string_from(map: &mut HashMap<String, usize>, key: &str) {
match map.get(key) {
Some(_) => {}
None => {
map.insert(String::from(key), 1);
}
}
}
fn insert_key_to_vec(map: &mut HashMap<Vec<u8>, usize>, key: &[u8]) {
match map.get(key) {
Some(_) => {}
None => {
map.insert(key.to_vec(), 1);
}
}
}
// The lookup and the insertion may each add their own borrow/deref layers.
fn insert_clone_of_deref(map: &mut HashMap<String, usize>, key: &String) {
match map.get(key) {
Some(_) => {}
None => {
map.insert((*key).clone(), 1);
}
}
}
// An immutable local alias of the cloned key is still the same key.
fn insert_through_alias(map: &mut HashMap<String, usize>, key: String) {
let owned = key.clone();
match map.get(&key) {
Some(_) => {}
None => {
map.insert(owned, 1);
}
}
}
fn set_insert_clone(set: &mut HashSet<String>, value: String) -> bool {
if let Some(existing) = set.get(&value) {
existing.is_empty()
} else {
set.insert(value.clone())
}
}
// ------------- SHOULD NOT FIRE -------------
// `contains_key` followed by `insert` is Clippy's `map_entry` lint.
fn clippy_handles_contains_key(map: &mut HashMap<String, usize>, key: String) {
if !map.contains_key(&key) {
map.insert(key, 1);
}
}
// `get(..).is_none()`/`is_some()` on std collections is Clippy's chain:
// `unnecessary_get_then_check` rewrites the condition to
// `contains_key`/`contains`, which `map_entry`/`set_contains_or_insert`
// rewrite again.
fn clippy_handles_hash_map_get_is_none(map: &mut HashMap<String, usize>, key: String) {
if map.get(&key).is_none() {
map.insert(key, 1);
}
}
fn clippy_handles_hash_map_get_is_some(map: &mut HashMap<String, usize>, key: String) {
if map.get(&key).is_some() {
map.insert(key, 2);
}
}
fn clippy_handles_btree_map_get_is_none(map: &mut BTreeMap<String, usize>, key: String) {
if map.get(&key).is_none() {
map.insert(key, 1);
}
}
fn clippy_handles_hash_set_get_is_none(set: &mut HashSet<String>, value: String) {
if set.get(&value).is_none() {
set.insert(value);
}
}
fn clippy_handles_btree_set_get_is_none(set: &mut BTreeSet<String>, value: String) {
if set.get(&value).is_none() {
set.insert(value);
}
}
fn already_uses_entry_or_insert(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).or_insert(1);
}
fn already_uses_entry_or_insert_with(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).or_insert_with(expensive);
}
fn already_uses_entry_or_insert_with_key(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).or_insert_with_key(|key| key.len());
}
fn already_uses_entry_or_default(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).or_default();
}
fn already_uses_entry_and_modify(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).and_modify(|value| *value += 1).or_insert(1);
}
fn already_uses_entry_insert_entry(map: &mut HashMap<String, usize>, key: String) {
map.entry(key).insert_entry(2);
}
fn already_uses_entry_key(map: &mut HashMap<String, usize>, key: String) -> usize {
map.entry(key).key().len()
}
// `btree_map::Entry` offers every method that `hash_map::Entry` does except
// `insert_entry`, so there is no BTreeMap mirror of that case.
fn btree_already_uses_entry_or_insert(map: &mut BTreeMap<String, usize>, key: String) {
map.entry(key).or_insert(1);
}
fn btree_already_uses_entry_or_insert_with(map: &mut BTreeMap<String, usize>, key: String) {
map.entry(key).or_insert_with(expensive);
}
fn btree_already_uses_entry_or_insert_with_key(map: &mut BTreeMap<String, usize>, key: String) {
map.entry(key).or_insert_with_key(|key| key.len());
}
fn btree_already_uses_entry_or_default(map: &mut BTreeMap<String, usize>, key: String) {
map.entry(key).or_default();
}
fn btree_already_uses_entry_and_modify(map: &mut BTreeMap<String, usize>, key: String) {
map.entry(key).and_modify(|value| *value += 1).or_insert(1);
}
fn btree_already_uses_entry_key(map: &mut BTreeMap<String, usize>, key: String) -> usize {
map.entry(key).key().len()
}
// `contains` followed by `insert` is Clippy's `set_contains_or_insert` lint.
fn clippy_handles_set_contains(set: &mut HashSet<String>, value: String) {
if !set.contains(&value) {
set.insert(value);
}
}
// A bare `insert` already searches only once.
fn set_insert_alone(set: &mut HashSet<String>, value: String) -> bool {
set.insert(value)
}
fn btree_set_insert_alone(set: &mut BTreeSet<String>, value: String) -> bool {
set.insert(value)
}
fn different_key(map: &mut HashMap<String, usize>, first: String, second: String) {
match map.get(&first) {
Some(_) => {}
None => {
map.insert(second, 1);
}
}
}
fn different_map(
first: &HashMap<String, usize>,
second: &mut HashMap<String, usize>,
key: String,
) {
match first.get(&key) {
Some(_) => {}
None => {
second.insert(key, 1);
}
}
}
fn lookup_without_insert(map: &HashMap<String, usize>, key: &str) -> Option<usize> {
map.get(key).copied()
}
fn insertion_in_deferred_closure(map: &mut HashMap<String, usize>, key: String) {
match map.get(&key).copied() {
Some(_) => {}
None => {
let _insert_later = || {
map.insert(key, 1);
};
}
}
}
// `CustomMap` has no `entry` method, so there is no entry API to suggest.
struct CustomMap;
impl CustomMap {
fn get(&self, _key: &str) -> Option<&usize> {
None
}
fn insert(&mut self, _key: String, _value: usize) {}
}
fn non_standard_map(map: &mut CustomMap, key: String) {
if map.get(&key).is_none() {
map.insert(key, 1);
}
}
fn next_key() -> String {
String::new()
}
fn side_effecting_key(map: &mut HashMap<String, usize>) {
match map.get(&next_key()) {
Some(_) => {}
None => {
map.insert(next_key(), 1);
}
}
}
fn global_map() -> &'static mut HashMap<String, usize> {
unimplemented!()
}
fn side_effecting_map(key: String) {
match global_map().get(&key) {
Some(_) => {}
None => {
global_map().insert(key, 1);
}
}
}
// Conversions are peeled by definition, not by name: `DisguisedKey`'s
// inherent `clone` and `to_owned` are not `Clone::clone`/`ToOwned::to_owned`
// and may return a different key.
#[derive(Clone, PartialEq, Eq, Hash)]
struct DisguisedKey(usize);
impl DisguisedKey {
fn clone(&self) -> DisguisedKey {
DisguisedKey(self.0 + 1)
}
fn to_owned(&self) -> DisguisedKey {
DisguisedKey(self.0 * 2)
}
}
fn inherent_clone_is_a_different_key(map: &mut HashMap<DisguisedKey, usize>, key: DisguisedKey) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(key.clone(), 1);
}
}
}
fn inherent_to_owned_is_a_different_key(
map: &mut HashMap<DisguisedKey, usize>,
key: DisguisedKey,
) {
match map.get(&key) {
Some(_) => {}
None => {
map.insert(key.to_owned(), 1);
}
}
}
// A mutable alias may no longer hold the looked-up key by insertion time.
fn mutated_alias(map: &mut HashMap<String, usize>, key: String) {
let mut owned = key.clone();
owned.push('!');
match map.get(&key) {
Some(_) => {}
None => {
map.insert(owned, 1);
}
}
}
// A clone of a different key is a different key.
fn clone_of_different_key(map: &mut HashMap<String, usize>, first: String, second: String) {
match map.get(&first) {
Some(_) => {}
None => {
map.insert(second.clone(), 1);
}
}
}
// `to_string` runs an arbitrary `Display` impl; only the string types are
// guaranteed to format as themselves, so a non-string receiver stays a
// side-effecting key and is not matched.
fn to_string_of_non_string_key(map: &mut HashMap<String, usize>, id: u32) {
match map.get(&id.to_string()) {
Some(_) => {}
None => {
map.insert(id.to_string(), 1);
}
}
}
// Alias resolution must not equate structurally equal but side-effecting
// initializers: the two `next_key()` calls produce two different keys.
fn aliases_of_two_generated_keys(map: &mut HashMap<String, usize>) {
let first = next_key();
let second = next_key();
match map.get(&first) {
Some(_) => {}
None => {
map.insert(second, 1);
}
}
}
@@ -0,0 +1,359 @@
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:12:5
|
LL | / if let Some(value) = map.get(&key) {
LL | | *value
LL | | } else {
LL | | map.insert(key, 1);
LL | | 1
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: `-D map-lookup-then-insert` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(map_lookup_then_insert)]`
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:21:5
|
LL | / match map.get(&key).copied() {
LL | | Some(value) => value,
LL | | None => {
LL | | map.insert(key, 1);
... |
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:32:5
|
LL | / match map.get_mut(&key) {
LL | | Some(value) => *value += 1,
LL | | None => {
LL | | map.insert(key, 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:41:5
|
LL | / match map.get_key_value(&key) {
LL | | Some((_, value)) => *value,
LL | | None => {
LL | | map.insert(key, 1);
... |
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:51:5
|
LL | / match map.get(&key) {
LL | | Some(value) => *value,
LL | | None => {
LL | | map.insert(key, 1);
... |
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:61:5
|
LL | / if let Some(value) = map.get(&key) {
LL | | *value
LL | | } else {
LL | | map.insert(key, 1);
LL | | 1
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:71:5
|
LL | / match map.get_mut(&key) {
LL | | Some(value) => *value += 1,
LL | | None => {
LL | | map.insert(key, 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:80:5
|
LL | / match map.get_key_value(&key) {
LL | | Some((_, value)) => *value,
LL | | None => {
LL | | map.insert(key, 1);
... |
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:90:5
|
LL | / let Some(value) = map.get(&key) else {
LL | | map.insert(key, 1);
LL | | return 1;
LL | | };
| |______^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:100:5
|
LL | / if let Some(_current) = map.get(&key) {
LL | | map.insert(key, 2);
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this set lookup and insertion search for the same value twice
--> $DIR/map_lookup_then_insert.rs:108:5
|
LL | / if let Some(existing) = set.get(&value) {
LL | | existing.is_empty()
LL | | } else {
LL | | set.insert(value)
LL | | }
| |_____^
|
= help: `insert` alone searches once, skips duplicates, and returns `false` when the value was already present
error: this set lookup and insertion search for the same value twice
--> $DIR/map_lookup_then_insert.rs:116:5
|
LL | / match set.get(&value) {
LL | | Some(_) => false,
LL | | None => set.insert(value),
LL | | }
| |_____^
|
= help: `insert` alone searches once, skips duplicates, and returns `false` when the value was already present
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:123:5
|
LL | / let Some(value) = map.get(&key) else {
LL | | map.insert(key, 1);
LL | | return 1;
LL | | };
| |______^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:136:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key, expensive());
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:146:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | let value = key.len();
... |
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:157:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key, usize::default());
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:167:5
|
LL | / if let Some(_current) = map.get(&key) {
LL | | map.insert(key, 2);
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:180:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key, 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:207:5
|
LL | / if map.get(&key).is_none() {
LL | | map.insert(key, 1);
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:217:5
|
LL | / if let Some(values) = map.get_mut(&key) {
LL | | values.push(value);
LL | | } else {
LL | | map.insert(key.clone(), vec![value]);
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:225:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(Clone::clone(&key), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:234:5
|
LL | / match map.get(key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key.to_owned(), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:243:5
|
LL | / match map.get(key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key.to_string(), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:252:5
|
LL | / match map.get(key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(String::from(key), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:261:5
|
LL | / match map.get(key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(key.to_vec(), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:271:5
|
LL | / match map.get(key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert((*key).clone(), 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this map lookup and insertion search for the same key twice
--> $DIR/map_lookup_then_insert.rs:282:5
|
LL | / match map.get(&key) {
LL | | Some(_) => {}
LL | | None => {
LL | | map.insert(owned, 1);
LL | | }
LL | | }
| |_____^
|
= help: to search the map only once, use the entry API: https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: this set lookup and insertion search for the same value twice
--> $DIR/map_lookup_then_insert.rs:291:5
|
LL | / if let Some(existing) = set.get(&value) {
LL | | existing.is_empty()
LL | | } else {
LL | | set.insert(value.clone())
LL | | }
| |_____^
|
= help: `insert` alone searches once, skips duplicates, and returns `false` when the value was already present
= note: the inserted key is an owned copy of the looked-up key; a rewrite that passes the key by value up front would make that copy on hits as well as misses
error: aborting due to 28 previous errors