Skip to main content

driven/validation/
linter.rs

1//! Rule linter
2
3use crate::{Result, parser::UnifiedRule};
4
5/// Lints rules for common issues
6#[derive(Debug, Default)]
7pub struct Linter {
8    /// Whether to check for duplicates
9    check_duplicates: bool,
10    /// Whether to check for vague descriptions
11    check_vague: bool,
12}
13
14impl Linter {
15    /// Create a new linter with default settings
16    pub fn new() -> Self {
17        Self {
18            check_duplicates: true,
19            check_vague: true,
20        }
21    }
22
23    /// Enable/disable duplicate checking
24    pub fn with_duplicate_check(mut self, enabled: bool) -> Self {
25        self.check_duplicates = enabled;
26        self
27    }
28
29    /// Lint a set of rules
30    pub fn lint(&self, rules: &[UnifiedRule]) -> Result<Vec<LintResult>> {
31        let mut results = Vec::new();
32
33        // Check for duplicates
34        if self.check_duplicates {
35            results.extend(self.check_for_duplicates(rules));
36        }
37
38        // Check for vague descriptions
39        if self.check_vague {
40            results.extend(self.check_for_vague(rules));
41        }
42
43        // Check for empty content
44        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/// Lint result
124#[derive(Debug, Clone)]
125pub struct LintResult {
126    /// Severity of the issue
127    pub severity: LintSeverity,
128    /// Description of the issue
129    pub message: String,
130    /// Suggested fix
131    pub suggestion: Option<String>,
132}
133
134/// Lint severity levels
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum LintSeverity {
137    /// Error - must be fixed
138    Error,
139    /// Warning - should be fixed
140    Warning,
141    /// Info - optional improvement
142    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}