Logo
Logo

Atharva Pandey/Lesson 23: Clippy Is Your Mentor — Listen to it

Created Sat, 18 May 2024 10:55:00 +0000 Modified Sat, 18 May 2024 10:55:00 +0000

I’ll say something controversial: Clippy taught me more about idiomatic Rust than any book. Not because it explains concepts — it doesn’t. But because it catches you every time you write non-idiomatic code and shows you the better way. It’s like having a senior Rust developer looking over your shoulder, constantly saying “there’s a cleaner way to do that.”

Run cargo clippy on every project. Every commit. No exceptions.


What Clippy Is

Clippy is Rust’s official linter — over 700 lint rules that catch everything from style issues to actual bugs. It ships with rustup, so you already have it.

# Run clippy
cargo clippy

# Run clippy and fail on warnings (for CI)
cargo clippy -- -D warnings

# Run clippy on all targets including tests
cargo clippy --all-targets --all-features -- -D warnings

Clippy Catches Bugs

Not just style — actual bugs:

Using == on floating-point numbers

fn main() {
    let x: f64 = 0.1 + 0.2;

    // Clippy warns: "strict comparison of `f64`"
    // BAD:
    if x == 0.3 {
        println!("equal");
    }

    // GOOD:
    if (x - 0.3).abs() < f64::EPSILON {
        println!("approximately equal");
    }
}

Forgetting to use a Result

fn do_something() -> Result<(), String> {
    Ok(())
}

fn main() {
    // Clippy warns: "unused `Result` that must be used"
    do_something(); // BAD: ignoring the result

    let _ = do_something(); // GOOD: explicitly discarding
    do_something().unwrap(); // GOOD: handling it (even if crudely)
}

Off-by-one with ranges

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    // Clippy may warn about manual range patterns
    // BAD:
    for i in 0..v.len() {
        println!("{}", v[i]);
    }

    // GOOD:
    for item in &v {
        println!("{}", item);
    }
}

Clippy Teaches Idiomatic Style

This is where Clippy really shines. It knows all the patterns from this course and enforces them:

Use if let instead of single-arm match

fn process(value: Option<i32>) {
    // Clippy: "this match could be replaced by `if let`"
    // BAD:
    match value {
        Some(n) => println!("{}", n),
        None => {}
    }

    // GOOD:
    if let Some(n) = value {
        println!("{}", n);
    }
}

Use unwrap_or_else instead of match

fn get_name(custom: Option<String>) -> String {
    // Clippy: "this match could be replaced by `unwrap_or_else`"
    // BAD:
    match custom {
        Some(n) => n,
        None => String::from("default"),
    }

    // GOOD:
    // custom.unwrap_or_else(|| String::from("default"))
}

Use iterator methods instead of manual loops

fn count_positives(numbers: &[i32]) -> usize {
    // Clippy might suggest:
    // BAD:
    let mut count = 0;
    for &n in numbers {
        if n > 0 {
            count += 1;
        }
    }
    // GOOD:
    // numbers.iter().filter(|&&n| n > 0).count()
    count
}

Take &str instead of &String

// Clippy: "writing `&String` instead of `&str` involves a new object"
// BAD:
fn greet(name: &String) {
    println!("Hello, {}", name);
}

// GOOD:
fn greet_better(name: &str) {
    println!("Hello, {}", name);
}

fn main() {
    let s = String::from("Atharva");
    greet(&s);
    greet_better(&s);
}

My Favorite Clippy Lints

clippy::needless_return

// BAD:
fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

// GOOD: last expression is the return value
fn add_clean(a: i32, b: i32) -> i32 {
    a + b
}

clippy::manual_map

fn double_if_present(x: Option<i32>) -> Option<i32> {
    // BAD:
    match x {
        Some(n) => Some(n * 2),
        None => None,
    }

    // GOOD:
    // x.map(|n| n * 2)
}

clippy::redundant_closure

fn main() {
    let numbers = vec![1, 2, 3];

    // BAD: redundant closure
    let strings: Vec<String> = numbers.iter().map(|n| n.to_string()).collect();

    // GOOD: pass the method directly
    let strings: Vec<String> = numbers.iter().map(ToString::to_string).collect();

    println!("{:?}", strings);
}

clippy::or_fun_call

fn main() {
    let x: Option<String> = None;

    // BAD: always constructs the default, even when Some
    let _val = x.clone().unwrap_or(String::from("default"));

    // GOOD: only constructs default when None
    let _val = x.unwrap_or_else(|| String::from("default"));
}

Configuring Clippy

Per-project configuration

Create a clippy.toml or .clippy.toml in your project root:

# Allow up to 7 function parameters (default is 7)
too-many-arguments-threshold = 7

# Set the cognitive complexity threshold
cognitive-complexity-threshold = 30

# Limit type complexity
type-complexity-threshold = 250

Allowing specific lints

Sometimes Clippy is wrong (or you disagree). Suppress specific warnings:

#[allow(clippy::too_many_arguments)]
fn complex_function(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) -> i32 {
    a + b + c + d + e + f + g + h
}

// Or at the module level:
#![allow(clippy::module_name_repetitions)]

Denying specific lints

Make certain lints hard errors:

// At the crate root (lib.rs or main.rs):
#![deny(clippy::unwrap_used)]        // No .unwrap() in production code
#![deny(clippy::expect_used)]        // No .expect() either
#![deny(clippy::panic)]              // No panic!() macro

// These are aggressive but great for library code

Clippy Lint Categories

Clippy organizes lints into categories. The defaults are sensible, but you can opt into stricter ones:

CategoryDefaultDescription
clippy::correctnessDenyActual bugs
clippy::suspiciousWarnLikely bugs
clippy::styleWarnStyle issues
clippy::complexityWarnUnnecessarily complex code
clippy::perfWarnPerformance issues
clippy::pedanticAllowStricter, opinionated lints
clippy::nurseryAllowExperimental lints
clippy::restrictionAllowVery strict, context-dependent

For libraries, I recommend enabling pedantic:

#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]  // this one is too noisy

Pedantic catches things like missing docs on public items, non-ergonomic APIs, and subtle style issues.


Clippy in CI

Add Clippy to your CI pipeline. Here’s a GitHub Actions snippet:

- name: Clippy
  run: cargo clippy --all-targets --all-features -- -D warnings

The -D warnings flag treats all warnings as errors, failing the build if Clippy finds issues. This prevents regressions — nobody can merge code with Clippy warnings.


Fixing Clippy Warnings Automatically

Many Clippy lints have auto-fixes:

# Apply Clippy's suggested fixes automatically
cargo clippy --fix --allow-dirty

This modifies your source files in place. Review the changes before committing — auto-fixes are usually correct, but not always.


The Clippy Learning Loop

Here’s how I use Clippy as a learning tool:

  1. Write code the way that feels natural.
  2. Run cargo clippy.
  3. Read the warning and the suggested fix.
  4. Understand why the suggestion is better.
  5. Apply the fix.
  6. Next time, write it the better way from the start.

After a few weeks of this loop, you’ll internalize most of the patterns. Clippy will find fewer and fewer issues in your code. That’s not because you’re suppressing warnings — it’s because you’ve absorbed the idioms.


Key Takeaways

  • Run cargo clippy on every project, every commit. It catches bugs and teaches idioms.
  • Clippy has 700+ lints covering correctness, style, complexity, and performance.
  • Use #[allow(clippy::lint_name)] for specific suppressions. Don’t blanket-suppress.
  • Enable clippy::pedantic for libraries — it catches API design issues.
  • Add cargo clippy -- -D warnings to your CI pipeline.
  • Clippy is a teacher. Read its suggestions, understand them, internalize them. After a few weeks, you’ll write idiomatic Rust by default.