1use crate::{Issue, IssueLevel, Result, ValidationResult};
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7
8pub trait Rule: Send + Sync {
10 fn name(&self) -> &str;
12
13 fn code(&self) -> &str;
15
16 fn severity(&self) -> IssueLevel;
18
19 fn validate(&self, input: &str) -> Result<Vec<Issue>>;
21}
22
23#[derive(Clone)]
25pub struct PatternRule {
26 name: String,
27 code: String,
28 severity: IssueLevel,
29 pattern: Regex,
30 message: String,
31 suggestion: Option<String>,
32}
33
34impl PatternRule {
35 pub fn new(
37 name: impl Into<String>,
38 code: impl Into<String>,
39 severity: IssueLevel,
40 pattern: &str,
41 message: impl Into<String>,
42 ) -> Result<Self> {
43 Ok(Self {
44 name: name.into(),
45 code: code.into(),
46 severity,
47 pattern: Regex::new(pattern)?,
48 message: message.into(),
49 suggestion: None,
50 })
51 }
52
53 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
55 self.suggestion = Some(suggestion.into());
56 self
57 }
58}
59
60impl Rule for PatternRule {
61 fn name(&self) -> &str {
62 &self.name
63 }
64
65 fn code(&self) -> &str {
66 &self.code
67 }
68
69 fn severity(&self) -> IssueLevel {
70 self.severity
71 }
72
73 fn validate(&self, input: &str) -> Result<Vec<Issue>> {
74 let mut issues = Vec::new();
75
76 for mat in self.pattern.find_iter(input) {
77 let mut issue = Issue::new(self.severity, self.code.clone(), self.message.clone())
78 .with_context(mat.as_str().to_string());
79
80 if let Some(ref suggestion) = self.suggestion {
81 issue = issue.with_suggestion(suggestion.clone());
82 }
83
84 issues.push(issue);
85 }
86
87 Ok(issues)
88 }
89}
90
91#[derive(Clone)]
93pub struct RuleEngine {
94 rules: Vec<Arc<dyn Rule>>,
95}
96
97impl RuleEngine {
98 pub fn new() -> Self {
100 Self { rules: Vec::new() }
101 }
102
103 pub fn add_rule(&mut self, rule: Arc<dyn Rule>) -> &mut Self {
105 self.rules.push(rule);
106 self
107 }
108
109 pub fn extend_rules(&mut self, rules: impl IntoIterator<Item = Arc<dyn Rule>>) -> &mut Self {
111 self.rules.extend(rules);
112 self
113 }
114
115 pub fn validate(&self, input: &str) -> Result<ValidationResult> {
117 let mut result = ValidationResult::new();
118
119 for rule in &self.rules {
120 let issues = rule.validate(input)?;
121 for mut issue in issues {
122 if issue.level < rule.severity() {
124 issue.level = rule.severity();
125 }
126 result.add_issue(issue);
127 }
128 }
129
130 Ok(result)
131 }
132
133 pub fn rules(&self) -> &[Arc<dyn Rule>] {
135 &self.rules
136 }
137
138 pub fn rule_count(&self) -> usize {
140 self.rules.len()
141 }
142}
143
144impl Default for RuleEngine {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RuleConfig {
153 pub name: String,
155
156 #[serde(default = "default_enabled")]
158 pub enabled: bool,
159
160 pub severity: Option<IssueLevel>,
162
163 pub patterns: Vec<String>,
165
166 pub message: Option<String>,
168
169 pub suggestion: Option<String>,
171}
172
173fn default_enabled() -> bool {
174 true
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_pattern_rule() {
183 let rule = PatternRule::new(
184 "no-console",
185 "no-console",
186 IssueLevel::Warning,
187 r"console\.(log|error|warn)\(",
188 "Don't use console in production",
189 )
190 .unwrap()
191 .with_suggestion("Use a proper logging library");
192
193 let code = r#"
194 function test() {
195 console.log("debug");
196 console.error("error");
197 }
198 "#;
199
200 let issues = rule.validate(code).unwrap();
201 assert_eq!(issues.len(), 2);
202 assert_eq!(issues[0].code, "no-console");
203 }
204
205 #[test]
206 fn test_rule_engine() {
207 let mut engine = RuleEngine::new();
208
209 let rule1 = PatternRule::new(
210 "no-debugger",
211 "no-debugger",
212 IssueLevel::Error,
213 r"debugger;",
214 "Remove debugger statement",
215 )
216 .unwrap();
217
218 let rule2 = PatternRule::new(
219 "no-alert",
220 "no-alert",
221 IssueLevel::Warning,
222 r"alert\(",
223 "Don't use alert()",
224 )
225 .unwrap();
226
227 engine.add_rule(Arc::new(rule1));
228 engine.add_rule(Arc::new(rule2));
229
230 let code = r#"
231 debugger;
232 alert("hello");
233 "#;
234
235 let result = engine.validate(code).unwrap();
236 assert_eq!(result.error_count(), 1);
237 assert_eq!(result.warning_count(), 1);
238 assert!(!result.passed);
239 }
240}