| name | rust |
| description | Idiomatic Rust conventions, ownership patterns, error handling, naming, clippy, and common AI pitfalls based on the Rust API Guidelines and Effective Rust. Use when writing, reviewing, or refactoring Rust code (.rs files). Do NOT use for Go, TypeScript, or other languages. |
When to use this skill
Load when writing, reviewing, or refactoring Rust code (.rs files), creating new Rust modules, or answering questions about Rust idioms and best practices.
Naming (RFC 430)
| Item | Convention | Example |
|---|
| Types, Traits, Enum variants | UpperCamelCase | HttpClient, ParseError |
| Functions, methods, modules | snake_case | parse_url, is_valid |
| Constants, statics | SCREAMING_SNAKE_CASE | MAX_RETRIES |
| Type parameters | Single uppercase or short CamelCase | T, E, Rhs |
| Lifetimes | Short lowercase | 'a, 'de, 'src |
| Macros | snake_case! | vec!, assert_eq! |
| Crate/feature names | snake_case | Never -rs or -rust suffix |
Acronyms are one word: Uuid not UUID, Stdin not StdIn, HttpError not HTTPError.
Conversion method prefixes
The prefix encodes cost and ownership:
fn as_bytes(&self) -> &[u8]
fn to_string(&self) -> String
fn into_bytes(self) -> Vec<u8>
Getters — no get_ prefix
pub fn name(&self) -> &str { &self.name }
pub fn name_mut(&mut self) -> &mut String { &mut self.name }
pub fn get_name(&self) -> &str { &self.name }
Iterator methods
fn iter(&self) -> Iter
fn iter_mut(&mut self) -> IterMut
fn into_iter(self) -> IntoIter
Constructor patterns
Self::new()
Self::with_capacity(n)
Self::from_bytes(b)
Self::try_from(v)
Error handling
thiserror vs anyhow
| Context | Use | Why |
|---|
| Library / public API | thiserror | Callers can match on typed variants |
| Application / binary | anyhow | Ergonomic context chaining |
| Internal module | thiserror | Structured, matchable within crate |
Error type pattern
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("not found: {resource} with id {id}")]
NotFound { resource: &'static str, id: u64 },
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, AppError>;
Error message style
- Lowercase, no trailing punctuation.
- Allows clean composition:
format!("reading config: {err}").
unwrap() and expect() rules
let val = data.unwrap();
let val = data.expect("parser guarantees non-empty after validation");
let val = data.ok_or(AppError::MissingField("name"))?;
panic! — never in library code
panic! is for programmer bugs (violated invariants), never for expected errors.
- Library functions must return
Result or Option.
Ownership and borrowing
Function parameter rules
fn greet(name: &str) { println!("Hello, {name}!"); }
fn greet(name: &String) { ... }
fn sum(values: &[i32]) -> i32 { values.iter().sum() }
fn sum(values: &Vec<i32>) -> i32 { ... }
Rule: Use &str not &String. Use &[T] not &Vec<T>. Accept the most general borrow.
When to take ownership vs borrow
- Take
&T when you only need to read.
- Take
T when the caller is done with it and you need to store/move it.
- Never clone inside a function when you could accept owned data.
Avoid unnecessary cloning
fn process(user: &User) {
log(&user.name.clone());
}
fn process(user: &User) {
log(&user.name);
}
Cow<str> for conditional allocation
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') {
Cow::Owned(s.replace(' ', "_"))
} else {
Cow::Borrowed(s)
}
}
Smart pointer hierarchy
| Type | Use when |
|---|
Box<T> | Heap allocation, recursive types, trait objects |
Rc<T> | Shared ownership, single-threaded |
Arc<T> | Shared ownership, multi-threaded |
Cell<T> | Interior mutability for Copy types, single-threaded |
RefCell<T> | Interior mutability for non-Copy, single-threaded |
Mutex<T> | Interior mutability, multi-threaded |
RwLock<T> | Read-heavy multi-threaded access |
Do NOT reach for Arc<Mutex<T>> by default. Consider channels, atomics, or redesigning to avoid shared state.
Type safety
Newtypes to prevent misuse
struct UserId(pub u64);
struct PostId(pub u64);
fn get_user_posts(user: UserId, post: PostId) -> Vec<Post> { ... }
Enums over booleans
let w = Widget::new(true, false);
let w = Widget::new(Size::Small, Shape::Round);
Parse, don't validate
fn send_email(email: &str) -> Result<()> {
if !is_valid(email) { return Err(...); }
...
}
pub struct Email(String);
impl Email {
pub fn parse(s: &str) -> Result<Self, EmailError> { ... }
}
fn send_email(email: &Email) -> Result<()> { }
Traits
Implement From, never Into
impl From<MyType> for u32 {
fn from(t: MyType) -> u32 { t.0 }
}
impl Into<u32> for MyType { ... }
Eagerly implement common traits
Due to the orphan rule, downstream crates cannot add trait impls. Always derive where applicable:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Config { ... }
Priority: Debug (always) > Clone > PartialEq/Eq > Hash > Display > Default > Serialize/Deserialize.
Generics vs trait objects
fn process<T: Handler>(handler: &T) { ... }
let handlers: Vec<Box<dyn Handler>> = vec![...];
No redundant bounds on structs
struct Cache<K: Hash + Eq, V: Clone> { ... }
struct Cache<K, V> { ... }
impl<K: Hash + Eq, V> Cache<K, V> { ... }
Module organization
Visibility
pub struct Client { ... }
pub(crate) fn helper() { ... }
fn private() { ... }
pub(super) fn sibling() { ... }
Re-export public API from crate root
pub use crate::client::Client;
pub use crate::error::{Error, Result};
File structure
src/
lib.rs // crate root
config.rs // module (no submodules → single file)
database/
mod.rs // only when submodules exist
queries.rs
migrations.rs
Async Rust
For async patterns, Tokio conventions, and Send + Sync rules, see async-patterns.md.
Key rules:
- Never hold
std::sync::MutexGuard across .await — use tokio::sync::Mutex.
- Wrap blocking I/O in
tokio::task::spawn_blocking.
- Prefer
JoinSet over manual tokio::spawn for structured concurrency.
API design
For builder patterns, serde conventions, trait design, and documentation rules, see api-design.md.
Key rules:
- Document
# Errors, # Panics, # Safety sections where applicable.
- Gate serde behind a feature flag in library crates.
- Use
impl AsRef<Path> / impl Into<String> to accept diverse input types.
Testing
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_input() {
assert_eq!(parse("42"), Ok(42));
}
#[test]
fn rejects_empty_input() {
assert!(parse("").is_err());
}
}
- Unit tests in
#[cfg(test)] mod tests at end of file.
- Integration tests in
tests/ directory (public API only).
- Use
#[tokio::test] for async tests.
- Test business logic only — never assert on CSS classes or visual styling.
- Use
tempfile::tempdir() for filesystem tests.
- Use table-driven tests for multiple input/output cases.
Clippy
Recommended lint configuration
#![deny(clippy::correctness)]
#![warn(clippy::suspicious)]
#![warn(clippy::perf)]
#![warn(clippy::style)]
#![warn(clippy::complexity)]
Key lints to always fix
clippy::unwrap_used — use ? or expect with reason
clippy::clone_on_ref_ptr — don't Arc::clone(&x), use x.clone()... actually DO use Arc::clone(&x) to be explicit
clippy::large_enum_variant — box large variants
clippy::await_holding_lock — release lock before .await
clippy::needless_pass_by_value — take &T unless ownership needed
clippy::manual_let_else — use let ... else { return } pattern
Unsafe code
- Only for FFI, performance-critical low-level ops, or OS primitives.
- Every
unsafe block MUST have a // SAFETY: comment explaining the invariant.
- Every
unsafe fn MUST have a # Safety doc section listing caller obligations.
- Minimize the unsafe surface — wrap in safe public API.
let value = unsafe {
slice.get_unchecked(index)
};
let value = unsafe { slice.get_unchecked(index) };
Common AI anti-patterns
| Anti-pattern | Fix |
|---|
&String / &Vec<T> parameters | Use &str / &[T] |
.clone() to satisfy borrow checker | Restructure code — split borrows, use entry API |
Arc<Mutex<T>> everywhere | Use channels, atomics, or redesign |
String in all struct fields | &str when borrowing is appropriate |
.unwrap() in production code | ? operator, expect with reason, or match |
impl Into<T> instead of From<T> | Always implement From — Into is auto-derived |
get_name() getter style | name() — no get_ prefix in Rust |
panic! in library code | Return Result or Option |
| Unnecessary lifetime annotations | Trust lifetime elision; annotate only when compiler asks |
as for numeric casts | TryFrom/TryInto for checked conversions |
| Fighting the borrow checker | Redesign: collect then mutate, split borrows, entry API |
| Ignoring clippy warnings | Every warning is a code smell; #[allow] requires a reason |
Tooling quick reference
cargo fmt
cargo fmt -- --check
cargo build
cargo build --release
cargo test
cargo test -- --nocapture
cargo test specific_test_name
cargo clippy
cargo clippy --all-targets --all-features -- -D warnings
cargo check
cargo doc --open
cargo doc --no-deps
cargo audit