# pedant
A fast, opinionated Rust linter for keeping AI-generated code clean.
## The Problem
LLMs write code that compiles but violates best practices. Common patterns:
- **Nested conditionals** — `if` inside `if`, `match` inside `match`
- **Panic-prone code** — `.unwrap()`, `.expect()`, `todo!()`
- **Silenced warnings** — `#[allow(dead_code)]`, `#[allow(clippy::*)]`
- **Lazy cloning** — `.clone()` to satisfy the borrow checker
- **Debug artifacts** — `dbg!()`, `println!()` left in production code
- **Mixed concerns** — unrelated types dumped into a single file
These patterns pass `cargo check` and often slip through code review. pedant catches them.
## Why pedant?
**Built for LLM workflows.** Designed to validate AI-generated code before it enters your codebase:
- **Stdin support** — pipe generated code directly: `echo "$code" | pedant --stdin`
- **JSON output** — structured violations for automated pipelines
- **Exit codes** — `0` clean, `1` violations, `2` error
- **Fast** — single-pass AST analysis, no type inference
**Catches what Clippy misses.** Clippy focuses on correctness. pedant enforces taste:
- Configurable nesting depth limits
- Branch-in-branch detection (all combinations of `if`/`match`)
- Else-chain detection for long `if/else if` sequences
- Mixed concerns via type-graph connectivity analysis
- Per-path overrides for tests and generated code
## Installation
```bash
cargo install pedant
```
## Usage
```bash
# Check files
pedant src/*.rs
pedant -d 2 src/lib.rs # custom max depth
pedant -f json src/ # JSON output for tooling
# Validate LLM output
echo "$generated_code" | pedant --stdin -f json
# CI integration
if ! pedant src/; then
echo "pedant violations found"
exit 1
fi
```
## LLM Integration
### Pre-commit validation
```bash
#!/bin/bash
pedant $(git diff --cached --name-only -- '*.rs')
```
### In automated pipelines
```bash
# Validate before accepting generated code
pedant --stdin -f json <<< "$llm_output"
if [ $? -eq 1 ]; then
# feed violations back to LLM for correction
fi
```
## Checks
| `max-depth` | `-d N` | Nesting exceeds limit (default: 3) |
| `nested-if` | `--no-nested-if` | `if` inside `if` |
| `if-in-match` | `--no-if-in-match` | `if` inside match arm |
| `nested-match` | `--no-nested-match` | `match` inside `match` |
| `match-in-if` | `--no-match-in-if` | `match` inside `if` |
| `else-chain` | `--no-else-chain` | Long `if/else if` chains (3+) |
### Pattern checks (config file only)
| `forbidden-attribute` | `forbid_attributes` | Banned attributes (e.g., `allow(dead_code)`) |
| `forbidden-type` | `forbid_types` | Banned type patterns (e.g., `Arc<String>`) |
| `forbidden-call` | `forbid_calls` | Banned method calls (e.g., `.unwrap()`) |
| `forbidden-macro` | `forbid_macros` | Banned macros (e.g., `panic!`, `dbg!`) |
| `forbidden-else` | `forbid_else` | Use of `else` keyword |
| `forbidden-unsafe` | `forbid_unsafe` | Use of `unsafe` blocks |
### Performance and dispatch checks (off by default)
| `dyn-return` | `check_dyn_return` | `Box<dyn T>` / `Arc<dyn T>` in return types |
| `dyn-param` | `check_dyn_param` | `&dyn T` / `Box<dyn T>` in function parameters |
| `vec-box-dyn` | `check_vec_box_dyn` | `Vec<Box<dyn T>>` anywhere |
| `dyn-field` | `check_dyn_field` | `Box<dyn T>` / `Arc<dyn T>` in struct fields |
| `clone-in-loop` | `check_clone_in_loop` | `.clone()` inside loop bodies |
| `default-hasher` | `check_default_hasher` | `HashMap`/`HashSet` with default SipHash |
### Structure checks (off by default)
| `mixed-concerns` | `check_mixed_concerns` | Disconnected type groups in a single file |
Use `pedant --explain <CHECK>` for detailed rationale and fix guidance.
## Configuration
Optional `.pedant.toml` in project root:
```toml
max_depth = 3
check_nested_if = true
check_if_in_match = true
check_nested_match = true
check_match_in_if = true
check_else_chain = true
else_chain_threshold = 3
# Pattern checks — disabled by default, enable with patterns
[forbid_attributes]
enabled = true
patterns = ["allow(dead_code)", "allow(clippy::*)"]
[forbid_types]
enabled = true
patterns = ["Arc<String>", "Arc<Vec<*>>", "Box<dyn*Error*>"]
[forbid_calls]
enabled = true
patterns = [".unwrap()", ".expect(*)", ".clone()"]
[forbid_macros]
enabled = true
patterns = ["panic!", "todo!", "dbg!", "println!"]
forbid_else = false
forbid_unsafe = true
# Performance and dispatch checks — off by default
check_dyn_return = false
check_dyn_param = false
check_vec_box_dyn = false
check_dyn_field = false
check_clone_in_loop = false
check_default_hasher = false
# Structure checks — off by default
check_mixed_concerns = false
[overrides."tests/**"]
max_depth = 4
[overrides."**/generated.rs"]
enabled = false
```
## Exit Codes
- `0` - No violations
- `1` - Violations found
- `2` - Error (file not found, parse error)