ryo-pattern 0.1.0

RyoPattern - AST pattern matching and lint rules for Ryo
Documentation
//! Rule execution engine for RyoPattern
//!
//! Executes lint rules and produces diagnostics.

use crate::{
    diagnostic::interpolate_message, Diagnostic, LintConfig, MatchResult, Rule, RuleOverride,
    Severity,
};
use std::collections::HashMap;
use std::path::Path;

/// Rule execution engine
pub struct RuleEngine {
    /// Loaded rules
    rules: Vec<Rule>,

    /// Rule overrides
    overrides: HashMap<String, RuleOverride>,

    /// Minimum severity threshold
    severity_threshold: Option<Severity>,
}

impl RuleEngine {
    /// Create a new engine with default settings
    pub fn new() -> Self {
        Self {
            rules: Vec::new(),
            overrides: HashMap::new(),
            severity_threshold: None,
        }
    }

    /// Create from a lint configuration
    pub fn from_config(config: LintConfig) -> Self {
        let mut engine = Self::new();

        // Add inline rules
        engine.rules.extend(config.inline_rules);

        // Apply settings
        if let Some(settings) = config.settings {
            engine.severity_threshold = settings.severity_threshold;
        }

        // Apply overrides
        if let Some(overrides) = config.rules {
            engine.overrides = overrides.0;
        }

        engine
    }

    /// Add a rule
    pub fn add_rule(&mut self, rule: Rule) {
        self.rules.push(rule);
    }

    /// Add multiple rules
    pub fn add_rules(&mut self, rules: impl IntoIterator<Item = Rule>) {
        self.rules.extend(rules);
    }

    /// Set severity threshold
    pub fn set_severity_threshold(&mut self, threshold: Severity) {
        self.severity_threshold = Some(threshold);
    }

    /// Get all active rules (respecting overrides)
    pub fn active_rules(&self) -> impl Iterator<Item = &Rule> {
        self.rules.iter().filter(|rule| {
            // Check if rule is enabled
            if let Some(override_) = self.overrides.get(&rule.id) {
                if !override_.enabled {
                    return false;
                }
            }

            // Check severity threshold
            if let Some(threshold) = self.severity_threshold {
                let effective_severity = self
                    .overrides
                    .get(&rule.id)
                    .and_then(|o| o.severity)
                    .unwrap_or(rule.severity);

                if !severity_meets_threshold(effective_severity, threshold) {
                    return false;
                }
            }

            true
        })
    }

    /// Get effective severity for a rule (considering overrides)
    pub fn effective_severity(&self, rule: &Rule) -> Severity {
        self.overrides
            .get(&rule.id)
            .and_then(|o| o.severity)
            .unwrap_or(rule.severity)
    }

    /// Execute a rule and produce a MatchResult
    ///
    /// Note: This is a placeholder that needs integration with ryo-analysis
    /// for actual AST matching. Currently returns the match result structure.
    pub fn execute_rule(&self, _rule: &Rule) -> Vec<MatchResult> {
        // TODO: Integrate with ryo-analysis for actual pattern matching
        // This will involve:
        // 1. Query symbol registry for matching symbols
        // 2. Match body patterns against function bodies
        // 3. Check relation conditions via CodeGraphV2
        // 4. Produce MatchResult with captures
        Vec::new()
    }

    /// Execute all active rules
    pub fn execute_all(&self) -> Vec<MatchResult> {
        self.active_rules()
            .flat_map(|rule| self.execute_rule(rule))
            .collect()
    }

    /// Produce diagnostics from match results
    pub fn diagnostics_from_results<'a>(
        &self,
        results: impl IntoIterator<Item = &'a MatchResult>,
        file_path: impl AsRef<Path>,
    ) -> Vec<Diagnostic> {
        results
            .into_iter()
            .filter_map(|result| Diagnostic::from_match_result(result, file_path.as_ref(), None))
            .collect()
    }

    /// Process a MatchResult through message interpolation
    pub fn process_result(&self, mut result: MatchResult, rule: &Rule) -> MatchResult {
        // Apply rule info
        result.rule_id = Some(rule.id.clone());
        result.severity = Some(self.effective_severity(rule));

        // Interpolate message
        result.message = Some(interpolate_message(&rule.message, &result.captures));

        // Copy suggestion
        result.suggestion = rule.suggestion.clone();

        result
    }
}

impl Default for RuleEngine {
    fn default() -> Self {
        Self::new()
    }
}

/// Check if a severity meets the threshold
fn severity_meets_threshold(severity: Severity, threshold: Severity) -> bool {
    severity_level(severity) >= severity_level(threshold)
}

/// Get numeric level for severity (higher = more severe)
fn severity_level(severity: Severity) -> u8 {
    match severity {
        Severity::Hint => 0,
        Severity::Info => 1,
        Severity::Warning => 2,
        Severity::Error => 3,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CapturedNode, PatternQuery, Position, Span, SymbolKind};

    fn test_rule() -> Rule {
        Rule::new(
            "RL001",
            "no-unwrap",
            Severity::Warning,
            PatternQuery::new().kind(SymbolKind::Function),
            "Found $UNWRAP in function",
        )
        .with_suggestion("Use ? operator")
    }

    #[test]
    fn test_engine_add_rules() {
        let mut engine = RuleEngine::new();
        engine.add_rule(test_rule());
        assert_eq!(engine.active_rules().count(), 1);
    }

    #[test]
    fn test_engine_from_config() {
        let config = LintConfig {
            inline_rules: vec![test_rule()],
            ..Default::default()
        };
        let engine = RuleEngine::from_config(config);
        assert_eq!(engine.active_rules().count(), 1);
    }

    #[test]
    fn test_severity_threshold() {
        let mut engine = RuleEngine::new();

        // Add rules with different severities
        engine.add_rule(Rule::new(
            "RL001",
            "error-rule",
            Severity::Error,
            PatternQuery::new(),
            "Error",
        ));
        engine.add_rule(Rule::new(
            "RL002",
            "warning-rule",
            Severity::Warning,
            PatternQuery::new(),
            "Warning",
        ));
        engine.add_rule(Rule::new(
            "RL003",
            "hint-rule",
            Severity::Hint,
            PatternQuery::new(),
            "Hint",
        ));

        // All rules active by default
        assert_eq!(engine.active_rules().count(), 3);

        // Set threshold to Warning
        engine.set_severity_threshold(Severity::Warning);
        assert_eq!(engine.active_rules().count(), 2);

        // Set threshold to Error
        engine.set_severity_threshold(Severity::Error);
        assert_eq!(engine.active_rules().count(), 1);
    }

    #[test]
    fn test_rule_override() {
        let config = LintConfig {
            inline_rules: vec![
                Rule::new(
                    "RL001",
                    "rule1",
                    Severity::Warning,
                    PatternQuery::new(),
                    "msg",
                ),
                Rule::new(
                    "RL002",
                    "rule2",
                    Severity::Warning,
                    PatternQuery::new(),
                    "msg",
                ),
            ],
            rules: Some(crate::RuleOverrides({
                let mut map = HashMap::new();
                map.insert(
                    "RL001".to_string(),
                    RuleOverride {
                        enabled: false,
                        severity: None,
                    },
                );
                map.insert(
                    "RL002".to_string(),
                    RuleOverride {
                        enabled: true,
                        severity: Some(Severity::Error),
                    },
                );
                map
            })),
            ..Default::default()
        };

        let engine = RuleEngine::from_config(config);

        // RL001 should be disabled
        // RL002 should be enabled with Error severity
        let active: Vec<_> = engine.active_rules().collect();
        assert_eq!(active.len(), 1);
        assert_eq!(active[0].id, "RL002");
        assert_eq!(engine.effective_severity(active[0]), Severity::Error);
    }

    #[test]
    fn test_process_result() {
        let engine = RuleEngine::new();
        let rule = test_rule();

        let result = MatchResult::matched().capture(
            "$UNWRAP",
            CapturedNode::new(
                Span::new(
                    Position {
                        line: 10,
                        column: 5,
                    },
                    Position {
                        line: 10,
                        column: 20,
                    },
                ),
                "x.unwrap()",
            ),
        );

        let processed = engine.process_result(result, &rule);
        assert_eq!(processed.rule_id, Some("RL001".to_string()));
        assert_eq!(processed.severity, Some(Severity::Warning));
        assert_eq!(
            processed.message,
            Some("Found x.unwrap() in function".to_string())
        );
    }
}