Rust Skills

repository·master·Indexed 43 days ago

https://github.com/leonardomso/rust-skills

A collection of 265 idiomatic Rust rules designed for AI coding agents to improve code quality, safety, and performance. It provides a structured system for agents to move beyond average Rust patterns—such as excessive cloning or .unwrap() abuse—toward expert-level code. Compatible with AI agents including Claude Code, Cursor, Windsurf, GitHub Copilot, Aider, and Roo Code.

Tokens
282K
Snippets
875
Records
1K
Agent score
60%

What's inside Rust Skills

  1. Overview of Rust Skills guidelines

    master
    The rust-skills project provides a comprehensive set of 265 coding guidelines organized into 26 categories. It is designed to assist in writing, reviewing, or refactoring Rust code to ensure it is idiomatic, high-quality, and highly optimized. The guidelines are prioritized by impact (CRITICAL, HIGH, MEDIUM, LOW) to help guide development and code generation efforts. It is currently updated for Rust 1.96 (2024 edition).
  2. Use `Result<T, E>` for fallible operations

    master

    In Rust, operations that can fail should return a Result<T, E> instead of using Option<T>, sentinel values (like returning -1 for error), or calling .unwrap(). Using Result makes failure explicit in the type system, forcing callers to acknowledge and handle potential errors. This prevents accidental loss of error context (which happens with Option) and avoids runtime crashes (which happen with unwrap).

    // Good: Result with clear error type
    fn read_config(path: &str) -> Result<Config, ConfigError> {
        let content = std::fs::read_to_string(path)
            .map_err(ConfigError::IoError)?;
        toml::from_str(&content)
            .map_err(ConfigError::ParseError)
    }
    
    fn divide(a: i32, b: i32) -> Result<i32, DivisionError> {
        if b == 0 {
            return Err(DivisionError::DivideByZero);
        }
        Ok(a / b)
    }
    
    // Caller must handle
    match divide(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
  3. Optimize memory footprint using appropriately-sized integers

    master

    To reduce memory usage and improve cache utilization, choose the smallest integer type that fits your domain requirements. Using unnecessarily large types (e.g., i64 instead of i16) wastes memory, which compounds significantly in large arrays, vectors, or structs.

    For example, a Pixel struct using u64 for color channels takes 32 bytes, whereas using u8 reduces it to 4 bytes (an 8x improvement).

    struct Pixel {
        r: u8,
        g: u8,
        b: u8,
        a: u8,
    }
    // Size: 4 bytes per pixel
  4. Trade-offs and performance of SmallVec

    master

    While SmallVec can reduce heap allocations, it introduces specific overheads that should be verified via profiling:

    1. Memory Footprint: SmallVec is larger than a standard Vec. For example, a Vec<i32> is typically 24 bytes (pointer, length, capacity), whereas a SmallVec<[i32; 4]> is 32 bytes (inline storage, length, and a discriminant to track if it is on the stack or heap).
    2. Branching Overhead: Every operation on a SmallVec incurs a small performance penalty because the implementation must check whether the data is currently stored inline or on the heap.

    Always profile your application to ensure the reduction in allocations outweighs the branching and memory overhead.

  5. Use the Entry API for efficient map insert-or-update

    master

    When working with HashMap or BTreeMap, use the Entry API to perform insert-or-update operations in a single lookup. Avoiding patterns like contains_key followed by insert or get followed by insert reduces the number of hash computations and lookups from two (or more) down to one, improving performance and making the code more idiomatic.

    Common Patterns

    Incrementing a value

    Instead of checking for existence and then inserting, use .or_insert(0) to ensure a value exists before modifying it.

    Grouping items

    Use .or_default() to initialize a collection (like a Vec) for a key if it doesn't exist, then push the item into it.

    Complex conditional updates

    For logic that requires checking the existing value before deciding whether to update or insert, use a match statement on map.entry(key) to handle Entry::Occupied and Entry::Vacant cases separately.

    use std::collections::HashMap;
    use std::collections::hash_map::Entry;
    
    // Incrementing a value
    fn increment(map: &mut HashMap<String, u32>, key: String) {
        *map.entry(key).or_insert(0) += 1;
    }
    
    // Grouping items
    fn group_by_category(items: Vec<Item>) -> HashMap<Category, Vec<Item>> {
        let mut groups: HashMap<Category, Vec<Item>> = HashMap::new();
        for item in items {
            groups.entry(item.category.clone())
                .or_default()
                .push(item);
        }
        groups
    }
    
    // Complex conditional updates
    match map.entry(key) {
        Entry::Occupied(mut entry) => {
            let value = entry.get_mut();
            if should_update(value) {
                *value = new_value;
            }
        }
        Entry::Vacant(entry) => {
            entry.insert(default_value);
        }
    }
  6. How to handle multiple operations in an unsafe block

    master

    When an unsafe block contains multiple operations, you have two choices to maintain clarity:

    1. Write a single // SAFETY: comment that addresses every distinct invariant involved in the block.
    2. Split the operations into multiple smaller unsafe blocks (refer to the unsafe-minimize-scope rule for best practices).

    Note: In an unsafe fn, you still need // SAFETY: comments for every inner unsafe {} block (enforced by the unsafe_op_in_unsafe_fn lint in the 2024 edition).

  7. When to overload operators in Rust

    master

    In Rust, operator overloading is achieved via traits in std::ops (such as Add, Sub, Mul, Index, Neg, etc.). According to the Rust API Guidelines (C-OVERLOAD), you should only overload operators when the semantics are natural, obvious, and follow the principle of least surprise.

    When to implement operators

    • Arithmetic: On numeric newtypes or geometric types (e.g., vectors, matrices).
    • Set Operations: Using | for union or & for intersection.
    • String Operations: Concatenation on custom string types.
    • Indexing: Implementing Index or IndexMut on containers that hold items.

    When to avoid operators

    • Side Effects: Never use + or * if they trigger visible side effects (like logging or network calls).
    • Ambiguity: If the operator's meaning depends on context not captured by the types, use a named method instead.
    • Mutation: Avoid using operators like + to mutate internal state in a way that surprises the reader.
  8. Access items in nested module hierarchies

    master

    In deeply nested module structures, you can traverse up the hierarchy using multiple super segments.

    • use super::*; imports items from the immediate parent module.
    • use super::super::*; imports items from the grandparent module.
    mod outer {
        pub fn outer_fn() -> i32 { 1 }
        
        mod inner {
            pub fn inner_fn() -> i32 { 2 }
            
            #[cfg(test)]
            mod tests {
                use super::*;           // Gets inner's items
                use super::super::*;    // Gets outer's items
                
                #[test]
                fn test_inner() {
                    assert_eq!(inner_fn(), 2);
                    assert_eq!(outer_fn(), 1);
                }
            }
        }
    }
  9. Use `Arc<T>` for thread-safe shared ownership

    master

    When multiple threads need to share ownership of the same data, use std::sync::Arc (Atomic Reference Counted). Unlike std::rc::Rc, Arc uses atomic operations to update its reference count, making it safe to send across thread boundaries. If you attempt to use Rc in a thread, the compiler will prevent it because Rc does not implement the Send trait.

    use std::sync::Arc;
    use std::thread;
    
    let data = Arc::new(vec![1, 2, 3]);
    let data_clone = Arc::clone(&data);
    
    thread::spawn(move || {
        println!("{:?}", data_clone);  // Safe!
    });
    
    println!("{:?}", data);  // Original still accessible
  10. Avoid using &String in function arguments

    master

    When designing APIs, prefer accepting &str instead of &String.

    &String is strictly less flexible because it requires the caller to provide exactly a String. In contrast, &str can be created from String, &str, string literals, and slices. Using &str allows for Deref Coercion, where a &String automatically coerces to a &str because String implements Deref<Target = str>.

    // Bad: Forces callers to have a String or allocate
    fn greet(name: &String) {
        println!("Hello, {}", name);
    }
    
    // Good: Works with String, &str, and literals
    fn greet(name: &str) {
        println!("Hello, {}", name);
    }
    
    // Usage examples for the good version:
    greet("Alice");           // String literal
    greet(&name);             // &String coerces to &str
    greet(name.as_str());     // Explicit &str
  11. Understand the `into_iter()` pattern and `IntoIterator` trait

    master

    In Rust, the into_iter() method (and the IntoIterator trait) is used to consume a collection and return an iterator that yields owned items rather than references.

    • into_iter(): Consumes the collection. The items yielded are owned (e.g., T).
    • iter(): Borrows the collection. The items yielded are references (e.g., &T).

    When implementing IntoIterator for a custom type, the into_iter method should consume self.

    impl IntoIterator for MyCollection {
        type Item = Element;
        type IntoIter = std::vec::IntoIter<Element>;
        
        fn into_iter(self) -> Self::IntoIter {
            self.elements.into_iter()  // Consumes self
        }
    }
  12. When to make serde a required dependency

    master

    While making serde optional is a best practice for general-purpose libraries, it should be a required dependency if:

    • Your library's primary purpose is serialization (e.g., a JSON schema generator or a config file parser).
    • Your library's core domain logic is heavily built around serde traits (e.g., an API client or a specific data format library).