1use crate::{
6 diagnostic::interpolate_message, Diagnostic, LintConfig, MatchResult, Rule, RuleOverride,
7 Severity,
8};
9use std::collections::HashMap;
10use std::path::Path;
11
12pub struct RuleEngine {
14 rules: Vec<Rule>,
16
17 overrides: HashMap<String, RuleOverride>,
19
20 severity_threshold: Option<Severity>,
22}
23
24impl RuleEngine {
25 pub fn new() -> Self {
27 Self {
28 rules: Vec::new(),
29 overrides: HashMap::new(),
30 severity_threshold: None,
31 }
32 }
33
34 pub fn from_config(config: LintConfig) -> Self {
36 let mut engine = Self::new();
37
38 engine.rules.extend(config.inline_rules);
40
41 if let Some(settings) = config.settings {
43 engine.severity_threshold = settings.severity_threshold;
44 }
45
46 if let Some(overrides) = config.rules {
48 engine.overrides = overrides.0;
49 }
50
51 engine
52 }
53
54 pub fn add_rule(&mut self, rule: Rule) {
56 self.rules.push(rule);
57 }
58
59 pub fn add_rules(&mut self, rules: impl IntoIterator<Item = Rule>) {
61 self.rules.extend(rules);
62 }
63
64 pub fn set_severity_threshold(&mut self, threshold: Severity) {
66 self.severity_threshold = Some(threshold);
67 }
68
69 pub fn active_rules(&self) -> impl Iterator<Item = &Rule> {
71 self.rules.iter().filter(|rule| {
72 if let Some(override_) = self.overrides.get(&rule.id) {
74 if !override_.enabled {
75 return false;
76 }
77 }
78
79 if let Some(threshold) = self.severity_threshold {
81 let effective_severity = self
82 .overrides
83 .get(&rule.id)
84 .and_then(|o| o.severity)
85 .unwrap_or(rule.severity);
86
87 if !severity_meets_threshold(effective_severity, threshold) {
88 return false;
89 }
90 }
91
92 true
93 })
94 }
95
96 pub fn effective_severity(&self, rule: &Rule) -> Severity {
98 self.overrides
99 .get(&rule.id)
100 .and_then(|o| o.severity)
101 .unwrap_or(rule.severity)
102 }
103
104 pub fn execute_rule(&self, _rule: &Rule) -> Vec<MatchResult> {
109 Vec::new()
116 }
117
118 pub fn execute_all(&self) -> Vec<MatchResult> {
120 self.active_rules()
121 .flat_map(|rule| self.execute_rule(rule))
122 .collect()
123 }
124
125 pub fn diagnostics_from_results<'a>(
127 &self,
128 results: impl IntoIterator<Item = &'a MatchResult>,
129 file_path: impl AsRef<Path>,
130 ) -> Vec<Diagnostic> {
131 results
132 .into_iter()
133 .filter_map(|result| Diagnostic::from_match_result(result, file_path.as_ref(), None))
134 .collect()
135 }
136
137 pub fn process_result(&self, mut result: MatchResult, rule: &Rule) -> MatchResult {
139 result.rule_id = Some(rule.id.clone());
141 result.severity = Some(self.effective_severity(rule));
142
143 result.message = Some(interpolate_message(&rule.message, &result.captures));
145
146 result.suggestion = rule.suggestion.clone();
148
149 result
150 }
151}
152
153impl Default for RuleEngine {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159fn severity_meets_threshold(severity: Severity, threshold: Severity) -> bool {
161 severity_level(severity) >= severity_level(threshold)
162}
163
164fn severity_level(severity: Severity) -> u8 {
166 match severity {
167 Severity::Hint => 0,
168 Severity::Info => 1,
169 Severity::Warning => 2,
170 Severity::Error => 3,
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::{CapturedNode, PatternQuery, Position, Span, SymbolKind};
178
179 fn test_rule() -> Rule {
180 Rule::new(
181 "RL001",
182 "no-unwrap",
183 Severity::Warning,
184 PatternQuery::new().kind(SymbolKind::Function),
185 "Found $UNWRAP in function",
186 )
187 .with_suggestion("Use ? operator")
188 }
189
190 #[test]
191 fn test_engine_add_rules() {
192 let mut engine = RuleEngine::new();
193 engine.add_rule(test_rule());
194 assert_eq!(engine.active_rules().count(), 1);
195 }
196
197 #[test]
198 fn test_engine_from_config() {
199 let config = LintConfig {
200 inline_rules: vec![test_rule()],
201 ..Default::default()
202 };
203 let engine = RuleEngine::from_config(config);
204 assert_eq!(engine.active_rules().count(), 1);
205 }
206
207 #[test]
208 fn test_severity_threshold() {
209 let mut engine = RuleEngine::new();
210
211 engine.add_rule(Rule::new(
213 "RL001",
214 "error-rule",
215 Severity::Error,
216 PatternQuery::new(),
217 "Error",
218 ));
219 engine.add_rule(Rule::new(
220 "RL002",
221 "warning-rule",
222 Severity::Warning,
223 PatternQuery::new(),
224 "Warning",
225 ));
226 engine.add_rule(Rule::new(
227 "RL003",
228 "hint-rule",
229 Severity::Hint,
230 PatternQuery::new(),
231 "Hint",
232 ));
233
234 assert_eq!(engine.active_rules().count(), 3);
236
237 engine.set_severity_threshold(Severity::Warning);
239 assert_eq!(engine.active_rules().count(), 2);
240
241 engine.set_severity_threshold(Severity::Error);
243 assert_eq!(engine.active_rules().count(), 1);
244 }
245
246 #[test]
247 fn test_rule_override() {
248 let config = LintConfig {
249 inline_rules: vec![
250 Rule::new(
251 "RL001",
252 "rule1",
253 Severity::Warning,
254 PatternQuery::new(),
255 "msg",
256 ),
257 Rule::new(
258 "RL002",
259 "rule2",
260 Severity::Warning,
261 PatternQuery::new(),
262 "msg",
263 ),
264 ],
265 rules: Some(crate::RuleOverrides({
266 let mut map = HashMap::new();
267 map.insert(
268 "RL001".to_string(),
269 RuleOverride {
270 enabled: false,
271 severity: None,
272 },
273 );
274 map.insert(
275 "RL002".to_string(),
276 RuleOverride {
277 enabled: true,
278 severity: Some(Severity::Error),
279 },
280 );
281 map
282 })),
283 ..Default::default()
284 };
285
286 let engine = RuleEngine::from_config(config);
287
288 let active: Vec<_> = engine.active_rules().collect();
291 assert_eq!(active.len(), 1);
292 assert_eq!(active[0].id, "RL002");
293 assert_eq!(engine.effective_severity(active[0]), Severity::Error);
294 }
295
296 #[test]
297 fn test_process_result() {
298 let engine = RuleEngine::new();
299 let rule = test_rule();
300
301 let result = MatchResult::matched().capture(
302 "$UNWRAP",
303 CapturedNode::new(
304 Span::new(
305 Position {
306 line: 10,
307 column: 5,
308 },
309 Position {
310 line: 10,
311 column: 20,
312 },
313 ),
314 "x.unwrap()",
315 ),
316 );
317
318 let processed = engine.process_result(result, &rule);
319 assert_eq!(processed.rule_id, Some("RL001".to_string()));
320 assert_eq!(processed.severity, Some(Severity::Warning));
321 assert_eq!(
322 processed.message,
323 Some("Found x.unwrap() in function".to_string())
324 );
325 }
326}