Rust Doctor
Open navigation
← Back to blog

What rust-doctor catches that clippy alone misses

Clippy reads compiled Rust. Half of what breaks a release is not Rust: a manifest, a git index, a config file, or the shape of code across files.

Arthur Jean

Clippy is very good, and rust-doctor runs it. 37 of the 62 rules in the catalog are Clippy lints, curated rather than the whole set.

The other 25 are native detectors, and they exist for a reason that has nothing to do with cleverness. Clippy reads one thing: Rust that compiles. A large share of what breaks a release never reaches the compiler at all. It sits in a manifest, in a config file, in the git index, or in the shape of code spread across files no single function ever sees.

Here is what falls outside Clippy's input, with what rust-doctor does about it.

Your manifest is not Rust

Cargo.toml, Cargo.lock and .cargo/config.toml decide what your code actually is: which versions resolve, which checks run, what ships. Clippy never opens them.

[dependencies]
internal-utils = { path = "../../shared/internal-utils" }
telemetry = { git = "https://github.com/example/telemetry" }
serde = "1"

Three problems, all invisible to the compiler on the machine where they were written, because on that machine everything resolves.

The path dependency leaves the workspace, so the build works for its author and for nobody else. The git dependency has no rev, so two builds a week apart compile different code under the same version number. And a binary crate whose Cargo.lock is not committed resolves fresh versions on every clone, which is the difference between a build you can reproduce and a build you can only hope about.

.cargo/config.toml is worse, because it silences things quietly:

[build]
rustflags = ["-A", "warnings", "-C", "overflow-checks=off"]

The first flag turns every lint in the workspace into nothing, including the ones you thought were protecting you. The second makes integer overflow wrap silently in release instead of panicking. Run Clippy against that workspace and it reports a clean tree, correctly: you asked it to.

rust-doctor judges the manifests as their own producer, with 11 rules. It also reads [lints] for allow entries that switch off a catalogued rule from the manifest, which is the same trick one file up.

Your git index is not Rust either

$ git ls-files | grep -i env
.env.production

The compiler does not care. Clippy does not care. Everyone who clones the repository gets your production credentials.

Three detectors read what git actually tracks, not what is on disk: a tracked file whose name marks it as secret-bearing, a credential literal matching a closed list of prefixes (AKIA, ghp_, github_pat_, sk-, xoxb-, private key blocks), and a Cargo target directory that .gitignore does not cover.

The matched value is never republished by the report. Naming what leaked, in a file that then travels to CI logs and issue trackers, would leak it again.

Duplication does not live inside one function

Clippy analyses items. It is very good at telling you that this function would read better a different way. It has no opinion about the fact that this function is the fourth copy of the same fifty lines with the field names changed.

fn handle_order_created(event: &Event) -> Result<(), Error> { /* 50 lines */ }
fn handle_order_updated(event: &Event) -> Result<(), Error> { /* the same 50 */ }
fn handle_order_shipped(event: &Event) -> Result<(), Error> { /* again */ }

That is the single most common shape in agent-written Rust, because generating a variant is cheaper than finding the abstraction. Each copy compiles. Each one passes Clippy. Together they are the reason the next change has to be made in four places, and will be made in three.

rust-doctor reports a clone family as one finding, with the other members named in its related locations, rather than as a pile of pairs. It also flags functions past a cyclomatic threshold of 20 or a cognitive threshold of 25, files no mod declaration reaches (Cargo compiles no such file, so it rots undetected), and units grown past the point where splitting them is cheap.

Exemptions are the interesting part

#![allow(clippy::unwrap_used)]

One line at the top of a file, and the rule you enabled stops existing for everything below it, including code written next year by someone who never saw that line. Clippy honours it, as it must.

rust-doctor catalogues that as a finding of its own, along with attributes stacked three deep on one item, and exemptions carrying no reason = "...". The point is not that exemptions are bad. It is that an exemption is a decision, and a decision nobody can see is not one.

Two patterns worth reading the source for

Two detectors work on source text because their pattern is textual, and both sit in the top tier:

let client = Client::builder()
    .danger_accept_invalid_certs(true)   // "just for staging"
    .build()?;

Command::new("sh").arg("-c").arg(format!("git clone {user_input}"))

TLS verification switched off, and a shell command built from runtime data. Both compile. Both are ordinary Rust. Neither is a Clippy lint, because neither is a language-level mistake.

The other half: what to fix first

Even where Clippy sees everything, it tells you everything, flat. Point it at a year-old codebase and you get 800 warnings, which is the same as getting none: nobody triages 800 of anything.

rust-doctor scores the workspace out of 100 and ends by naming three rules. Not the three most frequent, the three worth the most: what each one currently costs the score, discounted by how often that rule was measured wrong on healthy public code. Every rule in the catalog was adjudicated against ten repositories pinned by commit, and publishes that rate.

A rule expected to be worth nothing is left out of the ranking rather than listed last, and the report says it was withheld and why. A rule that is loud in your codebase and absent from the list of what to fix has to be legible without recomputing anything.

Use both

rust-doctor is not a Clippy replacement, and a scan that cannot run Clippy says so and drops its score's authoritative flag rather than pretending the missing pass found nothing.

The catalog is 37 curated lints plus the 25 things Clippy structurally cannot see, scored together, ranked once.

npx -y rust-doctor@latest .

No network, no upload, no telemetry.