rustfmt is not a style guide and that's fine
Introduction: The Formatting vs. Style Distinction
rustfmt formats Rust code according to defined style guidelines, but it is crucial to recognize that it is not a style guide itself—and this distinction is profoundly useful for team productivity and mental well-being. The formatter provides a mechanical baseline for code layout, ensuring aesthetic consistency across a large, distributed codebase. In contrast, "style"—which encompasses naming conventions, API design, error handling philosophies, module organization, and code review judgment—remains firmly in the realm of human, technical, and architectural decisions. The primary pitfall many teams fall into is treating rustfmt as a taste engine, leading to unnecessary friction, endless debates over tabs versus spaces, and resentment. By viewing rustfmt as infrastructure—a predictable, low-level tool for code presentation—and reserving style discussions for high-leverage, human-centric architectural points, development teams can vastly reduce diff noise and accelerate code review velocity. This shift is not about minimizing debate; it's about focusing the debate where it matters most: the quality, safety, and maintainability of the design, not the alignment of parentheses.
Understanding the Scope and Function of rustfmt
The rustfmt tool is straightforward: it formats Rust code based on its internal ruleset. Its usage typically involves invoking it via cargo fmt, which targets binary and library crates within a project or workspace, ensuring consistent formatting across the entire project structure. For nightly users needing the cutting edge of formatting behavior, running cargo +nightly fmt allows the adoption of newer feature sets, although caution must be exercised due to the pre-release nature of these behaviors. Beyond the integrated Cargo workflow, rustfmt can also operate on standalone files or streams (rustfmt filename or stdin), making it highly flexible for various integration points, such as pre-commit hooks, static analysis tooling, or editor LSP plugins.
For Continuous Integration (CI), the --check mode is the canonical operational pattern. This mode does not actually rewrite code; instead, it exits with 0 if the code is already correctly formatted, and 1 if any changes would be needed or if a formatting error occurs. The standard CI pattern, such as cargo fmt --all -- --check, ensures that no committed code violates the agreed-upon baseline without actually modifying the source files, thus maintaining a clean commit history of logic changes, not formatting changes.
Practical Limitations and Technical Boundaries of the Formatter
While rustfmt is highly effective, it is not a magical code transcriber. The project documentation is explicit about its constraints, which is critical to understanding why formatting cannot resolve all style disputes. The tool requires the input code to be syntactically valid Rust. Complex elements, such as custom macro declarations and usage, are subject to partial formatting, meaning rustfmt might not perfectly lay out a highly intricate macro definition if the macro itself is non-standard or complexly layered. Crucially, comments are generally treated as prose within the code structure, not as elements subject to the same mechanical layout rules as identifiers or expressions.
For advanced or generated code—for instance, code produced by procedural macros, parsing libraries (like pest), or complex DSL implementations—the formatter's ability to produce a "perfect" output is constrained by the initial input structure. If the generated code requires specific, intentional layout (e.g., for readability when dealing with complex trait implementations or multi-line attribute arrays), relying solely on rustfmt can introduce readability debt.
To handle these technical boundaries gracefully, rustfmt provides targeted escape hatches. The #[rustfmt::skip] attribute is the defined mechanism for this. Teams should utilize specific skips, such as #[rustfmt::skip::macros(target_macro_name)] or #[rustfmt::skip::attributes(target_attribute_name)], rather than globally disabling formatting. This disciplined approach ensures that the exceptions are tightly scoped, documented, and prevent the tool from becoming a passive victim of complex, often highly specialized, code generation patterns. The use of these skips should always be viewed as a documented technical exception required for code integrity or complex semantics, not as a preference.
Configuration: Controlled Policy, Not Personal Taste
The configurable nature of rustfmt—via rustfmt.toml (or .rustfmt.toml) in the project root or parent directories, supplemented by global configuration—is often misinterpreted. This configuration surface is not a mechanism to codify personal aesthetic preferences; rather, it is a tool for defining controlled project policy.
Configuration options are segmented into stable and unstable features. Stable options are guaranteed to remain consistent, making them safe for permanent team policy. Unstable options require the nightly Rust toolchain and must be explicitly opted into via the configuration (unstable_features = true) or command-line flags. This distinction is the most practical element of the "not a style guide" argument: a team cannot enforce a volatile, non-stable preference across all project members without introducing massive tooling overhead and maintenance risk.
For example, while a team might prefer a specific line wrapping behavior or brace placement, if that behavior relies on an unstable rustfmt feature, adopting it is a commitment to managing nightly dependencies and potential breakage. The sound engineering decision is to stick to the stable, documented set of features. Configuration should manage how the formatter behaves (e.g., maximum line width, specific token behaviors), not what the desired aesthetic outcome is. If the desired aesthetic is custom and unstable, the correct architectural decision is often to build a small, dedicated formatting utility or stick to established idiomatic patterns, rather than forcing the formatter into an unmaintainable role.
Nuances of Edition Handling and Reproducibility
A source of significant confusion involves the interplay between Rust's edition (which governs parsing rules, like macro capabilities and type system features) and the formatter's internal style edition. When using cargo fmt, the formatter typically infers the edition from the Cargo.toml file. However, when invoking rustfmt directly, it may fall back to a default, such as 2015.
To ensure absolute reproducibility across different execution environments (local development, CI pipelines, editor integrations), the best practice, as noted in the documentation, is to explicitly configure both the edition and the desired style_edition within the rustfmt.toml. For instance, setting edition = "2018" and style_edition = "2024" does not fundamentally change the aesthetic outcome; it ensures that the parser uses the correct language rules while the formatter applies a specific, agreed-upon set of presentation rules. This move elevates the discussion from subjective taste ("I prefer this brace") to objective engineering concern ("We must ensure the CI pipeline and local editor use the same parsing and formatting standard to avoid brittle, environment-dependent diffs"). This focus on reproducibility is the core value of using the formatter correctly—it reduces non-semantic, environment-driven noise in the codebase.
Operationalizing Separation of Concerns in Teams
Stating "formatting is not style" is intellectually correct but practically useless for a development team. The value of rustfmt is realized only when the separation of concerns is enforced and operationalized. Teams must move beyond simply having a rustfmt configuration and integrate it into the core development lifecycle:
1. Git Hooks (Pre-Commit): Implement pre-commit hooks (e.g., via pre-commit framework) that run cargo fmt -- --check on staged files. This prevents unformatted code from even entering the repository, making formatting a development invariant, not a code review checklist item.
2. CI/CD Gates: The CI pipeline must include the cargo fmt --all -- --check step before running linting or tests. This ensures code quality and stylistic conformity without creating extraneous diffs. The CI gate should fail if formatting is incorrect, providing immediate feedback to the developer.
3. Internal Coding Standards: The team's internal documentation (e.g., coding_standards.md) must not list formatting preferences (e.g., "use rustfmt"). Instead, it should mandate which tool is used (rustfmt), how it is enforced (pre-commit hook, CI gate), and where to find the configuration (rustfmt.toml). Style guidelines (e.g., "API functions should return Result or use functional patterns," "Private fields should use _snake_case") must be documented separately, reviewed by human experts, and enforced by lints (like Clippy), not by rustfmt.
This operational structure transforms rustfmt from a passive aesthetic suggestion into an active, automated guardrail, allowing human reviewers to focus 100% of their energy on architectural decisions, algorithmic correctness, safety invariants, and API ergonomics—the things that genuinely differentiate good Rust from average Rust.
Conclusion: From Mechanical Baseline to Engineering Efficiency
The useful compromise is clear: rustfmt must own mechanical formatting, while human experts own high-level design and style. This separation removes the tool from the context of aesthetic debate and places it firmly in the context of engineering efficiency. By operationalizing rustfmt through pre-commit hooks and CI gates, teams stop wasting time debating tabs versus spaces, and start gaining time by focusing code reviews on module boundaries, error type design, type system usage, and documentation quality. This shift transforms the perceived annoyance of a formatting check into a powerful, non-negotiable guarantee of codebase consistency. When the code is predictably formatted, the mental load on reviewers decreases dramatically. The goal is not perfect beauty; the goal is predictable, cheap, and safe Rust.
Sources
- rustfmt documentation
- rustfmt README (rust-lang/rustfmt)
- rustfmt configuration reference (Configurations.md)
- Rust Style Guide