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#[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#[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 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 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 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) => {} 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}