RustArena

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
[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

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