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)]
101#[serde(deny_unknown_fields)]
102pub struct NamingOverride {
103 pub enabled: Option<bool>,
105 #[serde(default, deserialize_with = "deserialize_option_arc_str_slice")]
107 pub generic_names: Option<Arc<[Arc<str>]>>,
108 pub max_generic_ratio: Option<f64>,
110 pub min_generic_count: Option<usize>,
112}
113
114static GENERIC_NAMES_ARC: LazyLock<Arc<[Arc<str>]>> = LazyLock::new(|| {
115 DEFAULT_GENERIC_NAMES
116 .iter()
117 .map(|s| Arc::from(*s))
118 .collect()
119});
120
121fn default_generic_names() -> Arc<[Arc<str>]> {
122 Arc::clone(&GENERIC_NAMES_ARC)
123}
124
125type ArcStrSlice = Arc<[Arc<str>]>;
126
127fn deserialize_option_arc_str_slice<'de, D: serde::Deserializer<'de>>(
128 deserializer: D,
129) -> Result<Option<ArcStrSlice>, D::Error> {
130 let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
131 Ok(opt.map(|v| v.into_iter().map(Arc::from).collect()))
132}
133
134fn default_max_generic_ratio() -> f64 {
135 0.3
136}
137
138fn default_min_generic_count() -> usize {
139 2
140}
141
142#[derive(Debug, Deserialize, Default, Clone)]
144#[serde(deny_unknown_fields)]
145pub struct PatternOverride {
146 pub enabled: Option<bool>,
148 #[serde(default, deserialize_with = "deserialize_arc_str_slice")]
150 pub patterns: Arc<[Arc<str>]>,
151}
152
153#[derive(Debug, Deserialize, Default)]
155#[serde(deny_unknown_fields)]
156pub struct ConfigFile {
157 #[serde(default)]
159 pub gate: GateConfig,
160 #[serde(default = "default_max_depth")]
162 pub max_depth: usize,
163 #[serde(default = "default_else_chain_threshold")]
165 pub else_chain_threshold: usize,
166 #[serde(default = "default_max_params")]
168 pub max_params: usize,
169 #[serde(default)]
171 pub forbid_attributes: PatternCheck,
172 #[serde(default)]
174 pub forbid_types: PatternCheck,
175 #[serde(default)]
177 pub forbid_calls: PatternCheck,
178 #[serde(default)]
180 pub forbid_macros: PatternCheck,
181 #[serde(default)]
183 pub check_naming: NamingCheck,
184 #[serde(default = "default_true")]
186 pub check_nested_if: bool,
187 #[serde(default = "default_true")]
189 pub check_if_in_match: bool,
190 #[serde(default = "default_true")]
192 pub check_nested_match: bool,
193 #[serde(default = "default_true")]
195 pub check_match_in_if: bool,
196 #[serde(default = "default_true")]
198 pub check_else_chain: bool,
199 #[serde(default)]
201 pub forbid_else: bool,
202 #[serde(default = "default_true")]
204 pub forbid_unsafe: bool,
205 #[serde(default)]
207 pub check_dyn_return: bool,
208 #[serde(default)]
210 pub check_dyn_param: bool,
211 #[serde(default)]
213 pub check_vec_box_dyn: bool,
214 #[serde(default)]
216 pub check_dyn_field: bool,
217 #[serde(default)]
219 pub check_clone_in_loop: bool,
220 #[serde(default)]
222 pub check_default_hasher: bool,
223 #[serde(default)]
225 pub check_mixed_concerns: bool,
226 #[serde(default)]
228 pub check_inline_tests: bool,
229 #[serde(default)]
231 pub check_let_underscore_result: bool,
232 #[serde(default)]
234 pub check_high_param_count: bool,
235 #[serde(default)]
237 pub overrides: BTreeMap<Box<str>, PathOverride>,
238}
239
240#[derive(Debug, Deserialize, Default)]
242#[serde(deny_unknown_fields)]
243pub struct PathOverride {
244 pub enabled: Option<bool>,
246 pub max_depth: Option<usize>,
248 pub max_params: Option<usize>,
250 pub forbid_attributes: Option<PatternOverride>,
252 pub forbid_types: Option<PatternOverride>,
254 pub forbid_calls: Option<PatternOverride>,
256 pub forbid_macros: Option<PatternOverride>,
258 pub check_naming: Option<NamingOverride>,
260 pub check_nested_if: Option<bool>,
262 pub check_if_in_match: Option<bool>,
264 pub check_nested_match: Option<bool>,
266 pub check_match_in_if: Option<bool>,
268 pub check_else_chain: Option<bool>,
270 pub forbid_else: Option<bool>,
272 pub forbid_unsafe: Option<bool>,
274 pub check_dyn_return: Option<bool>,
276 pub check_dyn_param: Option<bool>,
278 pub check_vec_box_dyn: Option<bool>,
280 pub check_dyn_field: Option<bool>,
282 pub check_clone_in_loop: Option<bool>,
284 pub check_default_hasher: Option<bool>,
286 pub check_mixed_concerns: Option<bool>,
288 pub check_inline_tests: Option<bool>,
290 pub check_let_underscore_result: Option<bool>,
292 pub check_high_param_count: Option<bool>,
294}
295
296fn default_max_depth() -> usize {
297 3
298}
299
300fn default_else_chain_threshold() -> usize {
301 3
302}
303
304fn default_max_params() -> usize {
305 5
306}
307
308fn default_true() -> bool {
309 true
310}
311
312pub fn check_path_override<'a>(
319 file_path: &str,
320 config: &'a ConfigFile,
321) -> Option<&'a PathOverride> {
322 config
323 .overrides
324 .iter()
325 .filter(|(pattern, _)| matches_glob(pattern, file_path))
326 .max_by_key(|(pattern, _)| pattern.len())
327 .map(|(_, override_config)| override_config)
328}
329
330macro_rules! for_each_bool_check {
344 ($callback:ident!) => {
345 $callback! {
346 "Flag `if` inside `if`.", check_nested_if, true;
347 "Flag `if` inside `match` arm.", check_if_in_match, true;
348 "Flag `match` inside `match`.", check_nested_match, true;
349 "Flag `match` inside `if` branch.", check_match_in_if, true;
350 "Flag long `if/else if` chains.", check_else_chain, true;
351 "Flag any use of the `else` keyword.", forbid_else, false;
352 "Flag any `unsafe` block.", forbid_unsafe, true;
353 "Flag dynamic dispatch in return types.", check_dyn_return, false;
354 "Flag dynamic dispatch in function parameters.", check_dyn_param, false;
355 "Flag `Vec<Box<dyn T>>`.", check_vec_box_dyn, false;
356 "Flag dynamic dispatch in struct fields.", check_dyn_field, false;
357 "Flag `.clone()` inside loop bodies.", check_clone_in_loop, false;
358 "Flag `HashMap`/`HashSet` with default hasher.", check_default_hasher, false;
359 "Flag disconnected type groups in a single file.", check_mixed_concerns, false;
360 "Flag `#[cfg(test)] mod` blocks in source files.", check_inline_tests, false;
361 "Flag `let _ = expr` that discards a Result.", check_let_underscore_result, false;
362 "Flag functions with too many parameters.", check_high_param_count, false;
363 }
364 };
365}
366
367macro_rules! impl_check_config {
370 ($($doc:literal, $field:ident, $default:expr;)*) => {
371 #[derive(Debug, Clone)]
373 pub struct CheckConfig {
374 pub max_depth: usize,
376 pub else_chain_threshold: usize,
378 pub max_params: usize,
380 pub forbid_attributes: PatternCheck,
382 pub forbid_types: PatternCheck,
384 pub forbid_calls: PatternCheck,
386 pub forbid_macros: PatternCheck,
388 pub check_naming: NamingCheck,
390 $(
391 #[doc = $doc]
392 pub $field: bool,
393 )*
394 }
395
396 impl Default for CheckConfig {
397 fn default() -> Self {
398 Self {
399 max_depth: default_max_depth(),
400 else_chain_threshold: default_else_chain_threshold(),
401 max_params: default_max_params(),
402 forbid_attributes: PatternCheck::default(),
403 forbid_types: PatternCheck::default(),
404 forbid_calls: PatternCheck::default(),
405 forbid_macros: PatternCheck::default(),
406 check_naming: NamingCheck::default(),
407 $( $field: $default, )*
408 }
409 }
410 }
411
412 impl CheckConfig {
413 pub fn from_config_file(fc: &ConfigFile) -> Self {
415 Self {
416 max_depth: fc.max_depth,
417 else_chain_threshold: fc.else_chain_threshold,
418 max_params: fc.max_params,
419 forbid_attributes: fc.forbid_attributes.clone(),
420 forbid_types: fc.forbid_types.clone(),
421 forbid_calls: fc.forbid_calls.clone(),
422 forbid_macros: fc.forbid_macros.clone(),
423 check_naming: fc.check_naming.clone(),
424 $( $field: fc.$field, )*
425 }
426 }
427
428 pub fn merge_bool_overrides(&mut self, ovr: &PathOverride) {
430 $(
431 if let Some(v) = ovr.$field {
432 self.$field = v;
433 }
434 )*
435 }
436 }
437 };
438}
439
440for_each_bool_check!(impl_check_config!);
441
442macro_rules! assert_bool_fields_in_sync {
446 ($($doc:literal, $field:ident, $default:expr;)*) => {
447 const _: () = {
448 const fn _check(cf: &ConfigFile, po: &PathOverride) {
451 $( let _ = (cf.$field, po.$field); )*
452 }
453 };
454 };
455}
456
457for_each_bool_check!(assert_bool_fields_in_sync!);
458
459impl CheckConfig {
460 pub fn resolve_for_path<'a>(
466 &'a self,
467 file_path: &str,
468 file_config: Option<&ConfigFile>,
469 ) -> Option<Cow<'a, Self>> {
470 let Some(fc) = file_config else {
471 return Some(Cow::Borrowed(self));
472 };
473
474 let Some(override_cfg) = check_path_override(file_path, fc) else {
475 return Some(Cow::Borrowed(self));
476 };
477
478 if override_cfg.enabled == Some(false) {
479 return None;
480 }
481
482 let mut config = self.clone();
483 if let Some(max_depth) = override_cfg.max_depth {
484 config.max_depth = max_depth;
485 }
486 if let Some(max_params) = override_cfg.max_params {
487 config.max_params = max_params;
488 }
489
490 config.merge_bool_overrides(override_cfg);
491
492 macro_rules! apply {
493 ($field:ident) => {
494 if let Some(ref ovr) = override_cfg.$field {
495 config.$field.apply_override(ovr);
496 }
497 };
498 }
499 apply!(forbid_attributes);
500 apply!(forbid_types);
501 apply!(forbid_calls);
502 apply!(forbid_macros);
503 apply!(check_naming);
504
505 Some(Cow::Owned(config))
506 }
507}
508
509#[derive(Debug, thiserror::Error)]
511pub enum ConfigError {
512 #[error("failed to read config file: {0}")]
514 Read(#[from] std::io::Error),
515 #[error("failed to parse config file: {0}")]
517 Parse(#[from] toml::de::Error),
518}
519
520pub fn load_config_file(path: &Path) -> Result<ConfigFile, ConfigError> {
522 let content = fs::read_to_string(path)?;
523 Ok(toml::from_str(&content)?)
524}
525
526pub fn find_config_file() -> Result<Option<std::path::PathBuf>, ConfigError> {
528 let project_config = find_project_config_file()?;
529 Ok(project_config.or_else(find_global_config_file))
530}
531
532fn find_project_config_file() -> Result<Option<std::path::PathBuf>, ConfigError> {
533 let config_path = std::env::current_dir()?.join(".pedant.toml");
534 Ok(config_path.exists().then_some(config_path))
535}
536
537fn find_global_config_file() -> Option<std::path::PathBuf> {
538 let config_dir = std::env::var_os("XDG_CONFIG_HOME")
539 .map(std::path::PathBuf::from)
540 .or_else(|| {
541 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
542 })?;
543 let config_path = config_dir.join("pedant").join("config.toml");
544 config_path.exists().then_some(config_path)
545}
546
547#[derive(Debug)]
549pub enum GateRuleOverride {
550 Disabled,
552 Severity(crate::gate::GateSeverity),
554}
555
556#[derive(Debug)]
561pub struct GateConfig {
562 pub enabled: bool,
564 pub overrides: BTreeMap<Box<str>, GateRuleOverride>,
566}
567
568impl Default for GateConfig {
569 fn default() -> Self {
570 Self {
571 enabled: true,
572 overrides: BTreeMap::new(),
573 }
574 }
575}
576
577impl<'de> Deserialize<'de> for GateConfig {
578 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
579 where
580 D: serde::Deserializer<'de>,
581 {
582 use serde::de::Error;
583
584 #[derive(Deserialize)]
585 #[serde(untagged)]
586 enum GateTomlValue {
587 Bool(bool),
588 String(String),
589 }
590
591 let raw: BTreeMap<Box<str>, GateTomlValue> = BTreeMap::deserialize(deserializer)?;
592 let mut enabled = true;
593 let mut overrides = BTreeMap::new();
594
595 for (key, value) in raw {
596 match (&*key, value) {
597 ("enabled", GateTomlValue::Bool(b)) => enabled = b,
598 ("enabled", GateTomlValue::String(_)) => {
599 return Err(D::Error::custom("'enabled' must be a boolean"));
600 }
601 (_, _) if !is_known_gate_rule(&key) => {
602 return Err(D::Error::custom(format!("unknown gate rule '{key}'")));
603 }
604 (_, GateTomlValue::Bool(false)) => {
605 overrides.insert(key, GateRuleOverride::Disabled);
606 }
607 (_, GateTomlValue::Bool(true)) => {} (_, GateTomlValue::String(s)) => {
609 let severity = parse_gate_severity(&s).ok_or_else(|| {
610 D::Error::custom(format!(
611 "invalid gate severity '{s}': expected \"deny\", \"warn\", or \"info\""
612 ))
613 })?;
614 overrides.insert(key, GateRuleOverride::Severity(severity));
615 }
616 }
617 }
618
619 Ok(GateConfig { enabled, overrides })
620 }
621}
622
623fn is_known_gate_rule(rule_name: &str) -> bool {
624 crate::gate::all_gate_rules()
625 .iter()
626 .any(|rule| rule.name == rule_name)
627}
628
629fn parse_gate_severity(s: &str) -> Option<crate::gate::GateSeverity> {
630 use crate::gate::GateSeverity;
631 match s {
632 "deny" => Some(GateSeverity::Deny),
633 "warn" => Some(GateSeverity::Warn),
634 "info" => Some(GateSeverity::Info),
635 _ => None,
636 }
637}