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, Clone)]
156#[serde(deny_unknown_fields)]
157pub struct ItemVisibilityRule {
158 pub path: Box<str>,
160 pub kind: Box<str>,
162 pub name: Box<str>,
164 pub visibility: Box<str>,
167}
168
169#[derive(Debug, Deserialize, Clone)]
171#[serde(deny_unknown_fields)]
172pub struct FeatureBoundaryRule {
173 pub package: Box<str>,
175 pub feature: Box<str>,
177 pub rule: Box<str>,
180}
181
182#[derive(Debug, Deserialize, Clone)]
185#[serde(deny_unknown_fields)]
186pub struct FlatModuleFamily {
187 pub parent: Box<str>,
189 pub package_root: Box<str>,
191 pub prefix: Box<str>,
193}
194
195#[derive(Debug, Deserialize, Default)]
197#[serde(deny_unknown_fields)]
198pub struct ConfigFile {
199 #[serde(default)]
201 pub gate: GateConfig,
202 #[serde(default = "default_max_depth")]
204 pub max_depth: usize,
205 #[serde(default = "default_else_chain_threshold")]
207 pub else_chain_threshold: usize,
208 #[serde(default = "default_max_params")]
210 pub max_params: usize,
211 #[serde(default = "default_max_function_body_lines")]
213 pub max_function_body_lines: usize,
214 #[serde(
217 default = "default_module_root_files",
218 deserialize_with = "deserialize_arc_str_slice"
219 )]
220 pub module_root_files: Arc<[Arc<str>]>,
221 #[serde(default = "default_source_file_warn_lines")]
223 pub source_file_warn_lines: usize,
224 #[serde(default = "default_source_file_deny_lines")]
226 pub source_file_deny_lines: usize,
227 #[serde(default = "default_max_methods")]
229 pub max_methods: usize,
230 #[serde(default)]
232 pub forbid_attributes: PatternCheck,
233 #[serde(default)]
235 pub forbid_types: PatternCheck,
236 #[serde(default)]
238 pub forbid_calls: PatternCheck,
239 #[serde(default)]
241 pub forbid_macros: PatternCheck,
242 #[serde(default)]
244 pub check_naming: NamingCheck,
245 #[serde(default = "default_true")]
247 pub check_nested_if: bool,
248 #[serde(default = "default_true")]
250 pub check_if_in_match: bool,
251 #[serde(default = "default_true")]
253 pub check_nested_match: bool,
254 #[serde(default = "default_true")]
256 pub check_match_in_if: bool,
257 #[serde(default = "default_true")]
259 pub check_else_chain: bool,
260 #[serde(default)]
262 pub forbid_else: bool,
263 #[serde(default = "default_true")]
265 pub forbid_unsafe: bool,
266 #[serde(default)]
268 pub check_dyn_return: bool,
269 #[serde(default)]
271 pub check_dyn_param: bool,
272 #[serde(default)]
274 pub check_vec_box_dyn: bool,
275 #[serde(default)]
277 pub check_dyn_field: bool,
278 #[serde(default)]
280 pub check_clone_in_loop: bool,
281 #[serde(default)]
283 pub check_default_hasher: bool,
284 #[serde(default)]
286 pub check_mixed_concerns: bool,
287 #[serde(default)]
289 pub check_inline_tests: bool,
290 #[serde(default)]
292 pub check_let_underscore_result: bool,
293 #[serde(default)]
295 pub check_high_param_count: bool,
296 #[serde(default)]
298 pub check_long_function_body: bool,
299 #[serde(default)]
301 pub check_module_root_definitions: bool,
302 #[serde(default)]
304 pub check_large_source_file: bool,
305 #[serde(default)]
307 pub check_high_method_count: bool,
308 #[serde(default)]
310 pub count_forwarders: bool,
311 #[serde(default = "default_true")]
313 pub check_item_visibility_policy: bool,
314 #[serde(default)]
316 pub item_visibility_policy: Vec<ItemVisibilityRule>,
317 #[serde(default)]
319 pub check_ungated_test_api: bool,
320 #[serde(default)]
322 pub check_conflicting_module_root: bool,
323 #[serde(
325 default = "default_test_api_patterns",
326 deserialize_with = "deserialize_arc_str_slice"
327 )]
328 pub test_api_patterns: Arc<[Arc<str>]>,
329 #[serde(default = "default_test_support_feature")]
331 pub test_support_feature: Box<str>,
332 #[serde(default = "default_true")]
334 pub check_flat_module_family: bool,
335 #[serde(default)]
337 pub flat_module_families: Vec<FlatModuleFamily>,
338 #[serde(default = "default_true")]
340 pub check_feature_boundary: bool,
341 #[serde(default)]
343 pub feature_boundaries: Vec<FeatureBoundaryRule>,
344 #[serde(default)]
346 pub overrides: BTreeMap<Box<str>, PathOverride>,
347}
348
349#[derive(Debug, Deserialize, Default)]
351#[serde(deny_unknown_fields)]
352pub struct PathOverride {
353 pub enabled: Option<bool>,
355 pub max_depth: Option<usize>,
357 pub max_params: Option<usize>,
359 pub max_function_body_lines: Option<usize>,
361 pub source_file_warn_lines: Option<usize>,
364 pub source_file_deny_lines: Option<usize>,
366 pub max_methods: Option<usize>,
368 pub forbid_attributes: Option<PatternOverride>,
370 pub forbid_types: Option<PatternOverride>,
372 pub forbid_calls: Option<PatternOverride>,
374 pub forbid_macros: Option<PatternOverride>,
376 pub check_naming: Option<NamingOverride>,
378 pub check_nested_if: Option<bool>,
380 pub check_if_in_match: Option<bool>,
382 pub check_nested_match: Option<bool>,
384 pub check_match_in_if: Option<bool>,
386 pub check_else_chain: Option<bool>,
388 pub forbid_else: Option<bool>,
390 pub forbid_unsafe: Option<bool>,
392 pub check_dyn_return: Option<bool>,
394 pub check_dyn_param: Option<bool>,
396 pub check_vec_box_dyn: Option<bool>,
398 pub check_dyn_field: Option<bool>,
400 pub check_clone_in_loop: Option<bool>,
402 pub check_default_hasher: Option<bool>,
404 pub check_mixed_concerns: Option<bool>,
406 pub check_inline_tests: Option<bool>,
408 pub check_let_underscore_result: Option<bool>,
410 pub check_high_param_count: Option<bool>,
412 pub check_long_function_body: Option<bool>,
414 pub check_module_root_definitions: Option<bool>,
416 pub check_large_source_file: Option<bool>,
418 pub check_high_method_count: Option<bool>,
420 pub count_forwarders: Option<bool>,
422 pub check_item_visibility_policy: Option<bool>,
424 pub check_ungated_test_api: Option<bool>,
426 pub check_conflicting_module_root: Option<bool>,
428 pub check_flat_module_family: Option<bool>,
430 pub check_feature_boundary: Option<bool>,
432}
433
434fn default_max_depth() -> usize {
435 3
436}
437
438fn default_else_chain_threshold() -> usize {
439 3
440}
441
442fn default_max_params() -> usize {
443 5
444}
445
446fn default_max_function_body_lines() -> usize {
447 120
448}
449
450static MODULE_ROOT_FILES_ARC: LazyLock<Arc<[Arc<str>]>> =
451 LazyLock::new(|| [Arc::from("mod.rs"), Arc::from("lib.rs")].into());
452
453fn default_module_root_files() -> Arc<[Arc<str>]> {
454 Arc::clone(&MODULE_ROOT_FILES_ARC)
455}
456
457fn default_source_file_warn_lines() -> usize {
458 500
459}
460
461fn default_source_file_deny_lines() -> usize {
462 1000
463}
464
465fn default_max_methods() -> usize {
466 40
467}
468
469static TEST_API_PATTERNS_ARC: LazyLock<Arc<[Arc<str>]>> =
470 LazyLock::new(|| [Arc::from("*_for_tests")].into());
471
472fn default_test_api_patterns() -> Arc<[Arc<str>]> {
473 Arc::clone(&TEST_API_PATTERNS_ARC)
474}
475
476fn default_test_support_feature() -> Box<str> {
477 "test-support".into()
478}
479
480fn default_true() -> bool {
481 true
482}
483
484pub fn check_path_override<'a>(
491 file_path: &str,
492 config: &'a ConfigFile,
493) -> Option<&'a PathOverride> {
494 config
495 .overrides
496 .iter()
497 .filter(|(pattern, _)| matches_glob(pattern, file_path))
498 .max_by_key(|(pattern, _)| pattern.len())
499 .map(|(_, override_config)| override_config)
500}
501
502macro_rules! for_each_bool_check {
516 ($callback:ident!) => {
517 $callback! {
518 "Flag `if` inside `if`.", check_nested_if, true;
519 "Flag `if` inside `match` arm.", check_if_in_match, true;
520 "Flag `match` inside `match`.", check_nested_match, true;
521 "Flag `match` inside `if` branch.", check_match_in_if, true;
522 "Flag long `if/else if` chains.", check_else_chain, true;
523 "Flag any use of the `else` keyword.", forbid_else, false;
524 "Flag any `unsafe` block.", forbid_unsafe, true;
525 "Flag dynamic dispatch in return types.", check_dyn_return, false;
526 "Flag dynamic dispatch in function parameters.", check_dyn_param, false;
527 "Flag `Vec<Box<dyn T>>`.", check_vec_box_dyn, false;
528 "Flag dynamic dispatch in struct fields.", check_dyn_field, false;
529 "Flag `.clone()` inside loop bodies.", check_clone_in_loop, false;
530 "Flag `HashMap`/`HashSet` with default hasher.", check_default_hasher, false;
531 "Flag disconnected type groups in a single file.", check_mixed_concerns, false;
532 "Flag `#[cfg(test)] mod` blocks in source files.", check_inline_tests, false;
533 "Flag `let _ = expr` that discards a Result.", check_let_underscore_result, false;
534 "Flag functions with too many parameters.", check_high_param_count, false;
535 "Flag function bodies that exceed the line ceiling.", check_long_function_body, false;
536 "Flag item definitions in module-root files.", check_module_root_definitions, false;
537 "Flag source files that exceed the line ceiling.", check_large_source_file, false;
538 "Flag god-object types by inherent-method count.", check_high_method_count, false;
539 "Count pure forwarders toward `high-method-count`.", count_forwarders, false;
540 "Enforce configured item-visibility policies.", check_item_visibility_policy, true;
541 "Flag ungated test-only APIs under `src/`.", check_ungated_test_api, false;
542 "Flag sibling `<stem>.rs` and `<stem>/` module roots.", check_conflicting_module_root, false;
543 "Enforce configured flat-module-family layout rules.", check_flat_module_family, true;
544 "Enforce configured Cargo feature-boundary invariants.", check_feature_boundary, true;
545 }
546 };
547}
548
549macro_rules! impl_check_config {
552 ($($doc:literal, $field:ident, $default:expr;)*) => {
553 #[derive(Debug, Clone)]
555 pub struct CheckConfig {
556 pub max_depth: usize,
558 pub else_chain_threshold: usize,
560 pub max_params: usize,
562 pub max_function_body_lines: usize,
564 pub module_root_files: Arc<[Arc<str>]>,
566 pub source_file_warn_lines: usize,
568 pub source_file_deny_lines: usize,
570 pub max_methods: usize,
572 pub item_visibility_policy: Arc<[ItemVisibilityRule]>,
574 pub flat_module_families: Arc<[FlatModuleFamily]>,
576 pub feature_boundaries: Arc<[FeatureBoundaryRule]>,
578 pub test_api_patterns: Arc<[Arc<str>]>,
580 pub test_support_feature: Arc<str>,
582 pub forbid_attributes: PatternCheck,
584 pub forbid_types: PatternCheck,
586 pub forbid_calls: PatternCheck,
588 pub forbid_macros: PatternCheck,
590 pub check_naming: NamingCheck,
592 $(
593 #[doc = $doc]
594 pub $field: bool,
595 )*
596 }
597
598 impl Default for CheckConfig {
599 fn default() -> Self {
600 Self {
601 max_depth: default_max_depth(),
602 else_chain_threshold: default_else_chain_threshold(),
603 max_params: default_max_params(),
604 max_function_body_lines: default_max_function_body_lines(),
605 module_root_files: default_module_root_files(),
606 source_file_warn_lines: default_source_file_warn_lines(),
607 source_file_deny_lines: default_source_file_deny_lines(),
608 max_methods: default_max_methods(),
609 item_visibility_policy: Arc::from([]),
610 flat_module_families: Arc::from([]),
611 feature_boundaries: Arc::from([]),
612 test_api_patterns: default_test_api_patterns(),
613 test_support_feature: Arc::from(default_test_support_feature()),
614 forbid_attributes: PatternCheck::default(),
615 forbid_types: PatternCheck::default(),
616 forbid_calls: PatternCheck::default(),
617 forbid_macros: PatternCheck::default(),
618 check_naming: NamingCheck::default(),
619 $( $field: $default, )*
620 }
621 }
622 }
623
624 impl CheckConfig {
625 pub fn from_config_file(fc: &ConfigFile) -> Self {
627 Self {
628 max_depth: fc.max_depth,
629 else_chain_threshold: fc.else_chain_threshold,
630 max_params: fc.max_params,
631 max_function_body_lines: fc.max_function_body_lines,
632 module_root_files: fc.module_root_files.clone(),
633 source_file_warn_lines: fc.source_file_warn_lines,
634 source_file_deny_lines: fc.source_file_deny_lines,
635 max_methods: fc.max_methods,
636 item_visibility_policy: fc.item_visibility_policy.iter().cloned().collect(),
637 flat_module_families: fc.flat_module_families.iter().cloned().collect(),
638 feature_boundaries: fc.feature_boundaries.iter().cloned().collect(),
639 test_api_patterns: fc.test_api_patterns.clone(),
640 test_support_feature: Arc::from(&*fc.test_support_feature),
641 forbid_attributes: fc.forbid_attributes.clone(),
642 forbid_types: fc.forbid_types.clone(),
643 forbid_calls: fc.forbid_calls.clone(),
644 forbid_macros: fc.forbid_macros.clone(),
645 check_naming: fc.check_naming.clone(),
646 $( $field: fc.$field, )*
647 }
648 }
649
650 pub fn merge_bool_overrides(&mut self, ovr: &PathOverride) {
652 $(
653 if let Some(v) = ovr.$field {
654 self.$field = v;
655 }
656 )*
657 }
658 }
659 };
660}
661
662for_each_bool_check!(impl_check_config!);
663
664macro_rules! assert_bool_fields_in_sync {
668 ($($doc:literal, $field:ident, $default:expr;)*) => {
669 const _: () = {
670 const fn _check(cf: &ConfigFile, po: &PathOverride) {
673 $( let _ = (cf.$field, po.$field); )*
674 }
675 };
676 };
677}
678
679for_each_bool_check!(assert_bool_fields_in_sync!);
680
681impl CheckConfig {
682 pub fn resolve_for_path<'a>(
688 &'a self,
689 file_path: &str,
690 file_config: Option<&ConfigFile>,
691 ) -> Option<Cow<'a, Self>> {
692 let Some(fc) = file_config else {
693 return Some(Cow::Borrowed(self));
694 };
695
696 let Some(override_cfg) = check_path_override(file_path, fc) else {
697 return Some(Cow::Borrowed(self));
698 };
699
700 if override_cfg.enabled == Some(false) {
701 return None;
702 }
703
704 let mut config = self.clone();
705 if let Some(max_depth) = override_cfg.max_depth {
706 config.max_depth = max_depth;
707 }
708 if let Some(max_params) = override_cfg.max_params {
709 config.max_params = max_params;
710 }
711 if let Some(max_body) = override_cfg.max_function_body_lines {
712 config.max_function_body_lines = max_body;
713 }
714 if let Some(warn) = override_cfg.source_file_warn_lines {
715 config.source_file_warn_lines = warn;
716 }
717 if let Some(deny) = override_cfg.source_file_deny_lines {
718 config.source_file_deny_lines = deny;
719 }
720 if let Some(max_methods) = override_cfg.max_methods {
721 config.max_methods = max_methods;
722 }
723
724 config.merge_bool_overrides(override_cfg);
725
726 macro_rules! apply {
727 ($field:ident) => {
728 if let Some(ref ovr) = override_cfg.$field {
729 config.$field.apply_override(ovr);
730 }
731 };
732 }
733 apply!(forbid_attributes);
734 apply!(forbid_types);
735 apply!(forbid_calls);
736 apply!(forbid_macros);
737 apply!(check_naming);
738
739 Some(Cow::Owned(config))
740 }
741}
742
743#[derive(Debug, thiserror::Error)]
745pub enum ConfigError {
746 #[error("failed to read config file: {0}")]
748 Read(#[from] std::io::Error),
749 #[error("failed to parse config file: {0}")]
751 Parse(#[from] toml::de::Error),
752}
753
754pub fn load_config_file(path: &Path) -> Result<ConfigFile, ConfigError> {
756 let content = fs::read_to_string(path)?;
757 Ok(toml::from_str(&content)?)
758}
759
760pub fn find_config_file() -> Result<Option<std::path::PathBuf>, ConfigError> {
762 let project_config = find_project_config_file()?;
763 Ok(project_config.or_else(find_global_config_file))
764}
765
766fn find_project_config_file() -> Result<Option<std::path::PathBuf>, ConfigError> {
767 let config_path = std::env::current_dir()?.join(".pedant.toml");
768 Ok(config_path.exists().then_some(config_path))
769}
770
771fn find_global_config_file() -> Option<std::path::PathBuf> {
772 let config_dir = std::env::var_os("XDG_CONFIG_HOME")
773 .map(std::path::PathBuf::from)
774 .or_else(|| {
775 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
776 })?;
777 let config_path = config_dir.join("pedant").join("config.toml");
778 config_path.exists().then_some(config_path)
779}
780
781#[derive(Debug)]
783pub enum GateRuleOverride {
784 Disabled,
786 Severity(crate::gate::GateSeverity),
788}
789
790#[derive(Debug)]
795pub struct GateConfig {
796 pub enabled: bool,
798 pub overrides: BTreeMap<Box<str>, GateRuleOverride>,
800}
801
802impl Default for GateConfig {
803 fn default() -> Self {
804 Self {
805 enabled: true,
806 overrides: BTreeMap::new(),
807 }
808 }
809}
810
811impl<'de> Deserialize<'de> for GateConfig {
812 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
813 where
814 D: serde::Deserializer<'de>,
815 {
816 use serde::de::Error;
817
818 #[derive(Deserialize)]
819 #[serde(untagged)]
820 enum GateTomlValue {
821 Bool(bool),
822 String(String),
823 }
824
825 let raw: BTreeMap<Box<str>, GateTomlValue> = BTreeMap::deserialize(deserializer)?;
826 let mut enabled = true;
827 let mut overrides = BTreeMap::new();
828
829 for (key, value) in raw {
830 match (&*key, value) {
831 ("enabled", GateTomlValue::Bool(b)) => enabled = b,
832 ("enabled", GateTomlValue::String(_)) => {
833 return Err(D::Error::custom("'enabled' must be a boolean"));
834 }
835 (_, _) if !is_known_gate_rule(&key) => {
836 return Err(D::Error::custom(format!("unknown gate rule '{key}'")));
837 }
838 (_, GateTomlValue::Bool(false)) => {
839 overrides.insert(key, GateRuleOverride::Disabled);
840 }
841 (_, GateTomlValue::Bool(true)) => {} (_, GateTomlValue::String(s)) => {
843 let severity = parse_gate_severity(&s).ok_or_else(|| {
844 D::Error::custom(format!(
845 "invalid gate severity '{s}': expected \"deny\", \"warn\", or \"info\""
846 ))
847 })?;
848 overrides.insert(key, GateRuleOverride::Severity(severity));
849 }
850 }
851 }
852
853 Ok(GateConfig { enabled, overrides })
854 }
855}
856
857fn is_known_gate_rule(rule_name: &str) -> bool {
858 crate::gate::all_gate_rules()
859 .iter()
860 .any(|rule| rule.name == rule_name)
861}
862
863fn parse_gate_severity(s: &str) -> Option<crate::gate::GateSeverity> {
864 use crate::gate::GateSeverity;
865 match s {
866 "deny" => Some(GateSeverity::Deny),
867 "warn" => Some(GateSeverity::Warn),
868 "info" => Some(GateSeverity::Info),
869 _ => None,
870 }
871}