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:
  1. No unwrap() in production code - Use ?, ok_or(), unwrap_or_default(), or expect("reason")
  2. No panic!() or unreachable!() in production - Use Result types instead
  3. No todo!() or unimplemented!() - Complete the implementation or use Err(anyhow!("Not implemented: X"))
  4. No correctness issues - These are deny and 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 acceptable

When to Use #[allow(...)]

Only use #[allow(...)] when:
  1. There's a legitimate reason the lint doesn't apply
  2. You add a comment explaining WHY
  3. 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,
}