Files
skills-public/code-languages/rust.md
T
BluscreamandClaude Opus 5 32b47b075b fix(coding-dev): prefer a test over the build script for size limits
A shell check only runs where that script runs. A test travels with the repo,
needs no extra tooling, and fails with a message naming the offending files.
Enforcement is now a preference order: native linter rule > test > build
script as fallback, with a worked example and a note on why a per-compile
build hook usually is not worth the incremental-build cost.

Propagated per language: rust (clippy has no file-length lint -> workspace
root test), js-typescript (ESLint max-lines covers both natively, no test
needed), go/python/csharp (linter for functions, test for files), bash (don't
reimplement in the gate what a test can assert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:52:37 +02:00

196 lines
8.2 KiB
Markdown

---
name: rust
description: >-
Use this skill when writing, building, linting, testing, or releasing Rust code — including
workspace lint configuration, clippy/fmt gates, error handling with Result/thiserror,
unsafe policy, async patterns, and cargo workspace/submodule layout.
---
# Rust — Rules & Preferences
Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds Rust specifics.
---
## 1. The Gate
Nothing is "done" until all of these are clean:
```bash
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features
cargo test --workspace
cargo doc --workspace --no-deps
cargo build --release
```
> [!IMPORTANT]
> **`--workspace` is not optional.** Without it only the root package is checked and every
> member crate's warnings stay invisible. This has hidden real warnings for weeks at a time.
> `--all-targets` additionally covers tests, benches, and examples — which are also code.
- `cargo fmt` before every commit. **No hand-formatting**, ever.
- `cargo doc` matters: a broken intra-doc link is a reference to something that no longer
exists, which is worth knowing about.
- Run the project's build script (`./scripts/build.sh`) if it has one — it wires these
together plus size checks and a smoke run.
---
## 2. Lints — Pedantic On, Configured In One Place
```toml
# Cargo.toml (workspace root)
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
too_many_lines = "warn"
unwrap_used = "warn"
[workspace.lints.rust]
unsafe_code = "forbid"
```
```toml
# every member crate
[lints]
workspace = true
```
> [!CAUTION]
> **Lint config lives in `[workspace.lints]` only.** A package that repeats the configuration
> carries a second copy that drifts — which is how a binary once kept reading its own stale
> allows while the workspace's were being actively edited.
- **Never `#[allow]` on a whim** — but when a lint is genuinely wrong for a piece of code,
**allow it at the narrowest scope that covers it** (the expression, the function, the
module) with a comment saying why:
```rust
// The FFI callback signature is fixed by the C API; we cannot take fewer arguments.
#[allow(clippy::too_many_arguments)]
extern "C" fn on_event(/* … */) { }
```
- A **workspace-wide** allow in `[workspace.lints]` is for the rare case where the lint is
wrong for the *entire* codebase — not a shortcut for one stubborn call site. It silences
the rule in code nobody has written yet.
- **Re-test every `#[allow]` periodically.** If removing one produces no warning, delete it.
- `too_many_lines` is the mechanical enforcement of the 100-line function limit. A function
that trips it wants extracting, not an allow.
- Clippy has **no file-length lint**, so the 1000-line file limit belongs in a test — see
[`coding-dev.md` §4](skills-public/coding-dev.md) for the pattern. Put it in the workspace
root's `tests/` so one test covers every member crate, walking `src/` plus each
`crates/*/src`. `cargo test --workspace` then enforces it for free.
---
## 3. Errors & Panics
- **`Result` and `Option` everywhere.** `thiserror` for library error enums;
`anyhow` is acceptable at the application/binary boundary only.
- **No `unwrap()` or `expect()` outside tests and `main`.** Anything that can be absent at
runtime — a device, a file, a network peer, an env var — must be handled. Hardware and
network resources appear and disappear while the program runs.
- No `panic!` as flow control. A library never panics on input it was designed to receive.
- Never `?`-swallow context — attach it (`.context(...)` / a typed variant) so the error says
where it came from.
---
## 4. `unsafe`
**Forbidden by default**, enforced in `Cargo.toml` with `unsafe_code = "forbid"`.
If an FFI binding genuinely requires it, it goes behind a **safe wrapper in exactly one
module** (see [`c-cpp.md`](skills-public/code-languages/c-cpp.md) for the other side of that boundary), with a `// SAFETY:` comment on every block stating the invariant being upheld.
`unsafe` scattered through call sites is not acceptable.
---
## 5. Dependencies & Workspace Layout
Follow the global ordering (reuse → fork → write). In Rust specifically:
- Search crates.io and check maintenance status before adding. Prefer pure-Rust crates over
ones requiring a C toolchain when the difference is not material — a C dependency turns a
one-command build into a container requirement.
- **Pin precisely when it matters.** If you need an API that only exists on a git revision,
pin the revision and **write down why** — a future reader will otherwise "helpfully"
bump it to the crates.io release that lacks the API.
- **Reusable logic goes in its own crate**, published separately, with the application as a
consumer. The workspace layout that follows from this:
```
crates/<name>-core/ # all reusable logic, no UI, no framework deps
crates/<name>-proto/ # wire types & protocol parsing, dependency-light
src/ # or crates/<name>-ui — presentation only
```
- **Submodule hygiene**: commit and push inside the submodule crate first, then commit the
updated SHA pointer in the parent. Match each repo's own commit-trailer and identity
conventions — a public library repo and a private app repo often differ deliberately.
---
## 6. Async
- Tokio runtime with `futures` streams; `async_trait` where trait methods must be async.
- **Model new flows as streams or async methods**, matching what the codebase already does.
- **Never block in async context.** Use `spawn_blocking` for genuinely blocking work.
- Subscribe to event streams; never poll on a timer (see the global events-over-polling rule).
- Don't short-circuit a hand-written `Stream` impl that manually pumps backends — it usually
encodes ordering guarantees that are not obvious from the signature.
---
## 7. Feature Flags & `cfg`
- Feature flags declared in the root `Cargo.toml`.
- **Gate at module level, not per function** — per-function `cfg` leaves a trail of empty
stubs and makes the real platform differences hard to see.
- Adding a platform backend: declare the feature, create the gated module, and **log which
backend was selected at startup**. Silent backend selection is very hard to debug remotely.
- Use `build.rs`-exported configs for conditions that aren't plain features.
---
## 8. Style Details
- `unsafe`-free, `unwrap`-free, and formatted — see above.
- **No `pub` on anything that doesn't need to cross a module boundary.** Default to private
and widen deliberately.
- Max ~5 parameters; past that, pass a struct.
- Prefer `f32` for UI/geometry when the framework's native unit is `f32` — mixing in `f64`
produces a stream of casts and rounding inconsistencies.
- Derive rather than hand-implement (`Debug`, `Clone`, `Default`, `PartialEq`) unless the
hand implementation is doing something a derive cannot.
- `#[non_exhaustive]` on public enums/structs that are likely to grow.
---
## 9. Testing
- Unit tests inline in `#[cfg(test)] mod tests`; integration tests in `tests/`.
- `cargo test --workspace` — plus `--release` when timing or optimization matters to the
behavior under test.
- **Regression test every fixed bug.**
- A dummy/mock backend that exercises the full pipeline without real hardware is worth
building early — it is the only way most device code gets tested at all.
- Never point test output at real hardware. Route test signal to a virtual/null endpoint and
**verify the route resolved** — most tools silently fall back to the system default when
the requested target does not exist, which sends your test straight at the user.
---
## 10. Building in Containers
When the crate needs system headers the host does not have (audio, graphics, webkit),
build inside the project's designated container. Chain `cd` inside the same command string —
entering a container resets the working directory:
```bash
distrobox enter <container> -- bash -c "cd /path/to/project && cargo build --release"
```
Prefer the project's `scripts/build.sh` over a hand-typed invocation; it also handles job
throttling so a release build does not saturate the machine.
**Split so that as little as possible needs the container**: a `-core` crate with no GUI
dependencies builds and tests directly on the host, which is seconds instead of minutes.