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)]
165 pub forbid_attributes: PatternCheck,
166 #[serde(default)]
168 pub forbid_types: PatternCheck,
169 #[serde(default)]
171 pub forbid_calls: PatternCheck,
172 #[serde(default)]
174 pub forbid_macros: PatternCheck,
175 #[serde(default)]
177 pub check_naming: NamingCheck,
178 #[serde(default = "default_true")]
180 pub check_nested_if: bool,
181 #[serde(default = "default_true")]
183 pub check_if_in_match: bool,
184 #[serde(default = "default_true")]
186 pub check_nested_match: bool,
187 #[serde(default = "default_true")]
189 pub check_match_in_if: bool,
190 #[serde(default = "default_true")]
192 pub check_else_chain: bool,
193 #[serde(default)]
195 pub forbid_else: bool,
196 #[serde(default = "default_true")]
198 pub forbid_unsafe: bool,
199 #[serde(default)]
201 pub check_dyn_return: bool,
202 #[serde(default)]
204 pub check_dyn_param: bool,
205 #[serde(default)]
207 pub check_vec_box_dyn: bool,
208 #[serde(default)]
210 pub check_dyn_field: bool,
211 #[serde(default)]
213 pub check_clone_in_loop: bool,
214 #[serde(default)]
216 pub check_default_hasher: bool,
217 #[serde(default)]
219 pub check_mixed_concerns: bool,
220 #[serde(default)]
222 pub check_inline_tests: bool,
223 #[serde(default)]
225 pub check_let_underscore_result: bool,
226 #[serde(default)]
228 pub overrides: BTreeMap<Box<str>, PathOverride>,
229}
230
231#[derive(Debug, Deserialize, Default)]
233pub struct PathOverride {
234 pub enabled: Option<bool>,
236 pub max_depth: Option<usize>,
238 pub forbid_attributes: Option<PatternOverride>,
240 pub forbid_types: Option<PatternOverride>,
242 pub forbid_calls: Option<PatternOverride>,
244 pub forbid_macros: Option<PatternOverride>,
246 pub check_naming: Option<NamingOverride>,
248 pub check_nested_if: Option<bool>,
250 pub check_if_in_match: Option<bool>,
252 pub check_nested_match: Option<bool>,
254 pub check_match_in_if: Option<bool>,
256 pub check_else_chain: Option<bool>,
258 pub forbid_else: Option<bool>,
260 pub forbid_unsafe: Option<bool>,
262 pub check_dyn_return: Option<bool>,
264 pub check_dyn_param: Option<bool>,
266 pub check_vec_box_dyn: Option<bool>,
268 pub check_dyn_field: Option<bool>,
270 pub check_clone_in_loop: Option<bool>,
272 pub check_default_hasher: Option<bool>,
274 pub check_mixed_concerns: Option<bool>,
276 pub check_inline_tests: Option<bool>,
278 pub check_let_underscore_result: Option<bool>,
280}
281
282fn default_max_depth() -> usize {
283 3
284}
285
286fn default_else_chain_threshold() -> usize {
287 3
288}
289
290fn default_true() -> bool {
291 true
292}
293
294pub fn check_path_override<'a>(
296 file_path: &str,
297 config: &'a ConfigFile,
298) -> Option<&'a PathOverride> {
299 for (pattern, override_config) in &config.overrides {
300 if matches_glob(pattern, file_path) {
301 return Some(override_config);
302 }
303 }
304 None
305}
306
307macro_rules! for_each_bool_check {
321 ($callback:ident!) => {
322 $callback! {
323 "Flag `if` inside `if`.", check_nested_if, true;
324 "Flag `if` inside `match` arm.", check_if_in_match, true;
325 "Flag `match` inside `match`.", check_nested_match, true;
326 "Flag `match` inside `if` branch.", check_match_in_if, true;
327 "Flag long `if/else if` chains.", check_else_chain, true;
328 "Flag any use of the `else` keyword.", forbid_else, false;
329 "Flag any `unsafe` block.", forbid_unsafe, true;
330 "Flag dynamic dispatch in return types.", check_dyn_return, false;
331 "Flag dynamic dispatch in function parameters.", check_dyn_param, false;
332 "Flag `Vec<Box<dyn T>>`.", check_vec_box_dyn, false;
333 "Flag dynamic dispatch in struct fields.", check_dyn_field, false;
334 "Flag `.clone()` inside loop bodies.", check_clone_in_loop, false;
335 "Flag `HashMap`/`HashSet` with default hasher.", check_default_hasher, false;
336 "Flag disconnected type groups in a single file.", check_mixed_concerns, false;
337 "Flag `#[cfg(test)] mod` blocks in source files.", check_inline_tests, false;
338 "Flag `let _ = expr` that discards a Result.", check_let_underscore_result, false;
339 }
340 };
341}
342
343macro_rules! impl_check_config {
346 ($($doc:literal, $field:ident, $default:expr;)*) => {
347 #[derive(Debug, Clone)]
349 pub struct CheckConfig {
350 pub max_depth: usize,
352 pub else_chain_threshold: usize,
354 pub forbid_attributes: PatternCheck,
356 pub forbid_types: PatternCheck,
358 pub forbid_calls: PatternCheck,
360 pub forbid_macros: PatternCheck,
362 pub check_naming: NamingCheck,
364 $(
365 #[doc = $doc]
366 pub $field: bool,
367 )*
368 }
369
370 impl Default for CheckConfig {
371 fn default() -> Self {
372 Self {
373 max_depth: 3,
374 else_chain_threshold: 3,
375 forbid_attributes: PatternCheck::default(),
376 forbid_types: PatternCheck::default(),
377 forbid_calls: PatternCheck::default(),
378 forbid_macros: PatternCheck::default(),
379 check_naming: NamingCheck::default(),
380 $( $field: $default, )*
381 }
382 }
383 }
384
385 impl CheckConfig {
386 pub fn from_config_file(fc: &ConfigFile) -> Self {
388 Self {
389 max_depth: fc.max_depth,
390 else_chain_threshold: fc.else_chain_threshold,
391 forbid_attributes: fc.forbid_attributes.clone(),
392 forbid_types: fc.forbid_types.clone(),
393 forbid_calls: fc.forbid_calls.clone(),
394 forbid_macros: fc.forbid_macros.clone(),
395 check_naming: fc.check_naming.clone(),
396 $( $field: fc.$field, )*
397 }
398 }
399
400 pub fn merge_bool_overrides(&mut self, ovr: &PathOverride) {
402 $(
403 if let Some(v) = ovr.$field {
404 self.$field = v;
405 }
406 )*
407 }
408 }
409 };
410}
411
412for_each_bool_check!(impl_check_config!);
413
414macro_rules! assert_bool_fields_in_sync {
418 ($($doc:literal, $field:ident, $default:expr;)*) => {
419 #[cfg(test)]
420 const _: () = {
421 $(
422 const fn $field(cf: &ConfigFile, po: &PathOverride) -> (bool, Option<bool>) {
423 (cf.$field, po.$field)
424 }
425 )*
426 };
427 };
428}
429
430for_each_bool_check!(assert_bool_fields_in_sync!);
431
432impl CheckConfig {
433 pub fn resolve_for_path<'a>(
439 &'a self,
440 file_path: &str,
441 file_config: Option<&ConfigFile>,
442 ) -> Option<Cow<'a, Self>> {
443 let Some(fc) = file_config else {
444 return Some(Cow::Borrowed(self));
445 };
446
447 let Some(override_cfg) = check_path_override(file_path, fc) else {
448 return Some(Cow::Borrowed(self));
449 };
450
451 if override_cfg.enabled == Some(false) {
452 return None;
453 }
454
455 let mut config = self.clone();
456 if let Some(max_depth) = override_cfg.max_depth {
457 config.max_depth = max_depth;
458 }
459
460 config.merge_bool_overrides(override_cfg);
461
462 macro_rules! apply {
463 ($field:ident) => {
464 if let Some(ref ovr) = override_cfg.$field {
465 config.$field.apply_override(ovr);
466 }
467 };
468 }
469 apply!(forbid_attributes);
470 apply!(forbid_types);
471 apply!(forbid_calls);
472 apply!(forbid_macros);
473 apply!(check_naming);
474
475 Some(Cow::Owned(config))
476 }
477}
478
479#[derive(Debug, thiserror::Error)]
481pub enum ConfigError {
482 #[error("failed to read config file: {0}")]
484 Read(#[from] std::io::Error),
485 #[error("failed to parse config file: {0}")]
487 Parse(#[from] toml::de::Error),
488}
489
490pub fn load_config_file(path: &Path) -> Result<ConfigFile, ConfigError> {
492 let content = fs::read_to_string(path)?;
493 Ok(toml::from_str(&content)?)
494}
495
496pub fn find_config_file() -> Option<std::path::PathBuf> {
498 find_project_config_file().or_else(find_global_config_file)
499}
500
501fn find_project_config_file() -> Option<std::path::PathBuf> {
502 let config_path = std::env::current_dir().ok()?.join(".pedant.toml");
503 config_path.exists().then_some(config_path)
504}
505
506fn find_global_config_file() -> Option<std::path::PathBuf> {
507 let config_dir = std::env::var_os("XDG_CONFIG_HOME")
508 .map(std::path::PathBuf::from)
509 .or_else(|| {
510 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
511 })?;
512 let config_path = config_dir.join("pedant").join("config.toml");
513 config_path.exists().then_some(config_path)
514}
515
516#[derive(Debug)]
518pub enum GateRuleOverride {
519 Disabled,
521 Severity(crate::gate::GateSeverity),
523}
524
525#[derive(Debug)]
530pub struct GateConfig {
531 pub enabled: bool,
533 pub overrides: BTreeMap<Box<str>, GateRuleOverride>,
535}
536
537impl Default for GateConfig {
538 fn default() -> Self {
539 Self {
540 enabled: true,
541 overrides: BTreeMap::new(),
542 }
543 }
544}
545
546impl<'de> Deserialize<'de> for GateConfig {
547 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
548 where
549 D: serde::Deserializer<'de>,
550 {
551 use serde::de::Error;
552
553 #[derive(Deserialize)]
554 #[serde(untagged)]
555 enum GateTomlValue {
556 Bool(bool),
557 String(String),
558 }
559
560 let raw: BTreeMap<Box<str>, GateTomlValue> = BTreeMap::deserialize(deserializer)?;
561 let mut enabled = true;
562 let mut overrides = BTreeMap::new();
563
564 for (key, value) in raw {
565 match (&*key, value) {
566 ("enabled", GateTomlValue::Bool(b)) => enabled = b,
567 ("enabled", GateTomlValue::String(_)) => {
568 return Err(D::Error::custom("'enabled' must be a boolean"));
569 }
570 (_, GateTomlValue::Bool(false)) => {
571 overrides.insert(key, GateRuleOverride::Disabled);
572 }
573 (_, GateTomlValue::Bool(true)) => {} (_, GateTomlValue::String(s)) => {
575 let severity = parse_gate_severity(&s).ok_or_else(|| {
576 D::Error::custom(format!(
577 "invalid gate severity '{s}': expected \"deny\", \"warn\", or \"info\""
578 ))
579 })?;
580 overrides.insert(key, GateRuleOverride::Severity(severity));
581 }
582 }
583 }
584
585 Ok(GateConfig { enabled, overrides })
586 }
587}
588
589fn parse_gate_severity(s: &str) -> Option<crate::gate::GateSeverity> {
590 use crate::gate::GateSeverity;
591 match s {
592 "deny" => Some(GateSeverity::Deny),
593 "warn" => Some(GateSeverity::Warn),
594 "info" => Some(GateSeverity::Info),
595 _ => None,
596 }
597}