1use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct CssConfig {
11 pub max_line_length: usize,
13 pub max_selector_specificity: u8,
15 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
29pub struct CssValidator {
31 config: CssConfig,
32}
33
34impl CssValidator {
35 pub fn new(config: CssConfig) -> Self {
37 Self { config }
38 }
39
40 pub fn default_config() -> Self {
42 Self::new(CssConfig::default())
43 }
44
45 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 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 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 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 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 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 assert!(!validator.supports(Language::JavaScript));
238 assert!(!validator.supports(Language::Rust));
239 }
240}