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