Skip to content
← All articles

What clippy -D warnings actually checks

Clippy is a graded gate here, so you should know what the gate is — the six lint groups, which are on by default, why the most useful ones are off, and the file that passes clippy while being obviously wrong.

Two things are true at once, and holding both is the difference between finding clippy useful and finding it insulting.

  1. Clippy catches an enormous amount of real, mechanical wrongness, and everything it catches you would otherwise have to catch by hand, in review, forever.
  2. Clippy is opinionated, occasionally wrong, and its default configuration is deliberately tuned for low noise rather than for high safety — so passing it is nowhere near the same thing as writing good Rust.

This site grades you against clippy-driver --edition 2024 -D warnings, so the tool is not optional here. Twenty minutes spent understanding what it is will save you several hours of feeling persecuted by it.

What it actually is

Clippy is not a separate parser or a linter bolted on the side. It is rustc — literally the same compiler, driven through a different front end (clippy-driver) that registers about eight hundred extra lints and then runs the normal compilation. That is why clippy sees types, traits, control flow and constant values rather than tokens, and why it can say things like “this Vec is never mutated” that no text-based linter could.

It also means clippy’s diagnostics have exactly the shape you already learned to read in the previous item: code, headline, span, label, help. And it means -D warnings denies rustc’s own lints too, not just clippy’s — unused_variables, unused_mut, dead_code, non_snake_case all become errors under the same flag.

💡A submission compiles cleanly with rustc and produces the right answer for every test, but the gate rejects it. Name three distinct categories of reason this can happen, and say which of them are clippy's doing. click to reveal

One — a rustc lint fired. unused_variables, unused_mut, dead_code, unreachable_code, non_snake_case. These are rustc‘s, not clippy’s; they were warnings during your normal compile and you probably scrolled past them. -D warnings promotes them.

Two — a default-on clippy lint fired. needless_return, len_zero, ptr_arg, manual_range_contains, needless_range_loop. Correct code, unidiomatic shape.

Three — a lint the problem opted into fired. Some problems carry an inner attribute like #![warn(clippy::float_cmp)] on their first line. A merely warned lint is still promoted to an error by -D warnings, so opting in at warn level is enough to make it a gate.

There is a fourth, rarer case worth knowing: if your function’s signature does not match what the problem’s harness calls, the compile fails at the call site — which is in injected code you cannot see. The grader detects that and tells you to check your signature rather than showing you code you did not write.

The six groups

Every clippy lint belongs to exactly one group, and the group sets its default level.

group default what it means
correctness deny This is a bug. Not a style opinion — the code does not do what it appears to do.
suspicious warn Probably a bug. Occasionally intentional.
style warn Idiomatic Rust does this differently. No behaviour change.
complexity warn Does the right thing the long way round.
perf warn Correct, and measurably slower than an easy alternative.
pedantic allow Opinionated, higher false-positive rate. Off unless you ask.
restriction allow Bans a language feature. Contradictory by design.
nursery allow New or heuristic. May be wrong.
cargo allow About your manifest. Irrelevant here — there is no manifest.

Only the first five are on by default. That is what clippy::all means, and it is what this site’s gate enforces through tracks 0 to 3.

Two entries deserve elaboration.

correctness is deny-by-default and it is not a style group. absurd_extreme_comparisons catches x >= 0 on an unsigned type — a test that is always true, so whatever you were checking, you are not checking it. not_unsafe_ptr_arg_deref is literally an unsoundness detector: a safe pub fn that dereferences a raw pointer parameter can be called from safe code and cause undefined behaviour. iter_next_loop, never_loop, mem_replace_with_uninit. If a correctness lint fires, stop and read it.

Never enable restriction wholesale. Its lints contradict one another on purpose: it contains both implicit_return (always write return) and needless_return‘s stricter cousins; both exhaustive_enums and exhaustive_structs, which most crates want off; arithmetic_side_effects, which bans +. It is a menu for a team with a specific policy — “no allocation in this module”, “no panics in this binary” — not a quality bar. Clippy even ships a lint called blanket_clippy_restriction_lints whose entire job is to tell you not to enable the group.

The uncomfortable part: the best teaching lints are off

Here is a file. It passes clippy -D warnings on 1.95 with no output at all.

pub fn report(names: Vec<String>, prefix: String) -> usize {
    let title = prefix.clone();
    let collected: Vec<&String> = names.iter().collect();
    let n = collected.len();
    println!("{title}: {n}");
    n
}

Three separate things are wrong with it.

  • prefix.clone() is redundant. prefix is owned and never used again; the clone allocates and copies a whole string for nothing. The lint that catches this, redundant_clone, is in nursery.
  • collected is a pointless allocation. Building a Vec of references purely to call .len() on it allocates and walks the whole iterator; names.len() is one field read. The lint, needless_collect, is also in nursery — and is documented as heuristic, because collecting is sometimes genuinely required to end a borrow early.
  • Both parameters are taken by value and never consumed. The function requires the caller to give up ownership of a Vec<String> and a String in order to read their lengths. Every caller must now either clone or lose their data. The lint, needless_pass_by_value, is in pedantic.

Not one of them fires by default. This is the single most important fact about clippy: its defaults are calibrated so that a large existing codebase can turn it on without drowning, and that calibration is about noise, not about quality.

💡Given that, why does this course deny only the default groups for the first few tracks instead of turning on pedantic immediately? click to reveal

Because of a rule worth stating explicitly: never deny a lint whose fix has not yet been taught.

A learner in Track 1 who has not met borrowing cannot act on needless_pass_by_value — the fix is “take &[String] instead”, and & does not appear until Track 3. Denying it would produce a failure the learner is structurally unable to repair, and the lesson they would actually learn is “the tool is an adversary and the site is broken”.

So the ladder is deliberate. Tracks 0 to 3 run default groups, with dead_code explicitly discussed rather than silently sprung. Tracks 4 to 6 tighten. Tracks 7 to 10 add a curated handful of pedantic lints — needless_pass_by_value, explicit_iter_loop, match_same_arms and a few more — never the whole group. Tracks 11 to 13 add the performance set, which is also the point where clippy and the benchmark start agreeing with each other, and agreeing with the benchmark is what earns clippy its credibility for the rest of the course. Only Track 18 turns pedantic on at deny, because by then idiom is the subject.

The same reasoning applies to your own projects. Turning on pedantic in an established codebase produces thousands of findings and a policy of ignoring clippy. Turning on five named pedantic lints produces a conversation.

#[allow], and its better sibling #[expect]

Sometimes the lint is wrong. Clippy’s own documentation has a “Known problems” section on many lints for exactly this reason, and you should read it before you contort your code to satisfy one.

The escape hatch is an attribute, scoped to whatever item it sits on:

#[allow(clippy::needless_range_loop)]
fn foo() { … }

Better, since Rust 1.81:

#[expect(clippy::needless_range_loop)]
fn foo() { … }

expect does everything allow does and warns you if the lint stops firing. That difference matters more than it sounds. An allow written in 2023 to work around a false positive stays in the file forever, silently suppressing a lint that has since been fixed and might now be catching a real bug. An expect tells you the day it becomes unnecessary. Prefer expect for anything you intend to be temporary, which is almost everything.

There are four levels, applicable to any lint or group: allow, warn, deny, forbid. The last one is special — forbid cannot be overridden by an inner allow further down the file. That is why #![forbid(unsafe_code)] is the standard way to make a crate provably safe, and why this course puts it on every track before Track 17.

Reading a lint properly

When one fires, three things are worth doing before you change any code.

  1. Read the help: line. Most clippy lints ship a machine-applicable suggestion. It is usually literally correct.
  2. Follow the link. Every message ends with a URL to the lint’s page, which has a “Why is this bad?” section and often a “Known problems” section. The first tells you whether you agree; the second tells you whether the lint does.
  3. Ask whether the lint is a symptom. A cluster of lints in one function — needless_range_loop, len_zero, ptr_arg all at once — is rarely three independent mistakes. It is usually one function written against the wrong shape of data, and the fix is one rewrite rather than three patches.

What to carry forward

  • Clippy is rustc with more lints, so it understands types, not text.
  • -D warnings denies rustc’s lints too. Unused variables and dead code fail you.
  • Five groups are on by default; correctness among them is deny-by-default and means “this is a bug”.
  • The most educational lints — the cast lints, float_cmp, needless_pass_by_value, redundant_clone — are off by default. Passing clippy is a floor, not a ceiling.
  • Never enable restriction as a group.
  • Prefer #[expect] over #[allow] so your suppressions cannot rot.