一键导入
language-rust
Use when writing, reviewing, or refactoring Rust — ownership, lifetimes, type-driven design, async, error handling, FFI, and performance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing, reviewing, or refactoring Rust — ownership, lifetimes, type-driven design, async, error handling, FFI, and performance.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | language-rust |
| description | Use when writing, reviewing, or refactoring Rust — ownership, lifetimes, type-driven design, async, error handling, FFI, and performance. |
build() returns Result — required fields are Option internally; build() errors when missing. Never unwrap_or_default() a required field — that silently produces a broken value:
// WRONG: pub fn build(self) -> Pool { Pool { dsn: self.dsn.unwrap_or_default(), .. } }
// RIGHT: pub fn build(self) -> Result<Pool, BuildError> {
// let dsn = self.dsn.ok_or(BuildError::MissingDsn)?;
// Ok(Pool { dsn, max_conns: self.max_conns.unwrap_or(10), .. }) }
pub trait whose impls you control (pipeline stages, visitor hooks, plugin interfaces) gets a private supertrait so downstream impl is impossible, letting you add methods later without breaking callers. Default to sealing a pub trait unless it is explicitly an extension point users are meant to implement. Adding only Send + Sync supertraits is NOT sealing — you need a private supertrait:
mod sealed { pub trait Sealed {} } // private module
pub trait Transform: sealed::Sealed + Send + Sync { // sealed: + sealed::Sealed
fn apply(&self, x: &[f64]) -> Vec<f64>;
}
impl sealed::Sealed for MyStage {} // only this crate can do this
impl Transform for MyStage { fn apply(&self, x: &[f64]) -> Vec<f64> { x.to_vec() } }
#[must_use] to catch silently ignored errors at compile time. Also use on types like MutexGuard wrappers.#[serde(deny_unknown_fields)] on deserialization types to catch typos. #[serde(rename_all = "camelCase")] for API boundaries. #[serde(default)] with explicit Default impl for forward-compatible schemas. Serde derives belong ONLY on API/config boundary types. Internal types (metrics, in-memory state, domain structs that never cross a wire) must NOT derive Serialize/Deserialize — strip the derive when a type is internal, even if it "could" be serialized.PhantomData<T> to distinguish structurally identical types. Zero-sized types as verification proofs. compile_error! for invalid feature flag combinations. #[must_use] on RAII guards and handlesfn transform<Source, Target>(input: Source) -> Target not fn transform<T, U>(input: T) -> U. Single param T is fine#[derive(Clone, Debug, PartialEq, Eq, Hash)] on newtypes. Validate at construction, trust downstream. Implement Display for user-facing types, FromStr for parseable onesconst _: () = { fn assert_send_sync<T: Send + Sync>() {} assert_send_sync::<MyType>(); };
Copy is not enough: the parameters must pass by value too. A Span { start: u32, end: u32 } is 8 bytes — fn overlaps(a: Span, b: Span), never &Span:
#[derive(Clone, Copy)] struct Span { start: u32, end: u32 } // 8 bytes
// WRONG: fn overlaps(a: &Span, b: &Span) -> bool (pointer-chasing a Copy type)
// RIGHT: fn overlaps(a: Span, b: Span) -> bool
&str/&[T] but sometimes needs to modify (normalize, escape, default), use Cow not .to_owned():
// WRONG: fn normalize(s: &str) -> String { if needs_change { modified } else { s.to_owned() } }
// RIGHT: fn normalize(s: &str) -> Cow<'_, str> { if needs_change { Cow::Owned(modified) } else { Cow::Borrowed(s) } }
&self methods but needs internal mutable state (caches, counters, lazy fields), use Cell<T>/RefCell<T>, not Mutex. Mutex in single-threaded code is a code smellArc<Mutex<Node>> (or Box<Node>/Rc<RefCell<Node>>) field web fights the borrow checker and leaks on cycles. Store every node once in an arena Vec and reference others by an index newtype (NodeId(usize)), not by pointer. Removing the Mutex/Arc but keeping a pointer tree is NOT the fix — the references themselves must become indices.
// WRONG: pointer tree — interior locking, cycle leaks, borrow-checker fights
struct AstNode { expr: Expr, parent: Option<Arc<Mutex<AstNode>>>, children: Vec<Arc<Mutex<AstNode>>> }
// RIGHT: arena owns the nodes; parent/children are indices into it
#[derive(Clone, Copy, PartialEq, Eq)]
struct NodeId(usize);
struct AstNode { expr: Expr, parent: Option<NodeId>, children: Vec<NodeId> }
struct Ast { nodes: Vec<AstNode> } // the arena; resolve a NodeId with self.nodes[id.0]
(bumpalo is the crate when you want true bump allocation; the Vec + NodeId shape above is the borrow-checker-friendly default.)HashMap, large struct, [u8; N]) and others are tiny (Literal(i64)), Box the big field so every value of the enum stays small:
// WRONG: Call { callee: Box<Expr>, args: Vec<Expr>, source_map: HashMap<String, Span> }
// RIGHT: Call { callee: Box<Expr>, args: Vec<Expr>, source_map: Box<HashMap<String, Span>> }
Clippy's large_enum_variant flags this.impl Display + impl std::error::Error for an error enum. If you see a manual Display match arm per variant plus impl Error for E {}, replace the whole thing with #[derive(thiserror::Error)] + #[error("...")] attributes:
// WRONG: impl fmt::Display for AppError { match self { Self::NotFound(m) => write!(f, "not found: {m}"), .. } }
// impl std::error::Error for AppError {}
// RIGHT:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("not found: {0}")] NotFound(String),
#[error("unauthorized")] Unauthorized,
}
anyhow/miette for applications..context()/.with_context() — explain WHY and include the inputs, not just WHAT failed. "read failed: {e}" is wrong (restates the io error). Name the operation and the path:
// WRONG: .map_err(|e| AppError::Internal(format!("read failed: {e}")))
// RIGHT: .with_context(|| format!("failed to load template from {}", path.display()))
pub type AppResult<T> = Result<T, AppError>; centralized in each workspace. All functions use the alias, never repeat Result<T, AppError> everywhereColumnNotFound(String), SchemaMismatch { expected, got }, never HashMapError(String) or Other(String) catch-all. No catch-all Other(String) variantInvalidRange { start: usize, end: usize, len: usize } not InvalidRange(String) with format!(...)spawn_blocking/spawned tasks, use mpsc::channel not Arc<Mutex<Vec>>:
// WRONG: Arc<Mutex<Vec>> for collecting task results
// RIGHT: let (tx, rx) = mpsc::channel(items.len());
// for item in items { let tx = tx.clone(); spawn(async move { tx.send(process(item)).await; }); }
// let results: Vec<_> = rx.collect().await;
.await with owned resources (files, connections, locks), document cancellation behavior or implement Drop. Silent resource leaks at .await points are bugstracing over println!/eprintln! everywhere — and instrument silent I/O paths. Replace every println!/eprintln! with the matching tracing macro (info!/warn!/error!/debug!). Beyond replacing existing prints: a function that loads config, opens files, or runs at startup must not be silent — add a tracing::info!/debug! recording what it did (and warn!/error! on failure paths). Use structured fields, not interpolation:
// WRONG: fn load_routes() -> Vec<Route> { let raw = read_to_string("routes.json").unwrap(); .. } // silent
// WRONG: eprintln!("fetch {url} failed: {e}");
// RIGHT:
fn load_routes() -> Vec<Route> {
let routes = /* ... */;
tracing::info!(count = routes.len(), "loaded routes"); // startup path is observable
routes
}
tracing::warn!(%url, error = %e, "fetch failed"); // structured fields, not format args
Vec/VecDeque over LinkedList, always — cache locality dominates; LinkedList is almost never the right choice even when the variable is named "queue". A FIFO queue is VecDeque, a stack/list is Vec. Rewrite any LinkedList to VecDeque (push_back/pop_front) — do not just delete the function.dyn Trait only for binary size or heterogeneous collections. A pipeline / per-element loop calling a trait method should be generic, not &[Box<dyn Trait>]:
// WRONG: fn run_pipeline(stages: &[Box<dyn Transform>], data: &[f64]) -> Vec<f64>
// RIGHT: fn run_pipeline<T: Transform>(stages: &[T], data: &[f64]) -> Vec<f64>
HashMap<u64, _>. Default HashMap uses SipHash (DoS-resistant, slow); integer keys want rustc_hash::FxHashMap or ahash::AHashMap. Changing only a code comment to say "FxHashMap" is wrong — change the actual type and its use/with_capacity_and_hasher constructor:
// WRONG: fn count_tags(t: &[u64]) -> HashMap<u64, usize> (SipHash)
// RIGHT: use rustc_hash::FxHashMap; fn count_tags(t: &[u64]) -> FxHashMap<u64, usize>
criterion (statistical, regression-detecting) or divan (simpler API). Compare against baselines. Never optimize without benchmarks proving the need.cargo flamegraph for CPU profiling, cargo-llvm-lines to find monomorphization bloat, cargo bloat for binary size analysis. Profile in release mode with debug symbols ([profile.release] debug = true).#[inline] only on hot paths proven by benchmarks. Never sprinkle "just in case". LTO + codegen-units=1 handles the rest. #[inline(always)] requires benchmark justificationpar_iter() or work-stealing, not intra-file. Share config via immutable Arc<Config> (never behind Mutex). Cap the thread pool. Directory scan once at startup with HashSet for O(1) lookupscargo miri test to verify undefined behavior. This is not optionalmod sys or -sys crate with safe public API on topcargo-fuzz (libFuzzer) for any code parsing untrusted input (network protocols, file formats, user strings). Add fuzz targets to fuzz/ directory. Run in CI with time-limited sessions.Debug/Display with masking (****). Sensitive buffers zeroed with Zeroizing<T> (from zeroize crate) at Drop. Logs never contain secrets. impl fmt::Debug for ApiKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ApiKey(****)") } }CancellationToken + join all tasks + flush traces. When threading is unavailable, transparent sequential fallback. Adapt rendering for TERM=dumb. Interruption via AtomicBool + signal handlerprintln! (use tracing), HashMap on hot paths (use FxHashMap), Instant::now() (use injectable TimeProvider). Enforce with clippy.toml disallowed-methods and disallowed-typesstd::sync::LazyLock replaces lazy_static! / once_cell::Lazy. A lazy_static! { static ref X: T = init(); } block maps directly to static X: LazyLock<T> = LazyLock::new(init); — prefer LazyLock over OnceLock for lazily-initialized globals (no get_or_init boilerplate):
// WRONG: lazy_static! { static ref ROUTES: Vec<Route> = load_routes(); }
// RIGHT: static ROUTES: LazyLock<Vec<Route>> = LazyLock::new(load_routes);
impl AsRef<Path> for filesystem functions, not String/&str — accepts String, &str, PathBuf, Path interchangeably. Changing a String param to &str is NOT the fix; the target is impl AsRef<Path>:
// WRONG: fn load_template(path: String) // and equally wrong: path: &str
// RIGHT: fn load_template(path: impl AsRef<Path>) -> AppResult<String> {
// let path = path.as_ref(); std::fs::read_to_string(path) .. }
[workspace] for multi-crate projects. [workspace.dependencies] to centralize versions. [workspace.lints] for shared clippy/rustc lint config. Keep -sys crates separate from safe wrapper crates.proptest! to generate random inputs and verify invariants (round-trip, idempotence, no-panic). Complement unit tests, don't replace them.cargo-mutants — after test suite passes, run cargo mutants to find code where mutations survive (tests don't catch behavioral changes). Prioritize fixing surviving mutants in critical paths.cargo nextest over cargo test — parallel test execution, better output formatting, per-test timeout support, JUnit XML output for CI. Drop-in replacement: cargo nextest run.cargo audit (known CVEs) and cargo deny check (licenses + advisories + banned crates) in CI. Pin dependencies with =version in security-critical crates. Use cargo-vet for first-party audit of new deps.cargo xtask codegen, cargo xtask release, cargo xtask bench. Type-safe, cross-platform, with error handling. No Makefiles with complex shell variables#![deny(missing_docs)] on public crates. Compiler forces documentation of every public item. doc(hidden) for generated code items#[expect] over #[allow] — #[expect(clippy::too_many_arguments, reason = "FFI boundary")] breaks compilation when the warning disappears, forcing cleanup. #[allow] hides forever. Every suppression requires a reasoncargo xtask codegen && git diff --exit-code. PR that modifies source but not generated output fails[workspace.lints.clippy] with pedantic = "warn", todo = "deny", dbg_macro = "deny". All crates inherit via [lints] workspace = true. -D warnings in CIBefore you output Rust, grep your own code for these triggers and apply the fix. These are the rules most often missed — applying one (e.g. Copy) is NOT enough, the whole pattern must change.
lazy_static! or once_cell::Lazy or static ref → LazyLock (not OnceLock). Trigger: any lazily-initialized global.impl fmt::Display for <Error> + impl Error → #[derive(thiserror::Error)] with #[error("...")]..map_err(|e| ...format!("X failed: {e}")) / context that restates the error → explain WHY + include the path/input.String or &str (path param) → impl AsRef<Path> (not &str).HashMap, big struct, big array) next to tiny ones → Box the big payload.Arc<Mutex<Node>> / Box<Node> / Rc<RefCell<Node>> in parent/children fields → arena Vec<Node> + NodeId(usize) index references (rewrite the reference fields to indices; dropping the Mutex/Arc while keeping a pointer tree is NOT the fix).HashMap<u64, _> / HashMap<integer, _> → FxHashMap / AHashMap (change the type, not a comment).LinkedList<_> → VecDeque (rewrite to push_back/pop_front; never drop the function).Builder::build(self) -> T returning the struct directly → -> Result<T, _>; required fields error via .ok_or(...)?, no unwrap_or_default().#[derive(Serialize, Deserialize)] → remove the serde derive; keep it only on API/config boundary types.Copy type passed as &T (e.g. &Span, two u32s) → pass by value (Span, not &Span).&[Box<dyn Trait>] (hot path) → generic <T: Trait> &[T].pub trait whose impls you control (pipeline/visitor/plugin), not an explicit user extension point → seal it with a private supertrait: mod sealed { pub trait Sealed {} } then pub trait Foo: sealed::Sealed { .. }. Adding Send + Sync is NOT sealing.println! / eprintln! anywhere → swap to tracing (info!/warn!/error!/debug!). Plus: any fn that loads config / reads files / runs at startup but logs nothing → add a tracing::info!/debug! (and warn!/error! on failure) so the path is observable. Use structured fields.MANDATORY: After ANY modification to Rust files, run clippy before considering your work done. No exceptions -- not for "small changes," not for "I'll run it later."
cargo clippy --all --all-features --all-targets -- -D warnings
Fix all warnings before committing. Clippy warnings are errors (-D warnings) — no #[allow] unless you can justify why the lint is wrong for that specific case.
Use when designing functions, modeling data, choosing types, drawing module boundaries, or deciding what depends on what. Use when evaluating architecture, extracting abstractions, or shaping vertical slices.
Use when writing, reviewing, or refactoring code in any language. Use for architecture decisions, system design, component boundaries, and code quality judgment. Always relevant when touching source code.
Use when writing or reviewing comments, docstrings, names, control flow, or file organization. Use when evaluating readability, choosing identifiers, splitting files, or applying naming conventions. Use when removing AI tells (slop) from code prose — comments, docs, error messages, commit messages, PR descriptions. Covers the visible surface of code.
Run kirby's review engine on a PR or a commit, OUTSIDE the orchestrator loop. Local output only — findings are presented in-conversation, nothing is posted, no Provider config needed.
Launch kirby-bot orchestrator in background, relay phase transitions live from run.jsonl.
Extract and organize existing code into reusable modules, functions, and components with thoughtful APIs.