1use crate::Rule;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum LoadError {
14 #[error("IO error: {0}")]
16 Io(#[from] std::io::Error),
17
18 #[error("YAML parse error: {0}")]
20 Yaml(#[from] serde_yaml::Error),
21
22 #[error("JSON parse error: {0}")]
24 Json(#[from] serde_json::Error),
25
26 #[error("Invalid file extension: {0}")]
28 InvalidExtension(String),
29}
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
33pub struct LintConfig {
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub extends: Vec<String>,
37
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub settings: Option<LintSettings>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub rules: Option<RuleOverrides>,
45
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub inline_rules: Vec<Rule>,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
53pub struct LintSettings {
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub severity_threshold: Option<crate::Severity>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
61pub struct RuleOverrides(pub std::collections::HashMap<String, RuleOverride>);
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
65pub struct RuleOverride {
66 #[serde(default)]
68 pub enabled: bool,
69
70 #[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
84pub struct RuleLoader;
86
87impl RuleLoader {
88 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 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 Self::from_yaml(content).or_else(|_| Self::from_json(content))
106 }
107 }
108 }
109
110 pub fn from_yaml(yaml: &str) -> Result<LintConfig, LoadError> {
112 Ok(serde_yaml::from_str(yaml)?)
113 }
114
115 pub fn from_json(json: &str) -> Result<LintConfig, LoadError> {
117 Ok(serde_json::from_str(json)?)
118 }
119
120 pub fn rule_from_yaml(yaml: &str) -> Result<Rule, LoadError> {
122 Ok(serde_yaml::from_str(yaml)?)
123 }
124
125 pub fn rule_from_json(json: &str) -> Result<Rule, LoadError> {
127 Ok(serde_json::from_str(json)?)
128 }
129
130 pub fn rules_from_yaml(yaml: &str) -> Result<Vec<Rule>, LoadError> {
132 Ok(serde_yaml::from_str(yaml)?)
133 }
134
135 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}