ryo-pattern 0.1.0

RyoPattern - AST pattern matching and lint rules for Ryo
Documentation
//! Rule loader for RyoPattern
//!
//! Loads lint rules from YAML/JSON configuration files.

use crate::Rule;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use thiserror::Error;

/// Errors from rule loading
#[derive(Debug, Error)]
pub enum LoadError {
    /// IO failure while reading the rule file.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// YAML parse failure.
    #[error("YAML parse error: {0}")]
    Yaml(#[from] serde_yaml::Error),

    /// JSON parse failure.
    #[error("JSON parse error: {0}")]
    Json(#[from] serde_json::Error),

    /// Unsupported / unknown rule file extension.
    #[error("Invalid file extension: {0}")]
    InvalidExtension(String),
}

/// Lint configuration file structure
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LintConfig {
    /// Extend other configurations
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub extends: Vec<String>,

    /// Global settings
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub settings: Option<LintSettings>,

    /// Rule overrides
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rules: Option<RuleOverrides>,

    /// Inline rules
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub inline_rules: Vec<Rule>,
}

/// Global lint settings
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LintSettings {
    /// Minimum severity to report
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub severity_threshold: Option<crate::Severity>,
}

/// Rule overrides configuration
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct RuleOverrides(pub std::collections::HashMap<String, RuleOverride>);

/// Override for a specific rule
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RuleOverride {
    /// Enable or disable the rule
    #[serde(default)]
    pub enabled: bool,

    /// Override severity
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub severity: Option<crate::Severity>,
}

impl Default for RuleOverride {
    fn default() -> Self {
        Self {
            enabled: true,
            severity: None,
        }
    }
}

/// Rule loader
pub struct RuleLoader;

impl RuleLoader {
    /// Load rules from a file (auto-detect format)
    pub fn load_file(path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path)?;
        Self::load_from_str(&content, path)
    }

    /// Load rules from string with path hint for format detection
    pub fn load_from_str(content: &str, path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
        let path = path.as_ref();
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");

        match ext {
            "yaml" | "yml" => Self::from_yaml(content),
            "json" => Self::from_json(content),
            _ => {
                // Try YAML first, then JSON
                Self::from_yaml(content).or_else(|_| Self::from_json(content))
            }
        }
    }

    /// Load from YAML string
    pub fn from_yaml(yaml: &str) -> Result<LintConfig, LoadError> {
        Ok(serde_yaml::from_str(yaml)?)
    }

    /// Load from JSON string
    pub fn from_json(json: &str) -> Result<LintConfig, LoadError> {
        Ok(serde_json::from_str(json)?)
    }

    /// Load a single rule from YAML
    pub fn rule_from_yaml(yaml: &str) -> Result<Rule, LoadError> {
        Ok(serde_yaml::from_str(yaml)?)
    }

    /// Load a single rule from JSON
    pub fn rule_from_json(json: &str) -> Result<Rule, LoadError> {
        Ok(serde_json::from_str(json)?)
    }

    /// Load rules list from YAML
    pub fn rules_from_yaml(yaml: &str) -> Result<Vec<Rule>, LoadError> {
        Ok(serde_yaml::from_str(yaml)?)
    }

    /// Load rules list from JSON
    pub fn rules_from_json(json: &str) -> Result<Vec<Rule>, LoadError> {
        Ok(serde_json::from_str(json)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Severity;

    #[test]
    fn test_load_lint_config() {
        let yaml = r#"
extends:
  - "default"

settings:
  severity_threshold: Warning

rules:
  RL001:
    enabled: true
    severity: Error
  RL003:
    enabled: false

inline_rules:
  - id: "PROJECT001"
    name: "no-println"
    severity: Warning
    query:
      kind: Function
    message: "Use tracing macros instead of println!"
"#;
        let config = RuleLoader::from_yaml(yaml).unwrap();
        assert_eq!(config.extends, vec!["default"]);
        assert!(config.settings.is_some());
        assert_eq!(config.inline_rules.len(), 1);
        assert_eq!(config.inline_rules[0].id, "PROJECT001");
    }

    #[test]
    fn test_load_single_rule() {
        let yaml = r#"
id: "RL001"
name: "no-unwrap"
severity: Error
query:
  kind: Function
  match:
    vis: Public
  body:
    contains:
      - node: MethodCall
message: "Avoid unwrap() in public function"
suggestion: "Use ? operator or expect()"
"#;
        let rule = RuleLoader::rule_from_yaml(yaml).unwrap();
        assert_eq!(rule.id, "RL001");
        assert_eq!(rule.name, "no-unwrap");
        assert_eq!(rule.severity, Severity::Error);
        assert!(rule.query.body.is_some());
    }

    #[test]
    fn test_load_rules_list() {
        let yaml = r#"
- id: "RL001"
  name: "no-unwrap"
  severity: Warning
  query:
    kind: Function
  message: "Avoid unwrap()"

- id: "RL002"
  name: "no-panic"
  severity: Error
  query:
    kind: Function
  message: "Avoid panic!()"
"#;
        let rules = RuleLoader::rules_from_yaml(yaml).unwrap();
        assert_eq!(rules.len(), 2);
        assert_eq!(rules[0].id, "RL001");
        assert_eq!(rules[1].id, "RL002");
    }

    #[test]
    fn test_rule_override() {
        let yaml = r#"
rules:
  RL001:
    enabled: true
    severity: Error
  RL002:
    enabled: false
"#;
        let config = RuleLoader::from_yaml(yaml).unwrap();
        let overrides = config.rules.unwrap();
        assert!(overrides.0.contains_key("RL001"));
        assert!(overrides.0.contains_key("RL002"));
        assert!(overrides.0["RL001"].enabled);
        assert!(!overrides.0["RL002"].enabled);
    }
}