8.6 KiB
trigger, description
| trigger | description |
|---|---|
| model_decision | When writing Rust code, working on a Rust or Cargo project, or reviewing Rust idioms and safety practices |
Rust Idioms and Patterns
Core Philosophy
Rust's type system and ownership model are your primary tools for correctness. Lean into the compiler — it is your strongest ally. Write code that is idiomatic, safe, and expressive.
Scope: This file covers Rust-specific coding idioms. For file layout, see
project-structure-rust-cargo.md. For test naming conventions, seetesting-strategy.md. For logging library choice, seelogging-and-observability-principles.md.
Ownership and Borrowing
-
Prefer borrowing (
&T,&mut T) over cloning- Never
.clone()to silence the borrow checker without a// CLONE:comment explaining why - Use
Cow<'_, T>when a function may or may not need ownership - Prefer
&stroverStringin function parameters,&[T]overVec<T>
- Never
-
Minimize owned data in structs
- Use references with explicit lifetimes when the struct is short-lived
- Use owned types (
String,Vec<T>) when the struct must outlive its inputs
-
Avoid unnecessary
Arc<Mutex<T>>- If data flows one direction, use channels (
tokio::sync::mpsc) - If data is read-heavy, consider
RwLockoverMutex - If data is immutable after init, use
Arc<T>without a lock
- If data flows one direction, use channels (
Error Handling
-
Use the
?operator for propagation — neverunwrap()in production codeunwrap()andexpect()are acceptable only in:- Tests (
#[test],#[tokio::test]) - Infallible operations where the invariant is proven (document with
// SAFETY:comment) - CLI
main()function with clear error messages viaexpect("reason")
- Tests (
-
Choose error crates by context:
- Library code:
thiserror— define typed error enums - Application code:
anyhow— ergonomic error chaining - Never mix: library crates should not depend on
anyhow
- Library code:
-
Error type design:
// ✅ Good — typed, matchable errors
#[derive(Debug, thiserror::Error)]
pub enum PathfinderError {
#[error("file not found: {path}")]
FileNotFound { path: PathBuf },
#[error("AST parse failed: {0}")]
ParseError(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
// ❌ Bad — stringly-typed, unmatchable
fn do_thing() -> Result<(), String> { ... }
// ✅ Annotate public Result-producing functions to force callers to handle them
#[must_use]
pub fn create_task(req: CreateTaskRequest) -> Result<Task, TaskError> { ... }
// Triggers a compiler warning when the return value is fully ignored:
// create_task(req); // warning: unused `Result` that must be used
//
// This does NOT trigger the warning (intentional discard — still valid when used deliberately):
// let _ = create_task(req); // explicit discard, silences warning by design
Async and Concurrency
-
Use
tokioas the async runtime- Mark async entry points with
#[tokio::main]or#[tokio::test] - Prefer
tokio::spawnfor concurrent tasks, notstd::thread::spawn - Use
tokio::select!for racing futures, not manual polling
- Mark async entry points with
-
Cancellation safety:
- Prefer
tokio::sync::mpscovertokio::sync::broadcastunless fan-out is needed - Document cancellation behavior on any
async fnthat holds resources across.await - Use
tokio_util::sync::CancellationTokenfor graceful shutdown
- Prefer
-
Blocking operations:
- Never call blocking I/O inside async context
- Use
tokio::task::spawn_blockingfor CPU-heavy or blocking work - Use
tokio::fsinstead ofstd::fsinside async functions
Unsafe Code
-
Zero
unsafeblocks unless in FFI boundaries- Tree-sitter C bindings and similar FFI are the only valid use case
- Every
unsafeblock must have a// SAFETY:comment explaining the invariant
-
Minimize unsafe surface area:
- Encapsulate
unsafein a safe wrapper function - The wrapper's public API must be safe to call from any context
- Write tests that exercise the boundary conditions of
unsafewrappers
- Encapsulate
-
Never use
unsafeto bypass the borrow checker — restructure the code instead
Lifetimes and Generics
-
Prefer
'_lifetime elision when possible- Only introduce named lifetimes when the compiler requires them or when they clarify intent
- Use
'afor single lifetime parameters, descriptive names ('input,'query) for multiple
-
Keep generic bounds simple:
- Prefer concrete types for prototyping, introduce generics when the pattern stabilizes
- Use
impl Traitin argument position for simple cases - Use
whereclauses for complex bounds — never inline complex bounds in<...>
-
Avoid lifetime gymnastics:
- If lifetime annotations become complex, restructure to use owned data or
Arc - Consider the "split borrow" pattern to avoid borrow checker issues in struct methods
- If lifetime annotations become complex, restructure to use owned data or
Idiomatic Patterns
-
Builder pattern for types with many optional fields:
- Return
Selffrom builder methods for chaining build()returnsResult<T, BuildError>, notT
- Return
-
Newtype pattern for domain types:
- Wrap primitives:
struct UserId(u64), not bareu64 - Implement
Derefonly when the newtype truly "is-a" the inner type
- Wrap primitives:
-
Typestate pattern for state machines:
- Different states = different types — invalid transitions are compile errors
- Use this for protocol implementations and lifecycle management
-
From/Intoconversions:- Implement
From<A> for B(neverIntodirectly) - Use
impl From<X> for Errorwiththiserror's#[from]attribute
- Implement
Testing
-
Test organization (Rust-specific — differs from Go/TS):
- Unit tests:
#[cfg(test)] mod testsblock at the bottom of each.rsfile — this is the idiomatic Rust convention, not a shortcut- Tests can access private functions via
use super::* - Code inside
#[cfg(test)]is stripped from production builds - Do NOT create separate
*_test.rsfiles — this breaks private access and is non-idiomatic
- Tests can access private functions via
- Integration tests:
tests/directory at crate root (each file compiled as a separate crate)- Only tests public API — use
use my_crate::function; - No
#[cfg(test)]annotation needed - Shared helpers:
tests/common/mod.rs(NOTtests/common.rs, which Cargo treats as a test file)
- Only tests public API — use
- Use
#[tokio::test]for async tests
- Unit tests:
-
Test naming:
fn test_<function>_<scenario>_<expected>()(snake_case) -
Assertions:
- Use
assert_eq!/assert_ne!overassert!(a == b)— better error messages - Use
assert!(matches!(result, Ok(_)))for enum variant checking
- Use
-
Property testing: Use
proptestorquickcheckfor functions with wide input spaces
Clippy and Formatting
-
cargo checkfor fast iteration during developmentcargo check: type-checks without producing a binary — fastest feedback loopcargo clippy: includescargo checkplus lint rules — use before committingcargo build: only when you need the actual binary/library artifact- Never run
cargo buildduring TDD cycles — it is significantly slower thancargo check
-
cargo clippymust pass with zero warnings before any commit- Use
#[allow(clippy::...)]only with a// ALLOW:comment explaining why - Prefer fixing the lint over suppressing it
- Use
-
cargo fmtis non-negotiable — all code must be formatted -
Recommended project-level Clippy configuration (
.clippy.tomlorCargo.toml):
[lints.clippy]
pedantic = "warn"
unwrap_used = "deny"
expect_used = "warn"
Dependency Management
- Minimize dependency count — each dependency is an attack surface and compile-time cost
- Pin major versions in
Cargo.toml— usedep = "1"notdep = "*" - Audit regularly — run
cargo auditto check for known vulnerabilities - Prefer well-maintained crates — check download count, last commit date, and issue tracker
Related Principles
- Error Handling Principles @error-handling-principles.md
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
- Performance Optimization Principles @performance-optimization-principles.md
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
- Security Mandate @security-mandate.md
- Code Idioms and Conventions @code-idioms-and-conventions.md
- Testing Strategy @testing-strategy.md
- Dependency Management Principles @dependency-management-principles.md