blog.rust.gay

The clippy lints that actually catch bugs

Clippy is a powerful static analysis tool for Rust, offering hundreds of lints. However, a critical mistake many developers make is treating it as a generic style guide rather than a defect detection mechanism. The useful question is not how many lints can be enabled, but which lints catch real defects—issues that the compiler accepts but which lead to runtime errors, deadlocks, data races, or performance bottlenecks in production systems. This post outlines a strategy for moving beyond basic configuration, focusing on high-signal correctness and suspicious patterns, and provides deep dives into practical, maintainable lint policies for systems programming, aiming for a robust CI/CD pipeline that prevents defects before they ship.

High-Signal Correctness and Suspicious Patterns

Clippy's correctness lints are the highest-signal group because they target code that is technically valid according to Rust's rules but structurally or logically flawed. These lints are not stylistic suggestions; they flag inherent programming errors.

For example, absurd_extreme_comparisons is not just about bad coding; it indicates a failure in input validation or type constraints. If a comparison like 100 > i32::MAX occurs, the result is non-deterministic or trivial, masking potential off-by-one errors in data handling. Similarly, almost_swapped catches a = b; b = a;, a classic anti-pattern that should always be replaced by std::mem::swap for clarity, performance, and safety guarantees against race conditions in concurrent contexts.

In asynchronous Rust, the suspicious group is perhaps the most vital for production stability. Lints like await_holding_lock and await_holding_refcell_ref warn against holding non-async-aware locks or RefCell borrows across an await point. In asynchronous systems, holding a lock while yielding execution guarantees a deadlock or severe contention. This lint moves the focus from \\"is the code compilable?\\" to \\"is the code safe and performant in a concurrent environment?\\" These warnings are often silent killers in large, distributed services, not just compile-time errors.

Practical Configuration Strategies for Monorepos

Applying lint policies universally (clippy::all) is rarely feasible in large codebases. The practical approach involves strategic adoption:

  1. Default Run: Always start with cargo clippy --all-targets to establish a baseline against all targets (tests, benchmarks, library code).
  2. Targeted Adoption: Only enable specific category lints (clippy::pedantic, or individual correctness/suspicious lints) where the potential defect signal matches the project's risk profile.
  3. Layered Policies: Use a combination of source attributes (#[allow(...)], #[warn(...)]) for local, item-specific exceptions, and the [lints.clippy] table in Cargo.toml for crate-level policies.

Crucially, avoid blanket clippy::restriction. This lint is opinionated and can often conflict with high-performance, idiomatic Rust, leading to unnecessary churn. If an opinionated lint is desired, it must be debated and applied surgically.

Integrating Clippy into CI/CD Pipelines

For CI, the goal is strictness, but not inflexibility.

The standard command, cargo clippy --all-targets, produces warnings. To enforce a failure, relying solely on cargo clippy's exit code for warnings can be brittle across Cargo versions. The superior, modern approach (available in newer Cargo versions) is to set build.warnings = \"deny\" within Cargo.toml. This integrates the lint policy directly into the build contract.

For compatibility with older systems, setting the environment variable CARGO_BUILD_WARNINGS=deny in the CI environment achieves the same enforcement.

A robust CI baseline typically includes:

cargo clippy --all-targets --all-features

Followed by ensuring the Cargo.toml adheres to [build] warnings = \"deny\". This ensures the failure is a fundamental build issue, not a CI tool quirk.

Managing Exceptions with Reviewability

No lint policy is perfect; exceptions are inevitable. The anti-pattern is the unchecked, module-wide #[allow(...)].

The best practice is narrow scope and mandatory reasoning. When an exception is necessary, it must be limited to the specific function or struct requiring it. Furthermore, teams should enforce a convention—perhaps requiring an allow_attributes_without_reason lint (or similar custom lint) to ensure a string explaining why the exception exists is present. This transforms an arbitrary bypass into a documented technical debt item that is visible during code review and scheduled for resolution, preventing the policy from becoming stale or permanent.

Clippy’s configuration capabilities, through clippy.toml or .clippy.toml, allow for project-wide tuning of thresholds or naming rules, but this configuration should only fine-tune the lint, never justify its presence or mitigate a core defect.

The Long-Term Value: Defect Prevention

The true value of Clippy lies in preventing subtle, hard-to-reproduce defects common in high-concurrency or complex data structures:

  • Data Race Potential: await_holding_lock directly targets the condition for a data race in asynchronous Rust.
  • Logic Bugs: absurd_extreme_comparisons flags logic errors rooted in type/range misuse, not just style.
  • Concurrency Failures: almost_swapped prevents subtle logic failures in critical paths.

By focusing the CI gate on these high-signal, defect-preventing lints, development teams shift from reactive debugging (finding bugs in staging/production) to proactive correctness (preventing bugs in CI).

Conclusion

Clippy is an effective tool when used as a precise technical reviewer, not a generic style enforcer. Start by validating lints against the primary documentation (rust-lang.github.io/rust-clippy/). Focus on correctness and suspicious categories. Establish a layered policy: default run, targeted adoption, and narrow exceptions with mandatory reasoning. By integrating these lints into the build contract (using build.warnings = \"deny\") and treating failures as opportunities for code improvement, teams can leverage Clippy to dramatically increase the reliability and robustness of systems code, ensuring that the code that ships is not just compilable, but genuinely correct and safe for production environments.

date: '2026-10-13' tags: - rust - tooling excerpt: Which lints earn their keep in CI. source: https://rust-lang.github.io/rust-clippy/ source_name: rust-lang.github.io


Clippy has a large lint catalog, but the lints that pay rent in CI are the ones that flag code that is already wrong or very likely to become wrong. The useful question is not "how many lints can I enable?" It is which lints catch real defects early, and how to configure them so failures stay actionable.

The Clippy lint list groups lints by category and shows each lint's default level and applicability. That page is the right starting point when you need to verify what a lint does, whether it defaults to warn or deny, and whether its suggestion is machine-applicable.

Start with correctness and suspicious lints

Clippy's correctness lints are the highest-signal group because they target code the compiler accepts but the lint considers wrong. Many of these lints are deny-by-default in Clippy. Examples from the lint catalog include:

  • absurd_extreme_comparisons — flags comparisons that are always true or always false because one side is outside a type's possible range, such as 100 > i32::MAX.
  • almost_swapped — catches a = b; b = a; sequences that look like a failed swap and suggests std::mem::swap.
  • approx_constant — warns when a floating-point literal approximates a standard constant and suggests values from std::f32::consts or std::f64::consts.
  • bad_bit_mask — detects bit-mask comparisons whose result is constant because the mask cannot produce the compared value.
  • async_yields_async — flags async blocks that yield a future-like value without awaiting it, which usually means an await is missing.

The suspicious group is also worth attention in CI. These lints warn about patterns that often indicate mistakes even when they are not strictly always wrong. Examples include await_holding_lock and await_holding_refcell_ref, which warn about holding non-async-aware locks or RefCell borrows across await points — a common source of deadlocks, contention, or runtime panics in async code.

Use categories before cherry-picking everything

Clippy exposes lint groups such as clippy::all (the default set), clippy::pedantic, clippy::restriction, and category groups. The practical path for most teams is:

  1. Run the default set first: cargo clippy.
  2. Fix or explicitly allow warnings that do not fit the codebase.
  3. Add selected pedantic or category lints where they catch defects you care about.
  4. Avoid blanket-enabling clippy::restriction; it contains opinionated lints that can conflict with idiomatic code.

For CI, cargo clippy --all-targets is useful because it checks tests, benches, and examples in addition to normal targets. If you want warnings to fail the build, prefer Cargo's build.warnings = "deny" configuration (available in newer Cargo versions) or set CARGO_BUILD_WARNINGS=deny in CI. The older cargo clippy -- -D warnings form still works, but it can invalidate build caches; treat it as a compatibility choice rather than the default recommendation.

Make exceptions explicit

No lint policy survives contact with a real codebase unless exceptions are narrow and reviewable. Clippy supports command-line lint levels (-A, -W, -D), source attributes (#[allow(...)], #[warn(...)], #[deny(...)]), and Cargo's [lints.clippy] table.

When you suppress a lint, keep the scope small. Allow it on the specific item or module that needs the exception, not the whole crate, unless there is a deliberate project-wide policy. Some teams also use restriction lints such as allow_attributes_without_reason to require a reason string; that is a style choice, but it makes future cleanup easier because reviewers can see why the exception exists.

Clippy can also be configured with clippy.toml or .clippy.toml for lints that accept project-level settings. This is useful when the right threshold or naming rule depends on the codebase, but it should not replace judgment: configuration can tune a lint, not make an irrelevant lint valuable.

A small CI baseline

A bug-focused Clippy baseline usually looks like this:

cargo clippy --all-targets --all-features

Then add policy deliberately:

[build]
warnings = "deny"

Or, in Cargo.toml, manage Clippy lint levels through [lints.clippy] so the policy lives with the crate. Whichever mechanism you choose, review the failures. A lint that is noisy in your codebase is a configuration problem, an exception, or evidence that the lint is not a fit — not a reason to ignore the whole report.

Conclusion

The Clippy lints that catch bugs are usually the unglamorous ones: impossible comparisons, broken swaps, suspicious async behavior, incorrect constants, and bit operations that can never be true. Start from correctness and suspicious categories, verify lint behavior in the official index, and keep CI strict only where the signal is real. Clippy is most effective when it behaves like a precise reviewer, not a wall of generic advice.

Sources


← back to posts