If there is one thing that kills velocity in a senior engineering team, it’s bike-shedding over code style during pull requests.
“Can you add a newline here?”
“This unwrap() looks dangerous.”
“Please sort these imports.”
It’s 2025. We shouldn’t be doing this manually anymore. As Rust developers, we have access to some of the most sophisticated static analysis tools in the programming world. If you aren’t ruthlessly automating your code quality checks, you are leaving productivity (and safety) on the table.
In this guide, we aren’t just going to run cargo fmt. We are going to build a production-grade linting strategy. We’ll cover configuring Rustfmt for strict consistency, dialing up Clippy to catch architectural flaws, and dipping our toes into Custom Lints for domain-specific rules.
Prerequisites #
Before we dive in, ensure your environment is ready. We assume you are working with a standard Rust project structure.
- Rust Toolchain: Stable channel (1.83+ recommended for latest lint features).
- VS Code (Optional): With the rust-analyzer extension installed.
- A Cargo Project: If you don’t have one, create a dummy one:
cargo new lint_masterclass cd lint_masterclass
Step 1: Strict Formatting with rustfmt.toml
#
By default, cargo fmt is great, but it can be better. The default settings are designed to be non-controversial, but for a professional codebase, we often want stricter controls, particularly regarding imports.
Create a file named rustfmt.toml in your project root.
The Configuration #
We are going to enable options that organize imports automatically, preventing merge conflicts and “messy header” syndrome.
# rustfmt.toml
# Sync with the edition you are using (likely 2021 or 2024)
edition = "2021"
# The killer feature: Merge imports from the same crate
# e.g., use std::sync::{Arc, Mutex};
imports_granularity = "Crate"
# Group imports: std, external crates, internal crates
group_imports = "StdExternalCrate"
# Wrap comments at 80 or 100 chars for readability
comment_width = 100
max_width = 100
# aggressive formatting for macro defs
format_macro_matchers = trueWhy this matters #
Without imports_granularity, you often end up with ten lines of use crate::module::... which makes reading the diffs difficult. By grouping them, you reduce visual noise.
Run it:
cargo fmtStep 2: The Enforcer - Configuring Clippy #
Clippy is more than a linter; it’s a teacher. However, the default “Warn” settings are often ignored by developers in a rush. In a professional setting, we want to promote specific warnings to errors, especially regarding safety (like unwrap) and performance.
Since Rust 1.74, the standard way to configure lints is via [workspace.lints] in your Cargo.toml. This is far superior to adding # ![deny(clippy::all) ] to every main.rs.
The Configuration #
Open your Cargo.toml and append this section:
# Cargo.toml
[package]
name = "lint_masterclass"
version = "0.1.0"
edition = "2021"
[dependencies]
# ... dependencies
[workspace.lints.rust]
# Standard Rust lints
unsafe_code = "forbid" # Only if you are writing pure safe Rust
missing_debug_implementations = "warn"
missing_docs = "warn"
[workspace.lints.clippy]
# The basics
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
# Specific rules we want to ENFORCE (Deny)
unwrap_used = "deny" # Force proper error handling
expect_used = "deny" # Force proper error handling
panic = "deny" # No panics in production code
# Specific rules we want to IGNORE (Allow)
# Sometimes pedantic is too pedantic
module_name_repetitions = "allow"The Logic Behind the Config #
unwrap_used= “deny”: This is arguably the most important rule for production software. Using.unwrap()is a ticking time bomb. It forces developers to usematch,if let, or the?operator.pedantic: We enable this but set priority to-1. This means we want to see these warnings, but specific overrides (likemodule_name_repetitions) will take precedence.
Running the Check #
To see this in action, write some “bad” code in src/main.rs:
fn main() {
let x: Option<i32> = None;
// This should trigger a compile error now, not just a warning
println!("{}", x.unwrap());
}Run:
cargo clippy --fixNote: --fix will attempt to automatically resolve trivial issues, though it can’t fix logic errors like unwrap.
Step 3: Domain-Specific Lints with Dylint #
Sometimes, Clippy isn’t enough. Maybe your company has a rule: “Never use the std::time::SystemTime directly; use our internal MonotonicClock wrapper.”
Writing a compiler plugin is hard. Using Dylint is much easier. Dylint allows you to load dynamic libraries as linting rules.
Setup Dylint #
First, install the tool:
cargo install cargo-dylint dylint-linkYou can use pre-built libraries or write your own. For this quick guide, let’s look at how to run a collection of extra lints.
# Add a dylint template to your project (conceptual flow)
# In reality, you define a workspace metadata entryAdd this to Cargo.toml:
[workspace.metadata.dylint]
libraries = [
{ git = "https://github.com/trailofbits/dylint", pattern = "examples/general/crate_wide_allow" }
]Run it:
cargo dylint --all --workspaceNote: Dylint adds compile-time overhead. I recommend running standard Clippy in your rapid dev loop and Dylint only in CI or pre-commit hooks.
Tooling Comparison #
It can be confusing to know which tool handles what responsibility. Here is a breakdown:
| Feature | Rustfmt | Clippy | Dylint / Custom |
|---|---|---|---|
| Primary Goal | Code Style & Formatting | Correctness & Performance | Domain Rules & Architecture |
| Config File | rustfmt.toml |
Cargo.toml / clippy.toml |
Cargo.toml metadata |
| Fix Capability | Excellent (Auto-format) | Good (--fix) |
Low (Manual fix usually) |
| Execution Speed | Instant | Fast | Slow (compiles drivers) |
| Example Rule | Sorting imports | unwrap_used (Safety) |
“Don’t use log::info!” |
The Automated Pipeline #
To make this effective, you cannot rely on human memory. You need a pipeline. Here is the ideal workflow for a high-performance Rust team in 2025.
Implementing a Git Hook #
Create a file .git/hooks/pre-commit and make it executable (chmod +x).
#!/bin/sh
# Fail fast if formatting is wrong
echo "Running Rustfmt..."
cargo fmt -- --check
if [ $? -ne 0 ]; then
echo "Rustfmt failed. Please run 'cargo fmt'."
exit 1
fi
# Fail if Clippy finds errors (deny warnings)
echo "Running Clippy..."
cargo clippy -- -D warnings
if [ $? -ne 0 ]; then
echo "Clippy failed. Please fix the issues above."
exit 1
fi
echo "Pre-commit checks passed!"Performance & Best Practices #
- Don’t lint everything in CI: Dylint or extensive Kani/verification checks can take minutes. Split your CI into “Fast Checks” (fmt, clippy) and “Deep Checks” (tests, dylint, audit).
- Shared Configuration: If you have multiple microservices, don’t copy-paste
clippy.toml. Create a workspace-level crate or a shared script to sync configs. - Use
#[allow(...)]sparingly: When you suppress a lint, always add a comment explaining why.#[allow(clippy::unwrap_used)] // Safe because we validated the regex above let regex = Regex::new("...").unwrap();
Conclusion #
Setting up proper linting is the highest ROI activity you can do for your Rust codebase. By configuring rustfmt to be strict and clippy to be pedantic, you eliminate entire classes of bugs and arguments before they even reach code review.
In 2025, the tools are mature. There is no excuse for unformatted code or unchecked unwraps. Take the rustfmt.toml and Cargo.toml configurations from this guide, drop them into your project, and watch your code quality soar.
Further Reading: