1use std::collections::HashMap;
7use regex::Regex;
8use super::types::*;
9
10pub struct CSSLinter {
12 rules: Vec<LintRule>,
13 config: LinterConfig,
14 reporter: LintReporter,
15 fixer: LintFixer,
16}
17
18impl CSSLinter {
19 pub fn new(config: LinterConfig) -> Self {
21 Self {
22 rules: Self::get_default_rules(),
23 config,
24 reporter: LintReporter::new(),
25 fixer: LintFixer::new(),
26 }
27 }
28
29 pub fn lint_css(&self, css: &str, options: &LintOptions) -> Result<LintResult, AdvancedFeatureError> {
31
32 let ast = self.parse_css(css)?;
34
35 let mut issues = Vec::new();
37 let mut fixes = Vec::new();
38
39 for rule in &self.rules {
40 if rule.enabled {
41 let rule_issues = self.apply_rule(&ast, rule)?;
42 issues.extend(rule_issues);
43 }
44 }
45
46 if options.auto_fix {
48 fixes = self.generate_fixes(&issues)?;
49 }
50
51 let suggestions = self.generate_suggestions(&ast)?;
53
54 let statistics = self.calculate_statistics(&issues);
56
57 Ok(LintResult {
58 issues,
59 fixes,
60 statistics,
61 suggestions,
62 })
63 }
64
65 pub fn fix_css(&self, css: &str, fixes: &[LintFix]) -> Result<String, AdvancedFeatureError> {
67 let mut fixed_css = css.to_string();
68
69 for fix in fixes {
71 fixed_css = self.apply_fix(&fixed_css, fix)?;
72 }
73
74 Ok(fixed_css)
75 }
76
77 pub fn get_suggestions(&self, css: &str) -> Result<Vec<LintSuggestion>, AdvancedFeatureError> {
79 let ast = self.parse_css(css)?;
80 self.generate_suggestions(&ast)
81 }
82
83 fn parse_css(&self, css: &str) -> Result<CSSAST, AdvancedFeatureError> {
85 Ok(CSSAST {
87 rules: self.parse_rules(css)?,
88 comments: self.parse_comments(css)?,
89 })
90 }
91
92 fn parse_rules(&self, css: &str) -> Result<Vec<CSSRule>, AdvancedFeatureError> {
94 let mut rules = Vec::new();
95 let rule_pattern = Regex::new(r"([^{]+)\s*\{([^}]+)\}").unwrap();
96
97 for cap in rule_pattern.captures_iter(css) {
98 let selector = cap[1].trim().to_string();
99 let properties = cap[2].trim().to_string();
100
101 rules.push(CSSRule {
102 selector,
103 properties: self.parse_properties(&properties)?,
104 line: 1, column: 1,
106 });
107 }
108
109 Ok(rules)
110 }
111
112 fn parse_properties(&self, properties_str: &str) -> Result<Vec<CSSProperty>, AdvancedFeatureError> {
114 let mut properties = Vec::new();
115 let property_pattern = Regex::new(r"([^:]+):\s*([^;]+);").unwrap();
116
117 for cap in property_pattern.captures_iter(properties_str) {
118 properties.push(CSSProperty {
119 name: cap[1].trim().to_string(),
120 value: cap[2].trim().to_string(),
121 important: cap[2].contains("!important"),
122 });
123 }
124
125 Ok(properties)
126 }
127
128 fn parse_comments(&self, css: &str) -> Result<Vec<CSSComment>, AdvancedFeatureError> {
130 let mut comments = Vec::new();
131 let comment_pattern = Regex::new(r"/\*([^*]|\*[^/])*\*/").unwrap();
132
133 for cap in comment_pattern.captures_iter(css) {
134 comments.push(CSSComment {
135 content: cap[0].to_string(),
136 line: 1, column: 1,
138 });
139 }
140
141 Ok(comments)
142 }
143
144 fn apply_rule(&self, ast: &CSSAST, rule: &LintRule) -> Result<Vec<LintIssue>, AdvancedFeatureError> {
146 let mut issues = Vec::new();
147
148 match rule.name.as_str() {
149 "no-duplicate-selectors" => {
150 issues.extend(self.check_duplicate_selectors(ast)?);
151 }
152 "no-empty-rules" => {
153 issues.extend(self.check_empty_rules(ast)?);
154 }
155 "no-important" => {
156 issues.extend(self.check_important_declarations(ast)?);
157 }
158 "selector-max-specificity" => {
159 issues.extend(self.check_selector_specificity(ast, rule)?);
160 }
161 _ => {
162 }
165 }
166
167 Ok(issues)
168 }
169
170 fn check_duplicate_selectors(&self, ast: &CSSAST) -> Result<Vec<LintIssue>, AdvancedFeatureError> {
172 let mut issues = Vec::new();
173 let mut selector_map: HashMap<String, Vec<&CSSRule>> = HashMap::new();
174
175 for rule in &ast.rules {
176 selector_map.entry(rule.selector.clone()).or_insert_with(Vec::new).push(rule);
177 }
178
179 for (selector, rules) in selector_map {
180 if rules.len() > 1 {
181 for (i, rule) in rules.iter().enumerate() {
182 if i > 0 {
183 issues.push(LintIssue {
184 rule: "no-duplicate-selectors".to_string(),
185 severity: SeverityLevel::Warning,
186 message: format!("Duplicate selector '{}'", selector),
187 line: rule.line,
188 column: rule.column,
189 end_line: None,
190 end_column: None,
191 fix: Some(LintFix {
192 rule: "no-duplicate-selectors".to_string(),
193 message: "Remove duplicate selector".to_string(),
194 fix_type: FixType::Delete,
195 replacement: String::new(),
196 range: TextRange::new(0, 0),
197 }),
198 });
199 }
200 }
201 }
202 }
203
204 Ok(issues)
205 }
206
207 fn check_empty_rules(&self, ast: &CSSAST) -> Result<Vec<LintIssue>, AdvancedFeatureError> {
209 let mut issues = Vec::new();
210
211 for rule in &ast.rules {
212 if rule.properties.is_empty() {
213 issues.push(LintIssue {
214 rule: "no-empty-rules".to_string(),
215 severity: SeverityLevel::Warning,
216 message: format!("Empty rule '{}'", rule.selector),
217 line: rule.line,
218 column: rule.column,
219 end_line: None,
220 end_column: None,
221 fix: Some(LintFix {
222 rule: "no-empty-rules".to_string(),
223 message: "Remove empty rule".to_string(),
224 fix_type: FixType::Delete,
225 replacement: String::new(),
226 range: TextRange::new(0, 0),
227 }),
228 });
229 }
230 }
231
232 Ok(issues)
233 }
234
235 fn check_important_declarations(&self, ast: &CSSAST) -> Result<Vec<LintIssue>, AdvancedFeatureError> {
237 let mut issues = Vec::new();
238
239 for rule in &ast.rules {
240 for property in &rule.properties {
241 if property.important {
242 issues.push(LintIssue {
243 rule: "no-important".to_string(),
244 severity: SeverityLevel::Warning,
245 message: format!("Avoid using !important in '{}'", property.name),
246 line: rule.line,
247 column: rule.column,
248 end_line: None,
249 end_column: None,
250 fix: Some(LintFix {
251 rule: "no-important".to_string(),
252 message: "Remove !important".to_string(),
253 fix_type: FixType::Replace,
254 replacement: property.value.replace("!important", "").trim().to_string(),
255 range: TextRange::new(0, 0),
256 }),
257 });
258 }
259 }
260 }
261
262 Ok(issues)
263 }
264
265 fn check_selector_specificity(&self, ast: &CSSAST, rule: &LintRule) -> Result<Vec<LintIssue>, AdvancedFeatureError> {
267 let mut issues = Vec::new();
268 let max_specificity = rule.options.get("max")
269 .and_then(|v| v.as_u64())
270 .unwrap_or(3) as usize;
271
272 for rule in &ast.rules {
273 let specificity = self.calculate_specificity(&rule.selector);
274 if specificity > max_specificity {
275 issues.push(LintIssue {
276 rule: "selector-max-specificity".to_string(),
277 severity: SeverityLevel::Warning,
278 message: format!("Selector '{}' has specificity {} (max: {})", rule.selector, specificity, max_specificity),
279 line: rule.line,
280 column: rule.column,
281 end_line: None,
282 end_column: None,
283 fix: None,
284 });
285 }
286 }
287
288 Ok(issues)
289 }
290
291 fn calculate_specificity(&self, selector: &str) -> usize {
293 let mut specificity = 0;
294
295 specificity += selector.matches('#').count() * 100;
297
298 specificity += selector.matches('.').count() * 10;
300 specificity += selector.matches('[').count() * 10;
301
302 specificity += selector.split_whitespace().count();
304
305 specificity
306 }
307
308 fn generate_fixes(&self, issues: &[LintIssue]) -> Result<Vec<LintFix>, AdvancedFeatureError> {
310 let mut fixes = Vec::new();
311
312 for issue in issues {
313 if let Some(fix) = &issue.fix {
314 fixes.push(fix.clone());
315 }
316 }
317
318 Ok(fixes)
319 }
320
321 fn generate_suggestions(&self, ast: &CSSAST) -> Result<Vec<LintSuggestion>, AdvancedFeatureError> {
323 let mut suggestions = Vec::new();
324
325 for rule in &ast.rules {
327 if rule.properties.len() > 10 {
328 suggestions.push(LintSuggestion {
329 rule: "rule-too-long".to_string(),
330 message: "Consider splitting this rule into smaller rules".to_string(),
331 severity: SeverityLevel::Info,
332 line: rule.line,
333 column: rule.column,
334 fix: None,
335 });
336 }
337 }
338
339 Ok(suggestions)
340 }
341
342 fn calculate_statistics(&self, issues: &[LintIssue]) -> LintStatistics {
344 let mut error_count = 0;
345 let mut warning_count = 0;
346 let mut info_count = 0;
347 let mut fixable_count = 0;
348
349 for issue in issues {
350 match issue.severity {
351 SeverityLevel::Error => error_count += 1,
352 SeverityLevel::Warning => warning_count += 1,
353 SeverityLevel::Info => info_count += 1,
354 SeverityLevel::Off => {},
355 }
356
357 if issue.fix.is_some() {
358 fixable_count += 1;
359 }
360 }
361
362 LintStatistics {
363 total_issues: issues.len(),
364 error_count,
365 warning_count,
366 info_count,
367 fixable_count,
368 }
369 }
370
371 fn apply_fix(&self, css: &str, fix: &LintFix) -> Result<String, AdvancedFeatureError> {
373 match fix.fix_type {
374 FixType::Replace => {
375 Ok(css.replace(&fix.replacement, ""))
376 }
377 FixType::Insert => {
378 Ok(format!("{}{}", css, fix.replacement))
379 }
380 FixType::Delete => {
381 Ok(css.replace(&fix.replacement, ""))
382 }
383 FixType::Reorder => {
384 Ok(css.to_string())
386 }
387 }
388 }
389
390 fn get_custom_rule(&self, rule_name: &str) -> Option<&CustomRule> {
392 self.config.custom_rules.iter().find(|rule| rule.name == rule_name)
393 }
394
395 fn get_default_rules() -> Vec<LintRule> {
397 vec![
398 LintRule {
399 name: "no-duplicate-selectors".to_string(),
400 description: "Disallow duplicate selectors".to_string(),
401 severity: SeverityLevel::Warning,
402 enabled: true,
403 options: HashMap::new(),
404 },
405 LintRule {
406 name: "no-empty-rules".to_string(),
407 description: "Disallow empty rules".to_string(),
408 severity: SeverityLevel::Warning,
409 enabled: true,
410 options: HashMap::new(),
411 },
412 LintRule {
413 name: "no-important".to_string(),
414 description: "Disallow !important declarations".to_string(),
415 severity: SeverityLevel::Warning,
416 enabled: true,
417 options: HashMap::new(),
418 },
419 LintRule {
420 name: "selector-max-specificity".to_string(),
421 description: "Limit selector specificity".to_string(),
422 severity: SeverityLevel::Warning,
423 enabled: true,
424 options: {
425 let mut opts = HashMap::new();
426 opts.insert("max".to_string(), serde_json::Value::Number(serde_json::Number::from(3)));
427 opts
428 },
429 },
430 ]
431 }
432}
433
434#[derive(Debug, Clone)]
436pub struct LintRule {
437 pub name: String,
438 pub description: String,
439 pub severity: SeverityLevel,
440 pub enabled: bool,
441 pub options: HashMap<String, serde_json::Value>,
442}
443
444pub struct LintReporter;
446
447impl LintReporter {
448 pub fn new() -> Self {
449 Self
450 }
451}
452
453pub struct LintFixer;
455
456impl LintFixer {
457 pub fn new() -> Self {
458 Self
459 }
460}
461
462#[derive(Debug, Clone)]
464pub struct CSSAST {
465 pub rules: Vec<CSSRule>,
466 pub comments: Vec<CSSComment>,
467}
468
469#[derive(Debug, Clone)]
470pub struct CSSRule {
471 pub selector: String,
472 pub properties: Vec<CSSProperty>,
473 pub line: usize,
474 pub column: usize,
475}
476
477#[derive(Debug, Clone)]
478pub struct CSSProperty {
479 pub name: String,
480 pub value: String,
481 pub important: bool,
482}
483
484#[derive(Debug, Clone)]
485pub struct CSSComment {
486 pub content: String,
487 pub line: usize,
488 pub column: usize,
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn test_css_linting() {
497 let config = LinterConfig::default();
498 let linter = CSSLinter::new(config);
499 let css = ".test { color: red; } .test { color: blue; }"; let result = linter.lint_css(css, &LintOptions::default());
501 assert!(result.is_ok());
502
503 let lint_result = result.unwrap();
504 assert!(!lint_result.issues.is_empty());
505 assert!(lint_result.issues.iter().any(|issue| issue.rule == "no-duplicate-selectors"));
506 }
507
508 #[test]
509 fn test_empty_rules_linting() {
510 let config = LinterConfig::default();
511 let linter = CSSLinter::new(config);
512 let css = ".empty { }"; let result = linter.lint_css(css, &LintOptions::default());
514 assert!(result.is_ok());
515
516 let lint_result = result.unwrap();
517 assert!(!lint_result.issues.is_empty());
518 assert!(lint_result.issues.iter().any(|issue| issue.rule == "no-empty-rules"));
519 }
520
521 #[test]
522 fn test_important_linting() {
523 let config = LinterConfig::default();
524 let linter = CSSLinter::new(config);
525 let css = ".test { color: red !important; }"; let result = linter.lint_css(css, &LintOptions::default());
527 assert!(result.is_ok());
528
529 let lint_result = result.unwrap();
530 assert!(!lint_result.issues.is_empty());
531 assert!(lint_result.issues.iter().any(|issue| issue.rule == "no-important"));
532 }
533
534 #[test]
535 fn test_specificity_linting() {
536 let config = LinterConfig::default();
537 let linter = CSSLinter::new(config);
538 let css = "#id .class .class .class { color: red; }"; let result = linter.lint_css(css, &LintOptions::default());
540 assert!(result.is_ok());
541
542 let lint_result = result.unwrap();
543 assert!(!lint_result.issues.is_empty());
544 assert!(lint_result.issues.iter().any(|issue| issue.rule == "selector-max-specificity"));
545 }
546
547 #[test]
548 fn test_lint_statistics() {
549 let config = LinterConfig::default();
550 let linter = CSSLinter::new(config);
551 let css = ".test { color: red !important; } .empty { }";
552 let result = linter.lint_css(css, &LintOptions::default());
553 assert!(result.is_ok());
554
555 let lint_result = result.unwrap();
556 assert!(lint_result.statistics.total_issues > 0);
557 assert!(lint_result.statistics.warning_count > 0);
558 }
559}