Rust coding
Workflow
-
Orient. Read the project's sources of truth before writing:
rust-toolchain.toml,Cargo.toml(edition,rust-version,[lints],[workspace]),clippy.toml,rustfmt.toml,.cargo/config.toml, and the CI config,justfile,Makefile, orxtaskfor the canonical check commands. Read the module you are changing and its neighbours; reuse their helpers, error types, and patterns. Done when: you can name the edition, the MSRV, the lint policy, everydisallowed-methods/disallowed-typesentry, and the project's check commands. -
Model the types. Design data before control flow, following Types. Changing a library's public API: read
references/api.md. Done when: every invariant the change relies on is encoded in a type, or held by anexpectwhose message names it. -
Implement in a tight loop. Work in small steps and run
cargo checkafter each. Read the whole diagnostic: itshelp:line usually names the fix, andrustc --explain E0502expands an error code. For a dependency's API, read its source at the locked version (Cargo.lock) under$CARGO_HOME/registry/src/. Mass renames go through rust-analyzer rename orast-grep;cargo clippy --fixapplies machine-applicable suggestions. Bug fix: write the regression test first and watch it go red. Done when: the change compiles with no warnings on lines you changed, and any regression test went red before the fix and green after. -
Verify. Run the project's commands from step 1; otherwise:
cargo fmt --all cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-features- Project with no
[lints]policy: also runcargo clippy --all-targets -- -W clippy::pedanticand fix hits on lines you changed. - Mutually exclusive features: replace
--all-featureswith each feature set CI tests. cargo nextestskips doctests: addcargo test --doc.- Public docs changed:
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features. unsafetouched: runreferences/unsafe.md.- Fix lints at the cause. When suppression is right, scope
#[expect(clippy::lint_name, reason = "...")]to the smallest item. Done when: every command exits 0, lines you changed raise no warnings, and every new suppression carries areason.
- Project with no
-
Self-review the diff. Re-read the full
git diff, not your memory of it, against Rules and the rules of each reference file whose branch the diff touches. Done when: every hunk is required by the task, prototype code anddbg!/todo!are gone, doc comments match behaviour, and every applicable rule holds for every hunk.
Reviewing someone else's code: run step 1, run step 4's commands when you can, then apply step 5 to their diff. Report each finding as path:line, the rule, and the fix.
Rules
Ownership
- Borrow in parameters:
&str,&[T],&Path,&T. TakeString,Vec<T>, orTby value only when the function stores or consumes it. - Every
.clone()earns its place: the callee needs ownership, or the value is a cheap handle (Arc::clone(&handle)). - When the borrow checker objects, restructure first: shorten the borrow, borrow disjoint fields separately,
std::mem::take/replace, indices into aVec. Shared mutable ownership (Rc<RefCell<_>>,Arc<Mutex<_>>) is the last resort. - Return
impl Iterator<Item = T>when the caller only iterates.
Types
- Make illegal states unrepresentable: an
enumcarrying per-state data, overboolflags plusOptionfields; newtypes for IDs, units, and validated values (struct Port(NonZeroU16)). - Parse, don't validate: convert untrusted input once at the boundary (
TryFrom,FromStr,#[serde(try_from = "Raw")]) and pass the parsed type inward. - Match exhaustively on enums you own. A
_arm is right only when a future variant should take that branch too. - Convert integers with
From/TryFrom;astruncates and sign-flips silently.
Errors and panics
- Library: a
thiserrorenum per failure domain. Binary:anyhow::Resultwith.context(...). - Panics mean bugs.
unwrap()lives in tests; production code uses?, orexpect("...")stating the invariant that makes failure impossible.
Current idioms
Older patterns dominate training data. Prefer the current form when rust-version allows it; new crates use edition = "2024". clippy::incompatible_msrv catches newer std APIs only; language and Cargo features need cargo +<msrv> check.
| Prefer | Over | Since |
|---|---|---|
std::sync::LazyLock, OnceLock | lazy_static!, once_cell | 1.80, 1.70 |
let Some(x) = opt else { return ... }; | match + early return | 1.65 |
if let Some(x) = a && x > 0 | nested if let | 1.88, edition 2024 |
native async fn in traits | #[async_trait], still needed for dyn Trait | 1.75 |
async || {} closures, AsyncFn bounds | F: Fn() -> Fut, Fut: Future | 1.85 |
#[expect(lint, reason = "...")] | #[allow(lint)] | 1.81 |
&raw const x, &raw mut x | ptr::addr_of! | 1.82 |
opt.is_some_and(f), opt.is_none_or(f) | map_or(false, f), map_or(true, f) | 1.70, 1.82 |
std::hint::black_box | criterion::black_box | 1.66 |
[lints] in Cargo.toml for shared policy | #![warn(...)] in every crate root | 1.74 |
cargo:: build-script directives | cargo: | 1.77 |
Docs and comments
- Public items get
///docs:# Errorson fallible functions,# Panicson panicking ones, examples as doctests using?. - Comments explain why; the code shows what.
Dependencies
- Reuse what the workspace already depends on (
[workspace.dependencies],cargo tree -i <crate>), and prefer std when it covers the need. - Add with
cargo add(MSRV-aware), enabling only the features you use.
References
Load the one matching the branch you are on:
references/api.md: designing or changing a library's public API, naming, trait impls, semver.references/errors.md: defining error types, propagation, panic versusResult.references/async.md: async, tokio, threads, channels, locks, atomics.references/unsafe.md:unsafe, FFI, raw pointers, manualSend/Sync.references/cargo.md:Cargo.toml, workspaces, features, profiles, build scripts, crate picks.references/lints.md: setting up lint configuration for a crate or workspace.references/testing.md: writing tests, property and snapshot tests, benchmarks.references/perf.md: optimizing a measured hot path.