Ratchet Lints Into the Build

Ratchet lints into the build

A lint policy written in a doc is a wish. A lint in [workspace.lints] with CI running -D warnings is a law. The gap between the two is the only thing that decides whether "we should prefer map_or" is still true in six months. Close it by ratcheting: drive a lint to zero, then turn it on, so it can never come back.

The chicken and egg

You can't just flip a lint to warn and call it done. CI runs cargo clippy --workspace --all-targets --all-features --locked -- -D warnings, so every warning is an error. Turn on a lint that has 134 hits and you've turned on 134 red builds. So the naive order — enable, then clean up later — never finishes, because the build is broken the whole time and nobody can merge.

The ratchet inverts it. Per lint:

  1. Clean it to zero in a branch.
  2. Add it to [workspace.lints.clippy] as warn.
  3. Let -D warnings make it permanent.

Now the lint isn't a habit you have to remember in review — it's a wall. A new format!("{}", x) doesn't get a comment; it fails CI.

clippy --fix is the lever for step 1

The mechanical idiom lints are rustfix-class: clippy emits a machine-applicable suggestion and --fix applies it. By construction those edits are behaviour-preserving, which is what makes the cleanup safe at scale.

cargo clippy --fix --workspace --all-targets --all-features \
  --allow-dirty --allow-staged -- \
  -W clippy::uninlined_format_args -W clippy::map_unwrap_or \
  -W clippy::use_self -W clippy::redundant_closure_for_method_calls

On Mxr this turned 4640 pedantic+nursery warnings into 200 applied fixes across 191 files (net −144 lines), with the proof being the unchanged test suite afterward. --fix does the typing; Refactor Behind a Behavior Test is what tells you it didn't lie.

Only ratchet what you can hold at zero

The discipline is: don't enable a lint you can't keep green. Three buckets after a --fix pass:

Nursery lints get extra suspicion. redundant_clone is high-value but false-positive-prone, so it's the wrong thing to gate CI on — apply selectively, don't ratchet. This is the same "cherry-pick, never enable the group" rule from Idiomatic Is Not Pedantic-Clean.

Levels here, parameters elsewhere

The ratchet lives in [workspace.lints.clippy] (Cargo.toml), which sets lint levels. Don't confuse it with clippy.toml, which sets lint parameters (thresholds). Both exist; they do different jobs. See Rust Project Conventions.

The gotcha that bit the cleanup

map_unwrap_or rewrites x.map(f).unwrap_or(&[]) to x.map_or(&[], f) — except a bare &[] infers as &[_; 0] (an array ref), not a slice, so the rewritten code fails to compile. The fix is &[][..] to force the slice type. A reminder that "machine-applicable" suggestions still need the gate to catch the residue: the lint policy isn't done until clippy -D warnings and the tests are both green.

See also