Skip to main content

alint_core/
registry.rs

1use std::collections::HashMap;
2
3use crate::config::RuleSpec;
4use crate::did_you_mean;
5use crate::error::{Error, Result};
6use crate::rule::Rule;
7
8pub type RuleBuilder = fn(&RuleSpec) -> Result<Box<dyn Rule>>;
9
10/// Map from `kind` string → factory function. Built-in rule crates register
11/// themselves here at startup, and plugin rules (in later phases) will too.
12#[derive(Default)]
13pub struct RuleRegistry {
14    builders: HashMap<String, RuleBuilder>,
15}
16
17impl std::fmt::Debug for RuleRegistry {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("RuleRegistry")
20            .field("kinds", &self.builders.keys().collect::<Vec<_>>())
21            .finish()
22    }
23}
24
25impl RuleRegistry {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn register(&mut self, kind: &str, builder: RuleBuilder) {
31        self.builders.insert(kind.to_string(), builder);
32    }
33
34    pub fn build(&self, spec: &RuleSpec) -> Result<Box<dyn Rule>> {
35        let builder = self
36            .builders
37            .get(&spec.kind)
38            .ok_or_else(|| Error::UnknownRuleKind(spec.kind.clone()))?;
39        builder(spec).map_err(|e| enrich_error(e, &spec.kind))
40    }
41
42    pub fn known_kinds(&self) -> impl Iterator<Item = &str> {
43        self.builders.keys().map(String::as_str)
44    }
45}
46
47/// Apply [`did_you_mean::enrich`] to the message of a `RuleConfig`
48/// error. Other error variants pass through unchanged.
49fn enrich_error(err: Error, kind: &str) -> Error {
50    match err {
51        Error::RuleConfig { rule_id, message } => {
52            let enriched = did_you_mean::enrich(kind, &message);
53            Error::RuleConfig {
54                rule_id,
55                message: enriched,
56            }
57        }
58        other => other,
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::level::Level;
66
67    fn fake_spec(kind: &str) -> RuleSpec {
68        RuleSpec {
69            id: "t".into(),
70            kind: kind.into(),
71            level: Level::Error,
72            paths: None,
73            message: None,
74            policy_url: None,
75            when: None,
76            fix: None,
77            git_tracked_only: false,
78            respect_gitignore: None,
79            scope_filter: None,
80            extra: serde_yaml_ng::Mapping::new(),
81        }
82    }
83
84    fn ok_builder(_spec: &RuleSpec) -> Result<Box<dyn Rule>> {
85        // Trait object can't be a unit struct without an `impl
86        // Rule for ()` somewhere; the build path doesn't actually
87        // call this in the unknown-kind tests so we leave it
88        // unreachable on the happy path.
89        unreachable!("test should not call this builder")
90    }
91
92    #[test]
93    fn new_registry_has_no_kinds() {
94        let r = RuleRegistry::new();
95        assert_eq!(r.known_kinds().count(), 0);
96    }
97
98    #[test]
99    fn register_inserts_a_kind() {
100        let mut r = RuleRegistry::new();
101        r.register("my_kind", ok_builder);
102        let kinds: Vec<&str> = r.known_kinds().collect();
103        assert_eq!(kinds, vec!["my_kind"]);
104    }
105
106    #[test]
107    fn register_overwrites_existing_kind() {
108        // Last-registered-wins. Plugin loaders may rely on this
109        // to override a built-in's behaviour.
110        let mut r = RuleRegistry::new();
111        r.register("my_kind", ok_builder);
112        r.register("my_kind", ok_builder);
113        assert_eq!(r.known_kinds().count(), 1);
114    }
115
116    #[test]
117    fn build_rejects_unknown_kind_with_clear_error() {
118        let r = RuleRegistry::new();
119        let err = r.build(&fake_spec("not_real")).unwrap_err();
120        match err {
121            Error::UnknownRuleKind(name) => assert_eq!(name, "not_real"),
122            other => panic!("expected UnknownRuleKind, got {other:?}"),
123        }
124    }
125
126    #[test]
127    fn known_kinds_iterator_lists_all_registered() {
128        let mut r = RuleRegistry::new();
129        r.register("a", ok_builder);
130        r.register("b", ok_builder);
131        r.register("c", ok_builder);
132        let mut kinds: Vec<&str> = r.known_kinds().collect();
133        kinds.sort_unstable();
134        assert_eq!(kinds, vec!["a", "b", "c"]);
135    }
136}