Skip to main content

alint_core/
engine.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use rayon::prelude::*;
5
6use crate::error::Result;
7use crate::facts::{FactSpec, FactValues, evaluate_facts};
8use crate::registry::RuleRegistry;
9use crate::report::{FixItem, FixReport, FixRuleResult, FixStatus, Report};
10use crate::rule::{Context, FixContext, FixOutcome, Rule, RuleResult, Violation};
11use crate::walker::FileIndex;
12use crate::when::{WhenEnv, WhenExpr};
13
14/// A rule bundled with an optional `when` expression. Rules with a `when`
15/// that evaluates to false at runtime are skipped (no `RuleResult` is
16/// produced) — same observable effect as `level: off`, but gated on facts.
17#[derive(Debug)]
18pub struct RuleEntry {
19    pub rule: Box<dyn Rule>,
20    pub when: Option<WhenExpr>,
21}
22
23impl RuleEntry {
24    pub fn new(rule: Box<dyn Rule>) -> Self {
25        Self { rule, when: None }
26    }
27
28    #[must_use]
29    pub fn with_when(mut self, expr: WhenExpr) -> Self {
30        self.when = Some(expr);
31        self
32    }
33}
34
35/// Executes a set of rules against a pre-built [`FileIndex`].
36///
37/// The engine owns a [`RuleRegistry`] so cross-file rules (e.g.
38/// `for_each_dir`) can build nested rules on demand during evaluation.
39/// Optional `facts` and `vars` (set via the builder chain) are evaluated
40/// at run time and threaded into each rule's [`Context`] and into the
41/// `when` expression evaluator that gates rules.
42#[derive(Debug)]
43pub struct Engine {
44    entries: Vec<RuleEntry>,
45    registry: RuleRegistry,
46    facts: Vec<FactSpec>,
47    vars: HashMap<String, String>,
48}
49
50impl Engine {
51    /// Backward-compatible: wrap each rule in a [`RuleEntry`] with no `when`.
52    pub fn new(rules: Vec<Box<dyn Rule>>, registry: RuleRegistry) -> Self {
53        let entries = rules.into_iter().map(RuleEntry::new).collect();
54        Self {
55            entries,
56            registry,
57            facts: Vec::new(),
58            vars: HashMap::new(),
59        }
60    }
61
62    /// Construct from rule entries (each carrying an optional `when`).
63    pub fn from_entries(entries: Vec<RuleEntry>, registry: RuleRegistry) -> Self {
64        Self {
65            entries,
66            registry,
67            facts: Vec::new(),
68            vars: HashMap::new(),
69        }
70    }
71
72    #[must_use]
73    pub fn with_facts(mut self, facts: Vec<FactSpec>) -> Self {
74        self.facts = facts;
75        self
76    }
77
78    #[must_use]
79    pub fn with_vars(mut self, vars: HashMap<String, String>) -> Self {
80        self.vars = vars;
81        self
82    }
83
84    pub fn rule_count(&self) -> usize {
85        self.entries.len()
86    }
87
88    pub fn run(&self, root: &Path, index: &FileIndex) -> Result<Report> {
89        let fact_values = evaluate_facts(&self.facts, root, index)?;
90        let ctx = Context {
91            root,
92            index,
93            registry: Some(&self.registry),
94            facts: Some(&fact_values),
95            vars: Some(&self.vars),
96        };
97        let when_env = WhenEnv {
98            facts: &fact_values,
99            vars: &self.vars,
100        };
101        let results: Vec<RuleResult> = self
102            .entries
103            .par_iter()
104            .filter_map(|entry| run_entry(entry, &ctx, &when_env, &fact_values))
105            .collect();
106        Ok(Report { results })
107    }
108
109    /// Evaluate every rule and apply fixers for their violations.
110    /// Fixes run sequentially — rules whose fixers touch the filesystem
111    /// must not race. Rules with no fixer contribute
112    /// [`FixStatus::Unfixable`] entries so the caller sees them in the
113    /// report. Rules that pass (no violations) are omitted from the
114    /// result, same as [`Engine::run`]'s usual behaviour.
115    pub fn fix(&self, root: &Path, index: &FileIndex, dry_run: bool) -> Result<FixReport> {
116        let fact_values = evaluate_facts(&self.facts, root, index)?;
117        let ctx = Context {
118            root,
119            index,
120            registry: Some(&self.registry),
121            facts: Some(&fact_values),
122            vars: Some(&self.vars),
123        };
124        let when_env = WhenEnv {
125            facts: &fact_values,
126            vars: &self.vars,
127        };
128        let fix_ctx = FixContext { root, dry_run };
129
130        let mut results: Vec<FixRuleResult> = Vec::new();
131        for entry in &self.entries {
132            if let Some(expr) = &entry.when {
133                match expr.evaluate(&when_env) {
134                    Ok(true) => {}
135                    Ok(false) => continue,
136                    Err(e) => {
137                        results.push(FixRuleResult {
138                            rule_id: entry.rule.id().to_string(),
139                            level: entry.rule.level(),
140                            items: vec![FixItem {
141                                violation: Violation::new(format!("when evaluation error: {e}")),
142                                status: FixStatus::Unfixable,
143                            }],
144                        });
145                        continue;
146                    }
147                }
148            }
149            let violations = match entry.rule.evaluate(&ctx) {
150                Ok(v) => v,
151                Err(e) => vec![Violation::new(format!("rule error: {e}"))],
152            };
153            if violations.is_empty() {
154                continue;
155            }
156            let fixer = entry.rule.fixer();
157            let items: Vec<FixItem> = violations
158                .into_iter()
159                .map(|v| {
160                    let status = match fixer {
161                        Some(f) => match f.apply(&v, &fix_ctx) {
162                            Ok(FixOutcome::Applied(s)) => FixStatus::Applied(s),
163                            Ok(FixOutcome::Skipped(s)) => FixStatus::Skipped(s),
164                            Err(e) => FixStatus::Skipped(format!("fix error: {e}")),
165                        },
166                        None => FixStatus::Unfixable,
167                    };
168                    FixItem {
169                        violation: v,
170                        status,
171                    }
172                })
173                .collect();
174            results.push(FixRuleResult {
175                rule_id: entry.rule.id().to_string(),
176                level: entry.rule.level(),
177                items,
178            });
179        }
180        Ok(FixReport { results })
181    }
182}
183
184fn run_entry(
185    entry: &RuleEntry,
186    ctx: &Context<'_>,
187    when_env: &WhenEnv<'_>,
188    _facts: &FactValues,
189) -> Option<RuleResult> {
190    if let Some(expr) = &entry.when {
191        match expr.evaluate(when_env) {
192            Ok(true) => {} // proceed
193            Ok(false) => return None,
194            Err(e) => {
195                return Some(RuleResult {
196                    rule_id: entry.rule.id().to_string(),
197                    level: entry.rule.level(),
198                    policy_url: entry.rule.policy_url().map(str::to_string),
199                    violations: vec![Violation::new(format!("when evaluation error: {e}"))],
200                });
201            }
202        }
203    }
204    Some(run_one(entry.rule.as_ref(), ctx))
205}
206
207fn run_one(rule: &dyn Rule, ctx: &Context<'_>) -> RuleResult {
208    let violations = match rule.evaluate(ctx) {
209        Ok(v) => v,
210        Err(e) => vec![Violation::new(format!("rule error: {e}"))],
211    };
212    RuleResult {
213        rule_id: rule.id().to_string(),
214        level: rule.level(),
215        policy_url: rule.policy_url().map(str::to_string),
216        violations,
217    }
218}