Skip to main content

ryo_pattern/
engine.rs

1//! Rule execution engine for RyoPattern
2//!
3//! Executes lint rules and produces diagnostics.
4
5use crate::{
6    diagnostic::interpolate_message, Diagnostic, LintConfig, MatchResult, Rule, RuleOverride,
7    Severity,
8};
9use std::collections::HashMap;
10use std::path::Path;
11
12/// Rule execution engine
13pub struct RuleEngine {
14    /// Loaded rules
15    rules: Vec<Rule>,
16
17    /// Rule overrides
18    overrides: HashMap<String, RuleOverride>,
19
20    /// Minimum severity threshold
21    severity_threshold: Option<Severity>,
22}
23
24impl RuleEngine {
25    /// Create a new engine with default settings
26    pub fn new() -> Self {
27        Self {
28            rules: Vec::new(),
29            overrides: HashMap::new(),
30            severity_threshold: None,
31        }
32    }
33
34    /// Create from a lint configuration
35    pub fn from_config(config: LintConfig) -> Self {
36        let mut engine = Self::new();
37
38        // Add inline rules
39        engine.rules.extend(config.inline_rules);
40
41        // Apply settings
42        if let Some(settings) = config.settings {
43            engine.severity_threshold = settings.severity_threshold;
44        }
45
46        // Apply overrides
47        if let Some(overrides) = config.rules {
48            engine.overrides = overrides.0;
49        }
50
51        engine
52    }
53
54    /// Add a rule
55    pub fn add_rule(&mut self, rule: Rule) {
56        self.rules.push(rule);
57    }
58
59    /// Add multiple rules
60    pub fn add_rules(&mut self, rules: impl IntoIterator<Item = Rule>) {
61        self.rules.extend(rules);
62    }
63
64    /// Set severity threshold
65    pub fn set_severity_threshold(&mut self, threshold: Severity) {
66        self.severity_threshold = Some(threshold);
67    }
68
69    /// Get all active rules (respecting overrides)
70    pub fn active_rules(&self) -> impl Iterator<Item = &Rule> {
71        self.rules.iter().filter(|rule| {
72            // Check if rule is enabled
73            if let Some(override_) = self.overrides.get(&rule.id) {
74                if !override_.enabled {
75                    return false;
76                }
77            }
78
79            // Check severity threshold
80            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    /// Get effective severity for a rule (considering overrides)
97    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    /// Execute a rule and produce a MatchResult
105    ///
106    /// Note: This is a placeholder that needs integration with ryo-analysis
107    /// for actual AST matching. Currently returns the match result structure.
108    pub fn execute_rule(&self, _rule: &Rule) -> Vec<MatchResult> {
109        // TODO: Integrate with ryo-analysis for actual pattern matching
110        // This will involve:
111        // 1. Query symbol registry for matching symbols
112        // 2. Match body patterns against function bodies
113        // 3. Check relation conditions via CodeGraphV2
114        // 4. Produce MatchResult with captures
115        Vec::new()
116    }
117
118    /// Execute all active rules
119    pub fn execute_all(&self) -> Vec<MatchResult> {
120        self.active_rules()
121            .flat_map(|rule| self.execute_rule(rule))
122            .collect()
123    }
124
125    /// Produce diagnostics from match results
126    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    /// Process a MatchResult through message interpolation
138    pub fn process_result(&self, mut result: MatchResult, rule: &Rule) -> MatchResult {
139        // Apply rule info
140        result.rule_id = Some(rule.id.clone());
141        result.severity = Some(self.effective_severity(rule));
142
143        // Interpolate message
144        result.message = Some(interpolate_message(&rule.message, &result.captures));
145
146        // Copy suggestion
147        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
159/// Check if a severity meets the threshold
160fn severity_meets_threshold(severity: Severity, threshold: Severity) -> bool {
161    severity_level(severity) >= severity_level(threshold)
162}
163
164/// Get numeric level for severity (higher = more severe)
165fn 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        // Add rules with different severities
212        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        // All rules active by default
235        assert_eq!(engine.active_rules().count(), 3);
236
237        // Set threshold to Warning
238        engine.set_severity_threshold(Severity::Warning);
239        assert_eq!(engine.active_rules().count(), 2);
240
241        // Set threshold to Error
242        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        // RL001 should be disabled
289        // RL002 should be enabled with Error severity
290        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}