---
name: rust-common-pitfalls
description: "Common Rust development pitfalls: frequent compiler errors, struct constructor patterns, test organization, and coverage enforcement for reliable codebases."
metadata:
  author: mte90
  version: "1.1.0"
  tags:
    - rust
    - compiler-errors
    - testing
    - patterns
    - best-practices
---

# Rust Common Development Pitfalls

Comprehensive guide for avoiding and fixing the most frequent issues encountered when developing in Rust.

## When to Use

- Resolving compiler errors in Rust projects
- Designing struct constructors and builders
- Organizing tests in Rust crates
- Setting up code coverage gates
- Debugging common runtime issues

## How It Works

This skill addresses the four most common pain points identified in Rust development:

1. **Frequent compiler errors** — Quick reference for error codes and solutions
2. **Struct constructor patterns** — Builder, factory, and newtype patterns
3. **Test organization** — Module placement, naming, and integration tests
4. **Coverage enforcement** — CI integration and threshold configuration

---

## Part 1: Common Compiler Errors Quick Reference

### E0433: Cannot find type in scope

**Cause**: Missing import or typo in type name.

**Solution**:
```rust
// Wrong: use chrono::NaiveDate;
use chrono::NaiveDate;  // Add import or check Cargo.toml

// Check for typos in type names
struct User { name: String }  // typo in "name" vs "named"
```

### E0597: Value does not live long enough

**Cause**: Lifetime mismatch between borrowed value and its container.

**Solution**:
```rust
// Problem: returning reference to temporary
fn get_str() -> &str {
    let s = String::from("temp");
    &s  // ERROR: s dropped before reference returned
}

// Fix: Return owned value or use static lifetime
fn get_str() -> String {
    String::from("temp")  // Ownership moves
}

// Or with static lifetime for constants
fn get_str() -> &'static str {
    "temp"  // Static lifetime
}
```

### E0308: Mismatched types

**Cause**: Type inference failure or expected vs actual type mismatch.

**Real-World Examples from baco** (ses_1015b0539ffe):
- String vs &String drift in security_agent_verification.rs
- u32 vs usize mismatch in loop counters and array indexing

**Solution**:
```rust
// Problem: Expected i32, got &str
fn add(a: i32, b: i32) -> i32 { a + b }
let result = add("1", "2");  // ERROR

// Fix: Convert string to number
let result = add("1".parse::<i32>().unwrap(), "2".parse().unwrap());

// Or use type annotation
let a: i32 = "1".parse().unwrap();
let b: i32 = "2".parse().unwrap();
```

### E0596: Cannot borrow as mutable because it is also borrowed as immutable

**Cause**: Simultaneous mutable and immutable borrows.

**Solution**:
```rust
// Problem
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);  // ERROR: cannot mutate while borrowed

// Fix: Separate borrow scopes
let mut v = vec![1, 2, 3];
{
    let first = &v[0];
    println!("{}", first);
}  // borrow ends
v.push(4);  // now works
```

### E0277: Trait not satisfied

**Cause**: Type doesn't implement required trait.

**Solution**:
```rust
// Problem: T doesn't implement Display
fn print<T>(val: T) {
    println!("{}", val);  // ERROR
}

// Fix: Add trait bound
fn print<T: std::fmt::Display>(val: T) {
    println!("{}", val);
}

// Or use generic formatting
fn print(val: &impl std::fmt::Display) {
    println!("{}", val);
}
```

### E0282: Cannot infer type

**Cause**: Compiler cannot determine type from context.

**Solution**:
```rust
// Problem: Cannot infer type of iterator
let v = vec![1, 2, 3].iter().map(|x| x * 2).collect();  // ERROR

// Fix: Add type annotation
let v: Vec<i32> = vec![1, 2, 3].iter().map(|x| x * 2).collect();

// Or collect into specific type
use std::collections::HashMap;
let m: HashMap<_, _> = vec![(1, "a"), (2, "b")].into_iter().collect();
```

### E0382: Use of moved value

**Cause**: Value moved into another owner, then used again.

**Solution**:
```rust
// Problem: String moved to vec, then used
let s = String::from("hello");
let v = vec![s];  // s moved
println!("{}", s);  // ERROR: use after move

// Fix: Clone or borrow
let s = String::from("hello");
let v = vec![s.clone()];  // Clone ownership
println!("{}", s);  // OK

// Or borrow if clone is expensive
let s = String::from("hello");
let v = vec![&s];  // Borrow
println!("{}", s);  // OK
```

### E0502: Cannot borrow as immutable because it is also borrowed as mutable

**Cause**: Simultaneous mutable and immutable borrows violate Rust's borrowing rules.

**Solution**:
```rust
// Problem: mutable borrow while immutable borrow active
let mut v = vec![1, 2, 3];
let first = &v[0];  // immutable borrow
v.push(4);  // ERROR: mutable borrow while immutable active
println!("{}", first);

// Fix: Limit immutable borrow scope
let mut v = vec![1, 2, 3];
let first = v[0];  // Copy (i32 implements Copy)
v.push(4);
println!("{}", first);

// Or separate scopes for references
let mut v = vec![1, 2, 3];
{
    let first = &v[0];  // immutable borrow
    println!("{}", first);
}  // borrow ends
v.push(4);  // now OK
```

### E0507: Cannot move out of borrowed content

**Cause**: Trying to move a value out of a reference or container.

**Solution**:
```rust
// Problem: Moving out of Option in reference
struct Container { data: Option<String> }
fn extract(c: &Container) -> String {
    c.data.take().unwrap()  // ERROR: cannot move out of &Option
}

// Fix: Use take() to replace with None
fn extract(c: &mut Container) -> String {
    c.data.take().unwrap()  // OK: takes ownership, leaves None
}

// Or clone if mutation not possible
fn extract(c: &Container) -> String {
    c.data.clone().unwrap()  // OK: clones the String
}
```

### E0716: Temporary value dropped while borrowed

**Cause**: Reference to temporary that gets dropped at end of statement.

**Solution**:
```rust
// Problem: Temporary string literal dropped
fn get_line() -> &str {
    String::from("hello").as_str()  // ERROR: temp dropped
}

// Fix: Return owned String
fn get_line() -> String {
    String::from("hello")
}

// Or use static lifetime for constants
fn get_line() -> &'static str {
    "hello"  // Static string literal
}
```
```

---

## Part 2: Struct Constructor Patterns

### Pattern 1: Simple Constructor with Validation

```rust
pub struct User {
    name: String,
    email: String,
    age: u8,
}

impl User {
    /// Creates a new user with validation.
    /// Returns Err if validation fails.
    pub fn new(name: impl Into<String>, email: impl Into<String>, age: u8) -> Result<Self, UserError> {
        let name = name.into();
        let email = email.into();

        // Validate
        if name.trim().is_empty() {
            return Err(UserError::EmptyName);
        }
        if !email.contains('@') {
            return Err(UserError::InvalidEmail(email));
        }
        if age > 150 {
            return Err(UserError::InvalidAge(age));
        }

        Ok(Self { name, email, age })
    }
}

#[derive(Debug)]
pub enum UserError {
    EmptyName,
    InvalidEmail(String),
    InvalidAge(u8),
}
```

### Pattern 2: Builder Pattern with Validation

```rust
pub struct UserBuilder {
    name: Option<String>,
    email: Option<String>,
    age: Option<u8>,
}

impl UserBuilder {
    pub fn new() -> Self {
        Self {
            name: None,
            email: None,
            age: None,
        }
    }

    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    pub fn email(mut self, email: impl Into<String>) -> Self {
        self.email = Some(email.into());
        self
    }

    pub fn age(mut self, age: u8) -> Self {
        self.age = Some(age);
        self
    }

    /// Builds the User, performing validation.
    /// # Errors
    /// Returns UserError if required fields are missing or invalid.
    pub fn build(self) -> Result<User, UserError> {
        let name = self.name.ok_or(UserError::MissingField("name"))?;
        let email = self.email.ok_or(UserError::MissingField("email"))?;
        let age = self.age.unwrap_or(0);  // default

        User::new(name, email, age)
    }
}

impl Default for UserBuilder {
    fn default() -> Self {
        Self::new()
    }
}

// Usage
let user = UserBuilder::new()
    .name("Alice")
    .email("alice@example.com")
    .age(30)
    .build()
    .expect("valid input");
```

### Pattern 3: Factory Pattern for Multiple Variants

```rust
pub struct VulnerabilityFinding {
    id: String,
    severity: Severity,
    message: String,
    location: Location,
    // ... many more fields
}

pub enum Severity {
    Info,
    Low,
    Medium,
    High,
    Critical,
}

impl VulnerabilityFinding {
    /// Factory for SQL injection findings
    pub fn sql_injection(location: Location, query: &str) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            severity: Severity::High,
            message: format!("Potential SQL injection in: {}", query),
            location,
            // ... set other fields appropriately
        }
    }

    /// Factory for hardcoded credentials
    pub fn hardcoded_credential(location: Location, credential_type: &str) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            severity: Severity::Critical,
            message: format!("Hardcoded {} detected", credential_type),
            location,
            // ...
        }
    }
}
```

### Pattern 4: Newtype for Type Safety

```rust
/// Newtype wrapper to prevent mixing up UserId and OrderId
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct UserId(pub u64);

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct OrderId(pub u64);

impl UserId {
    pub fn new(id: u64) -> Self {
        Self(id)
    }
}

impl OrderId {
    pub fn new(id: u64) -> Self {
        Self(id)
    }
}

// This prevents accidental argument swapping
fn get_user_orders(user_id: UserId, order_id: OrderId) -> Result<Order, ()> {
    // Cannot accidentally swap - type system catches it
    todo!()
}

// Usage
let user_id = UserId::new(42);
let order_id = OrderId::new(1);
get_user_orders(user_id, order_id).ok();
```

---

## Part 3: Test Organization

### Module Structure

```text
my_crate/
├── src/
│   └── lib.rs
├── tests/
│   ├── integration_test.rs    # One file = one test binary
│   └── common/
│       └── mod.rs             # Shared test utilities
└── src/
    └── some_module.rs         # Inline tests below
```

### Inline Tests in Source

```rust
// src/some_module.rs

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    // Unit tests for this module
    #[test]
    fn test_add_positive() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_add_returns_error_when_overflow() {
        // Test error conditions
        let result = add(i32::MAX, 1);
        assert!(result.is_negative());  // Wraps to negative
    }
}
```

### Integration Tests

```rust
// tests/integration_test.rs
use my_crate::{add, User, UserBuilder};

#[test]
fn test_full_user_flow() {
    // Integration test - tests components working together
    let user = UserBuilder::new()
        .name("Test")
        .email("test@example.com")
        .age(25)
        .build()
        .unwrap();

    assert_eq!(user.name(), "Test");
}

#[test]
fn test_invalid_email_rejected() {
    let result = UserBuilder::new()
        .name("Test")
        .email("invalid-email")
        .build();

    assert!(result.is_err());
}
```

### Test Modules Inside impl Blocks (Advanced)

**⚠️ Rare pattern - use only when necessary:**

```rust
pub struct Config {
    value: i32,
}

impl Config {
    pub fn new(value: i32) -> Self {
        Self { value }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn test_new_creates_config() {
            let cfg = Config::new(42);
            assert_eq!(cfg.value, 42);
        }
    }
}
```

### Test Naming Conventions

```rust
#[cfg(test)]
mod tests {
    use super::*;

    // DESCRIPTIVE: test_function_scenario_expected_behavior
    #[test]
    fn test_user_new_rejects_empty_email() {
        assert!(User::new("name", "").is_err());
    }

    #[test]
    fn test_builder_provides_defaults_for_optional_fields() {
        let user = UserBuilder::new()
            .name("Test")
            .email("test@example.com")
            .build()
            .unwrap();
        assert_eq!(user.age(), 0);  // default
    }

    // Group related tests with prefix
    #[test]
    fn test_vulnerability_sql_injection_severity_is_high() {
        let finding = VulnerabilityFinding::sql_injection(
            Location::new("test.rs", 1),
            "SELECT * FROM users"
        );
        assert!(matches!(finding.severity(), Severity::High));
    }
}
```

---

## Part 4: Code Coverage Enforcement

### Cargo Configuration

```toml
# .cargo/config.toml
[profile.release]
lto = true
opt-level = 3

[profile.dev]
debug = true
```

### CI Integration with cargo-llvm-cov

```yaml
# .github/workflows/coverage.yml
name: Coverage

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov
      
      - name: Generate coverage
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info
      
      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          fail_ci_if_error: true
          threshold: 80%
```

### Coverage with Failure Threshold

```bash
# Run with minimum coverage requirement
cargo llvm-cov --fail-under-lines 80

# Or in CI with specific targets
cargo llvm-cov --fail-under-lines 80 \
  --fail-under-functions 70 \
  --fail-under-regions 60
```

### Excluding Code from Coverage

```rust
// Exclude generated code
#[cfg(test)]
mod generated_tests {
    // Tests for generated code - exclude from coverage
    include!("generated.rs");
}

// Exclude platform-specific code
#[cfg(target_os = "linux")]
fn linux_only_function() { /* ... */ }

#[cfg(not(target_os = "linux"))]
fn linux_only_function() {
    unreachable!("Linux only");
}
```

### Coverage Reports

```bash
# HTML report
cargo llvm-cov --html

# Terminal summary
cargo llvm-cov

# JSON for CI tools
cargo llvm-cov --json --output-path coverage.json
```

---

## Part 5: Common Runtime Issues Prevention

### Thread Safety with Send + Sync

**Thin Send/Sync for async**: Most async types are `!Send` by default. Use `Arc<T>` for shared state across tasks.

```rust
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};

// Rc/RefCell are !Send and !Sync — use only in single-threaded contexts
// BAD for async: Rc<String> cannot cross task boundaries

// GOOD: Arc for shared ownership across tasks
#[derive(Clone)]
struct SharedState {
    data: Arc<RwLock<Vec<String>>>,  // RwLock better for read-heavy workloads
}

// Arc<Mutex<T>> vs Arc<RwLock<T>>:
// - Mutex: single writer, any reader (but only one holder total)
// - RwLock: multiple readers OR single writer (better for read-heavy)
// In async: prefer tokio::sync::{Mutex, RwLock}, not std::sync::*
```

**Async-specific Send/Sync issues**:
```rust
use tokio::task::JoinSet;

// !Send futures: Cannot be sent to another thread
async fn non_send_task() {
    let data = std::rc::Rc::new(42);  // Rc is !Send
    // This future cannot be .spawn()ed on multi-threaded runtime
}

// JoinSet requires Send + 'static
let mut set = JoinSet::new();
set.spawn(async {
    // Must be Send + 'static
});

// select! cancellation: Ensure futures are cancel-safe
// Avoid holding locks across .await points when possible
select! {
    result = async_task => {
        // Handle result
    },
    _ = cancellation_token.cancelled() => {
        // Cleanup: ensure locks are released
    }
}
```

```rust
use std::sync::{Arc, Mutex};

// Shared state must be Send + Sync to cross thread boundaries
struct AppState {
    counter: Mutex<i32>,
}

// Derive automatically when possible
#[derive(Clone)]
struct CloneableState {
    data: Arc<Mutex<Vec<String>>>,
}

// Explicit bounds for generics
fn process_in_background<T: Send + 'static>(data: T) {
    std::thread::spawn(move || {
        // Process data
    });
}
```

### Avoiding Deadlocks

```rust
use std::sync::{Mutex, MutexGuard};

// Always acquire locks in consistent order
// BAD: Potential deadlock
// fn bad_example(m1: &Mutex<T>, m2: &Mutex<U>) { ... }

// GOOD: Always acquire in same order, use scoping
fn good_example(m1: &Mutex<i32>, m2: &Mutex<String>) {
    let _g1 = m1.lock().unwrap();
    let _g2 = m2.lock().unwrap();  // Always second
    
    // Work here
}  // Locks released in reverse order
```

### Testing: #[tokio::test] and rstest

```rust
// Basic async test
#[tokio::test]
async fn test_async_function() {
    let result = async_fn().await;
    assert_eq!(result, expected);
}

// Parametrized tests with rstest
use rstest::rstest;

#[rstest]
#[case(1, 2)]
#[case(5, 10)]
async fn test_with_cases(#[case] input: i32, #[case] expected: i32) {
    assert_eq!(process(input).await, expected);
}
```

### Async Best Practices

```rust
use tokio::time::{sleep, Duration};

// Use async-specific utilities
async fn fetch_with_timeout() -> Result<String, reqwest::Error> {
    Ok(
        tokio::time::timeout(
            Duration::from_secs(5),
            reqwest::get("https://example.com")
        )
        .await??  // ? for timeout error, ? for request error
        .text()
        .await?
    )
}

// NEVER block the async executor
async fn bad_example() {
    std::thread::sleep(Duration::from_secs(1));  // BAD: blocks executor
    // Use instead:
    sleep(Duration::from_secs(1)).await;  // GOOD: yields to executor
}
```

---
## Part 6: Module Splitting Strategies and Dead Code Detection

### Field Log from baco: Undeclared Modules and #[ignore] Accumulation

**Pitfall #1: Module file exists but never declared in lib.rs** (ses_125f87e23ffe)

Silent dead code: 9 dead modules, 3274 lines in baco (e.g., attack_scenarios.rs:474, endpoints.rs:248).

**Detection**:
```bash
# Audit mod declarations vs file existence
cargo-modules tree --format json  # Requires cargo-modules crate

# Manual grep: find .rs files without corresponding `mod` in lib.rs
find src -name "*.rs" -exec grep -L "mod {}" lib.rs \;

# Check for orphaned modules
grep -r "^mod " src/ | cut -d: -f2 | sed 's/mod //;s/;//' | sort > /tmp/declared_mods.txt
find src -name "*.rs" | sed 's|.*/||;s/\.rs$//' | sort > /tmp/files.txt
comm -23 /tmp/files.txt /tmp/declared_mods.txt  # Shows undeclared files
```

**Pitfall #8: Disabled #[ignore]/#[cfg] tests as dead weight** (ses_125f87e23ffe)

3 tests, 208 lines of dead code in baco from accumulated `#[ignore]` tests.

**Detection**:
```bash
# Find ignored tests
grep -rn "#\[ignore\]" src/ tests/

# Find cfg-gated dead code
grep -rn "#\[cfg(not(test))\]" src/ | grep -v "// "

# Count disabled tests
cargo test -- --list 2>/dev/null | grep -c "ignored"
```

### Duplicate Code Detection

**Pitfall #5-7 from baco** (ses_125f87e23ffe, ses_1015b0539ffe):

- Cross-phase boilerplate duplication: LLM client config x3 → deduplicated to `create_llm_client_with_metrics()` in src/llm.rs (-36 lines)
- Intra-file duplicate blocks: conflict_resolver.rs:24-33 == :37-41 (39 tokens)
- Cross-file duplicates: indexing.rs:46-51 == llm_static.rs:83-88 (44 tokens; 73 groups total)

**Detection with aft_inspect**:
```bash
# Run inspect to find duplicate groups
aft_inspect --sections duplicates
```

**Manual detection**:
```bash
# Use fdupes or rdfind for exact duplicates
fdupes -r src/

# For semantic duplicates: use cargo-deny or custom diff scripts
# Pattern: extract function, check if same logic appears elsewhere
grep -rn "fn create_llm_client" src/  # Should be 1, not 3
```

**Fix pattern**: Extract common logic to shared helper:
```rust
// Before: 3 copies of LLM client creation
async fn scan_llm_client() -> LLMClient { ... }
async fn index_llm_client() -> LLMClient { ... }
async fn report_llm_client() -> LLMClient { ... }

// After: Single source of truth
async fn create_llm_client_with_metrics(phase: &str) -> LLMClient {
    // Shared logic with phase-specific metrics
}
```

### When to Split a Module

Signs a module has outgrown its single file:
- **>300 lines** — readability degrades, navigation becomes painful
- **Multiple responsibilities** — scanner logic mixed with staging, error handling, and output formatting
- **Frequent merge conflicts** — multiple developers editing the same large file
- **Hard to test** — too many internal dependencies to isolate units

### The `mod.rs` vs `mod/` Directory Pattern

**Before** (single large file):
```rust
// scanner.rs (500 lines)
pub fn scan_phase(phase: ScanPhase) -> Result<Vec<Finding>, ScanError> { ... }
pub fn stage_results(findings: &[Finding]) -> StagedResults { ... }
pub fn run_semgrep(path: &Path) -> Result<SemgrepOutput, SemgrepError> { ... }
// ... 490 more lines
```

**After** (split into submodules):
```rust
// scanner/mod.rs
pub mod phases;
pub mod staging;
pub mod semgrep;

// Re-export public API so callers see no difference
pub use phases::scan_phase;
pub use staging::stage_results;
pub use semgrep::run_semgrep;

// Also re-export types used in public signatures
pub use phases::ScanPhase;
pub use staging::StagedResults;
```

```rust
// scanner/phases.rs
use crate::scanner::{ScanPhase, Finding, ScanError};

pub fn scan_phase(phase: ScanPhase) -> Result<Vec<Finding>, ScanError> {
    // Implementation
}
```

### Visibility Strategy

| Visibility | Use Case |
|------------|----------|
| `pub` | True public API — stable across versions |
| `pub(crate)` | Internal cross-module access within the crate |
| (no `pub`) | Private to the module — implementation detail |

```rust
// scanner/mod.rs
pub mod phases;
mod staging_internal;  // Private helper module

pub use phases::scan_phase;

// Internal function, not exposed
pub(crate) fn internal_helper() -> Result<(), ScanError> {
    // Used by multiple modules but not part of public API
}
```

```rust
// scanner/phases.rs
use crate::scanner::internal_helper;  // Can access pub(crate)

pub fn scan_phase(phase: ScanPhase) -> Result<Vec<Finding>, ScanError> {
    internal_helper()?;  // Cross-module internal access
    // ...
}
```

### Preserving API Compatibility During Refactor

When splitting a module, maintain the **public facade**:

```rust
// Before split — callers import from scanner
use scanner::scan_phase;
use scanner::ScanPhase;

// After split — same imports still work
use scanner::scan_phase;  // Re-exported from scanner/mod.rs
use scanner::ScanPhase;   // Re-exported type
```

**Key steps:**
1. Create `scanner/mod.rs` with `pub mod` declarations
2. Move functions/types to appropriate submodules
3. Add `pub use` re-exports in `mod.rs` for all public symbols
4. Run tests — integration tests should pass without modification

### Common Pitfalls

| Pitfall | Consequence | Fix |
|---------|-------------|-----|
| Forgetting to re-export types in public signatures | Callers get "type not found" errors | Add `pub use submodule::TypeName` in `mod.rs` |
| Using `mod` instead of `pub mod` | Submodule not accessible outside parent | Change to `pub mod` if submodule is part of public API |
| Circular dependencies between submodules | Compilation error | Restructure — extract shared code to a separate module |
| Forgetting `pub` on items in submodules | Items not visible even with `pub use` | Ensure items are `pub` in their defining module |
| Integration test imports break | Test fails to compile | Verify `mod.rs` re-exports match the old single-file API |

### Integration Test Imports

After splitting, integration tests continue to work if `mod.rs` re-exports correctly:

```rust
// tests/integration_test.rs
use my_crate::scanner::{scan_phase, ScanPhase};  // Still works!

#[test]
fn test_scan_phase() {
    let results = scan_phase(ScanPhase::Semgrep).unwrap();
    assert!(!results.is_empty());
}
```

---

## Part 7: Error Design with thiserror

### When to Use `thiserror` vs `anyhow`

| Tool | Best For | Example |
|------|----------|---------|
| **`thiserror`** | Library errors, enum-based errors, public APIs | `ScanError::FileNotFound`, `ConfigError::InvalidPath` |
| **`anyhow`** | Application-level error aggregation, CLI tools | `Result<T, anyhow::Error>` in `main()` |

baco uses `thiserror` 9:1 over `anyhow` (225 vs 26 mentions) — this is the standard pattern for libraries.

### Designing Error Enums

```rust
use thiserror::Error;
use std::path::PathBuf;

#[derive(Debug, Error)]
pub enum ScanError {
    #[error("file not found: {path}")]
    FileNotFound { path: PathBuf },

    #[error("invalid configuration: {0}")]
    InvalidConfig(String),

    #[error("scan failed: {source}")]
    ScanFailed {
        #[from]
        source: std::io::Error,
    },

    #[error("semgrep error: {0}")]
    Semgrep(#[from] SemgrepError),

    #[error("phase {phase} timed out after {duration}s")]
    Timeout { phase: String, duration: u64 },
}
```

### The `#[from]` Attribute

Automatically implements `From<E>` for your error type, enabling the `?` operator:

```rust
// With #[from]
#[derive(Debug, Error)]
pub enum ScanError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

// Now this works:
fn read_file(path: &Path) -> Result<String, ScanError> {
    let content = std::fs::read_to_string(path)?;  // Auto-converts io::Error
    Ok(content)
}

// Without #[from], you'd need:
fn read_file(path: &Path) -> Result<String, ScanError> {
    let content = std::fs::read_to_string(path)
        .map_err(ScanError::Io)?;  // Manual conversion
    Ok(content)
}
```

### Error Context Chaining

Wrap errors at each layer with structured context:

```rust
fn load_config(path: &Path) -> Result<Config, ScanError> {
    // Layer 1: IO error → FileNotFound
    let content = std::fs::read_to_string(path)
        .map_err(|e| ScanError::FileNotFound { path: path.to_path_buf() })?;
    
    // Layer 2: Parse error → InvalidConfig
    let config: Config = serde_json::from_str(&content)
        .map_err(|e| ScanError::InvalidConfig(format!("JSON parse: {}", e)))?;
    
    Ok(config)
}
```

### Unifying Errors Across Modules

When multiple modules produce different error types, create a top-level enum:

```rust
// scanner/error.rs
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ScanError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("semgrep error: {0}")]
    Semgrep(#[from] semgrep::SemgrepError),

    #[error("config error: {0}")]
    Config(#[from] config::ConfigError),

    #[error("scan failed: {message}")]
    ScanFailed { message: String },
}

// semgrep/error.rs
#[derive(Debug, Error)]
pub enum SemgrepError {
    #[error("semgrep not found")]
    NotInstalled,
    #[error("semgrep exited with code {code}")]
    ExitCode { code: i32 },
}

// config/error.rs
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("missing field: {field}")]
    MissingField { field: String },
}
```

### Common Pitfalls

| Pitfall | Consequence | Fix |
|---------|-------------|-----|
| Forgetting `#[from]` and writing manual `From` impls | Boilerplate, error-prone | Use `#[from]` for automatic conversion |
| Using `String` error messages instead of structured variants | Lost context, hard to match on | Use enum variants with typed fields |
| Not deriving `Debug` on the error enum | Compilation error (required by `Error` trait) | Add `#[derive(Debug, Error)]` |
| Flat enum with 20+ variants | Hard to navigate, unclear domain boundaries | Nest by domain — create submodule error types |
| Mixing `thiserror` and `anyhow` in the same module | Confusing error handling strategy | Pick one per module — `thiserror` for libraries, `anyhow` for app entry points |

### Example: Complete Error Design

```rust
use thiserror::Error;
use std::path::PathBuf;

// Domain-specific error types
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("config file not found: {0}")]
    NotFound(PathBuf),

    #[error("invalid config: {0}")]
    ParseError(String),
}

#[derive(Debug, Error)]
pub enum ScanError {
    #[error("config error: {0}")]
    Config(#[from] ConfigError),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("no findings")]
    NoFindings,
}

// Usage with context
fn run_scan(config_path: &Path) -> Result<(), ScanError> {
    let config = load_config(config_path)?;  // ConfigError auto-converted
    let results = scan(&config)?;            // Io error auto-converted
    
    if results.is_empty() {
        return Err(ScanError::NoFindings);
    }
    
    Ok(())
}
```

---

## Part 8: Panic Elimination Strategies

### Why Panics in Hot Paths Are Dangerous

- **Crash on unexpected input** — no graceful degradation
- **No recovery** — process dies, user loses work
- **Hard to test** — panic paths often untested
- **Poor UX** — users see raw error messages or silent crashes

### `.unwrap()` → `?` Migration

```rust
// Bad: panics on None
fn load_config() -> Config {
    let content = std::fs::read_to_string("config.json").unwrap();
    serde_json::from_str(&content).unwrap()
}

// Good: propagates error
fn load_config() -> Result<Config, ScanError> {
    let content = std::fs::read_to_string("config.json")?
        .map_err(|e| ScanError::FileNotFound { path: "config.json".into() })?;
    serde_json::from_str(&content)
        .map_err(|e| ScanError::ConfigParse(e.to_string()))?
}
```

### `.expect()` with Context

`.expect()` is acceptable **only** for true invariants that are impossible to violate:

```rust
// Acceptable — invariant: process must have current thread
fn spawn_worker() -> JoinHandle<()> {
    let handle = std::thread::current()
        .expect("must have current thread to spawn worker");
    // ...
}

// NOT acceptable — file may legitimately not exist
fn load_data(path: &Path) -> Data {
    let file = File::open(path).expect("file should exist"); // BAD
}

// Good — handle the error
fn load_data(path: &Path) -> Result<Data, ScanError> {
    let file = File::open(path)?;
    // ...
}
```

### Result Propagation Pattern

Make functions return `Result<T, E>` instead of panicking:

```rust
// Bad
fn parse_config(content: &str) -> Config {
    let json: Value = serde_json::from_str(content).unwrap();
    Config {
        name: json["name"].as_str().unwrap().to_string(),
        port: json["port"].as_u16().unwrap(),
    }
}

// Good
fn parse_config(content: &str) -> Result<Config, ConfigError> {
    let json: Value = serde_json::from_str(content)
        .map_err(|e| ConfigError::ParseError(e.to_string()))?;
    
    let name = json["name"]
        .as_str()
        .ok_or(ConfigError::MissingField("name".into()))?
        .to_string();
    
    let port = json["port"]
        .as_u16()
        .ok_or(ConfigError::MissingField("port".into()))?;
    
    Ok(Config { name, port })
}
```

### Common Panic Sources and Fixes

| Source | Panic Risk | Fix |
|--------|------------|-----|
| Array indexing `arr[i]` | Out of bounds | `arr.get(i).ok_or(Error)?` or bounds check first |
| `unwrap()` on `Option` | `None` value | `ok_or(error)?` or `ok_or_else(|| ...)?` |
| `unwrap()` on `Result` | Error variant | `?` with error mapping via `map_err()` |
| Integer division | Divide by zero | `checked_div()` / `saturating_div()` |
| `Vec::remove` out of bounds | Index >= len | Bounds check: `if i < vec.len() { vec.remove(i) }` |
| `unwrap()` on `parse()` | Invalid format | `parse().map_err(...)?` |
| `expect()` on fallible I/O | File not found, permissions | Return `Result` instead |

### Audit Techniques with Clippy

Replace raw grep with clippy lints and structured error handling:

```bash
# Use clippy lints instead of grep
cargo clippy -- -D clippy::expect_used -D clippy::unwrap_used
```

```rust
// Add #[track_caller] for better panic locations
#[track_caller]
fn panic_with_location(msg: &str) {
    panic!("{}", msg);  // Panic reports caller's location
}

// For FFI boundaries: catch_unwind
use std::panic::{catch_unwind, AssertUnwindSafe};

fn safe_ffi_call(f: impl FnOnce() + Send) -> Result<(), ()> {
    catch_unwind(AssertUnwindSafe(f)).map_err(|_| ())
}
```

```bash
# Find all unwraps except in test code
grep -rn '\.unwrap()' src/ | grep -v test

# Find expect calls with context
grep -rn '\.expect(' src/ | grep -v "test\|cfg(test)"

# Use cargo-udeps to find unused error handling
cargo udeps
```

### CI Gate: Deny `unwrap()` in Non-Test Code

Add to `clippy.toml`:

```toml
# clippy.toml
allow-unwrap-in-tests = true

# Or in Cargo.toml with rustfmt
[lints.clippy]
panic_in_result_fn = "deny"
unwrap_used = "deny"
```

Run in CI:

```bash
cargo clippy -- -D clippy::unwrap_used -D clippy::panic_in_result_fn
```

### When Panics ARE Acceptable

| Scenario | Example | Rationale |
|----------|---------|----------|
| True invariants | `debug_assert!`, `unreachable!()` after exhaustive match | Logic guarantees impossibility |
| Test setup | `#[test] fn foo() { setup().unwrap(); }` | Test failure is expected on bad setup |
| `unreachable!()` | `match value { A => ..., B => ..., _ => unreachable!() }` | Exhaustive match proves impossibility |
| `unimplemented!()` | Stub for future work during development | Explicit marker, not production code |
| `todo!()` | Placeholder during implementation | Development-only, should be removed |

```rust
// Acceptable — exhaustive match proves unreachable
match status {
    Status::Active => process_active(),
    Status::Inactive => process_inactive(),
    _ => unreachable!("All status variants handled"),
}

// Acceptable — test setup
#[test]
fn test_scan() {
    let config = load_test_config().unwrap();  // Test fails if config invalid
    let results = scan(&config).unwrap();
    assert!(!results.is_empty());
}
```

---

## Quick Reference Card

| Issue | Error Code | Quick Fix |
|-------|-----------|-----------|
| Type not found | E0433 | Add import, check spelling |
| Lifetime mismatch | E0597 | Return owned value or 'static |
| Type mismatch | E0308 | Add type annotation or convert |
| Borrow conflict | E0596 | Separate borrow scopes |
| Trait not satisfied | E0277 | Add trait bound |
| Cannot infer type | E0282 | Add type annotation |

### Essential Commands

```bash
# Check code quickly
cargo check

# Run with all warnings
cargo build --all-targets

# Run clippy
cargo clippy -- -D warnings

# Format code
cargo fmt

# Run tests
cargo test

# Coverage report
cargo llvm-cov --html

# Audit dependencies
cargo audit
```

---

## Anti-Patterns to Avoid

```rust
// BAD: unwrap() in production
let value = map.get("key").unwrap();  // Panics on missing key!

// GOOD: Handle missing case
let value = map.get("key")
    .ok_or_else(|| Error::KeyNotFound)?;

// BAD: Clone to avoid borrow checker
fn process(data: &Vec<u8>) -> usize {
    let cloned = data.clone();  // Wasteful
    cloned.len()
}

// GOOD: Use reference directly
fn process(data: &[u8]) -> usize {
    data.len()
}

// BAD: String when &str suffices
fn greet(name: String) { ... }

// GOOD: Borrow when read-only
fn greet(name: &str) { ... }

// BAD: Ignoring Result
let _ = validate(input);  // Silently ignores error

// GOOD: Handle or expect
let _ = validate(input).expect("validation should pass");
```

## Best Practices

- **Read compiler errors fully before acting**: Rust's error messages are verbose for a reason—read the entire diagnostic including suggestions.
- **Use `cargo check` in a tight loop**: Faster than full builds; run after every small change to catch errors early.
- **Structure tests in `tests/` for integration and `#[cfg(test)] mod tests` for unit**: Keep unit tests close to code; use `tests/` for cross-module integration tests.
- **Enforce coverage with `cargo tarpaulin` or `cargo-llvm-cov`**: Set CI gates (e.g., 80% line coverage) to prevent regression.
- **Prefer `Result<T, E>` over `Option<T>` when errors carry meaning**: Use `Option` for absence, `Result` for recoverable failures with context.

---

## References

- [The Rust Programming Language (The Book)](https://doc.rust-lang.org/book/)
- [Rust Compiler Error Index](https://doc.rust-lang.org/error-index.html)
- [rustc Error Documentation](https://doc.rust-lang.org/rustc/errors/)
- [Cargo Book: Testing Chapter](https://doc.rust-lang.org/cargo/reference/testing.html)
- [cargo-llvm-cov Documentation](https://github.com/taiki-e/cargo-llvm-cov)
- [cargo-tarpaulin Documentation](https://github.com/xd009642/tarpaulin)

---

## Summary

**Remember**:
1. Read compiler errors literally — Rust's compiler is helpful
2. Use builders for complex construction with validation
3. Keep tests close to code they test (inline) or in `tests/`
4. Enforce coverage in CI — 80% is a good starting target
5. Never use `unwrap()` in production code — always handle errors explicitly
6. Derive `Clone`, `Debug`, `Eq`, `PartialEq` when possible — let the compiler do work