driven/validation/
linter.rs1use crate::{Result, parser::UnifiedRule};
4
5#[derive(Debug, Default)]
7pub struct Linter {
8 check_duplicates: bool,
10 check_vague: bool,
12}
13
14impl Linter {
15 pub fn new() -> Self {
17 Self {
18 check_duplicates: true,
19 check_vague: true,
20 }
21 }
22
23 pub fn with_duplicate_check(mut self, enabled: bool) -> Self {
25 self.check_duplicates = enabled;
26 self
27 }
28
29 pub fn lint(&self, rules: &[UnifiedRule]) -> Result<Vec<LintResult>> {
31 let mut results = Vec::new();
32
33 if self.check_duplicates {
35 results.extend(self.check_for_duplicates(rules));
36 }
37
38 if self.check_vague {
40 results.extend(self.check_for_vague(rules));
41 }
42
43 results.extend(self.check_for_empty(rules));
45
46 Ok(results)
47 }
48
49 fn check_for_duplicates(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
50 let mut results = Vec::new();
51 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
52
53 for rule in rules {
54 if let UnifiedRule::Standard { description, .. } = rule {
55 if !seen.insert(description.clone()) {
56 results.push(LintResult {
57 severity: LintSeverity::Warning,
58 message: format!("Duplicate rule: {}", description),
59 suggestion: Some("Remove one of the duplicate rules".to_string()),
60 });
61 }
62 }
63 }
64
65 results
66 }
67
68 fn check_for_vague(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
69 let mut results = Vec::new();
70 let vague_words = ["good", "proper", "appropriate", "nice", "clean", "better"];
71
72 for rule in rules {
73 if let UnifiedRule::Standard { description, .. } = rule {
74 let lower = description.to_lowercase();
75 for word in &vague_words {
76 if lower.contains(word) && description.len() < 30 {
77 results.push(LintResult {
78 severity: LintSeverity::Info,
79 message: format!(
80 "Rule may be too vague: '{}' contains '{}'",
81 description, word
82 ),
83 suggestion: Some("Consider being more specific".to_string()),
84 });
85 break;
86 }
87 }
88 }
89 }
90
91 results
92 }
93
94 fn check_for_empty(&self, rules: &[UnifiedRule]) -> Vec<LintResult> {
95 let mut results = Vec::new();
96
97 for rule in rules {
98 match rule {
99 UnifiedRule::Standard { description, .. } if description.trim().is_empty() => {
100 results.push(LintResult {
101 severity: LintSeverity::Error,
102 message: "Empty rule description".to_string(),
103 suggestion: Some("Add a meaningful description".to_string()),
104 });
105 }
106 UnifiedRule::Persona { name, role, .. }
107 if name.trim().is_empty() || role.trim().is_empty() =>
108 {
109 results.push(LintResult {
110 severity: LintSeverity::Error,
111 message: "Persona missing name or role".to_string(),
112 suggestion: Some("Add name and role to persona".to_string()),
113 });
114 }
115 _ => {}
116 }
117 }
118
119 results
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct LintResult {
126 pub severity: LintSeverity,
128 pub message: String,
130 pub suggestion: Option<String>,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum LintSeverity {
137 Error,
139 Warning,
141 Info,
143}
144
145impl std::fmt::Display for LintSeverity {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 LintSeverity::Error => write!(f, "error"),
149 LintSeverity::Warning => write!(f, "warning"),
150 LintSeverity::Info => write!(f, "info"),
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::format::RuleCategory;
159
160 #[test]
161 fn test_linter_empty() {
162 let linter = Linter::new();
163 let results = linter.lint(&[]).unwrap();
164 assert!(results.is_empty());
165 }
166
167 #[test]
168 fn test_linter_detects_duplicates() {
169 let linter = Linter::new();
170 let rules = vec![
171 UnifiedRule::Standard {
172 category: RuleCategory::Style,
173 priority: 0,
174 description: "Same rule".to_string(),
175 pattern: None,
176 },
177 UnifiedRule::Standard {
178 category: RuleCategory::Style,
179 priority: 1,
180 description: "Same rule".to_string(),
181 pattern: None,
182 },
183 ];
184
185 let results = linter.lint(&rules).unwrap();
186 assert!(!results.is_empty());
187 assert!(results.iter().any(|r| r.message.contains("Duplicate")));
188 }
189
190 #[test]
191 fn test_linter_detects_empty() {
192 let linter = Linter::new();
193 let rules = vec![UnifiedRule::Standard {
194 category: RuleCategory::Style,
195 priority: 0,
196 description: "".to_string(),
197 pattern: None,
198 }];
199
200 let results = linter.lint(&rules).unwrap();
201 assert!(!results.is_empty());
202 assert!(
203 results
204 .iter()
205 .any(|r| matches!(r.severity, LintSeverity::Error))
206 );
207 }
208}