Skip to main content

pedant_core/
checks.rs

1use std::sync::Arc;
2
3use crate::violation::CheckRationale;
4
5const NESTED_CONDITIONAL_PROBLEM: &str = "Conditional-in-conditional creates combinatorial complexity. A 2-branch if inside a 3-branch match is 6 paths. Hard to ensure all paths are tested.";
6const NESTED_CONDITIONAL_FIX: &str = "Use tuple patterns `match (a, b) { ... }`, match guards `Some(x) if x > 0 => ...`, or extract to functions.";
7const NESTED_CONDITIONAL_EXCEPTION: &str = "None. Refactoring is always possible.";
8
9/// Catalog entry for a single check, displayed by `--list-checks` and `--explain`.
10#[derive(Debug, Clone, Copy)]
11pub struct CheckInfo {
12    /// Kebab-case identifier (e.g., `"max-depth"`).
13    pub code: &'static str,
14    /// One-line summary for the checks table.
15    pub description: &'static str,
16    /// Grouping key (e.g., `"nesting"`, `"dispatch"`, `"structure"`).
17    pub category: &'static str,
18    /// `true` when the pattern is disproportionately common in LLM output.
19    pub llm_specific: bool,
20}
21
22/// Defines all check metadata in one place and generates:
23/// - `ViolationType` enum (unit and data-carrying variants)
24/// - `ViolationType::code()` returning the short code string
25/// - `ViolationType::check_name()` returning the category
26/// - `ViolationType::rationale()` returning `CheckRationale`
27/// - `lookup_rationale()` free function
28/// - `ALL_CHECKS` constant array of `CheckInfo`
29macro_rules! define_checks {
30    // Entry point: collect all check declarations, then emit everything.
31    (
32        $(
33            $variant:ident $({ $field:ident : $ftype:ty })? => {
34                code: $code:expr,
35                description: $desc:literal,
36                category: $cat:expr,
37                problem: $problem:expr,
38                fix: $fix:expr,
39                exception: $exception:expr,
40                llm_specific: $llm:expr $(,)?
41            }
42        ),+ $(,)?
43    ) => {
44        /// The kind of violation detected.
45        #[derive(Debug, Clone, PartialEq, Eq)]
46        pub enum ViolationType {
47            $(
48                #[doc = $desc]
49                $variant $({
50                    #[doc = "The matched pattern."]
51                    $field: $ftype
52                })?,
53            )+
54        }
55
56        impl ViolationType {
57            /// Returns the short code string used in output (e.g., `"max-depth"`).
58            pub fn code(&self) -> &'static str {
59                match self {
60                    $(
61                        Self::$variant $({ $field: _ })? => $code,
62                    )+
63                }
64            }
65
66            /// Returns the check category name (e.g., `"nesting"`, `"dispatch"`).
67            pub fn check_name(&self) -> &'static str {
68                match self {
69                    $(
70                        Self::$variant $({ $field: _ })? => $cat,
71                    )+
72                }
73            }
74
75            /// Returns the detailed rationale explaining why this check exists.
76            pub fn rationale(&self) -> CheckRationale {
77                match self {
78                    $(
79                        Self::$variant $({ $field: _ })? => CheckRationale {
80                            problem: $problem,
81                            fix: $fix,
82                            exception: $exception,
83                            llm_specific: $llm,
84                        },
85                    )+
86                }
87            }
88        }
89
90        /// Look up a ViolationType by its code string for rationale display.
91        pub fn lookup_rationale(code: &str) -> Option<CheckRationale> {
92            match code {
93                $(
94                    $code => Some(CheckRationale {
95                        problem: $problem,
96                        fix: $fix,
97                        exception: $exception,
98                        llm_specific: $llm,
99                    }),
100                )+
101                _ => None,
102            }
103        }
104
105        /// All available checks.
106        pub const ALL_CHECKS: &[CheckInfo] = &[
107            $(
108                CheckInfo {
109                    code: $code,
110                    description: $desc,
111                    category: $cat,
112                    llm_specific: $llm,
113                },
114            )+
115        ];
116    };
117}
118
119define_checks! {
120    MaxDepth => {
121        code: "max-depth",
122        description: "Excessive nesting depth",
123        category: "nesting",
124        problem: "Deeply nested code is hard to read, test, and modify. Each nesting level adds cognitive load. Bugs hide in deep branches.",
125        fix: "Extract functions, use early returns, flatten with guard clauses.",
126        exception: "Complex parsers or state machines may need deeper nesting locally.",
127        llm_specific: false,
128    },
129    NestedIf => {
130        code: "nested-if",
131        description: "If nested inside if",
132        category: "nesting",
133        problem: NESTED_CONDITIONAL_PROBLEM,
134        fix: NESTED_CONDITIONAL_FIX,
135        exception: NESTED_CONDITIONAL_EXCEPTION,
136        llm_specific: false,
137    },
138    IfInMatch => {
139        code: "if-in-match",
140        description: "If inside match arm",
141        category: "nesting",
142        problem: NESTED_CONDITIONAL_PROBLEM,
143        fix: NESTED_CONDITIONAL_FIX,
144        exception: NESTED_CONDITIONAL_EXCEPTION,
145        llm_specific: false,
146    },
147    NestedMatch => {
148        code: "nested-match",
149        description: "Match nested inside match",
150        category: "nesting",
151        problem: NESTED_CONDITIONAL_PROBLEM,
152        fix: NESTED_CONDITIONAL_FIX,
153        exception: NESTED_CONDITIONAL_EXCEPTION,
154        llm_specific: false,
155    },
156    MatchInIf => {
157        code: "match-in-if",
158        description: "Match inside if branch",
159        category: "nesting",
160        problem: NESTED_CONDITIONAL_PROBLEM,
161        fix: NESTED_CONDITIONAL_FIX,
162        exception: NESTED_CONDITIONAL_EXCEPTION,
163        llm_specific: false,
164    },
165    ElseChain => {
166        code: "else-chain",
167        description: "Long if/else if chain",
168        category: "nesting",
169        problem: "Long if/else if/else if chains are unordered match arms in disguise. Easy to miss cases, hard to verify exhaustiveness.",
170        fix: "Use `match` on boolean tuples. Precedence becomes explicit, compiler checks exhaustiveness.",
171        exception: "None. Any boolean chain can be refactored to a tuple match.",
172        llm_specific: false,
173    },
174    ForbiddenAttribute { pattern: Arc<str> } => {
175        code: "forbidden-attribute",
176        description: "Forbidden attribute pattern",
177        category: "forbid_attributes",
178        problem: "Silences warnings that indicate real problems. Dead code is maintenance burden. Unused variables often signal logic errors.",
179        fix: "Remove dead code. Use `_` prefix for intentionally unused bindings. Address the underlying issue rather than suppressing.",
180        exception: "Generated code, FFI bindings, conditional compilation.",
181        llm_specific: true,
182    },
183    ForbiddenType { pattern: Arc<str> } => {
184        code: "forbidden-type",
185        description: "Forbidden type pattern",
186        category: "forbid_types",
187        problem: "Certain type patterns indicate suboptimal design. Arc<String> has double indirection. Box<dyn Error> is superseded by better alternatives.",
188        fix: "Use Arc<str> instead of Arc<String>. Use thiserror or anyhow instead of Box<dyn Error>.",
189        exception: "When mutation methods are needed via Arc::make_mut(), or legacy API interop.",
190        llm_specific: true,
191    },
192    ForbiddenCall { pattern: Arc<str> } => {
193        code: "forbidden-call",
194        description: "Forbidden method call pattern",
195        category: "forbid_calls",
196        problem: ".unwrap() and .expect() panic on failure with no recovery. .clone() hides allocations.",
197        fix: "Use `?` for propagation. Use .unwrap_or(), .unwrap_or_default() for defaults. Restructure ownership to avoid clone.",
198        exception: "Human-authored code may use .unwrap() on provably infallible paths with documented invariants. Does not apply to LLM-generated code.",
199        llm_specific: true,
200    },
201    ForbiddenMacro { pattern: Arc<str> } => {
202        code: "forbidden-macro",
203        description: "Forbidden macro pattern",
204        category: "forbid_macros",
205        problem: "panic!/todo!/unimplemented! crash at runtime. dbg!/println! are debug artifacts that shouldn't be committed.",
206        fix: "Return Result instead of panicking. Use proper logging (tracing, log) for diagnostics. Implement functionality instead of stubbing.",
207        exception: "Invariant assertions for bugs (not expected failures). CLI tools where stdout is the interface.",
208        llm_specific: true,
209    },
210    ForbiddenElse => {
211        code: "forbidden-else",
212        description: "Use of `else` keyword (style preference)",
213        category: "forbid_else",
214        problem: "`else` creates implicit branches. `match` makes all branches explicit and compiler-checked.",
215        fix: "Use `match` for multi-way branches. Use early return with guard clauses instead of if/else.",
216        exception: "This is a style preference. Clippy recommends if/else for simple boolean conditions (match_bool lint). Disable with `forbid_else = false` if you disagree.",
217        llm_specific: false,
218    },
219    ForbiddenUnsafe => {
220        code: "forbidden-unsafe",
221        description: "Use of `unsafe` keyword",
222        category: "forbid_unsafe",
223        problem: "`unsafe` bypasses Rust's safety guarantees. Memory corruption, undefined behavior, and security vulnerabilities become possible.",
224        fix: "Use safe abstractions. Wrap unsafe in minimal, well-audited modules with safe public APIs.",
225        exception: "FFI bindings, performance-critical code with proven safety invariants, implementing safe abstractions over unsafe primitives.",
226        llm_specific: false,
227    },
228    DynReturn => {
229        code: "dyn-return",
230        description: "Dynamic dispatch in return type (`Box<dyn T>`, `Arc<dyn T>`)",
231        category: "dispatch",
232        problem: "Returning Box<dyn Trait> or Arc<dyn Trait> forces vtable dispatch on every call. The vtable lookup prevents inlining and all downstream optimizations.",
233        fix: "Use enum dispatch for a closed set of types. Use `impl Trait` when the caller doesn't need to store heterogeneously. Use a generic type parameter when the concrete type varies per call site.",
234        exception: "Plugin systems or FFI boundaries where the set of concrete types is truly open-ended and unknown at compile time.",
235        llm_specific: true,
236    },
237    DynParam => {
238        code: "dyn-param",
239        description: "Dynamic dispatch in function parameter (`&dyn T`, `Box<dyn T>`)",
240        category: "dispatch",
241        problem: "Accepting &dyn Trait or Box<dyn Trait> as a parameter forces vtable dispatch per call. The compiler cannot monomorphize or inline the callee's methods.",
242        fix: "Use a generic parameter `T: Trait` or `impl Trait` to enable monomorphization. The compiler generates specialized code for each concrete type, enabling inlining.",
243        exception: "When the function is called with many distinct concrete types and binary size is a concern, or when storing heterogeneous collections.",
244        llm_specific: true,
245    },
246    VecBoxDyn => {
247        code: "vec-box-dyn",
248        description: "`Vec<Box<dyn T>>` prevents cache locality and inlining",
249        category: "dispatch",
250        problem: "Vec<Box<dyn Trait>> incurs per-element heap allocation, vtable dispatch on every access, and scattered memory that defeats cache prefetching.",
251        fix: "Use an enum wrapping the known concrete types. Elements are stored inline in the Vec with no vtable and no per-element allocation.",
252        exception: "Plugin systems where concrete types are loaded at runtime and cannot be enumerated at compile time.",
253        llm_specific: true,
254    },
255    DynField => {
256        code: "dyn-field",
257        description: "Dynamic dispatch in struct field (`Box<dyn T>`, `Arc<dyn T>`)",
258        category: "dispatch",
259        problem: "A Box<dyn Trait> or Arc<dyn Trait> struct field permanently commits every method call on that field to vtable dispatch. This prevents inlining for the lifetime of the struct.",
260        fix: "Make the struct generic over the trait: `struct Foo<T: Trait> { field: T }`. The compiler monomorphizes each instantiation, enabling static dispatch and inlining.",
261        exception: "When the struct must hold different concrete types at different times, or when the concrete type is determined at runtime (e.g., configuration-driven).",
262        llm_specific: true,
263    },
264    CloneInLoop => {
265        code: "clone-in-loop",
266        description: "clone() called inside loop body (Arc/Rc suppressed when type is visible)",
267        category: "performance",
268        problem: ".clone() inside a loop body means N heap allocations where N is the iteration count. LLMs add .clone() to satisfy the borrow checker without considering the per-iteration cost. Arc/Rc clones are automatically suppressed when the type is visible (explicit type annotations or containers with Arc/Rc generic args). Type aliases that hide Arc/Rc (e.g., type MyMap = BTreeMap<Arc<str>, Arc<str>>) cannot be resolved and may cause false positives.",
269        fix: "Borrow instead of cloning. Use Cow<T> for conditional ownership. Use Rc/Arc for shared ownership. Restructure to move ownership before the loop.",
270        exception: "When the cloned value is mutated independently per iteration and borrowing is not possible.",
271        llm_specific: true,
272    },
273    DefaultHasher => {
274        code: "default-hasher",
275        description: "HashMap/HashSet with default SipHash hasher",
276        category: "performance",
277        problem: "HashMap/HashSet default to SipHash, designed for HashDoS resistance. SipHash is 2-5x slower than FxHash or AHash for typical keys (integers, short strings).",
278        fix: "Use rustc_hash::FxHashMap for integer keys. Use ahash::AHashMap for general-purpose fast hashing. Specify the hasher explicitly: HashMap<K, V, S>.",
279        exception: "When keys come from untrusted input (network, user-provided) and HashDoS resistance is required.",
280        llm_specific: true,
281    },
282    MixedConcerns => {
283        code: "mixed-concerns",
284        description: "Disconnected type groups indicate mixed concerns",
285        category: "structure",
286        problem: "Disconnected type groups in a single file indicate mixed concerns. Types that share no fields, trait bounds, or function signatures belong in separate modules.",
287        fix: "Split the file along connected components. Each group of related types becomes its own module.",
288        exception: "Re-export modules or files that intentionally collect small, independent items (e.g., error enums).",
289        llm_specific: true,
290    },
291    InlineTests => {
292        code: "inline-tests",
293        description: "Test module embedded in source file",
294        category: "structure",
295        problem: "Test modules embedded in source files mix production code with test code. This inflates source files and makes test organization harder to navigate.",
296        fix: "Move tests to the tests/ directory as integration tests, or to a separate test file alongside the source.",
297        exception: "Small utility modules where colocated unit tests are preferred for locality.",
298        llm_specific: true,
299    },
300    GenericNaming => {
301        code: "generic-naming",
302        description: "High ratio of generic variable names in a function",
303        category: "naming",
304        problem: "LLMs generate generic names like `tmp`, `data`, `val` because training data is saturated with them. System prompt rules like 'use descriptive names' compete with this statistical bias and lose.",
305        fix: "Use domain-specific names that describe what the value represents: `user_id` not `val`, `retry_count` not `tmp`, `response_body` not `data`.",
306        exception: "Small utility functions (fewer than 2 generic names) where short names are conventional.",
307        llm_specific: true,
308    },
309    LetUnderscoreResult => {
310        code: "let-underscore-result",
311        description: "let _ = discards a potentially fallible Result",
312        category: "structure",
313        problem: "Silently discarding a Result hides errors that surface only in production.",
314        fix: "Handle the error with `?`, `match`, or `if let Err`; or use `.expect()` with a reason if the error is truly impossible.",
315        exception: "`write!`/`writeln!` to a `String` binding — fmt::Write for String is infallible.",
316        llm_specific: true,
317    },
318    HighParamCount => {
319        code: "high-param-count",
320        description: "Function has too many parameters",
321        category: "structure",
322        problem: "Functions with many parameters are hard to call correctly. Callers must remember argument order, and adding parameters is a breaking change at every call site.",
323        fix: "Group related parameters into a struct. Use the builder pattern for optional configuration. Split the function if parameters serve different concerns.",
324        exception: "FFI bindings that must match an external C signature.",
325        llm_specific: true,
326    },
327}