pedant-core 0.19.0

Analysis engine for pedant: IR extraction, style checks, and capability detection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
use std::sync::Arc;

use crate::violation::CheckRationale;

const 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.";
const NESTED_CONDITIONAL_FIX: &str = "Use tuple patterns `match (a, b) { ... }`, match guards `Some(x) if x > 0 => ...`, or extract to functions.";
const NESTED_CONDITIONAL_EXCEPTION: &str = "None. Refactoring is always possible.";

/// Structured detail carried by an `item-visibility-policy` finding, surfaced
/// as `subject`/`expected`/`observed` in JSON output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisibilityDetail {
    /// The configured item name.
    pub subject: Arc<str>,
    /// The visibility the policy requires.
    pub expected: Arc<str>,
    /// The observed visibility, or `missing`/`duplicate`/`wrong-kind`.
    pub observed: Arc<str>,
}

/// Catalog entry for a single check, displayed by `--list-checks` and `--explain`.
#[derive(Debug, Clone, Copy)]
pub struct CheckInfo {
    /// Kebab-case identifier (e.g., `"max-depth"`).
    pub code: &'static str,
    /// One-line summary for the checks table.
    pub description: &'static str,
    /// Grouping key (e.g., `"nesting"`, `"dispatch"`, `"structure"`).
    pub category: &'static str,
    /// `true` when the pattern is disproportionately common in LLM output.
    pub llm_specific: bool,
}

/// Defines all check metadata in one place and generates:
/// - `ViolationType` enum (unit and data-carrying variants)
/// - `ViolationType::code()` returning the short code string
/// - `ViolationType::category()` returning the category
/// - `ViolationType::rationale()` returning `CheckRationale`
/// - `lookup_rationale()` free function
/// - `ALL_CHECKS` constant array of `CheckInfo`
macro_rules! define_checks {
    // Entry point: collect all check declarations, then emit everything.
    (
        $(
            $variant:ident $({ $field:ident : $ftype:ty })? => {
                code: $code:expr,
                description: $desc:literal,
                category: $cat:expr,
                problem: $problem:expr,
                fix: $fix:expr,
                exception: $exception:expr,
                llm_specific: $llm:expr $(,)?
            }
        ),+ $(,)?
    ) => {
        /// The kind of violation detected.
        #[derive(Debug, Clone, PartialEq, Eq)]
        pub enum ViolationType {
            $(
                #[doc = $desc]
                $variant $({
                    #[doc = "The matched pattern."]
                    $field: $ftype
                })?,
            )+
        }

        impl ViolationType {
            /// Returns the short code string used in output (e.g., `"max-depth"`).
            pub fn code(&self) -> &'static str {
                match self {
                    $(
                        Self::$variant $({ $field: _ })? => $code,
                    )+
                }
            }

            /// Returns the check category name (e.g., `"nesting"`, `"dispatch"`).
            pub fn category(&self) -> &'static str {
                match self {
                    $(
                        Self::$variant $({ $field: _ })? => $cat,
                    )+
                }
            }

            /// Returns the detailed rationale explaining why this check exists.
            pub fn rationale(&self) -> CheckRationale {
                match self {
                    $(
                        Self::$variant $({ $field: _ })? => CheckRationale {
                            problem: $problem,
                            fix: $fix,
                            exception: $exception,
                            llm_specific: $llm,
                        },
                    )+
                }
            }
        }

        /// Look up a ViolationType by its code string for rationale display.
        pub fn lookup_rationale(code: &str) -> Option<CheckRationale> {
            match code {
                $(
                    $code => Some(CheckRationale {
                        problem: $problem,
                        fix: $fix,
                        exception: $exception,
                        llm_specific: $llm,
                    }),
                )+
                _ => None,
            }
        }

        /// All available checks.
        pub const ALL_CHECKS: &[CheckInfo] = &[
            $(
                CheckInfo {
                    code: $code,
                    description: $desc,
                    category: $cat,
                    llm_specific: $llm,
                },
            )+
        ];
    };
}

define_checks! {
    MaxDepth => {
        code: "max-depth",
        description: "Excessive nesting depth",
        category: "nesting",
        problem: "Deeply nested code is hard to read, test, and modify. Each nesting level adds cognitive load. Bugs hide in deep branches.",
        fix: "Extract functions, use early returns, flatten with guard clauses.",
        exception: "Complex parsers or state machines may need deeper nesting locally.",
        llm_specific: false,
    },
    NestedIf => {
        code: "nested-if",
        description: "If nested inside if",
        category: "nesting",
        problem: NESTED_CONDITIONAL_PROBLEM,
        fix: NESTED_CONDITIONAL_FIX,
        exception: NESTED_CONDITIONAL_EXCEPTION,
        llm_specific: false,
    },
    IfInMatch => {
        code: "if-in-match",
        description: "If inside match arm",
        category: "nesting",
        problem: NESTED_CONDITIONAL_PROBLEM,
        fix: NESTED_CONDITIONAL_FIX,
        exception: NESTED_CONDITIONAL_EXCEPTION,
        llm_specific: false,
    },
    NestedMatch => {
        code: "nested-match",
        description: "Match nested inside match",
        category: "nesting",
        problem: NESTED_CONDITIONAL_PROBLEM,
        fix: NESTED_CONDITIONAL_FIX,
        exception: NESTED_CONDITIONAL_EXCEPTION,
        llm_specific: false,
    },
    MatchInIf => {
        code: "match-in-if",
        description: "Match inside if branch",
        category: "nesting",
        problem: NESTED_CONDITIONAL_PROBLEM,
        fix: NESTED_CONDITIONAL_FIX,
        exception: NESTED_CONDITIONAL_EXCEPTION,
        llm_specific: false,
    },
    ElseChain => {
        code: "else-chain",
        description: "Long if/else if chain",
        category: "nesting",
        problem: "Long if/else if/else if chains are unordered match arms in disguise. Easy to miss cases, hard to verify exhaustiveness.",
        fix: "Use `match` on boolean tuples. Precedence becomes explicit, compiler checks exhaustiveness.",
        exception: "None. Any boolean chain can be refactored to a tuple match.",
        llm_specific: false,
    },
    ForbiddenAttribute { pattern: Arc<str> } => {
        code: "forbidden-attribute",
        description: "Forbidden attribute pattern",
        category: "forbid_attributes",
        problem: "Silences warnings that indicate real problems. Dead code is maintenance burden. Unused variables often signal logic errors.",
        fix: "Remove dead code. Use `_` prefix for intentionally unused bindings. Address the underlying issue rather than suppressing.",
        exception: "Generated code, FFI bindings, conditional compilation.",
        llm_specific: true,
    },
    ForbiddenType { pattern: Arc<str> } => {
        code: "forbidden-type",
        description: "Forbidden type pattern",
        category: "forbid_types",
        problem: "Certain type patterns indicate suboptimal design. Arc<String> has double indirection. Box<dyn Error> is superseded by better alternatives.",
        fix: "Use Arc<str> instead of Arc<String>. Use thiserror or anyhow instead of Box<dyn Error>.",
        exception: "When mutation methods are needed via Arc::make_mut(), or legacy API interop.",
        llm_specific: true,
    },
    ForbiddenCall { pattern: Arc<str> } => {
        code: "forbidden-call",
        description: "Forbidden method call pattern",
        category: "forbid_calls",
        problem: ".unwrap() and .expect() panic on failure with no recovery. .clone() hides allocations.",
        fix: "Use `?` for propagation. Use .unwrap_or(), .unwrap_or_default() for defaults. Restructure ownership to avoid clone.",
        exception: "Human-authored code may use .unwrap() on provably infallible paths with documented invariants. Does not apply to LLM-generated code.",
        llm_specific: true,
    },
    ForbiddenMacro { pattern: Arc<str> } => {
        code: "forbidden-macro",
        description: "Forbidden macro pattern",
        category: "forbid_macros",
        problem: "panic!/todo!/unimplemented! crash at runtime. dbg!/println! are debug artifacts that shouldn't be committed.",
        fix: "Return Result instead of panicking. Use proper logging (tracing, log) for diagnostics. Implement functionality instead of stubbing.",
        exception: "Invariant assertions for bugs (not expected failures). CLI tools where stdout is the interface.",
        llm_specific: true,
    },
    ForbiddenElse => {
        code: "forbidden-else",
        description: "Use of `else` keyword (style preference)",
        category: "forbid_else",
        problem: "`else` creates implicit branches. `match` makes all branches explicit and compiler-checked.",
        fix: "Use `match` for multi-way branches. Use early return with guard clauses instead of if/else.",
        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.",
        llm_specific: false,
    },
    ForbiddenUnsafe => {
        code: "forbidden-unsafe",
        description: "Use of `unsafe` keyword",
        category: "forbid_unsafe",
        problem: "`unsafe` bypasses Rust's safety guarantees. Memory corruption, undefined behavior, and security vulnerabilities become possible.",
        fix: "Use safe abstractions. Wrap unsafe in minimal, well-audited modules with safe public APIs.",
        exception: "FFI bindings, performance-critical code with proven safety invariants, implementing safe abstractions over unsafe primitives.",
        llm_specific: false,
    },
    DynReturn => {
        code: "dyn-return",
        description: "Dynamic dispatch in return type (`Box<dyn T>`, `Arc<dyn T>`)",
        category: "dispatch",
        problem: "Returning Box<dyn Trait> or Arc<dyn Trait> forces vtable dispatch on every call. The vtable lookup prevents inlining and all downstream optimizations.",
        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.",
        exception: "Plugin systems or FFI boundaries where the set of concrete types is truly open-ended and unknown at compile time.",
        llm_specific: true,
    },
    DynParam => {
        code: "dyn-param",
        description: "Dynamic dispatch in function parameter (`&dyn T`, `Box<dyn T>`)",
        category: "dispatch",
        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.",
        fix: "Use a generic parameter `T: Trait` or `impl Trait` to enable monomorphization. The compiler generates specialized code for each concrete type, enabling inlining.",
        exception: "When the function is called with many distinct concrete types and binary size is a concern, or when storing heterogeneous collections.",
        llm_specific: true,
    },
    VecBoxDyn => {
        code: "vec-box-dyn",
        description: "`Vec<Box<dyn T>>` prevents cache locality and inlining",
        category: "dispatch",
        problem: "Vec<Box<dyn Trait>> incurs per-element heap allocation, vtable dispatch on every access, and scattered memory that defeats cache prefetching.",
        fix: "Use an enum wrapping the known concrete types. Elements are stored inline in the Vec with no vtable and no per-element allocation.",
        exception: "Plugin systems where concrete types are loaded at runtime and cannot be enumerated at compile time.",
        llm_specific: true,
    },
    DynField => {
        code: "dyn-field",
        description: "Dynamic dispatch in struct field (`Box<dyn T>`, `Arc<dyn T>`)",
        category: "dispatch",
        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.",
        fix: "Make the struct generic over the trait: `struct Foo<T: Trait> { field: T }`. The compiler monomorphizes each instantiation, enabling static dispatch and inlining.",
        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).",
        llm_specific: true,
    },
    CloneInLoop => {
        code: "clone-in-loop",
        description: "clone() called inside loop body (Arc/Rc suppressed when type is visible)",
        category: "performance",
        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.",
        fix: "Borrow instead of cloning. Use Cow<T> for conditional ownership. Use Rc/Arc for shared ownership. Restructure to move ownership before the loop.",
        exception: "When the cloned value is mutated independently per iteration and borrowing is not possible.",
        llm_specific: true,
    },
    DefaultHasher => {
        code: "default-hasher",
        description: "HashMap/HashSet with default SipHash hasher",
        category: "performance",
        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).",
        fix: "Use rustc_hash::FxHashMap for integer keys. Use ahash::AHashMap for general-purpose fast hashing. Specify the hasher explicitly: HashMap<K, V, S>.",
        exception: "When keys come from untrusted input (network, user-provided) and HashDoS resistance is required.",
        llm_specific: true,
    },
    MixedConcerns => {
        code: "mixed-concerns",
        description: "Disconnected type groups indicate mixed concerns",
        category: "structure",
        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.",
        fix: "Split the file along connected components. Each group of related types becomes its own module.",
        exception: "Re-export modules or files that intentionally collect small, independent items (e.g., error enums).",
        llm_specific: true,
    },
    InlineTests => {
        code: "inline-tests",
        description: "Test module embedded in source file",
        category: "structure",
        problem: "Test modules embedded in source files mix production code with test code. This inflates source files and makes test organization harder to navigate.",
        fix: "Move tests to the tests/ directory as integration tests, or to a separate test file alongside the source.",
        exception: "Small utility modules where colocated unit tests are preferred for locality.",
        llm_specific: true,
    },
    GenericNaming => {
        code: "generic-naming",
        description: "High ratio of generic variable names in a function",
        category: "naming",
        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.",
        fix: "Use domain-specific names that describe what the value represents: `user_id` not `val`, `retry_count` not `tmp`, `response_body` not `data`.",
        exception: "Small utility functions (fewer than 2 generic names) where short names are conventional.",
        llm_specific: true,
    },
    LetUnderscoreResult => {
        code: "let-underscore-result",
        description: "let _ = discards a potentially fallible Result",
        category: "structure",
        problem: "Silently discarding a Result hides errors that surface only in production.",
        fix: "Handle the error with `?`, `match`, or `if let Err`; or use `.expect()` with a reason if the error is truly impossible.",
        exception: "`write!`/`writeln!` to a `String` binding — fmt::Write for String is infallible.",
        llm_specific: true,
    },
    HighParamCount => {
        code: "high-param-count",
        description: "Function has too many parameters",
        category: "structure",
        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.",
        fix: "Group related parameters into a struct. Use the builder pattern for optional configuration. Split the function if parameters serve different concerns.",
        exception: "FFI bindings that must match an external C signature.",
        llm_specific: true,
    },
    LongFunctionBody => {
        code: "long-function-body",
        description: "Function body exceeds the configured line ceiling",
        category: "structure",
        problem: "A single oversized function body concentrates many responsibilities in one scope. It resists testing, hides bugs in the middle, and is the dominant single-responsibility failure mode in AI-generated Rust. Nesting and parameter checks measure body shape, not body extent.",
        fix: "Extract cohesive sections into named helper functions. Each function should do one job describable without the word `and`.",
        exception: "Generated code or exhaustive `match` dispatchers where every arm is a trivial one-liner.",
        llm_specific: false,
    },
    ModuleRootDefinitions => {
        code: "module-root-definitions",
        description: "Item defined in a module-root file (mod.rs/lib.rs)",
        category: "structure",
        problem: "Module-root files should only wire the module tree together with declarations and re-exports. Defining types, functions, or impls in them buries real logic in the file that is supposed to be a table of contents, and creates a decomposed-facade ambiguity with sibling module files.",
        fix: "Move the definition into a dedicated module file and re-export it from the root with `pub use`.",
        exception: "None. A module root is for declarations and re-exports; definitions belong in leaf modules.",
        llm_specific: false,
    },
    ItemVisibilityPolicy { detail: VisibilityDetail } => {
        code: "item-visibility-policy",
        description: "Configured item does not match its required visibility",
        category: "structure",
        problem: "Some items must keep an exact visibility to preserve an architectural boundary — a type sealed to its module, an API kept crate-internal. A drift to `pub`, a rename, a duplicate, or a wrong item kind silently widens or breaks that boundary.",
        fix: "Restore the item to the configured visibility, or update the policy in `.pedant.toml` if the boundary intentionally changed.",
        exception: "None. The policy is an explicit, per-item contract; change the contract rather than ignore it.",
        llm_specific: false,
    },
    FeatureBoundary => {
        code: "feature-boundary",
        description: "Cargo feature crosses a configured boundary",
        category: "structure",
        problem: "Dev-only or test-support features must stay sealed: enabled by no default feature, and reachable only through dev-dependency edges. A normal or build edge — or a default-feature chain — that enables such a feature leaks test scaffolding into production builds.",
        fix: "Move the feature-enabling dependency to `[dev-dependencies]`, drop it from default features, or stop requesting the feature on normal/build edges.",
        exception: "None. Change the boundary rule if the feature is intentionally public.",
        llm_specific: false,
    },
    FlatModuleFamily => {
        code: "flat-module-family",
        description: "Prefixed module family member outside its package directory",
        category: "structure",
        problem: "A configured module family must live below a single directory module. A `prefix.rs`, `prefix_*.rs`, or `prefix_*/` sitting flat beside its package directory scatters the family across the parent, obscuring that the members form one cohesive unit.",
        fix: "Move the member below the configured package directory (e.g. `parent/package_root/`).",
        exception: "None. Keep a prefixed family under its one package directory.",
        llm_specific: false,
    },
    ConflictingModuleRoot => {
        code: "conflicting-module-root",
        description: "Sibling `<stem>.rs` and `<stem>/` module roots",
        category: "structure",
        problem: "A `<stem>.rs` file beside a `<stem>/` directory gives a module two possible roots. The convention is directory modules rooted at `<stem>/mod.rs`; the stray sibling file recreates a decomposed-facade/root ambiguity that hides where the module actually lives.",
        fix: "Fold the `<stem>.rs` contents into `<stem>/mod.rs` and delete the sibling file.",
        exception: "None. Pick one module-root form per module.",
        llm_specific: false,
    },
    UngatedTestApi => {
        code: "ungated-test-api",
        description: "Test-only API under src/ not gated behind a feature",
        category: "structure",
        problem: "A test-only helper (e.g. `*_for_tests`) compiled into production `src/` without a feature gate ships test scaffolding to every consumer, widening the API surface and inviting misuse in non-test code.",
        fix: "Move the item behind `#[cfg(feature = \"test-support\")]` (on the item or an enclosing module), or relocate it into a test module.",
        exception: "None. Test-only APIs belong behind the configured feature; adjust the naming pattern or feature in config if the convention differs.",
        llm_specific: false,
    },
    HighMethodCount { type_name: Box<str> } => {
        code: "high-method-count",
        description: "Type has too many inherent methods (god-object)",
        category: "structure",
        problem: "A type whose inherent methods span many unrelated concerns is a god-object: maximally connected, so `mixed-concerns` stays silent, yet carrying far more than one responsibility. It is the dominant single-responsibility failure mode in large AI-generated Rust.",
        fix: "Extract cohesive groups of methods onto collaborator types the god-object delegates to. Pure forwarders that preserve the public API are not counted.",
        exception: "A facade that has genuinely shed its logic into collaborators and keeps only thin forwarders — those are excluded by default.",
        llm_specific: false,
    },
    ScatteredInherentImpl => {
        code: "scattered-inherent-impl",
        description: "A type's inherent impls span more than one file",
        category: "structure",
        problem: "A type whose own API is spread across files has no single place to read what it does. It also hides god-objects: a per-file method ceiling counts only the slice in front of it, so splitting an `impl` in two silences the ceiling while the type keeps every method it had.",
        fix: "Gather the type's inherent impls into the file that defines it, or split the type itself so each file owns a type with its own responsibility.",
        exception: "Impls that never coexist in one build — platform or feature `#[cfg]` splits — are already excluded.",
        llm_specific: false,
    },
    LargeSourceFile => {
        code: "large-source-file",
        description: "Source file exceeds the configured line ceiling",
        category: "structure",
        problem: "A file that accumulates many unrelated items becomes a dumping ground: hard to navigate, review, and reason about, and a sign that distinct concerns were never split into modules. Per-function and per-type size checks miss it because each item can be small while the file as a whole is enormous.",
        fix: "Split the file into focused modules grouped by concern, and re-export from the module root.",
        exception: "Generated files, or a documented aggregation point with a per-path threshold override recording the rationale.",
        llm_specific: false,
    },
}