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:
- Clean it to zero in a branch.
- Add it to
[workspace.lints.clippy]aswarn. - Let
-D warningsmake 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:
- Zero leftovers → ratchet immediately.
uninlined_format_args,use_self,redundant_closure_for_method_calls,manual_string_new,implicit_clone,unnested_or_patterns,derive_partial_eq_without_eq,semicolon_if_nothing_returned. - A handful of non-autofixable leftovers → finish by hand, then ratchet.
or_fun_call,map_unwrap_or,significant_drop_in_scrutineewere ~10 each on mxr; cheap to close. - Too many leftovers, or judgment-heavy → leave the
--fix-applied ones in place but don't enable the lint.match_same_arms(84) anddefault_trait_access(56) stayed off. A lint you can't hold at zero is a lint that breaks the next person's build.
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
- Idiomatic Rust Rubric — the ratchet is how you enforce the rubric's "signal" lints permanently
- Idiomatic Is Not Pedantic-Clean — which lints are worth ratcheting vs allowing
- Refactor Behind a Behavior Test — the proof half of a
--fixsweep - Rust Project Conventions —
[workspace.lints]levels vsclippy.tomlparameters