- New engineering-principle: never #[allow], @ts-ignore, # noqa — fix instead - Rust skill: add explicit #[allow(clippy::...)] anti-pattern with examples - Also covers eslint-disable, ts-ignore, noqa, go vet - Single exceptions: false positives, generated code, phased migration (<30d) - CI enforcement commands for Rust/TS/Python/Go
5.5 KiB
5.5 KiB
name, description
| name | description |
|---|---|
| rust | Rust best practices — ownership, error handling, async, project structure, clippy rules, testing, and idiomatic Rust. Use when writing Rust code, reviewing Rust projects. Triggers when working with this language's files, regardless of spoken language." |
Rust Best Practices
Project Structure (Clean Architecture)
src/
├── domain/ # Entities, value objects, domain events
├── application/ # Use cases, repository ports (traits)
├── infrastructure/ # Adapters (DB, HTTP client, cache)
├── api/ # Axum/Actix handlers, middleware
├── config/ # App configuration
└── main.rs # Entry point, composition root
Error Handling
// Use thiserror for application errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum OrderError {
#[error("order not found: {0}")]
NotFound(String),
#[error("validation error: {0}")]
Validation(String),
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
}
// Use Result in application layer
pub async fn create_order(input: CreateOrderInput) -> Result<Order, OrderError> {
let user = user_repo.find_by_id(&input.user_id)
.await?
.ok_or_else(|| OrderError::NotFound("user".into()))?;
// ...
}
Rules:
thiserrorfor library/app errors.anyhowfor binary/CLI errors.- Never
unwrap()orexpect()in production code. Use?or match. Resultfor recoverable errors.panic!only for unrecoverable states.
Ownership & Borrowing
// Prefer borrowing over taking ownership
fn process(items: &[Item]) -> usize {
items.iter().filter(|i| i.active).count()
}
// Clone when ownership is truly needed (and it actually needs to be owned)
fn save(items: Vec<Item>) { ... }
- One mutable reference (
&mut) XOR many immutable refs (&). - Use
Cowfor "borrow if possible, own if modified." - Prefer
&strover&Stringfor function params. - Rc/Arc — only when shared ownership is needed (graphs, caches).
Async (Tokio)
#[tokio::main]
async fn main() -> Result<()> {
// ...
Ok(())
}
// Use tokio::spawn for concurrent tasks
let handle = tokio::spawn(async move {
process_batch(items).await
});
let result = handle.await??;
Rules:
tokiofor runtime. Axum for HTTP.tokio::select!for timeouts, race conditions.- No
block_onin async code. No sync mutex in async — usetokio::sync::Mutex. - Use
tokio::sync::Semaphorefor rate limiting concurrent tasks.
Traits (Interfaces)
// Port — defined in domain/application layer
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: &str) -> Result<Option<User>, DbError>;
async fn save(&self, user: &User) -> Result<(), DbError>;
}
// Adapter — implemented in infrastructure layer
pub struct PostgresUserRepository {
pool: sqlx::PgPool,
}
#[async_trait]
impl UserRepository for PostgresUserRepository {
async fn find_by_id(&self, id: &str) -> Result<Option<User>, DbError> {
sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(Into::into)
}
}
Database (sqlx / SeaORM)
// sqlx — prefer raw SQL with compile-time checking
let user = sqlx::query_as::<_, User>(
"SELECT id, email, name FROM users WHERE id = $1"
)
.bind(user_id)
.fetch_optional(&pool)
.await?;
- sqlx for type-safe raw SQL. SeaORM when you need a full ORM.
- Use migrations (
sqlx migrateorsea-orm-cli). - Connection pooling via
sqlx::PgPoolordeadpool.
Testing
// Unit tests inline
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn order_total_calculates_correctly() {
let order = Order::new(vec![
OrderItem::new(10.0, 2),
OrderItem::new(5.0, 1),
]);
assert_eq!(order.total(), 25.0);
}
#[tokio::test]
async fn create_order_requires_user() {
let repo = InMemoryUserRepo::new();
let result = create_order(CreateOrderInput { user_id: "nonexistent".into() }, &repo).await;
assert!(result.is_err());
}
}
// Integration tests in tests/
- Unit tests inline in module. Integration in
tests/directory. - Fakes (in-memory repos) over mocking libraries.
- Property-based testing with
proptestfor complex logic.
Clippy Rules
# .clippy.toml
# Enable in Cargo.toml or rustfmt.toml:
# [lints.clippy]
# pedantic = "warn"
# nursery = "warn"
Default: #![warn(clippy::pedantic, clippy::nursery)]
Serialization (serde)
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct UserResponse {
pub id: String,
pub email: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
Anti-patterns
- ❌
unwrap()/expect()in production — crash on any error - ❌ Large
main.rs— everything in one file - ❌
unsafewithout documented safety invariants - ❌
Rc<RefCell<...>>in async contexts — useArc<Mutex<...>> - ❌
Box<dyn Trait>where generics work —impl Traitor generic param - ❌
#[allow(clippy::...)]— never suppress warnings; fix the code instead.too_many_argumentsmeans extract a struct.type_complexitymeans extract a type alias or refactor. - ❌ Ignoring clippy warnings — run
clippybefore every commit.cargo clippy -- -D warningsin CI. - ❌
#[tokio::main]on library code — only in binary entry points