Universal Guidelines
Follow the Upstream Guidelines (M-UPSTREAM-GUIDELINES)
The guidelines in this book complement existing Rust guidelines, in particular:
We recommend you read through these as well, and apply them in addition to this book's items. Pay special attention to the ones below, as they are frequently forgotten:
-
C-CONV - Ad-hoc conversions
follow
as_
,to_
,into_
conventions - C-GETTER - Getter names follow Rust convention
-
C-COMMON-TRAITS - Types eagerly implement common traits
Copy
,Clone
,Eq
,PartialEq
,Ord
,PartialOrd
,Hash
,Default
,Debug
Display
where type wants to be displayed
-
C-CTOR -
Constructors are static, inherent methods
- In particular, have
Foo::new()
, even if you haveFoo::default()
- In particular, have
- C-FEATURE - Feature names are free of placeholder words
Use Static Verification (M-STATIC-VERIFICATION)
Projects should use the following static verification tools to help maintain the quality of the code. These tools can be configured to run on a developer's machine during normal work, and should be used as part of check-in gates.
- compiler lints offer many lints to avoid bugs and improve code quality.
- clippy lints contain hundreds of lints to avoid bugs and improve code quality.
- rustfmt ensures consistent source formatting.
- cargo-audit verifies crate dependencies for security vulnerabilities.
- cargo-hack validates that all combinations of crate features work correctly.
- cargo-udeps detects unused dependencies in Cargo.toml files.
- miri validates the correctness of unsafe code.
Compiler Lints
The Rust compiler generally produces exceptionally good diagnostics. In addition to the default set of diagnostics, projects should explicitly enable the following set of compiler lints:
[lints.rust]
ambiguous_negative_literals = "warn"
missing_debug_implementations = "warn"
redundant_imports = "warn"
redundant_lifetimes = "warn"
trivial_numeric_casts = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_lifetimes = "warn"
Clippy Lints
For clippy, projects should enable all major lint categories, and additionally enable some lints from the restriction
lint group.
Undesired lints (e.g., numeric casts) can be opted back out of on a case-by-case basis:
[lints.clippy]
cargo = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
correctness = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
style = { level = "warn", priority = -1 }
# These lints are from the `restriction` lint group and prevent specific
# constructs being used in source code in order to drive up consistency,
# quality, and brevity
allow_attributes_without_reason = "warn"
as_pointer_underscore = "warn"
assertions_on_result_states = "warn"
clone_on_ref_ptr = "warn"
deref_by_slicing = "warn"
disallowed_script_idents = "warn"
empty_drop = "warn"
empty_enum_variants_with_brackets = "warn"
empty_structs_with_brackets = "warn"
fn_to_numeric_cast_any = "warn"
if_then_some_else_none = "warn"
map_err_ignore = "warn"
redundant_type_annotations = "warn"
renamed_function_params = "warn"
semicolon_outside_block = "warn"
string_to_string = "warn"
undocumented_unsafe_blocks = "warn"
unnecessary_safety_comment = "warn"
unnecessary_safety_doc = "warn"
unneeded_field_pattern = "warn"
unused_result_ok = "warn"
# Define custom opt outs here
# ...
Lint Overrides Should Use #[expect]
(M-LINT-OVERRIDE-EXPECT)
When overriding project-global lints inside a submodule or item, you should do so via #[expect]
, not #[allow]
.
Expected lints emit a warning if the marked warning was not encountered, thus preventing the accumulation of stale lints.
That said, #[allow]
lints are still useful when applied to generated code, and can appear in macros.
Overrides should be accompanied by a reason
:
#![allow(unused)] fn main() { #[expect(clippy::unused_async, reason = "API fixed, will use I/O later")] pub async fn ping_server() { // Stubbed out for now } }
Public Types are Debug (M-PUBLIC-DEBUG)
All public types exposed by a crate should implement Debug
. Most types can do so via #[derive(Debug)]
:
#![allow(unused)] fn main() { #[derive(Debug)] struct Endpoint(String); }
Types designed to hold sensitive data should also implement Debug
, but do so via a custom implementation.
This implementation must employ unit tests to ensure sensitive data isn't actually leaked, and will not be in the future.
#![allow(unused)] fn main() { use std::fmt::{Debug, Formatter}; struct UserSecret(String); impl Debug for UserSecret { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "UserSecret(...)") } } #[test] fn test() { let key = "552d3454-d0d5-445d-ab9f-ef2ae3a8896a"; let secret = UserSecret(key.to_string()); let rendered = format!("{:?}", secret); assert!(rendered.contains("UserSecret")); assert!(!rendered.contains(key)); } }
Public Types Meant to be Read are Display (M-PUBLIC-DISPLAY)
If your type is expected to be read by upstream consumers, be it developers or end users, it should implement Display
. This in particular includes:
- Error types, which are mandated by
std::error::Error
to implementDisplay
- Wrappers around string-like data
Implementations of Display
should follow Rust customs; this includes rendering newlines and escape sequences.
The handling of sensitive data outlined in M-PUBLIC-DEBUG applies analogously.
If in Doubt, Split the Crate (M-SMALLER-CRATES)
You should err on the side of having too many crates rather than too few, as this leads to dramatic compile time improvements—especially during the development of these crates—and prevents cyclic component dependencies.
Essentially, if a submodule can be used independently, its contents should be moved into a separate crate.
Performing this crate split may cause you to lose access to some pub(crate)
fields or methods. In many situations, this is a desirable
side-effect and should prompt you to design more flexible abstractions that would give your users similar affordances.
In some cases, it is desirable to re-join individual crates back into a single umbrella crate, such as when dealing with proc macros, or runtimes.
Functionality split for technical reasons (e.g., a foo_proc
proc macro crate) should always be re-exported. Otherwise, re-exports should be used sparingly.
Features vs. Crates As a rule of thumb, crates are for items that can reasonably be used on their own. Features should unlock extra functionality that can't live on its own. In the case of umbrella crates, see below, features may also be used to enable constituents (but then that functionality was extracted into crates already).
For example, if you defined a
web
crate with the following modules, users only needing client calls would also have to pay for the compilation of server code:web::server web::client web::protocols
Instead, you should introduce individual crates that give users the ability to pick and choose:
web_server web_client web_protocols
Names are Free of Weasel Words (M-CONCISE-NAMES)
Symbol names, especially types and traits names, should be free of weasel words that do not meaningfully
add information. Common offenders include Service
, Manager
, and Factory
. For example:
While your library may very well contain or communicate with a booking service—or even hold an HttpClient
instance named booking_service
—one should rarely encounter a BookingService
type in code.
An item handling many bookings can just be called Bookings
. If it does anything more specific, then that quality
should be appended instead. It submits these items elsewhere? Calling it BookingDispatcher
would be more helpful.
The same is true for Manager
s. Every code manages something, so that moniker is rarely useful. With rare
exceptions, life cycle issues should likewise not be made the subject of some manager. Items are created in whatever
way they are needed, their disposal is governed by Drop
, and only Drop
.
Regarding factories, at least the term should be avoided. While the concept FooFactory
has its use, its canonical
Rust name is Builder
(compare M-INIT-BUILDER). A builder that can produce items repeatedly is still a builder.
In addition, accepting factories (builders) as parameters is an unidiomatic import of OO concepts into Rust. If
repeatable instantiation is required, functions should ask for an impl Fn() -> Foo
over a FooBuilder
or
similar. In contrast, standalone builders have their use, but primarily to reduce parametric permutation complexity
around optional values (again, M-INIT-BUILDER).
Prefer Regular over Associated Functions (M-REGULAR-FN)
Associated functions should primarily be used for instance creation, not general purpose computation.
In contrast to some OO languages, regular functions are first-class citizens in Rust and need no module or class to host them. Functionality that
does not clearly belong to a receiver should therefore not reside in a type's impl
block:
struct Database {}
impl Database {
// Ok, associated function creates an instance
fn new() -> Self {}
// Ok, regular method with `&self` as receiver
fn query(&self) {}
// Not ok, this function is not directly related to `Database`,
// it should therefore not live under `Database` as an associated
// function.
fn check_parameters(p: &str) {}
}
// As a regular function this is fine
fn check_parameters(p: &str) {}
Regular functions are more idiomatic, and reduce unnecessary noise on the caller side. Associated trait functions are perfectly idiomatic though:
#![allow(unused)] fn main() { pub trait Default { fn default() -> Self; } struct Foo; impl Default for Foo { fn default() -> Self { Self } } }
Panic Means 'Stop the Program' (M-PANIC-IS-STOP)
Panics are not exceptions. Instead, they suggest immediate program termination.
Although your code must be panic-safe (i.e., a survived panic may not lead to inconsistent state), invoking a panic means this program should stop now. It is not valid to:
- use panics to communicate (errors) upstream,
- use panics to handle self-inflicted error conditions,
- assume panics will be caught, even by your own code.
For example, if the application calling you is compiled with a Cargo.toml
containing
[profile.release]
panic = "abort"
then any invocation of panic will cause an otherwise functioning program to needlessly abort. Valid reasons to panic are:
- when encountering a programming error, e.g.,
x.expect("must never happen")
, - anything invoked from const contexts, e.g.,
const { foo.unwrap() }
, - when user requested, e.g., providing an
unwrap()
method yourself, - when encountering a poison, e.g., by calling
unwrap()
on a lock result (a poisoned lock signals another thread has panicked already).
Any of those are directly or indirectly linked to programming errors.
Detected Programming Bugs are Panics, Not Errors (M-PANIC-ON-BUG)
As an extension of M-PANIC-IS-STOP above, when an unrecoverable programming error has been detected, libraries and applications must panic, i.e., request program termination.
In these cases, no Error
type should be introduced or returned, as any such error could not be acted upon at runtime.
Contract violations, i.e., the breaking of invariants either within a library or by a caller, are programming errors and must therefore panic.
However, what constitutes a violation is situational. APIs are not expected to go out of their way to detect them, as such
checks can be impossible or expensive. Encountering must_be_even == 3
during an already existing check clearly warrants
a panic, while a function parse(&str)
clearly must return a Result
. If in doubt, we recommend you take inspiration from the standard library.
// Generally, a function with bad parameters must either
// - Ignore a parameter and/or return the wrong result
// - Signal an issue via Result or similar
// - Panic
// If in this `divide_by` we see that y == 0, panicking is
// the correct approach.
fn divide_by(x: u32, y: u32) -> u32 { ... }
// However, it can also be permissible to omit such checks
// and return an unspecified (but not an undefined) result.
fn divide_by_fast(x: u32, y: u32) -> u32 { ... }
// Here, passing an invalid URI is not a contract violation.
// Since parsing is inherently fallible, a Result must be returned.
fn parse_uri(s: &str) -> Result<Uri, ParseError> { };
Make it 'Correct by Construction' While panicking on a detected programming error is the 'least bad option', your panic might still ruin someone's day. For any user input or calling sequence that would otherwise panic, you should also explore if you can use the type system to avoid panicking code paths altogether.
Magic Values are Documented (M-DOCUMENTED-MAGIC)
Hardcoded magic values in production code must be accompanied by a comment. The comment should outline:
- why this value was chosen,
- non-obvious side effects if that value is changed,
- external systems that interact with this constant.
You should prefer named constants over inline values.
// Bad: it's relatively obvious that this waits for a day, but not why
wait_timeout(60 * 60 * 24).await // Wait at most a day
// Better
wait_timeout(60 * 60 * 24).await // Large enough value to ensure the server
// can finish. Setting this too low might
// make us abort a valid request. Based on
// `api.foo.com` timeout policies.
// Best
/// How long we wait for the server.
///
/// Large enough value to ensure the server
/// can finish. Setting this too low might
/// make us abort a valid request. Based on
/// `api.foo.com` timeout policies.
const UPSTREAM_SERVER_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 24);