Skip to content

Instantly share code, notes, and snippets.

@davidbarsky
Last active January 24, 2026 19:04
Show Gist options
  • Select an option

  • Save davidbarsky/8fae6dc45c294297db582378284bd1f2 to your computer and use it in GitHub Desktop.

Select an option

Save davidbarsky/8fae6dc45c294297db582378284bd1f2 to your computer and use it in GitHub Desktop.
my Claude skills for authoring Rust. See the first comment for installation instructions!
name description
rustdoc
Rust documentation conventions (RFC 1574). Apply when writing doc comments on public Rust items. Covers summary sentences, section headings, type references, and examples.

Rust Documentation Conventions (RFC 1574)

Apply these rules when writing doc comments (///) on public Rust items.

Summary Sentence

Every doc comment starts with a single-line summary sentence.

// DO: third person singular present indicative, ends with period
/// Returns the length of the string.
/// Creates a new instance with default settings.
/// Parses the input and returns the result.

// DON'T: imperative, missing period, or verbose
/// Return the length of the string
/// This function creates a new instance with default settings.
/// Use this to parse the input and get the result back.

Comment Style

Use line comments, not block comments.

// DO
/// Summary sentence here.
///
/// More details if needed.

// DON'T
/**
 * Summary sentence here.
 *
 * More details if needed.
 */

Use //! only for crate-level and module-level docs at the top of the file.

Section Headings

Use these exact headings (always plural):

/// Summary sentence.
///
/// # Examples
///
/// # Panics
///
/// # Errors
///
/// # Safety
///
/// # Aborts
///
/// # Undefined Behavior
// DO
/// # Examples

// DON'T
/// # Example
/// ## Examples
/// **Examples:**

Type References

Use full generic forms and link with reference-style markdown.

// DO
/// Returns [`Option<T>`] if the value exists.
///
/// [`Option<T>`]: std::option::Option

// DON'T
/// Returns `Option` if the value exists.
/// Returns an optional value.

Examples

Every public item should have examples showing usage.

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

For multiple patterns:

/// Parses a string into a number.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// let n: i32 = my_crate::parse("42").unwrap();
/// assert_eq!(n, 42);
/// ```
///
/// Handling errors:
///
/// ```
/// let result = my_crate::parse::<i32>("not a number");
/// assert!(result.is_err());
/// ```

Errors Section

Document what errors can be returned and when.

/// Reads a file from disk.
///
/// # Errors
///
/// Returns [`io::Error`] if the file does not exist or cannot be read.
///
/// [`io::Error`]: std::io::Error

Panics Section

Document conditions that cause panics.

/// Divides two numbers.
///
/// # Panics
///
/// Panics if `divisor` is zero.
pub fn divide(dividend: i32, divisor: i32) -> i32 {
    assert!(divisor != 0, "divisor must not be zero");
    dividend / divisor
}

Safety Section

Required for unsafe functions.

/// Dereferences a raw pointer.
///
/// # Safety
///
/// The pointer must be non-null and properly aligned.
/// The pointed-to memory must be valid for the lifetime `'a`.
pub unsafe fn deref<'a, T>(ptr: *const T) -> &'a T {
    &*ptr
}

Module vs Type Docs

  • Module docs (//!): high-level summaries, when to use this module
  • Type docs (///): comprehensive, self-contained

Some duplication is acceptable.

Language

Use American English spelling: "color" not "colour", "serialize" not "serialise".

name description
rust-style
Rust coding style guide. Apply automatically when writing or modifying Rust code. Enforces for-loops over iterators, let-else for early returns, variable shadowing, newtypes, explicit matching, and minimal comments.

Rust Coding Style

Apply these rules when writing or modifying any Rust code.

Control Flow: Use for Loops, Not Iterator Chains

Write for loops with mutable accumulators instead of iterator combinators.

// DO
let mut results = Vec::new();
for item in items {
    if item.is_valid() {
        results.push(item.process());
    }
}

// DON'T
let results: Vec<_> = items
    .iter()
    .filter(|item| item.is_valid())
    .map(|item| item.process())
    .collect();
// DO
let mut total = 0;
for value in values {
    total += value.amount();
}

// DON'T
let total: i64 = values.iter().map(|v| v.amount()).sum();
// DO
let mut found = None;
for item in items {
    if item.matches(query) {
        found = Some(item);
        break;
    }
}

// DON'T
let found = items.iter().find(|item| item.matches(query));

Early Returns: Use let ... else

Use let ... else to extract values and exit early on failure. This keeps the happy path unindented.

// DO
let Some(user) = get_user(id) else {
    return Err(Error::NotFound);
};
let Ok(session) = user.active_session() else {
    return Err(Error::NoSession);
};
// continue with user and session

// DON'T
if let Some(user) = get_user(id) {
    if let Ok(session) = user.active_session() {
        // deeply nested code
    } else {
        return Err(Error::NoSession);
    }
} else {
    return Err(Error::NotFound);
}
// DO
let Some(value) = maybe_value else { continue };
let Ok(parsed) = input.parse::<i32>() else { continue };

// DON'T
if let Some(value) = maybe_value {
    if let Ok(parsed) = input.parse::<i32>() {
        // ...
    }
}

Pattern Matching: Minimize if let

Use if let only when the Some/Ok branch is short and there's no else branch.

// ACCEPTABLE: short action, no else
if let Some(callback) = self.on_change {
    callback();
}

// DO: use let-else when you need the value
let Some(config) = load_config() else {
    return default_config();
};

// DO: use match for multiple cases
match result {
    Ok(value) => process(value),
    Err(Error::NotFound) => use_default(),
    Err(e) => return Err(e),
}

Variable Naming: Shadow, Don't Rename

Shadow variables through transformations. Avoid prefixes like raw_, parsed_, trimmed_.

// DO
let input = get_raw_input();
let input = input.trim();
let input = input.to_lowercase();
let input = parse(input)?;

// DON'T
let raw_input = get_raw_input();
let trimmed_input = raw_input.trim();
let lowercase_input = trimmed_input.to_lowercase();
let parsed_input = parse(lowercase_input)?;
// DO
let path = args.path;
let path = path.canonicalize()?;
let path = path.join("config.toml");

// DON'T
let input_path = args.path;
let canonical_path = input_path.canonicalize()?;
let config_path = canonical_path.join("config.toml");

Comments: Don't Write Them

  • No inline comments explaining what code does
  • No section headers or dividers (// --- Section ---)
  • No TODO comments (use issue tracker)
  • No commented-out code (use version control)

Exception: Doc comments (///) on public items are required. See the rustdoc skill.

// DON'T
// Check if user is valid
if user.is_valid() {
    // Update the timestamp
    user.touch();
}

// --- Helper functions ---

// TODO: refactor this later
fn helper() { }

// Old implementation:
// fn old_way() { }

// DO
if user.is_valid() {
    user.touch();
}

fn helper() { }

Type Safety: Prefer Newtypes Over Strings

Wrap strings in newtypes to add semantic meaning and prevent mixing different string types.

// DO
struct UserId(String);
struct Email(String);

fn send_email(to: Email, from: UserId) { }

// DON'T
fn send_email(to: String, from: String) { }

Type Safety: Prefer Strongly-Typed Enums Over Bools

Use enums with meaningful variant names instead of bool parameters.

// DO
enum Visibility {
    Public,
    Private,
}

fn create_repo(name: &str, visibility: Visibility) { }

// DON'T
fn create_repo(name: &str, is_public: bool) { }
// DO
enum Direction {
    Forward,
    Backward,
}

fn traverse(dir: Direction) { }

// DON'T
fn traverse(forward: bool) { }

Pattern Matching: Never Use Wildcard Matches

Always match all variants explicitly to get compiler errors when variants are added.

// DO
match status {
    Status::Pending => handle_pending(),
    Status::Active => handle_active(),
    Status::Completed => handle_completed(),
}

// DON'T
match status {
    Status::Pending => handle_pending(),
    _ => handle_other(),
}

If a wildcard seems necessary, ask the user before using it.

Pattern Matching: Avoid matches! Macro

Use full match expressions instead of matches!. Full matches provide better compiler diagnostics when the matched type changes.

// DO
let is_ready = match state {
    State::Ready => true,
    State::Pending => false,
    State::Failed => false,
};

// DON'T
let is_ready = matches!(state, State::Ready);

Destructuring: Always Use Explicit Destructuring

Destructure structs and tuples explicitly to get compiler errors when fields change.

// DO
let User { id, name, email } = user;
process(id, name, email);

// DON'T
process(user.id, user.name, user.email);
// DO
for Entry { key, value } in entries {
    map.insert(key, value);
}

// DON'T
for entry in entries {
    map.insert(entry.key, entry.value);
}

Code Navigation: Always Use rust-analyzer LSP

When searching or navigating Rust code, always use the LSP tool with rust-analyzer operations:

  • goToDefinition - Find where a symbol is defined
  • findReferences - Find all references to a symbol
  • hover - Get type info and documentation
  • documentSymbol - Get all symbols in a file
  • goToImplementation - Find trait implementations
name description
rust-analyzer-ssr
Use rust-analyzer's Structural Search and Replace (SSR) to change lots of Rust code. SSR matches by AST structure and semantic meaning, understanding type resolution and path equivalence.

rust-analyzer Structural Search and Replace (SSR)

Use rust-analyzer's SSR for semantic code transformations in Rust projects. SSR matches code by AST structure and semantic meaning, not text.

When to Use

  • Refactoring patterns across a codebase (rename, restructure, migrate APIs)
  • Converting between equivalent forms (UFCS to method calls, struct literals to constructors)
  • Finding all usages of a specific code pattern
  • Semantic-aware search that understands type resolution

Basic Syntax

<search_pattern> ==>> <replacement_pattern>

Placeholders capture matched code:

  • $name — matches any expression/type/pattern in that position
  • ${name:constraint} — matches with constraints

Common Patterns

Swap function arguments

foo($a, $b) ==>> foo($b, $a)

Convert struct literal to constructor

Foo { a: $a, b: $b } ==>> Foo::new($a, $b)

UFCS to method call

Foo::method($receiver, $arg) ==>> $receiver.method($arg)

Method to UFCS

$receiver.method($arg) ==>> Foo::method($receiver, $arg)

Wrap in Result

Option<$t> ==>> Result<$t, Error>

Unwrap to expect

$e.unwrap() ==>> $e.expect("TODO")

Match only literals

Some(${a:kind(literal)}) ==>> ...

Match non-literals

Some(${a:not(kind(literal))}) ==>> ...

Constraints

Constraint Matches
kind(literal) Literal values: 42, "foo", true
not(...) Negates inner constraint

How to Invoke

Via Comment Assist (Interactive)

Write a comment containing an SSR rule, then trigger code actions:

// foo($a, $b) ==>> bar($b, $a)

Actions appear: "Apply SSR in file" or "Apply SSR in workspace"

Via LSP Command

{
  "command": "rust-analyzer.ssr",
  "arguments": [{
    "query": "foo($a) ==>> bar($a)",
    "parseOnly": false
  }]
}

Via CLI

rust-analyzer ssr 'foo($a, $b) ==>> bar($b, $a)'

Key Behaviors

Path Resolution: Paths match semantically. foo::Bar matches Bar if imported from foo.

Auto-qualification: Replacement paths are qualified appropriately for each insertion site.

Parenthesization: Automatic parens added when needed (e.g., $a + $b becoming ($a + $b).method()).

Comment Preservation: Comments within matched ranges are preserved.

Macro Handling

SSR can match code inside macro expansions, but with an important restriction: all matched tokens must originate from the same source.

Example: Macro Boundary

macro_rules! my_macro {
    ($x:expr) => {
        foo($x, 42)  // "42" comes from macro definition
    };
}

my_macro!(bar);  // "bar" comes from call site

The expanded code is foo(bar, 42). Here:

  • bar originates from the call site (what the user wrote)
  • foo, 42 originate from the macro definition

If you search for foo($a, $b):

  • It would NOT match the expanded foo(bar, 42) because $a would capture bar (call site) but $b would capture 42 (definition site) — these cross the macro boundary.

Why This Limitation Exists

SSR can only generate edits for code the user actually wrote. If a match spans both user code and macro-generated code, SSR couldn't produce a valid edit — it would need to modify the macro definition, which is a different (and potentially shared) piece of code.

What SSR CAN Do With Macros

  • Match code entirely within macro arguments: my_macro!(foo($a)) can match foo($a)
  • Match the macro call itself: my_macro!($x) works
  • Match expanded code where all tokens come from call-site arguments

Other Limitations

  • Constraints limited to kind(literal) and not()
  • Single-identifier patterns (foo ==>> bar) may be filtered if ambiguous
  • Cannot modify use declarations with braces

More Examples

Convert Option methods

$o.map_or(None, Some) ==>> $o

Change field access

$s.foo ==>> $s.bar

Reorder struct fields

Foo { a: $a, b: $b } ==>> Foo { b: $b, a: $a }

Generic type transformation

Vec<$t> ==>> SmallVec<[$t; 4]>

Source Files

  • Core implementation: crates/ide-ssr/src/
  • IDE integration: crates/ide/src/ssr.rs
  • Tests with examples: crates/ide-ssr/src/tests.rs
@davidbarsky
Copy link
Author

To install: Note that you must must place these in separate directories in ~/.claude/skills, as described in https://agentskills.io/. When placing each file into ~/.claude/skills directory, remove the -$NUMBER, such that you end up with ~/.claude/skills/rust-analyzer-ssr/SKILL.md, ~/.claude/skills/rust-style/SKILL.md, and ~/.claude/skills/rustdoc/SKILL.md.

@bryantbiggs
Copy link

bryantbiggs commented Jan 24, 2026

thank you for sharing these! I was just curious about one point, the for loops over iterators - if you had time and would be open to sharing your thoughts on that, I'd really appreciate it. Either way, thanks for sharing!

@davidbarsky
Copy link
Author

thank you for sharing these! I was just curious about one point, the for loops over iterators - if you had time and would be open to sharing your thoughts on that, I'd really appreciate it. Either way, thanks for sharing!

Sure! I prefer for loops over combinators for a few reasons:

  1. If want to add some debug logging, early returns/breaks, or handle an error in a loop, the amount of code that needs to change is more localized with an imperative loop than with combinators. I like to optimize for making code easy to edit.
  2. The compiler errors are nicer from a loop than combinators.
  3. Rust is imperative language! People try to run away from mutation in other languages, but it’s fine in Rust because of the borrow checker. It’s also why I prefer to shadow variables: fewer new names to existing bindings, enforced by the borrow checker.
  4. Aesthetically, iterator chains feel like Claude slop to me. This comes down to personal taste, however.

Two caveats about this guidance, however:

  1. A lot of people at my company disagree with me, including my cofounder, and we’ve each been writing Rust for an over a decade!
  2. Iterator combinators are preferable if you want to return an impl Iterator. Generators/yield statements would be a big help here, but unfortunately, they’re still unstable.

@bryantbiggs
Copy link

fantastic, thank you for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment