Guide for Rust development including code style, testing, building, and quality checks using cargo tools. Apply when working with Rust code, Cargo.toml, or running cargo commands.
Scanned 6/1/2026
Install via CLI
openskills install opencue/cuecards---
name: claude-skill-rust
description: Guide for Rust development including code style, testing, building, and quality checks using cargo tools. Apply when working with Rust code, Cargo.toml, or running cargo commands.
user-invocable: true
allowed-tools: Read, Grep, Bash
---
# Rust Development Guide
## Code Style
- Follow standard Rust conventions and idioms
- Use `rustfmt` for code formatting
- Configure lints in `lib.rs` (see Crate-Level Lint Configuration below)
- Prefer descriptive variable and function names
## Module File Organization
Prefer the modern `<module_name>.rs` style over the legacy `mod.rs` style for module files.
**Preferred (modern style):**
```text
src/
├── lib.rs
├── config.rs # mod config
├── config/
│ └── parser.rs # mod config::parser
├── network.rs # mod network
└── network/
├── client.rs # mod network::client
└── server.rs # mod network::server
```
**Avoid (legacy style):**
```text
src/
├── lib.rs
├── config/
│ ├── mod.rs # mod config
│ └── parser.rs # mod config::parser
└── network/
├── mod.rs # mod network
├── client.rs # mod network::client
└── server.rs # mod network::server
```
Benefits of the modern style:
- File names directly indicate the module name (no ambiguous `mod.rs` files)
- Easier navigation in editors and file browsers
- Clear correspondence between module path and file path
- Supported since Rust 2018 edition
## Crate-Level Lint Configuration
Define lint rules at the top of `lib.rs` (or `main.rs` for binaries) rather than via command-line flags. This ensures consistent enforcement and documents project standards.
```rust
#![deny(unsafe_code)]
#![cfg_attr(all(not(debug_assertions), not(test)), deny(clippy::all))]
#![cfg_attr(all(not(debug_assertions), not(test)), deny(clippy::pedantic))]
#![cfg_attr(all(not(debug_assertions), not(test)), deny(missing_docs))]
// Allow some pedantic lints that are too strict for this project
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::enum_variant_names)]
// Until 1.0.0, allow dead code and unused dependency warnings
#![allow(dead_code)]
#![allow(unused_crate_dependencies)]
```
Key principles:
- Deny `unsafe_code` unless explicitly required
- Use `cfg_attr` to deny lints only in release builds (not debug or test)
- Allow specific pedantic lints that conflict with project conventions
- Document why each `allow` is necessary
**Important**: These are good defaults for new crates. Do not override existing lint configurations - if a crate already specifies `deny` or `allow` for a rule, respect that choice.
## Error Handling
Avoid `.unwrap()` and `.expect()` - these cause panics and should not be used in production code. Instead:
- Use `Result<T, E>` return types with the `?` operator for error propagation
- Prefer early returns for error conditions
- Handle errors explicitly at appropriate boundaries
### Error Type Crates
**Note**: The following recommendations apply to `std` crates only. For `no_std` embedded targets, use custom error enums or `thiserror` with `default-features = false`.
- **Libraries**: Use `thiserror` to define custom error types with derive macros
- **Applications**: Use `anyhow` for convenient error handling with context
Example library error type with `thiserror`:
```rust
use thiserror::Error;
/// Errors that can occur during data processing.
#[derive(Debug, Error)]
pub enum ProcessError {
/// The input data was empty.
#[error("input data cannot be empty")]
EmptyInput,
/// Failed to parse the input data.
#[error("failed to parse input: {0}")]
ParseFailure(String),
/// An I/O error occurred.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
```
Example application error handling with `anyhow`:
```rust
use anyhow::{Context, Result};
fn load_config(path: &Path) -> Result<Config> {
let contents = std::fs::read_to_string(path)
.context("failed to read config file")?;
let config: Config = serde_json::from_str(&contents)
.context("failed to parse config JSON")?;
Ok(config)
}
```
## Logging
**Note**: The following recommendations apply to `std` crates only. For `no_std` embedded targets, use `defmt` or platform-specific logging mechanisms.
Avoid `println!` and `eprintln!` outside of main application entry points. Use the `log` crate for structured logging instead.
```rust
use log::{debug, info, warn, error};
fn process_data(data: &[u8]) -> Result<(), ProcessError> {
debug!("Processing {} bytes", data.len());
if data.is_empty() {
warn!("Received empty data buffer");
return Err(ProcessError::EmptyInput);
}
info!("Data processed successfully");
Ok(())
}
```
Log levels:
- `error!` - Unrecoverable errors or failures
- `warn!` - Unexpected conditions that are handled
- `info!` - Significant events in normal operation
- `debug!` - Detailed information for debugging
- `trace!` - Very detailed tracing information
Applications should initialise a log implementation (e.g., `env_logger`, `tracing-subscriber`) in `main()`.
## Whitespace and Formatting
Maintain blank lines for readability:
- Between module-level items (functions, structs, enums, traits, constants, impl blocks)
- Between struct and enum members when they have doc comments
- Between constant declarations
- After `use` statements before the first item
- After code blocks (loops, conditionals, match arms) before subsequent statements
### Struct Field Formatting
When struct or enum fields have documentation comments, add a blank line before each doc comment to visually separate the fields:
```rust
pub struct Handle<T> {
/// Index into the arena's resource vector.
index: usize,
/// Generation counter to detect stale handles after resource reuse.
generation: u32,
/// Type marker to prevent mixing handles of different resource types.
_marker: PhantomData<T>,
}
```
For fields without documentation, blank lines are optional but may still aid readability for complex types.
## Documentation
All items require rustdoc documentation comments.
For documenting the following item (functions, types, constants, fields):
```rust
/// Documentation for the item below.
```
For documenting the enclosing item (modules, crate root):
```rust
//! Documentation for the containing module.
```
Items requiring documentation include:
- Functions and methods
- Types (structs, enums, type aliases)
- Traits and trait implementations
- Constants and statics
- Modules
- Test functions
- Struct and enum fields
### Function Documentation
Function documentation must include:
- Brief description of purpose
- `# Arguments` section documenting each parameter
- `# Returns` section documenting the return value
- `# Errors` section if the function returns `Result`
- `# Panics` section if the function can panic
Example:
```rust
/// Calculates the checksum for the given data buffer.
///
/// # Arguments
///
/// * `data` - The byte slice to calculate the checksum for.
/// * `seed` - Initial seed value for the checksum algorithm.
///
/// # Returns
///
/// The computed 32-bit checksum value.
///
/// # Errors
///
/// Returns `ChecksumError::EmptyBuffer` if `data` is empty.
fn calculate_checksum(data: &[u8], seed: u32) -> Result<u32, ChecksumError> {
// ...
}
```
### Constant Documentation
Constant documentation must include:
- Description of the constant's purpose
- Reference to specification, document, or section where applicable
Example:
```rust
/// Maximum transmission unit size in bytes.
///
/// Per RFC 894, Section 3 - Ethernet frames have a maximum payload of 1500 bytes.
const MTU_SIZE: usize = 1500;
```
## Testing
- Write unit tests in the same file as the code being tested
- Use integration tests in the `tests/` directory for end-to-end functionality
- Run tests with `cargo test`
- Aim for meaningful test coverage of core functionality
- Document test functions with their purpose and what they verify
## Build Verification
After completing changes to Rust code, run the following checks in order:
1. **Format Code**: `cargo fmt`
2. **Format Check**: `cargo fmt --check`
3. **Lint Check**: `cargo clippy -- -D warnings`
4. **Tests**: `cargo test`
5. **Release Build**: `cargo build --release`
6. **Documentation**: `cargo doc --no-deps --document-private-items`
7. **License Check**: `cargo deny check`
8. **Security Audit**: `cargo audit`
If any check fails, fix the issues before proceeding.
## Security Tools
Projects should use these security tools:
- **cargo-deny**: License and advisory checker. Install with `cargo install cargo-deny`. Configuration in `deny.toml` enforces permissive licenses (MIT, Apache-2.0, BSD) and denies copyleft licenses (GPL, LGPL, AGPL).
- **cargo-audit**: Security vulnerability scanner. Install with `cargo install cargo-audit`. Scans dependencies against the RustSec Advisory Database.
## Dependencies
When adding dependencies:
- Choose well-maintained crates widely used in the Rust ecosystem
- For workspaces, add shared dependencies to `[workspace.dependencies]` in root `Cargo.toml`
- Justify each dependency with a clear use case
- Disable default features and explicitly enable only required features
### Minimising Dependencies
Always add dependencies with `default-features = false` and explicitly specify the features you need. This reduces compile times and binary size by avoiding unnecessary transitive dependencies.
```toml
# Prefer this:
serde = { version = "1.0", default-features = false, features = ["derive"] }
# Avoid this:
serde = "1.0"
```
When adding a new dependency:
1. Check the crate's documentation for available features
2. Identify the minimum set of features required for your use case
3. Add with `default-features = false`
4. Explicitly list only the features you need
No comments yet. Be the first to comment!