rust-clean-code
Rust code style guidelines to prevent clippy warnings
Rust Clean Code Guidelines
Purpose: Write clean Rust code that passes strict clippy checks without warnings. Follow these patterns to ensure code quality and prevent pedantic lint issues.
Critical Rules (Errors if Violated)
These are enforced as
warn or deny in our clippy config:- No
unwrap()in production code - Use?,ok_or(),unwrap_or_default(), orexpect("reason") - No
panic!()orunreachable!()in production - UseResulttypes instead - No
todo!()orunimplemented!()- Complete the implementation or useErr(anyhow!("Not implemented: X")) - No correctness issues - These are
denyand will fail the build
Struct Initialization Patterns
✅ GOOD: Use Default or constructors for complex structs
// When creating test data or mock objects, use Default trait
let player = Player {
id: uuid,
name: "Test".to_string(),
..Default::default() // Fill all other fields with defaults
};
// Or use a constructor method
let player = Player::new(uuid, "Test".to_string());❌ BAD: Verbose struct literals
// This breaks when new fields are added
let player = Player {
id: uuid,
name: "Test".to_string(),
x: 0,
y: 0,
hp: 10,
// ... 20 more fields
// Easy to miss newly added fields!
};Async Functions
✅ GOOD: Only use async when you have await
// Has await points, async is needed
pub async fn fetch_player(pool: &PgPool, id: Uuid) -> Result<Player> {
sqlx::query_as!(Player, "SELECT * FROM players WHERE id = $1", id)
.fetch_one(pool)
.await?
}
// No await points, don't use async
pub fn calculate_damage(attack: i32, defense: i32) -> i32 {
(attack - defense).max(0)
}Note: Axum Handlers
Axum handlers require
async even without await. This is allowed by our clippy config:// OK - Axum requires this signature
pub async fn health_check() -> impl IntoResponse {
"OK"
}Variable Naming
✅ GOOD: Clear, distinct names
let player_stats = calculate_stats(&player);
let game_state = load_state(&pool).await?;
let item_data = fetch_item(&item_id)?;⚠️ Acceptable: Similar names when context is clear
// state vs stats in same function is OK (allowed by config)
fn handle_request(state: &GameState) {
let stats = calculate_stats(state);
}Casting and Type Conversions
✅ GOOD: Use explicit casts (our config allows them)
// Direct casts are fine when you know the value range
let damage = (attack_power as i32) - defense;
let index = slot_number as usize;✅ ALSO GOOD: Use From/Into when clearer
let damage: i64 = i64::from(attack_power);SQL Strings
✅ GOOD: Use raw strings for SQL (hashes optional)
// Both are acceptable
sqlx::query(r#"
SELECT * FROM players WHERE id = $1
"#)
sqlx::query(r"
SELECT * FROM players WHERE id = $1
")Error Handling
✅ GOOD: Use ? operator and anyhow
pub async fn load_player(pool: &PgPool, id: Uuid) -> anyhow::Result<Player> {
let player = sqlx::query_as!(Player, "SELECT * FROM players WHERE id = $1", id)
.fetch_one(pool)
.await?;
Ok(player)
}✅ GOOD: Use expect() with descriptive message
// Only in initialization code or where panic is acceptable
let config = Config::from_env().expect("Failed to load configuration");Match Statements
✅ GOOD: Use match for multiple cases
match order_type {
OrderType::Buy => process_buy(order),
OrderType::Sell => process_sell(order),
}✅ ALSO GOOD: Use if-let for single pattern
if let Some(player) = state.players.get(&id) {
process_player(player);
}Closures
✅ GOOD: Use closures for clarity
// Direct closure is fine
items.iter().map(|item| item.value).sum()
// Method reference is also fine
items.iter().map(Item::value).sum()Documentation
For public APIs, add doc comments:
/// Creates a new player with the given ID and name.
///
/// # Errors
/// Returns an error if the database insert fails.
pub async fn create_player(pool: &PgPool, id: Uuid, name: String) -> Result<Player> {
// ...
}For internal code, doc comments are optional
Our config allows
missing_errors_doc and missing_panics_doc for flexibility.Pre-Commit Checklist
Before committing Rust code, run:
# Format check
cargo fmt --all -- --check
# Full clippy check
cargo clippy --workspace
# If warnings appear, fix them or document why they're acceptableWhen to Use #[allow(...)]
Only use
#[allow(...)] when:- There's a legitimate reason the lint doesn't apply
- You add a comment explaining WHY
- You've tried to fix it properly first
// OK - Axum requires this signature pattern
#[allow(clippy::unused_async)]
pub async fn health() -> impl IntoResponse {
"OK"
}
// OK - Field is read via serde deserialization
#[allow(dead_code)]
pub struct Config {
api_key: String,
}