rustfmt Ends the Style Argument: One Formatter, Every File
Rust solved the formatter wars before they started: rustfmt is the official formatter, ships with the toolchain, and its defaults are what nearly every project uses. The pitch to a skeptical team is simple — formatting stops being a code-review topic and becomes a mechanical cargo fmt.
cargo fmt # rewrite every crate in place
cargo fmt -- --check # CI mode: print a diff, exit non-zero (do NOT rewrite)
cargo fmt -- --edition 2021 # format with a specific edition's rules
rustfmt src/main.rs # format one file directly
What rustfmt guarantees: consistent indentation (4 spaces), line wrapping at 100 columns, standardized comma and brace placement, and — the one people underestimate — it reorders nothing. rustfmt never moves statements or imports for you (import sorting arrives via cargo fmt's nightly group_imports option only), so a format pass can never change behavior. That reordering guarantee is why running cargo fmt on a colleague's branch produces reviewable, mechanical diffs.
Project configuration lives in rustfmt.toml (or .rustfmt.toml) at the crate root, read automatically:
# rustfmt.toml — only fields your team actually changes
max_width = 100
use_small_heuristics = "Max"
tab_spaces = 4
edition = "2021"
Resist the urge to configure. Most options require nightly rustfmt (unstable_features = true), and a file full of experimental knobs guarantees your CI formatter version will disagree with someone's local one — the exact "works on my machine" the tool exists to prevent. The professional default is zero config, or at most max_width and edition.
Two workflow notes. First, rustfmt formats whole module trees — cargo fmt reaches every mod your crate root declares, including files it finds through #[path]; stray files not in the tree get formatted by naming them directly. Second, format on save in your editor plus cargo fmt --check in CI makes the whole question vanish; the pre-commit hook is optional belt-and-suspenders (.git/hooks or a tool like pre-commit). When a review comment says "nit: spacing" appears zero times per sprint, that is rustfmt working.
Where rustfmt stops is worth knowing before you fight it. Macro invocations are reformatted only when rustfmt can parse the contents as Rust tokens; anything it cannot parse — a macro with its own grammar, a complex println! format string, generated code — is left byte-for-byte alone. Comments are preserved and moved only as far as necessary, so a comment above a wrapped line stays above it.
Changing max_width is the one config edit with teeth: it reformats the entire tree on the next run. Do it in a dedicated commit (cargo fmt with nothing else staged), never mixed with logic changes, or the review becomes unreadable. Same rule for the nightly-only import options, which are the only rustfmt settings teams regularly want:
rustup component add rustfmt --toolchain nightly
cargo +nightly fmt -- -g imports_granularity=crate # merge/sort imports once
They are gated behind unstable_features = true, which is exactly why a repository must not depend on them for its default cargo fmt — a stable-only CI runner cannot reproduce them, and the formatter version disagreement you were avoiding comes back through the side door.
The CI gate deserves its final form, because both halves matter:
- run: cargo fmt --all -- --check # style: fail the build, rewrite nothing
- run: cargo clippy --all-targets -- -D warnings
The second half is the interesting one: cargo clippy alone reports warnings and still exits 0, so a pipeline that "runs clippy" without -D warnings enforces nothing. A gate that cannot fail is not a gate — this single flag is the difference between a linting culture and a linting costume.
clippy Catches What the Compiler Allows: Lint Levels in Practice
rustc only rejects code that is wrong. clippy flags code that is legal but suspicious — it is a separate lint driver built on rustc's own AST, shipped as a rustup component, run through cargo:
cargo clippy # current crate + deps that build from source
cargo clippy --all-targets # also tests, examples, benches — do this in CI
cargo clippy -- -D warnings # every clippy warning becomes an error (CI gate)
cargo clippy --fix --allow-dirty # apply machine-applicable suggestions
A taste of what it finds on idiomatic-looking code:
for i in 0..v.len() { use(&v[i]); } // clippy::needless_range_loop
let s = x.clone(); if x > 0 { } // clippy::clone_on_copy (for Copy x)
if x == true { ... } // clippy::bool_comparison
let _ = std::mem::size_of::<T>() / 4; // clippy::size_of_in_element_count etc.
Each lint has a name (clippy::needless_range_loop) and a level that controls what happens when it fires:
| Level | Effect |
|---|---|
allow |
silent |
warn |
reported (clippy's default for most lints) |
deny |
reported and cannot be downgraded by attributes |
forbid |
like deny, and nested allow attributes cannot override it |
You steer clippy from three altitudes. Globally, in Cargo.toml:
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
needless_range_loop = "allow"
Per item or module, with an attribute — and the house rule is that every #[allow] carries a stated reason:
#[allow(clippy::cast_precision_loss)] // counts fit f64 exactly: < 2^53 items
fn mean(counts: &[u64]) -> f64 { ... }
#[allow] without a comment is how lint debt starts; reviewers should treat it as a question. The lint groups scale deliberately: correctness (deny-by-default bugs), suspicious, style, complexity, perf, then the opt-in tiers pedantic (strict but reasonable) and nursery (experimental). Most production crates run default clippy plus a few pedantic picks — turning on pedantic = "warn" wholesale and silencing it in bulk is worse than not enabling it.
This is what a clippy diagnostic actually gives you, and why teams adopt it wholesale:
warning: the loop variable `i` is only used to index `v`
--> src/lib.rs:14:5
|
14 | for i in 0..v.len() { probe(&v[i]); }
| ^^^^^^^^^^^^^^^^^^^^ help: consider using an iterator
|
= help: for further information, run `cargo clippy -- -W clippy::needless_range_loop`
Name, file, offending span, a rewording of the fix, and the flag to learn more. cargo clippy --fix --allow-dirty applies the machine-applicable subset directly, which turns most of these into a one-command cleanup — but read the diff, because an automatic iterator rewrite can change borrow behavior at the edges.
Rustc's own lints belong in the same discussion. unused_variables, dead_code, and unreachable_patterns are on by default as warnings, and you can steer them from the crate root:
#![warn(unused)]
#![deny(clippy::unwrap_used)] // library code: panics belong to the caller
Resist #![deny(warnings)] inside source. It freezes today's warning set into your crate, so the next compiler release — which adds new lints — breaks your build without a single line of your code changing. The robust arrangement is warn in the source, deny in the invocation: keep attributes at warn, and let CI pass -D warnings where the rustc version is pinned by the toolchain file.
For a library crate, the restriction group is where policy becomes enforcement: unwrap_used, expect_used, indexing_slicing, and integer_arithmetic are off by default because they are opinionated, and enabling them per-module (#![deny(clippy::unwrap_used)] only in src/core/) marks the code where a panic would be a real defect rather than a fast-fail at the boundary.
Doc Tests Make Your Examples Executable
Rust documentation is a build artifact, not a wiki page. Triple-slash /// comments attach to the item that follows and are parsed by rustdoc into structured docs:
/// Parses a comma-separated list of ports.
///
/// # Errors
/// Returns [`ParseIntError`] when any field is not a number.
///
/// # Examples
/// ```
/// let ports = orders::parse_ports("80,443").unwrap();
/// assert_eq!(ports, vec![80, 443]);
/// ```
pub fn parse_ports(s: &str) -> Result<Vec<u16>, std::num::ParseIntError> { ... }
Three properties make this better than prose. First, the fenced rust block is a doc test: cargo test compiles it as its own crate against your public API and runs the asserts. A drifted example fails CI with the exact snippet that broke. Second, rustdoc's sections are conventions the community reads everywhere — # Examples, # Errors, # Panics, and # Safety for unsafe fn. Rust API guidelines make "document the errors" part of review culture. Third, intra-doc links in square brackets — [ParseIntError], [String::push], [crate::parser] — resolve to real items and become hyperlinks in the rendered docs; cargo doc will warn (rustdoc::broken_intra_doc_links) when a link target does not exist.
Doc-test control details you will need within a week of real use:
/// ```no_run // compiles, does not execute (starts a server)
/// ```ignore // does not compile at all — last resort, prefer fixing it
/// ```should_panic # Panics on empty input
/// assert!(orders::parse_ports("").is_err());
/// ```rust,no_run // combine modifiers
/// ```text // never treated as a test (for non-Rust output)
Private items take //! inner docs at the top of a file or /// on the item — and private doc examples do not run unless you enable --document-private-items plus --test flags, so keep runnable examples on the public surface. Building and reading the result:
cargo doc --no-deps # just your crate — fast
cargo doc --no-deps --open # render and open in a browser
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps # CI: docs are compile-checked
The --no-deps flag matters the moment a dependency pulls 300 transitive crates into the build. Add #![warn(missing_docs)] at the crate root once a library goes public: every undocumented public item then produces a warning, and the compiler — not a policy document — enforces the standard.
Three habits separate documentation that ships from documentation that rots.
First, module-level docs use //! at the top of the file and answer "what is this module for?" — the entry point a reader hits before any item. Second, do not maintain the same prose twice: a crate's front matter can be pulled straight from its README into the crate-level docs with one line at the top of src/lib.rs:
#![doc = include_str!("../README.md")]
The README becomes the crate's landing page on docs.rs with no copy-paste step and no drift.
Third, build docs the way the ecosystem will read them. docs.rs compiles with --all-features, so a feature-gated example that only compiles without your async feature will pass local CI and break the published page — cargo doc --no-deps --all-features reproduces the real conditions. Doc tests are a real test target with real cost: cargo test --doc runs only them, cargo test --doc parse_ports narrows to a single example, and cargo test -- --test-threads=1 serializes examples that share a port or a temp file. And for unsafe fn, the # Safety section is not decorative — it is the contract #![deny(unsafe_op_in_unsafe_fn)] and reviewers enforce: document the invariants the caller must uphold, or the function should not be unsafe.
Quiz
1. What is the correct CI invocation to enforce both formatting and lint hygiene for a crate?
2. A crate sets clippy::pedantic = "warn" in Cargo.toml but one numeric-heavy module floods with cast warnings. What is the right way to silence just that module?
3. What happens when you run `cargo test` on a crate whose doc comment contains a fenced rust block with a failing assert_eq!?
4. Which statement about rustfmt is accurate?
Flashcards
Question
What is the CI-safe way to enforce formatting, and why not just run cargo fmt?
Click to reveal answer
Answer
`cargo fmt -- --check` — it prints a diff and exits non-zero without touching files; CI must never rewrite and commit code. Locally you run `cargo fmt` (or format-on-save). rustfmt is deterministic and behavior-preserving (wrapping/spacing only, no reordering), which is why blanket format passes are safe.
Question
What are clippy's four lint levels, and which pair cannot be overridden by attributes?
Click to reveal answer
Answer
allow (silent), warn (reported), deny (reported, error), forbid (reported, un-overridable). deny and forbid cannot be relaxed back to allow by an attribute in the code being linted — forbid additionally cannot even be shadowed by nested allows. That is why correctness lints default to deny while style lints default to warn.
Question
How do you add a dependency and lint configuration in Cargo.toml, and how does priority interact with groups?
Click to reveal answer
Answer
[lints.clippy] section with entries like `pedantic = { level = "warn", priority = -1 }` — priority -1 makes the group apply first so individual lint entries (e.g. `needless_range_loop = "allow"`) can override it. Without priority, a group and a single lint at the same level conflict in Cargo's lint table.
Question
Name the doc-test modifiers and what each does.
Click to reveal answer
Answer
```rust (default: compile AND run) · no_run (compile only) · should_panic (test passes if it panics) · ignore (skip entirely — last resort) · compile_fail (asserts the snippet fails to compile) · text (not Rust, never touched). Modifiers combine with commas: ```rust,no_run.
Question
What are intra-doc links, and what enforces them?
Click to reveal answer
Answer
Square-bracket links like [`String::push`] or [`ParseIntError`] in doc comments that rustdoc resolves to the actual item and renders as hyperlinks. rustdoc::broken_intra_doc_links (on by default as a warning) reports dead links; `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps` in CI turns doc problems into build failures.
Revision Notes
Key Takeaways
- 1. rustfmt is deterministic and behavior-preserving — it never reorders code, so blanket format passes are safe; CI enforces with --check, never by rewriting
- 2. clippy runs on top of rustc and flags legal-but-suspicious code; lint levels escalate allow → warn → deny → forbid, and every #[allow] should carry a written justification
- 3. Fenced rust examples in /// comments are compiled and executed as doc tests by cargo test — documentation that drifts fails CI
- 4. Intra-doc links resolve to real items and are checked; rustdoc warnings promoted with RUSTDOCFLAGS=-D warnings make docs part of the build contract
- 5. The three tools form one pipeline: fmt normalizes shape, clippy questions intent, rustdoc executes examples — each has a one-line CI gate
Interview Tips
- • "How do you keep a large team's code style consistent?" — you don't debate it: rustfmt defaults checked with cargo fmt --check in CI plus format-on-save; mention that rustfmt guarantees no reordering, which is why it is safe on shared branches
- • "What does clippy give you beyond rustc?" — name a concrete lint (needless_range_loop, bool_comparison) and the level system; showing #[allow] with a justification comment signals review maturity
- • "Are doc examples real tests?" — yes: extracted, compiled against the public API, executed by the harness; mention modifiers like no_run for servers and compile_fail for API misuse demos
- • "How would you enforce docs in CI?" — RUSTDOCFLAGS=-D warnings cargo doc --no-deps plus broken intra-doc links; optionally #![warn(missing_docs)] to keep public API docs complete
Cheat Sheet
Formatting, Lints, Docs
fmt:
cargo fmt # rewrite in place
cargo fmt -- --check # CI: diff + non-zero exit, no writes
Config: rustfmt.toml at crate root — keep it near-empty (max_width, edition); most knobs need nightly.
clippy:
cargo clippy --all-targets -- -D warnings # the CI gate
cargo clippy --fix --allow-dirty # auto-apply suggestions
| Level | Meaning |
|---|---|
| allow | silent |
| warn | clippy default for style-ish lints |
| deny | error, not overridable by attributes |
| forbid | deny + nothing can relax it |
Scope: crate-wide via [lints.clippy] in Cargo.toml (groups need priority = -1); per-item via #[allow(clippy::name)] // justification.
Docs:
/// Summary line.
/// # Errors / # Panics / # Safety / # Examples sections
/// ```rust,no_run (modifiers: no_run, should_panic, compile_fail, ignore, text)
/// [`ParseIntError`] <- intra-doc link, checked by rustdoc
- Doc tests compile+run under
cargo test(cargo test --docfor only these) cargo doc --no-deps --open· CI:RUSTDOCFLAGS="-D warnings" cargo doc --no-deps#![warn(missing_docs)]at crate root to enforce doc coverage