1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::Path;
5use std::sync::{Arc, LazyLock};
6
7use serde::Deserialize;
8
9use crate::pattern::matches_glob;
10
11#[derive(Debug, Deserialize, Default, Clone)]
13#[serde(deny_unknown_fields)]
14pub struct PatternCheck {
15 #[serde(default)]
17 pub enabled: bool,
18 #[serde(default, deserialize_with = "deserialize_arc_str_slice")]
20 pub patterns: Arc<[Arc<str>]>,
21}
22
23impl PatternCheck {
24 pub fn apply_override(&mut self, ovr: &PatternOverride) {
26 if let Some(enabled) = ovr.enabled {
27 self.enabled = enabled;
28 }
29 if !ovr.patterns.is_empty() {
30 self.patterns = ovr.patterns.clone();
31 }
32 }
33}
34
35fn deserialize_arc_str_slice<'de, D: serde::Deserializer<'de>>(
36 deserializer: D,
37) -> Result<Arc<[Arc<str>]>, D::Error> {
38 let strings: Vec<String> = Vec::deserialize(deserializer)?;
39 Ok(strings.into_iter().map(Arc::from).collect())
40}
41
42const DEFAULT_GENERIC_NAMES: &[&str] = &[
44 "tmp", "temp", "data", "val", "value", "result", "res", "ret", "buf", "buffer", "item", "elem",
45 "obj", "input", "output", "info", "ctx", "args", "params", "thing", "stuff", "foo", "bar",
46 "baz",
47];
48
49#[derive(Debug, Deserialize, Clone)]
51#[serde(deny_unknown_fields)]
52pub struct NamingCheck {
53 #[serde(default)]
55 pub enabled: bool,
56 #[serde(
58 default = "default_generic_names",
59 deserialize_with = "deserialize_arc_str_slice"
60 )]
61 pub generic_names: Arc<[Arc<str>]>,
62 #[serde(default = "default_max_generic_ratio")]
64 pub max_generic_ratio: f64,
65 #[serde(default = "default_min_generic_count")]
67 pub min_generic_count: usize,
68}
69
70impl Default for NamingCheck {
71 fn default() -> Self {
72 Self {
73 enabled: false,
74 generic_names: default_generic_names(),
75 max_generic_ratio: default_max_generic_ratio(),
76 min_generic_count: default_min_generic_count(),
77 }
78 }
79}
80
81impl NamingCheck {
82 pub fn apply_override(&mut self, ovr: &NamingOverride) {
84 if let Some(enabled) = ovr.enabled {
85 self.enabled = enabled;
86 }
87 if let Some(ref names) = ovr.generic_names {
88 self.generic_names = names.clone();
89 }
90 if let Some(ratio) = ovr.max_generic_ratio {
91 self.max_generic_ratio = ratio;
92 }
93 if let Some(count) = ovr.min_generic_count {
94 self.min_generic_count = count;
95 }
96 }
97}
98
99#[derive(Debug, Deserialize, Default, Clone)]
101pub struct NamingOverride {
102 pub enabled: Option<bool>,
104 #[serde(default, deserialize_with = "deserialize_option_arc_str_slice")]
106 pub generic_names: Option<Arc<[Arc<str>]>>,
107 pub max_generic_ratio: Option<f64>,
109 pub min_generic_count: Option<usize>,
111}
112
113static GENERIC_NAMES_ARC: LazyLock<Arc<[Arc<str>]>> = LazyLock::new(|| {
114 DEFAULT_GENERIC_NAMES
115 .iter()
116 .map(|s| Arc::from(*s))
117 .collect()
118});
119
120fn default_generic_names() -> Arc<[Arc<str>]> {
121 Arc::clone(&GENERIC_NAMES_ARC)
122}
123
124type ArcStrSlice = Arc<[Arc<str>]>;
125
126fn deserialize_option_arc_str_slice<'de, D: serde::Deserializer<'de>>(
127 deserializer: D,
128) -> Result<Option<ArcStrSlice>, D::Error> {
129 let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
130 Ok(opt.map(|v| v.into_iter().map(Arc::from).collect()))
131}
132
133fn default_max_generic_ratio() -> f64 {
134 0.3
135}
136
137fn default_min_generic_count() -> usize {
138 2
139}
140
141#[derive(Debug, Deserialize, Default, Clone)]
143pub struct PatternOverride {
144 pub enabled: Option<bool>,
146 #[serde(default, deserialize_with = "deserialize_arc_str_slice")]
148 pub patterns: Arc<[Arc<str>]>,
149}
150
151#[derive(Debug, Deserialize, Default)]
153pub struct ConfigFile {
154 #[serde(default)]
156 pub gate: GateConfig,
157 #[serde(default = "default_max_depth")]
159 pub max_depth: usize,
160 #[serde(default = "default_else_chain_threshold")]
162 pub else_chain_threshold: usize,
163 #[serde(default = "default_max_params")]
165 pub max_params: usize,
166 #[serde(default)]
168 pub forbid_attributes: PatternCheck,
169 #[serde(default)]
171 pub forbid_types: PatternCheck,
172 #[serde(default)]
174 pub forbid_calls: PatternCheck,
175 #[serde(default)]
177 pub forbid_macros: PatternCheck,
178 #[serde(default)]
180 pub check_naming: NamingCheck,
181 #[serde(default = "default_true")]
183 pub check_nested_if: bool,
184 #[serde(default = "default_true")]
186 pub check_if_in_match: bool,
187 #[serde(default = "default_true")]
189 pub check_nested_match: bool,
190 #[serde(default = "default_true")]
192 pub check_match_in_if: bool,
193 #[serde(default = "default_true")]
195 pub check_else_chain: bool,
196 #[serde(default)]
198 pub forbid_else: bool,
199 #[serde(default = "default_true")]
201 pub forbid_unsafe: bool,
202 #[serde(default)]
204 pub check_dyn_return: bool,
205 #[serde(default)]
207 pub check_dyn_param: bool,
208 #[serde(default)]
210 pub check_vec_box_dyn: bool,
211 #[serde(default)]
213 pub check_dyn_field: bool,
214 #[serde(default)]
216 pub check_clone_in_loop: bool,
217 #[serde(default)]
219 pub check_default_hasher: bool,
220 #[serde(default)]
222 pub check_mixed_concerns: bool,
223 #[serde(default)]
225 pub check_inline_tests: bool,
226 #[serde(default)]
228 pub check_let_underscore_result: bool,
229 #[serde(default)]
231 pub check_high_param_count: bool,
232 #[serde(default)]
234 pub overrides: BTreeMap<Box<str>, PathOverride>,
235}
236
237#[derive(Debug, Deserialize, Default)]
239pub struct PathOverride {
240 pub enabled: Option<bool>,
242 pub max_depth: Option<usize>,
244 pub max_params: Option<usize>,
246 pub forbid_attributes: Option<PatternOverride>,
248 pub forbid_types: Option<PatternOverride>,
250 pub forbid_calls: Option<PatternOverride>,
252 pub forbid_macros: Option<PatternOverride>,
254 pub check_naming: Option<NamingOverride>,
256 pub check_nested_if: Option<bool>,
258 pub check_if_in_match: Option<bool>,
260 pub check_nested_match: Option<bool>,
262 pub check_match_in_if: Option<bool>,
264 pub check_else_chain: Option<bool>,
266 pub forbid_else: Option<bool>,
268 pub forbid_unsafe: Option<bool>,
270 pub check_dyn_return: Option<bool>,
272 pub check_dyn_param: Option<bool>,
274 pub check_vec_box_dyn: Option<bool>,
276 pub check_dyn_field: Option<bool>,
278 pub check_clone_in_loop: Option<bool>,
280 pub check_default_hasher: Option<bool>,
282 pub check_mixed_concerns: Option<bool>,
284 pub check_inline_tests: Option<bool>,
286 pub check_let_underscore_result: Option<bool>,
288 pub check_high_param_count: Option<bool>,
290}
291
292fn default_max_depth() -> usize {
293 3
294}
295
296fn default_else_chain_threshold() -> usize {
297 3
298}
299
300fn default_max_params() -> usize {
301 5
302}
303
304fn default_true() -> bool {
305 true
306}
307
308pub fn check_path_override<'a>(
310 file_path: &str,
311 config: &'a ConfigFile,
312) -> Option<&'a PathOverride> {
313 for (pattern, override_config) in &config.overrides {
314 if matches_glob(pattern, file_path) {
315 return Some(override_config);
316 }
317 }
318 None
319}
320
321macro_rules! for_each_bool_check {
335 ($callback:ident!) => {
336 $callback! {
337 "Flag `if` inside `if`.", check_nested_if, true;
338 "Flag `if` inside `match` arm.", check_if_in_match, true;
339 "Flag `match` inside `match`.", check_nested_match, true;
340 "Flag `match` inside `if` branch.", check_match_in_if, true;
341 "Flag long `if/else if` chains.", check_else_chain, true;
342 "Flag any use of the `else` keyword.", forbid_else, false;
343 "Flag any `unsafe` block.", forbid_unsafe, true;
344 "Flag dynamic dispatch in return types.", check_dyn_return, false;
345 "Flag dynamic dispatch in function parameters.", check_dyn_param, false;
346 "Flag `Vec<Box<dyn T>>`.", check_vec_box_dyn, false;
347 "Flag dynamic dispatch in struct fields.", check_dyn_field, false;
348 "Flag `.clone()` inside loop bodies.", check_clone_in_loop, false;
349 "Flag `HashMap`/`HashSet` with default hasher.", check_default_hasher, false;
350 "Flag disconnected type groups in a single file.", check_mixed_concerns, false;
351 "Flag `#[cfg(test)] mod` blocks in source files.", check_inline_tests, false;
352 "Flag `let _ = expr` that discards a Result.", check_let_underscore_result, false;
353 "Flag functions with too many parameters.", check_high_param_count, false;
354 }
355 };
356}
357
358macro_rules! impl_check_config {
361 ($($doc:literal, $field:ident, $default:expr;)*) => {
362 #[derive(Debug, Clone)]
364 pub struct CheckConfig {
365 pub max_depth: usize,
367 pub else_chain_threshold: usize,
369 pub max_params: usize,
371 pub forbid_attributes: PatternCheck,
373 pub forbid_types: PatternCheck,
375 pub forbid_calls: PatternCheck,
377 pub forbid_macros: PatternCheck,
379 pub check_naming: NamingCheck,
381 $(
382 #[doc = $doc]
383 pub $field: bool,
384 )*
385 }
386
387 impl Default for CheckConfig {
388 fn default() -> Self {
389 Self {
390 max_depth: default_max_depth(),
391 else_chain_threshold: default_else_chain_threshold(),
392 max_params: default_max_params(),
393 forbid_attributes: PatternCheck::default(),
394 forbid_types: PatternCheck::default(),
395 forbid_calls: PatternCheck::default(),
396 forbid_macros: PatternCheck::default(),
397 check_naming: NamingCheck::default(),
398 $( $field: $default, )*
399 }
400 }
401 }
402
403 impl CheckConfig {
404 pub fn from_config_file(fc: &ConfigFile) -> Self {
406 Self {
407 max_depth: fc.max_depth,
408 else_chain_threshold: fc.else_chain_threshold,
409 max_params: fc.max_params,
410 forbid_attributes: fc.forbid_attributes.clone(),
411 forbid_types: fc.forbid_types.clone(),
412 forbid_calls: fc.forbid_calls.clone(),
413 forbid_macros: fc.forbid_macros.clone(),
414 check_naming: fc.check_naming.clone(),
415 $( $field: fc.$field, )*
416 }
417 }
418
419 pub fn merge_bool_overrides(&mut self, ovr: &PathOverride) {
421 $(
422 if let Some(v) = ovr.$field {
423 self.$field = v;
424 }
425 )*
426 }
427 }
428 };
429}
430
431for_each_bool_check!(impl_check_config!);
432
433macro_rules! assert_bool_fields_in_sync {
437 ($($doc:literal, $field:ident, $default:expr;)*) => {
438 #[cfg(test)]
439 const _: () = {
440 const fn _check(cf: &ConfigFile, po: &PathOverride) {
443 $( let _ = (cf.$field, po.$field); )*
444 }
445 };
446 };
447}
448
449for_each_bool_check!(assert_bool_fields_in_sync!);
450
451impl CheckConfig {
452 pub fn resolve_for_path<'a>(
458 &'a self,
459 file_path: &str,
460 file_config: Option<&ConfigFile>,
461 ) -> Option<Cow<'a, Self>> {
462 let Some(fc) = file_config else {
463 return Some(Cow::Borrowed(self));
464 };
465
466 let Some(override_cfg) = check_path_override(file_path, fc) else {
467 return Some(Cow::Borrowed(self));
468 };
469
470 if override_cfg.enabled == Some(false) {
471 return None;
472 }
473
474 let mut config = self.clone();
475 if let Some(max_depth) = override_cfg.max_depth {
476 config.max_depth = max_depth;
477 }
478 if let Some(max_params) = override_cfg.max_params {
479 config.max_params = max_params;
480 }
481
482 config.merge_bool_overrides(override_cfg);
483
484 macro_rules! apply {
485 ($field:ident) => {
486 if let Some(ref ovr) = override_cfg.$field {
487 config.$field.apply_override(ovr);
488 }
489 };
490 }
491 apply!(forbid_attributes);
492 apply!(forbid_types);
493 apply!(forbid_calls);
494 apply!(forbid_macros);
495 apply!(check_naming);
496
497 Some(Cow::Owned(config))
498 }
499}
500
501#[derive(Debug, thiserror::Error)]
503pub enum ConfigError {
504 #[error("failed to read config file: {0}")]
506 Read(#[from] std::io::Error),
507 #[error("failed to parse config file: {0}")]
509 Parse(#[from] toml::de::Error),
510}
511
512pub fn load_config_file(path: &Path) -> Result<ConfigFile, ConfigError> {
514 let content = fs::read_to_string(path)?;
515 Ok(toml::from_str(&content)?)
516}
517
518pub fn find_config_file() -> Option<std::path::PathBuf> {
520 find_project_config_file().or_else(find_global_config_file)
521}
522
523fn find_project_config_file() -> Option<std::path::PathBuf> {
524 let config_path = std::env::current_dir().ok()?.join(".pedant.toml");
525 config_path.exists().then_some(config_path)
526}
527
528fn find_global_config_file() -> Option<std::path::PathBuf> {
529 let config_dir = std::env::var_os("XDG_CONFIG_HOME")
530 .map(std::path::PathBuf::from)
531 .or_else(|| {
532 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
533 })?;
534 let config_path = config_dir.join("pedant").join("config.toml");
535 config_path.exists().then_some(config_path)
536}
537
538#[derive(Debug)]
540pub enum GateRuleOverride {
541 Disabled,
543 Severity(crate::gate::GateSeverity),
545}
546
547#[derive(Debug)]
552pub struct GateConfig {
553 pub enabled: bool,
555 pub overrides: BTreeMap<Box<str>, GateRuleOverride>,
557}
558
559impl Default for GateConfig {
560 fn default() -> Self {
561 Self {
562 enabled: true,
563 overrides: BTreeMap::new(),
564 }
565 }
566}
567
568impl<'de> Deserialize<'de> for GateConfig {
569 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
570 where
571 D: serde::Deserializer<'de>,
572 {
573 use serde::de::Error;
574
575 #[derive(Deserialize)]
576 #[serde(untagged)]
577 enum GateTomlValue {
578 Bool(bool),
579 String(String),
580 }
581
582 let raw: BTreeMap<Box<str>, GateTomlValue> = BTreeMap::deserialize(deserializer)?;
583 let mut enabled = true;
584 let mut overrides = BTreeMap::new();
585
586 for (key, value) in raw {
587 match (&*key, value) {
588 ("enabled", GateTomlValue::Bool(b)) => enabled = b,
589 ("enabled", GateTomlValue::String(_)) => {
590 return Err(D::Error::custom("'enabled' must be a boolean"));
591 }
592 (_, GateTomlValue::Bool(false)) => {
593 overrides.insert(key, GateRuleOverride::Disabled);
594 }
595 (_, GateTomlValue::Bool(true)) => {} (_, GateTomlValue::String(s)) => {
597 let severity = parse_gate_severity(&s).ok_or_else(|| {
598 D::Error::custom(format!(
599 "invalid gate severity '{s}': expected \"deny\", \"warn\", or \"info\""
600 ))
601 })?;
602 overrides.insert(key, GateRuleOverride::Severity(severity));
603 }
604 }
605 }
606
607 Ok(GateConfig { enabled, overrides })
608 }
609}
610
611fn parse_gate_severity(s: &str) -> Option<crate::gate::GateSeverity> {
612 use crate::gate::GateSeverity;
613 match s {
614 "deny" => Some(GateSeverity::Deny),
615 "warn" => Some(GateSeverity::Warn),
616 "info" => Some(GateSeverity::Info),
617 _ => None,
618 }
619}