Skip to main content

oparry_validators/
css.rs

1//! CSS validator - line length, selectors, and best practices
2
3use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use std::path::Path;
7
8/// CSS validation configuration
9#[derive(Debug, Clone)]
10pub struct CssConfig {
11    /// Maximum line length
12    pub max_line_length: usize,
13    /// Enforce selector specificity limits
14    pub max_selector_specificity: u8,
15    /// Block !important
16    pub block_important: bool,
17}
18
19impl Default for CssConfig {
20    fn default() -> Self {
21        Self {
22            max_line_length: 80,
23            max_selector_specificity: 0,
24            block_important: true,
25        }
26    }
27}
28
29/// CSS validator
30pub struct CssValidator {
31    config: CssConfig,
32}
33
34impl CssValidator {
35    /// Create new CSS validator
36    pub fn new(config: CssConfig) -> Self {
37        Self { config }
38    }
39
40    /// Create with default config
41    pub fn default_config() -> Self {
42        Self::new(CssConfig::default())
43    }
44
45    /// Check line length
46    fn check_line_length(&self, source: &str, file: &str) -> Vec<Issue> {
47        let mut issues = Vec::new();
48
49        for (idx, line) in source.lines().enumerate() {
50            if line.len() > self.config.max_line_length {
51                issues.push(Issue::warning(
52                    "css-line-too-long",
53                    format!("Line too long: {} chars (max: {})", line.len(), self.config.max_line_length),
54                )
55                .with_file(file)
56                .with_line(idx)
57                .with_suggestion("Break line or use shorter selector"));
58            }
59        }
60
61        issues
62    }
63
64    /// Check for !important
65    fn check_important(&self, source: &str, file: &str) -> Vec<Issue> {
66        let mut issues = Vec::new();
67
68        if !self.config.block_important {
69            return issues;
70        }
71
72        for (idx, line) in source.lines().enumerate() {
73            if line.contains("!important") {
74                issues.push(Issue::error(
75                    "css-important",
76                    "!important should not be used",
77                )
78                .with_file(file)
79                .with_line(idx)
80                .with_suggestion("Increase specificity or refactor CSS cascade"));
81            }
82        }
83
84        issues
85    }
86}
87
88impl Validator for CssValidator {
89    fn name(&self) -> &str {
90        "CSS"
91    }
92
93    fn supports(&self, language: Language) -> bool {
94        matches!(language, Language::Unknown)
95    }
96
97    fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
98        let mut result = ValidationResult::new();
99        let source = code.source();
100
101        let file_str = file.to_string_lossy().to_string();
102
103        // Only validate CSS files
104        if file.to_string_lossy().ends_with(".css") {
105            for issue in self.check_line_length(source, &file_str) {
106                result.add_issue(issue);
107            }
108            for issue in self.check_important(source, &file_str) {
109                result.add_issue(issue);
110            }
111        }
112
113        Ok(result)
114    }
115
116    fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
117        let parsed = ParsedCode::Generic(source.to_string());
118        self.validate_parsed(&parsed, file)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_css_validator_valid() {
128        let validator = CssValidator::default_config();
129        let code = r#"
130            .button {
131                padding: 8px;
132            }
133        "#;
134
135        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
136        assert!(result.passed);
137    }
138
139    #[test]
140    fn test_css_validator_important() {
141        let validator = CssValidator::default_config();
142        let code = r#"
143            .button {
144                padding: 8px !important;
145            }
146        "#;
147
148        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
149        assert!(!result.passed);
150        assert_eq!(result.issues[0].code, "css-important");
151    }
152
153    #[test]
154    fn test_css_validator_line_too_long() {
155        let validator = CssValidator::default_config();
156        let code = ".button { padding: 8px; margin: 16px; border: 1px solid red; color: blue; background: white; }";
157
158        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
159        // Warnings don't fail by default (only in strict mode)
160        assert!(result.warning_count() > 0, "Should detect line too long");
161        assert_eq!(result.issues[0].code, "css-line-too-long");
162    }
163
164    #[test]
165    fn test_css_validator_non_css_file() {
166        let validator = CssValidator::default_config();
167        let code = r#"
168            .button {
169                padding: 8px !important;
170            }
171        "#;
172
173        let result = validator.validate_raw(code, Path::new("test.ts")).unwrap();
174        // Should not validate non-CSS files
175        assert!(result.passed);
176    }
177
178    #[test]
179    fn test_css_config_no_important_blocking() {
180        let config = CssConfig {
181            block_important: false,
182            ..Default::default()
183        };
184        let validator = CssValidator::new(config);
185        let code = r#"
186            .button {
187                padding: 8px !important;
188            }
189        "#;
190
191        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
192        assert!(result.passed);
193    }
194
195    #[test]
196    fn test_css_validator_multiple_issues() {
197        let validator = CssValidator::default_config();
198        let code = r#"
199            .button {
200                padding: 8px !important;
201            }
202            .alert {
203                background: red !important;
204            }
205        "#;
206
207        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
208        assert_eq!(result.issues.len(), 2);
209    }
210
211    #[test]
212    fn test_css_config_custom_max_line_length() {
213        let config = CssConfig {
214            max_line_length: 20,
215            ..Default::default()
216        };
217        let validator = CssValidator::new(config);
218        let code = ".button { padding: 8px; }";
219
220        let result = validator.validate_raw(code, Path::new("test.css")).unwrap();
221        // Warnings don't fail by default (only in strict mode)
222        assert!(result.warning_count() > 0, "Should detect line too long with custom limit");
223        assert_eq!(result.issues[0].code, "css-line-too-long");
224    }
225
226    #[test]
227    fn test_css_config_default() {
228        let config = CssConfig::default();
229        assert_eq!(config.max_line_length, 80);
230        assert!(config.block_important);
231    }
232
233    #[test]
234    fn test_css_validator_supports() {
235        let validator = CssValidator::default_config();
236        // CSS validator should not "support" specific languages (used as fallback)
237        assert!(!validator.supports(Language::JavaScript));
238        assert!(!validator.supports(Language::Rust));
239    }
240}