Step 6 of 7
Configuring clippy and rustfmt
Three more lints come up constantly in code review, then the settings that let a team agree on its rules, and finally the formatter.
manual_map, single_match, collapsible_if
// Before: three clippy warnings
fn coupon(code: Option<&str>) -> Option<String> {
match code {
Some(c) => Some(c.to_uppercase()), // manual_map
None => None,
}
}
fn note(express: bool, gift: bool, coupon: Option<&str>) {
if express {
if gift { // collapsible_if
println!("wrap first");
}
}
match coupon {
Some(c) => println!("coupon {c}"), // single_match
_ => {}
}
}
fn coupon(code: Option<&str>) -> Option<String> {
code.map(|c| c.to_uppercase())
}
fn note(express: bool, gift: bool, coupon: Option<&str>) {
if express && gift {
println!("wrap first");
}
if let Some(c) = coupon {
println!("coupon {c}");
}
}
fn main() {
println!("{:?} {:?}", coupon(Some("save10")), coupon(None));
note(true, true, Some("SAVE10"));
note(true, false, None);
}
Some("SAVE10") None
wrap first
coupon SAVE10
Unnecessary clones
.clone() to satisfy the borrow checker is the most common habit in new Rust code. Often the real fix is a borrowing parameter (&str, &[T]) so nothing needs copying. Clippy catches some cases (clone_on_copy for cloning a Copy value, redundant_clone in the nursery group for a clone of a value that's never used again), but code review catches the rest.
Allowing a lint (on purpose)
Sometimes a lint is wrong for your situation. Silence it as narrowly as possible, and say why:
// The physics formula reads best with all its inputs listed.
#[allow(clippy::too_many_arguments)]
fn landing_x(x: f64, y: f64, vx: f64, vy: f64, g: f64, wind: f64, drag: f64, dt: f64) -> f64 {
let mut x = x;
let mut y = y;
let mut vx = vx;
let mut vy = vy;
while y > 0.0 {
vx += (wind - vx * drag) * dt;
vy -= g * dt;
x += vx * dt;
y += vy * dt;
}
x
}
#[rustfmt::skip]
const LEVEL: [[u8; 4]; 3] = [
[1, 0, 0, 1],
[0, 1, 1, 0],
[1, 0, 0, 1],
];
fn main() {
let x = landing_x(0.0, 10.0, 5.0, 0.0, 9.81, 0.0, 0.0, 0.01);
println!("lands near x = {x:.1}");
println!("walls in row 0: {}", LEVEL[0].iter().filter(|&&c| c == 1).count());
}
lands near x = 7.1
walls in row 0: 2
#[allow(clippy::lint_name)]on one item silences it there only.#[expect(clippy::lint_name)]does the same but warns if the lint stops firing, so stale exceptions get cleaned up.#![allow(...)]or#![warn(clippy::pedantic)]at the top oflib.rs/main.rssets a rule for the whole crate.- For a whole project, put the rules in
Cargo.toml, where everyone (and CI) gets them:
[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
dbg_macro = "deny"
unwrap_used = "warn"
A clippy.toml file next to Cargo.toml tunes individual lints, like too-many-arguments-threshold = 9.
rustfmt
rustfmt formats code in the one standard Rust style: 4-space indents, spaces around operators and after commas, one statement per line, line width 100. cargo fmt formats the whole project; cargo fmt --check changes nothing and fails if anything would change, which is what CI runs. Teams almost never argue about formatting in Rust: whatever rustfmt does is the style. Settings go in rustfmt.toml, but most projects use few or none:
max_width = 120
#[rustfmt::skip] keeps hand-aligned code, like the LEVEL grid above, as you wrote it. Set your editor to format on save and you'll never think about it again.
Common mistakes
- Crate-wide
#![allow(clippy::all)]to make warnings go away. That switches off the bug-catching lints too. - Turning on
clippy::pedanticand following every suggestion blindly. It's opinionated; teams usually enable it and then allow the few lints they disagree with. - Formatting by hand in a review comment ("add a space here"). Let
cargo fmt --checkin CI do it.
Your task
coupon_code and notes trigger manual_map, collapsible_if and single_match. Rewrite them clippy-clean with the same behavior: .map instead of the match, one if with && instead of two nested ones, and if let instead of the one-armed match.
Challenge 2
This program clones review twice only because count_words and shout take a String by value. Change both functions to borrow a &str, and remove every .clone(). The output must stay:
4 words
RUST MAKES REFACTORING FEARLESS
original: rust makes refactoring fearless
Challenge 3
CI's cargo fmt --check failed on this file. Format it by hand the way rustfmt would: fn main() { with one space, 4-space indentation per level, one statement per line, every block's body on its own lines, spaces around =, += and >, a space after each comma, and } else { on one line. It must still print big order: 515.
Previous: Clippy: fixing common lints Next: Continuous integration: keeping main green