Skip to content
← Back to explainers

Rust explainer

What clippy -D warnings actually checks

About 13 min to read. Allow extra time to work through examples.

How the Rust grading gate runs Clippy, which warnings it turns into errors, how Clippy's nine lint groups are switched on or off by default, and why passing the gate does not prove the code is well designed.

This article uses Rust. You can make it your practice mode to browse related content across the site.

In Rust mode, a submission must pass two checks: the tests must produce the right answers, and the code must pass a lint gate. A lint is an automatic check that reports code which compiles but is likely to be a mistake, unidiomatic or slower than it needs to be. The gate uses Clippy, the official collection of extra lints for Rust. This article explains what the gate runs, what it rejects, and what it does not check.

What the gate runs

The local runtime on your machine builds one source file from your code followed by code the problem injects (the harness that calls your function). It then runs:

clippy-driver --edition 2024 --emit=metadata -D warnings <file>
  • clippy-driver is the program behind cargo clippy. It runs the normal Rust compiler front end (parsing, name resolution, type checking) with Clippy's lints added.
  • --edition 2024 selects the Rust 2024 edition.
  • --emit=metadata stops after checking, without generating machine code. The gate does not run your code; the tests do that separately.
  • -D warnings sets every lint that would normally print a warning to deny, which makes it an error.

Only diagnostics (the errors and warnings the compiler prints) that point at lines you wrote count against you. The runtime uses whichever Clippy version is installed on your machine, and lints are added, renamed and moved between groups over time, so a different version can report slightly different findings. The examples here were checked with Clippy and rustc 1.98.1.

Because Clippy runs inside the compiler, its lints can use the same information the compiler has: the type of every expression, which traits are implemented, and whether a value is used again later. A purely text-based tool cannot know, for example, that a variable has type Vec<String> or that it is never read after a certain line.

-D warnings applies to rustc's own lints as well as Clippy's. Lints such as unused_variables (a variable is never read), unused_mut (a variable is declared mut but never changed), dead_code (a function or item is never used), unreachable_code and non_snake_case (a variable or function name is not in snake_case) are warnings in a normal build. Under the gate they are errors.

Check your understanding A submission compiles with plain rustc and passes every test, but the gate rejects it. What kinds of reasons can cause this, and which of them come from Clippy? Show answer

A rustc lint fired. unused_variables, unused_mut, dead_code, unreachable_code and non_snake_case are rustc's lints, not Clippy's. A plain rustc build prints them as warnings and still succeeds, so they are easy to miss. -D warnings turns them into errors.

A Clippy lint that is on by default fired. Examples are needless_return (an explicit return on the last line where the value alone would do), len_zero (v.len() == 0 instead of v.is_empty()), ptr_arg (a &Vec<T> parameter where &[T] would accept more callers), manual_range_contains (x >= 1 && x <= 9 instead of (1..=9).contains(&x)) and needless_range_loop (looping over indices only to index one slice). The code is correct; Clippy recommends a different form.

A lint the problem switched on fired. Some starters contain a crate-level attribute such as #![warn(clippy::float_cmp)]. An attribute written #![...] at the top of the file is crate-level: it applies to the whole compiled unit, not to one function. It turns on a lint that is normally off. Because -D warnings turns every warning into an error, a lint enabled at warn level is enough to fail the gate. Keep such lines when the problem tells you to; they are part of the exercise.

The gate also fails in two other situations. If the combined file does not compile at all, nothing was linted, so the check fails. A common cause is a function name or signature that does not match what the harness calls; the error is then in harness code you cannot see, and the runtime reports that your function's name or signature does not match instead of showing that code. And if your code switches the gate off with a crate-level attribute that silences a whole category, such as #![allow(warnings)] or #![allow(clippy::all)], the runtime reports that as a suppression and fails the check.

The nine groups

Each Clippy lint belongs to exactly one group, and the group determines the lint's default level. A level is what the compiler does when the lint finds something: allow (stay silent), warn (print a warning) or deny (report an error). In Clippy 1.98 there are 822 lints:

group lints in 1.98 default level what its lints report
correctness 67 deny Code that is almost certainly wrong or useless.
suspicious 83 warn Code that is probably wrong but may occasionally be intended.
style 158 warn Code that works but is usually written differently in idiomatic Rust.
complexity 143 warn Code that does something simple in a more complicated way.
perf 38 warn Code that has a simple alternative which is usually faster.
pedantic 143 allow Stricter checks with more false positives (reports on code that is actually fine).
restriction 132 allow Checks that forbid particular language or library features. Useful one at a time for a specific policy.
nursery 53 allow Lints that are still being developed and may report incorrectly.
cargo 5 allow Checks on the Cargo.toml manifest. The gate compiles a single file with no manifest, so these do not apply.

The first five groups are on by default. Together they form the group clippy::all, which is what the gate enforces unless a problem turns on extra lints. The counts change between Clippy releases.

Two groups need more explanation.

correctness lints are errors even without -D warnings. For example, absurd_extreme_comparisons reports a comparison with a type's minimum or maximum value in which some case can never happen, or whose result is always the same. For an unsigned n, which cannot be negative, n <= 0 can only be true when n == 0, so writing it as a range check is misleading. The related n >= 0 is always true; Clippy reports it too, and so does rustc's own unused_comparisons lint. not_unsafe_ptr_arg_deref reports a public function that is not marked unsafe but dereferences a raw pointer it received as an argument: safe code could pass it an invalid pointer and cause undefined behavior. never_loop reports a loop whose body can never run a second time. When a correctness lint fires, read it before changing anything, because it usually points at a real bug.

Do not turn on restriction as a whole group. Its lints are meant to be chosen individually, and some contradict lints in other groups. implicit_return (restriction) asks for an explicit return at the end of every function, while needless_return (style, on by default) asks for the opposite, so with both enabled no way of returning a value satisfies both. arithmetic_side_effects reports operations such as a + b on integers, because they can overflow. These lints suit a project with a specific rule, such as "this module must never panic", not a general quality standard. Clippy even includes a lint, blanket_clippy_restriction_lints (in suspicious, so on by default), that warns when the whole restriction group is enabled.

Passing the gate does not mean the code is well designed

The following function produces no diagnostics under the gate with Clippy 1.98:

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
}

It still has three problems:

  • prefix.clone() is unnecessary. prefix is owned by the function and never used after the clone, so title could simply be prefix. Cloning a nonempty String allocates new memory and copies its bytes. The lint that reports this, redundant_clone, is in nursery.
  • collected is unnecessary. The function builds a new Vec of references (allocating when names is nonempty) only to read its length, which equals names.len(). The lint, needless_collect, is also in nursery.
  • Both parameters are taken by value but never consumed. Taking a parameter by value moves it into the function, so a caller who wants to keep its Vec<String> or String has to clone it first. The function only reads them, so borrowed parameters would do. The lint, needless_pass_by_value, is in pedantic.

Compiling the same function with -W clippy::nursery -W clippy::pedantic added reports all three. It also reports must_use_candidate, a pedantic lint that suggests marking the function #[must_use] so callers are warned when they ignore its result. A version that fixes the three problems:

pub fn report(names: &[String], prefix: &str) -> usize {
    let n = names.len();
    println!("{prefix}: {n}");
    n
}

The default groups leave these lints off because they are new, can report code that is actually fine, or reflect a preference that not every project shares. Turning them on in a large existing project tends to produce many reports, and some of them are not worth acting on. So passing the gate means the code avoided the problems the default lints look for. It does not mean the code is well designed.

Check your understanding If some useful lints are off by default, why do problems here not simply enable the whole pedantic group? Show answer

A lint is only useful in an exercise when the learner can act on it. needless_pass_by_value asks you to borrow instead of taking ownership; if borrowing has not been introduced yet, the report gives you a failure you cannot reasonably fix. Enabling the whole group would also reject correct solutions for reasons unrelated to the lesson, such as must_use_candidate in the example above.

So the gate runs the default groups, and a problem that teaches a specific idea turns on the one or two lints that check that idea, with a crate-level #![warn(...)] or #![deny(...)] attribute in the starter code. The same approach works in your own projects: enabling a few chosen lints that the team agrees with is easier to act on than enabling a whole group and facing hundreds of reports at once.

Silencing a lint: #[allow] and #[expect]

Sometimes a lint is wrong for a particular piece of code. Clippy's documentation for many lints has a "Known problems" section that lists cases where the lint reports code that is fine, and it is worth reading before you rewrite code just to satisfy one.

You can change a lint's level for a single item with an attribute. For example, this function triggers needless_range_loop, because it loops over indices only to read one slice element by element. The attribute silences that one lint for that one function:

#[expect(clippy::needless_range_loop)]
pub fn sum(values: &[i32]) -> i32 {
    let mut total = 0;
    for i in 0..values.len() {
        total += values[i];
    }
    total
}

#[allow(clippy::needless_range_loop)] would also silence it. The difference shows up when the lint stops firing, for example after the code is changed or the lint is fixed in a newer Clippy. An unneeded #[allow] stays in the file without any message and keeps silencing that lint. An unneeded #[expect] (stable since Rust 1.81) produces the warning unfulfilled_lint_expectations, which tells you it can be removed. Under -D warnings that warning is an error, so on this gate an #[expect] whose lint does not fire fails the check.

On this site, silencing a single named lint on one item, as in the example above, is accepted. Silencing a whole category at the crate level, such as #![allow(warnings)], #![allow(clippy::all)] or #![expect(clippy::pedantic)], is treated as switching the gate off and fails the check.

The levels you will write most often are allow, warn, deny and forbid, and they can be applied to a single lint or a whole group. forbid is like deny, except that code further in cannot lower it: an #[allow(unsafe_code)] on a function inside a crate marked #![forbid(unsafe_code)] is itself a compile error (E0453). That makes #![forbid(unsafe_code)] a reliable way to ensure that no unsafe block, unsafe fn or unsafe impl appears in that crate's own source. It does not check the crate's dependencies or the standard library, which use unsafe internally.

Reading a Clippy report

When a lint fires, three steps help before you change code:

  1. Read the help: lines. Many Clippy lints include a suggested replacement. Check it against what the code is meant to do before applying it.
  2. Follow the link. Each Clippy diagnostic includes a URL to that lint's documentation, which explains what the lint checks, why it reports it, and often lists known problems.
  3. Look for a shared cause. Several lints in one function, such as needless_range_loop, len_zero and ptr_arg together, often come from one design choice, such as taking a &Vec<T> and indexing it by position. Changing that choice can fix all of them at once.

Summary

  • Clippy runs inside the Rust compiler, so its lints can use type information, not just the text of your code.
  • The gate is clippy-driver --edition 2024 -D warnings. It turns every warning into an error, including rustc's own warnings such as unused variables and dead code. Lints that are off by default stay off unless the problem turns them on.
  • Five of Clippy's nine groups are on by default. Lints in correctness are errors even without -D warnings and usually indicate a real bug.
  • Many useful lints, including needless_pass_by_value, redundant_clone, float_cmp and the cast lints such as cast_possible_truncation, are off by default. Passing the gate is a minimum standard, not proof of good design.
  • Turn on restriction lints one at a time, never as a group.
  • Prefer #[expect] to #[allow] for a single lint, so that you find out when the suppression is no longer needed.

step 4 of 6

Orientation and the Gate

Up next Comments, doc comments, and how to read docs.rs