refactor-rust · git:20260930.dc5adef · 2026-09-30 · sha256 0068cacf141a8660

refactor-rust git:20260930.dc5adefA

Immutable. This exact content never changes and is served at /api/v1/blob/0068cacf141a8660.

---
name: refactor-rust
description: >
  Rust norms: borrowing over cloning, newtypes over primitive obsession, module layout
  that mirrors the domain and no panics on a public path. Use when editing Rust files.
paths:
  - "**/*.rs"
user-invocable: false
metadata:
  stage: rule
  since: "1.0.0"
---

# Rust Refactoring

## Idioms

- Max 40 lines per function. Max 4 parameters; beyond that, a struct with a builder
  only if the builder earns its rung (`rules/minimalism-ladder.md`).
- Borrow by default. Take `&str` not `String`, `&[T]` not `Vec<T>`, and return owned
  values only when the caller needs ownership.
- `impl Trait` in argument position for simple generic bounds; a named generic when the
  caller must be able to name the type.
- Derive rather than implement: `Debug`, `Clone`, `PartialEq`, `Default` — hand-written
  versions drift from the fields.
- Newtypes over primitive obsession: `struct UserId(u64)` makes the wrong argument a
  compile error.
- Modules mirror the domain, not the layer. `pub(crate)` by default; `pub` is a
  contract (the `api-convention` skill).

## Errors are types, not strings

- Libraries define an error enum with `thiserror`; binaries use `anyhow` at the edge.
  Do not make a library return `anyhow::Error` — it erases the match.
- `?` for propagation. Never `.unwrap()` or `.expect()` on a path that can fail at
  runtime; in tests and in `main` they are fine, and `expect` there carries the reason.
- `panic!` means a bug in this code, never bad input from outside.
- No `unwrap_or_default()` where the default silently hides a failure.

## Ownership is the memory model — do not fight it

- **Do not hand-roll pools or arenas to satisfy the borrow checker.** A lifetime
  problem is a design problem; restructure the ownership first
  (`rules/dsa-and-memory.md`).
- Reach for `Rc`/`RefCell` only when the data genuinely has shared ownership, and
  `Arc`/`Mutex` only across threads. Each one is a rung; state why.
- An arena (`bumpalo`, `typed-arena`) is rung 6: only after a profiler shows allocation
  is the bottleneck.
- `clone()` is not a failure. An unnecessary clone in a hot loop is — and the profiler
  says which.

## Unsafe

- `unsafe` requires a `// SAFETY:` comment stating the invariant the caller must uphold
  and why it holds here. No comment, no `unsafe`.
- Keep the block as small as the operation. Wrap it in a safe API and test that API.
- `cargo miri test` on any module containing `unsafe`.

## Async

- Do not hold a `std::sync::Mutex` guard across an `.await` — use `tokio::sync::Mutex`
  or restructure so the lock is released first.
- No blocking IO inside an async fn; `spawn_blocking` for it.
- Every spawned task's `JoinHandle` is awaited or deliberately detached with a comment.
- Prefer structured concurrency (`JoinSet`, scoped tasks) over detached tasks nobody
  owns.

## After refactoring

- `cargo fmt --check` — clean.
- `cargo clippy --all-targets --all-features -- -D warnings` — zero. Consider
  `-W clippy::pedantic` and silence individually with a reason.
- `cargo test` and `cargo test --release` (debug assertions hide overflow panics).
- `cargo audit` / `cargo deny check` — no known-vuln dependency (`rules/quality-bar.md`).
- `#![deny(missing_docs)]` on a published crate; doctests count as tests and run in CI.

## Rust-specific ladder notes

- Rung 2: `std::iter` chains, `slice::binary_search`, `entry()` API — before any crate.
- Rung 4: the project already has `serde`/`tokio`/`regex`? Use it. A second
  serialization or runtime crate is a fork in the codebase.
- Rung 7: a trait with one implementor is not an abstraction, it is indirection. Delete
  it and call the type.
- `#[allow(...)]` carries an issue link and a one-line reason, like any suppression.