Files
zed/tooling/lints/ui/map_lookup_then_insert.rs
T
Miguel Raz Guzmán Macedo 81299967df 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
2026-08-28 20:01:28 +00:00

574 lines
15 KiB
Rust

// 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);
}
}
}