Skip to main content

ailint_core/rules/
registry.rs

1//! Central registry of all built-in rules.
2
3use std::collections::HashMap;
4use std::sync::OnceLock;
5
6use crate::config::Config;
7use crate::parser::ParsedDocument;
8use crate::rules::{consistency, security, semantic, structural};
9use crate::rules::{BatchRule, Rule, RuleContext, RuleId, Severity, Violation};
10
11/// Static metadata for a rule, cached for reporters to look up.
12#[derive(Debug, Clone, Copy)]
13pub struct RuleMeta {
14    /// One-line human description of what the rule enforces.
15    pub description: &'static str,
16    /// One-line suggested remediation.
17    pub fix_hint: &'static str,
18    /// Default severity before any config overrides.
19    pub default_severity: Severity,
20}
21
22/// Look up cached metadata for a rule by its ID. Returns `None` only for
23/// rule IDs not present in the built-in registry.
24pub fn rule_meta(id: RuleId) -> Option<RuleMeta> {
25    rule_meta_cache().get(&id).copied()
26}
27
28fn rule_meta_cache() -> &'static HashMap<RuleId, RuleMeta> {
29    static CACHE: OnceLock<HashMap<RuleId, RuleMeta>> = OnceLock::new();
30    CACHE.get_or_init(|| {
31        let mut m = HashMap::new();
32        for r in all_rules() {
33            m.insert(
34                r.id(),
35                RuleMeta {
36                    description: r.description(),
37                    fix_hint: r.fix_hint(),
38                    default_severity: r.default_severity(),
39                },
40            );
41        }
42        for r in all_batch_rules() {
43            m.insert(
44                r.id(),
45                RuleMeta {
46                    description: r.description(),
47                    fix_hint: r.fix_hint(),
48                    default_severity: r.default_severity(),
49                },
50            );
51        }
52        m
53    })
54}
55
56/// Return the full set of built-in per-document rules.
57pub fn all_rules() -> Vec<Box<dyn Rule>> {
58    let mut out: Vec<Box<dyn Rule>> = Vec::new();
59    out.extend(structural::all_rules());
60    out.extend(semantic::all_rules());
61    out.extend(security::all_rules());
62    out
63}
64
65/// Return the full set of built-in batch (cross-file) rules.
66pub fn all_batch_rules() -> Vec<Box<dyn BatchRule>> {
67    consistency::all_batch_rules()
68}
69
70/// Run every enabled per-document rule against a document.
71pub fn run_all(doc: &ParsedDocument, config: &Config) -> Vec<Violation> {
72    let mut out = Vec::new();
73    for rule in all_rules() {
74        let id = rule.id();
75        if is_disabled(&id, &config.rules.disabled) {
76            continue;
77        }
78        if !rule.applies_to(doc.file_type) {
79            continue;
80        }
81        let severity = effective_severity(
82            &id,
83            rule.default_severity(),
84            &config.rules.severity_overrides,
85        );
86        let options = lookup_options(&id, &config.rules.options);
87        let ctx = RuleContext {
88            config,
89            options,
90            severity,
91        };
92        let mut vs = rule.run(doc, &ctx);
93        for v in &mut vs {
94            v.severity = severity;
95        }
96        out.extend(vs);
97    }
98    out
99}
100
101/// Run every enabled batch rule against the full corpus.
102pub fn run_all_batch(docs: &[ParsedDocument], config: &Config) -> Vec<Violation> {
103    let mut out = Vec::new();
104    for rule in all_batch_rules() {
105        let id = rule.id();
106        if is_disabled(&id, &config.rules.disabled) {
107            continue;
108        }
109        let severity = effective_severity(
110            &id,
111            rule.default_severity(),
112            &config.rules.severity_overrides,
113        );
114        let options = lookup_options(&id, &config.rules.options);
115        let ctx = RuleContext {
116            config,
117            options,
118            severity,
119        };
120        let filtered: Vec<ParsedDocument> = docs
121            .iter()
122            .filter(|d| rule.applies_to(d.file_type))
123            .cloned()
124            .collect();
125        let mut vs = rule.run_batch(&filtered, &ctx);
126        for v in &mut vs {
127            v.severity = severity;
128        }
129        out.extend(vs);
130    }
131    out
132}
133
134fn is_disabled(id: &RuleId, disabled: &[String]) -> bool {
135    let code_str = id.code_str();
136    disabled.iter().any(|d| d == &code_str || d == id.slug)
137}
138
139fn effective_severity(
140    id: &RuleId,
141    default: Severity,
142    overrides: &std::collections::BTreeMap<String, Severity>,
143) -> Severity {
144    let code_str = id.code_str();
145    overrides
146        .get(&code_str)
147        .or_else(|| overrides.get(id.slug))
148        .copied()
149        .unwrap_or(default)
150}
151
152fn lookup_options<'a>(
153    id: &RuleId,
154    options: &'a std::collections::BTreeMap<String, serde_yaml::Value>,
155) -> Option<&'a serde_yaml::Value> {
156    options.get(id.slug).or_else(|| options.get(&id.code_str()))
157}