Skip to main content

pedant_core/check_config/
runtime.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use super::file::{
5    ConfigFile, FeatureBoundaryRule, FlatModuleFamily, ItemVisibilityRule, PathOverride,
6    default_else_chain_threshold, default_max_depth, default_max_function_body_lines,
7    default_max_methods, default_max_params, default_module_root_files,
8    default_source_file_deny_lines, default_source_file_warn_lines, default_test_api_patterns,
9    default_test_support_feature,
10};
11use super::naming::NamingCheck;
12use super::pattern::PatternCheck;
13use crate::pattern::matches_glob;
14
15/// Find the most specific `[overrides]` entry whose glob matches `file_path`.
16///
17/// When multiple patterns match, the longest pattern wins. If two patterns
18/// have the same length, lexicographic (sorted) order breaks the tie.
19/// This makes override precedence deterministic and independent of
20/// declaration order in the config file.
21pub fn check_path_override<'a>(
22    file_path: &str,
23    config: &'a ConfigFile,
24) -> Option<&'a PathOverride> {
25    config
26        .overrides
27        .iter()
28        .filter(|(pattern, _)| matches_glob(pattern, file_path))
29        .max_by_key(|(pattern, _)| pattern.len())
30        .map(|(_, override_config)| override_config)
31}
32
33/// Single source of truth for boolean check fields.
34///
35/// Each entry: `"doc", field_name, default_value;`
36///
37/// Adding a new boolean check requires:
38/// 1. Add one line here
39/// 2. Add the field to `ConfigFile` (bool) and `PathOverride` (Option<bool>)
40///
41/// The macro generates `CheckConfig` fields + Default + from_config_file +
42/// merge_bool_overrides. A compile-time assertion (`assert_bool_fields_in_sync`)
43/// catches missing fields in `ConfigFile` or `PathOverride`.
44///
45/// Non-boolean fields (max_depth, forbid_*, check_naming, etc.) stay hand-written.
46macro_rules! for_each_bool_check {
47    ($callback:ident!) => {
48        $callback! {
49            "Flag `if` inside `if`.", check_nested_if, true;
50            "Flag `if` inside `match` arm.", check_if_in_match, true;
51            "Flag `match` inside `match`.", check_nested_match, true;
52            "Flag `match` inside `if` branch.", check_match_in_if, true;
53            "Flag long `if/else if` chains.", check_else_chain, true;
54            "Flag any use of the `else` keyword.", forbid_else, false;
55            "Flag any `unsafe` block.", forbid_unsafe, true;
56            "Flag dynamic dispatch in return types.", check_dyn_return, false;
57            "Flag dynamic dispatch in function parameters.", check_dyn_param, false;
58            "Flag `Vec<Box<dyn T>>`.", check_vec_box_dyn, false;
59            "Flag dynamic dispatch in struct fields.", check_dyn_field, false;
60            "Flag `.clone()` inside loop bodies.", check_clone_in_loop, false;
61            "Flag `HashMap`/`HashSet` with default hasher.", check_default_hasher, false;
62            "Flag disconnected type groups in a single file.", check_mixed_concerns, false;
63            "Flag `#[cfg(test)] mod` blocks in source files.", check_inline_tests, false;
64            "Flag `let _ = expr` that discards a Result.", check_let_underscore_result, false;
65            "Flag functions with too many parameters.", check_high_param_count, false;
66            "Flag function bodies that exceed the line ceiling.", check_long_function_body, false;
67            "Flag item definitions in module-root files.", check_module_root_definitions, false;
68            "Flag source files that exceed the line ceiling.", check_large_source_file, false;
69            "Flag god-object types by inherent-method count.", check_high_method_count, false;
70            "Count pure forwarders toward `high-method-count`.", count_forwarders, false;
71            "Enforce configured item-visibility policies.", check_item_visibility_policy, true;
72            "Flag ungated test-only APIs under `src/`.", check_ungated_test_api, false;
73            "Flag sibling `<stem>.rs` and `<stem>/` module roots.", check_conflicting_module_root, false;
74            "Enforce configured flat-module-family layout rules.", check_flat_module_family, true;
75            "Enforce configured Cargo feature-boundary invariants.", check_feature_boundary, true;
76            "Flag types whose inherent impls span more than one file.", check_scattered_inherent_impl, false;
77        }
78    };
79}
80
81/// Generates `CheckConfig` struct (boolean fields + non-boolean fields),
82/// `Default`, `from_config_file`, and `merge_bool_overrides`.
83macro_rules! impl_check_config {
84    ($($doc:literal, $field:ident, $default:expr;)*) => {
85        /// Configuration controlling which checks are enabled and their thresholds.
86        #[derive(Debug, Clone)]
87        pub struct CheckConfig {
88            /// Maximum allowed nesting depth.
89            pub max_depth: usize,
90            /// Minimum branches to trigger `else-chain`.
91            pub else_chain_threshold: usize,
92            /// Maximum parameter count before `high-param-count` fires.
93            pub max_params: usize,
94            /// Body line count before `long-function-body` fires.
95            pub max_function_body_lines: usize,
96            /// File names treated as module roots by `module-root-definitions`.
97            pub module_root_files: Arc<[Arc<str>]>,
98            /// Line count at which `large-source-file` emits a `Warn`.
99            pub source_file_warn_lines: usize,
100            /// Line count at which `large-source-file` emits a `Deny`.
101            pub source_file_deny_lines: usize,
102            /// Inherent-method count before `high-method-count` fires.
103            pub max_methods: usize,
104            /// Item-visibility policy rules.
105            pub item_visibility_policy: Arc<[ItemVisibilityRule]>,
106            /// Flat-module-family layout rules.
107            pub flat_module_families: Arc<[FlatModuleFamily]>,
108            /// Cargo feature-boundary invariants.
109            pub feature_boundaries: Arc<[FeatureBoundaryRule]>,
110            /// Name globs marking test-only APIs for `ungated-test-api`.
111            pub test_api_patterns: Arc<[Arc<str>]>,
112            /// Feature that must gate a test-only API.
113            pub test_support_feature: Arc<str>,
114            /// Banned attribute patterns.
115            pub forbid_attributes: PatternCheck,
116            /// Banned type patterns.
117            pub forbid_types: PatternCheck,
118            /// Banned method call patterns.
119            pub forbid_calls: PatternCheck,
120            /// Banned macro patterns.
121            pub forbid_macros: PatternCheck,
122            /// Generic naming check configuration.
123            pub check_naming: NamingCheck,
124            $(
125                #[doc = $doc]
126                pub $field: bool,
127            )*
128        }
129
130        impl Default for CheckConfig {
131            fn default() -> Self {
132                Self {
133                    max_depth: default_max_depth(),
134                    else_chain_threshold: default_else_chain_threshold(),
135                    max_params: default_max_params(),
136                    max_function_body_lines: default_max_function_body_lines(),
137                    module_root_files: default_module_root_files(),
138                    source_file_warn_lines: default_source_file_warn_lines(),
139                    source_file_deny_lines: default_source_file_deny_lines(),
140                    max_methods: default_max_methods(),
141                    item_visibility_policy: Arc::from([]),
142                    flat_module_families: Arc::from([]),
143                    feature_boundaries: Arc::from([]),
144                    test_api_patterns: default_test_api_patterns(),
145                    test_support_feature: Arc::from(default_test_support_feature()),
146                    forbid_attributes: PatternCheck::default(),
147                    forbid_types: PatternCheck::default(),
148                    forbid_calls: PatternCheck::default(),
149                    forbid_macros: PatternCheck::default(),
150                    check_naming: NamingCheck::default(),
151                    $( $field: $default, )*
152                }
153            }
154        }
155
156        impl CheckConfig {
157            /// Build from a [`ConfigFile`], copying all fields.
158            pub fn from_config_file(fc: &ConfigFile) -> Self {
159                Self {
160                    max_depth: fc.max_depth,
161                    else_chain_threshold: fc.else_chain_threshold,
162                    max_params: fc.max_params,
163                    max_function_body_lines: fc.max_function_body_lines,
164                    module_root_files: fc.module_root_files.clone(),
165                    source_file_warn_lines: fc.source_file_warn_lines,
166                    source_file_deny_lines: fc.source_file_deny_lines,
167                    max_methods: fc.max_methods,
168                    item_visibility_policy: fc.item_visibility_policy.iter().cloned().collect(),
169                    flat_module_families: fc.flat_module_families.iter().cloned().collect(),
170                    feature_boundaries: fc.feature_boundaries.iter().cloned().collect(),
171                    test_api_patterns: fc.test_api_patterns.clone(),
172                    test_support_feature: Arc::from(&*fc.test_support_feature),
173                    forbid_attributes: fc.forbid_attributes.clone(),
174                    forbid_types: fc.forbid_types.clone(),
175                    forbid_calls: fc.forbid_calls.clone(),
176                    forbid_macros: fc.forbid_macros.clone(),
177                    check_naming: fc.check_naming.clone(),
178                    $( $field: fc.$field, )*
179                }
180            }
181
182            /// Apply `Option<bool>` overrides from a [`PathOverride`].
183            pub fn merge_bool_overrides(&mut self, ovr: &PathOverride) {
184                $(
185                    if let Some(v) = ovr.$field {
186                        self.$field = v;
187                    }
188                )*
189            }
190        }
191    };
192}
193
194for_each_bool_check!(impl_check_config!);
195
196/// Compile-time assertion: every boolean check field in `for_each_bool_check!`
197/// must exist in `ConfigFile` (as `bool`) and `PathOverride` (as `Option<bool>`).
198/// Adding a field to the macro without updating these structs is a compile error.
199macro_rules! assert_bool_fields_in_sync {
200    ($($doc:literal, $field:ident, $default:expr;)*) => {
201        const _: () = {
202            // Access each field on both structs. If a field is missing
203            // from either, this block fails to compile.
204            const fn _check(cf: &ConfigFile, po: &PathOverride) {
205                $( let _ = (cf.$field, po.$field); )*
206            }
207        };
208    };
209}
210
211for_each_bool_check!(assert_bool_fields_in_sync!);
212
213impl CheckConfig {
214    /// Returns the effective config for a file path.
215    ///
216    /// Borrows `self` when no overrides match (zero clones).
217    /// Clones and mutates only when a path override applies.
218    /// Returns `None` when the override disables analysis for this path.
219    pub fn resolve_for_path<'a>(
220        &'a self,
221        file_path: &str,
222        file_config: Option<&ConfigFile>,
223    ) -> Option<Cow<'a, Self>> {
224        let Some(fc) = file_config else {
225            return Some(Cow::Borrowed(self));
226        };
227
228        let Some(override_cfg) = check_path_override(file_path, fc) else {
229            return Some(Cow::Borrowed(self));
230        };
231
232        if override_cfg.enabled == Some(false) {
233            return None;
234        }
235
236        let mut config = self.clone();
237        if let Some(max_depth) = override_cfg.max_depth {
238            config.max_depth = max_depth;
239        }
240        if let Some(max_params) = override_cfg.max_params {
241            config.max_params = max_params;
242        }
243        if let Some(max_body) = override_cfg.max_function_body_lines {
244            config.max_function_body_lines = max_body;
245        }
246        if let Some(warn) = override_cfg.source_file_warn_lines {
247            config.source_file_warn_lines = warn;
248        }
249        if let Some(deny) = override_cfg.source_file_deny_lines {
250            config.source_file_deny_lines = deny;
251        }
252        if let Some(max_methods) = override_cfg.max_methods {
253            config.max_methods = max_methods;
254        }
255
256        config.merge_bool_overrides(override_cfg);
257
258        macro_rules! apply {
259            ($field:ident) => {
260                if let Some(ref ovr) = override_cfg.$field {
261                    config.$field.apply_override(ovr);
262                }
263            };
264        }
265        apply!(forbid_attributes);
266        apply!(forbid_types);
267        apply!(forbid_calls);
268        apply!(forbid_macros);
269        apply!(check_naming);
270
271        Some(Cow::Owned(config))
272    }
273}