Skip to main content

ryo_pattern/
loader.rs

1//! Rule loader for RyoPattern
2//!
3//! Loads lint rules from YAML/JSON configuration files.
4
5use crate::Rule;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9use thiserror::Error;
10
11/// Errors from rule loading
12#[derive(Debug, Error)]
13pub enum LoadError {
14    /// IO failure while reading the rule file.
15    #[error("IO error: {0}")]
16    Io(#[from] std::io::Error),
17
18    /// YAML parse failure.
19    #[error("YAML parse error: {0}")]
20    Yaml(#[from] serde_yaml::Error),
21
22    /// JSON parse failure.
23    #[error("JSON parse error: {0}")]
24    Json(#[from] serde_json::Error),
25
26    /// Unsupported / unknown rule file extension.
27    #[error("Invalid file extension: {0}")]
28    InvalidExtension(String),
29}
30
31/// Lint configuration file structure
32#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
33pub struct LintConfig {
34    /// Extend other configurations
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub extends: Vec<String>,
37
38    /// Global settings
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub settings: Option<LintSettings>,
41
42    /// Rule overrides
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub rules: Option<RuleOverrides>,
45
46    /// Inline rules
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub inline_rules: Vec<Rule>,
49}
50
51/// Global lint settings
52#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
53pub struct LintSettings {
54    /// Minimum severity to report
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub severity_threshold: Option<crate::Severity>,
57}
58
59/// Rule overrides configuration
60#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
61pub struct RuleOverrides(pub std::collections::HashMap<String, RuleOverride>);
62
63/// Override for a specific rule
64#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
65pub struct RuleOverride {
66    /// Enable or disable the rule
67    #[serde(default)]
68    pub enabled: bool,
69
70    /// Override severity
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub severity: Option<crate::Severity>,
73}
74
75impl Default for RuleOverride {
76    fn default() -> Self {
77        Self {
78            enabled: true,
79            severity: None,
80        }
81    }
82}
83
84/// Rule loader
85pub struct RuleLoader;
86
87impl RuleLoader {
88    /// Load rules from a file (auto-detect format)
89    pub fn load_file(path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
90        let path = path.as_ref();
91        let content = std::fs::read_to_string(path)?;
92        Self::load_from_str(&content, path)
93    }
94
95    /// Load rules from string with path hint for format detection
96    pub fn load_from_str(content: &str, path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
97        let path = path.as_ref();
98        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
99
100        match ext {
101            "yaml" | "yml" => Self::from_yaml(content),
102            "json" => Self::from_json(content),
103            _ => {
104                // Try YAML first, then JSON
105                Self::from_yaml(content).or_else(|_| Self::from_json(content))
106            }
107        }
108    }
109
110    /// Load from YAML string
111    pub fn from_yaml(yaml: &str) -> Result<LintConfig, LoadError> {
112        Ok(serde_yaml::from_str(yaml)?)
113    }
114
115    /// Load from JSON string
116    pub fn from_json(json: &str) -> Result<LintConfig, LoadError> {
117        Ok(serde_json::from_str(json)?)
118    }
119
120    /// Load a single rule from YAML
121    pub fn rule_from_yaml(yaml: &str) -> Result<Rule, LoadError> {
122        Ok(serde_yaml::from_str(yaml)?)
123    }
124
125    /// Load a single rule from JSON
126    pub fn rule_from_json(json: &str) -> Result<Rule, LoadError> {
127        Ok(serde_json::from_str(json)?)
128    }
129
130    /// Load rules list from YAML
131    pub fn rules_from_yaml(yaml: &str) -> Result<Vec<Rule>, LoadError> {
132        Ok(serde_yaml::from_str(yaml)?)
133    }
134
135    /// Load rules list from JSON
136    pub fn rules_from_json(json: &str) -> Result<Vec<Rule>, LoadError> {
137        Ok(serde_json::from_str(json)?)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::Severity;
145
146    #[test]
147    fn test_load_lint_config() {
148        let yaml = r#"
149extends:
150  - "default"
151
152settings:
153  severity_threshold: Warning
154
155rules:
156  RL001:
157    enabled: true
158    severity: Error
159  RL003:
160    enabled: false
161
162inline_rules:
163  - id: "PROJECT001"
164    name: "no-println"
165    severity: Warning
166    query:
167      kind: Function
168    message: "Use tracing macros instead of println!"
169"#;
170        let config = RuleLoader::from_yaml(yaml).unwrap();
171        assert_eq!(config.extends, vec!["default"]);
172        assert!(config.settings.is_some());
173        assert_eq!(config.inline_rules.len(), 1);
174        assert_eq!(config.inline_rules[0].id, "PROJECT001");
175    }
176
177    #[test]
178    fn test_load_single_rule() {
179        let yaml = r#"
180id: "RL001"
181name: "no-unwrap"
182severity: Error
183query:
184  kind: Function
185  match:
186    vis: Public
187  body:
188    contains:
189      - node: MethodCall
190message: "Avoid unwrap() in public function"
191suggestion: "Use ? operator or expect()"
192"#;
193        let rule = RuleLoader::rule_from_yaml(yaml).unwrap();
194        assert_eq!(rule.id, "RL001");
195        assert_eq!(rule.name, "no-unwrap");
196        assert_eq!(rule.severity, Severity::Error);
197        assert!(rule.query.body.is_some());
198    }
199
200    #[test]
201    fn test_load_rules_list() {
202        let yaml = r#"
203- id: "RL001"
204  name: "no-unwrap"
205  severity: Warning
206  query:
207    kind: Function
208  message: "Avoid unwrap()"
209
210- id: "RL002"
211  name: "no-panic"
212  severity: Error
213  query:
214    kind: Function
215  message: "Avoid panic!()"
216"#;
217        let rules = RuleLoader::rules_from_yaml(yaml).unwrap();
218        assert_eq!(rules.len(), 2);
219        assert_eq!(rules[0].id, "RL001");
220        assert_eq!(rules[1].id, "RL002");
221    }
222
223    #[test]
224    fn test_rule_override() {
225        let yaml = r#"
226rules:
227  RL001:
228    enabled: true
229    severity: Error
230  RL002:
231    enabled: false
232"#;
233        let config = RuleLoader::from_yaml(yaml).unwrap();
234        let overrides = config.rules.unwrap();
235        assert!(overrides.0.contains_key("RL001"));
236        assert!(overrides.0.contains_key("RL002"));
237        assert!(overrides.0["RL001"].enabled);
238        assert!(!overrides.0["RL002"].enabled);
239    }
240}