tailwind_rs_postcss/advanced_features/
css_linter.rs

1//! CSS Linting System
2//!
3//! This module provides comprehensive CSS linting functionality with
4//! rule-based analysis, auto-fixing, and suggestions.
5
6use super::types::*;
7use regex::Regex;
8use std::collections::HashMap;
9
10/// CSS linter with comprehensive rules
11pub struct CSSLinter {
12    rules: Vec<LintRule>,
13    config: LinterConfig,
14    reporter: LintReporter,
15    fixer: LintFixer,
16}
17
18impl CSSLinter {
19    /// Create new CSS linter
20    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    /// Lint CSS with comprehensive rules
30    pub fn lint_css(
31        &self,
32        css: &str,
33        options: &LintOptions,
34    ) -> Result<LintResult, AdvancedFeatureError> {
35        // Parse CSS
36        let ast = self.parse_css(css)?;
37
38        // Apply linting rules
39        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        // Generate fixes if requested
50        if options.auto_fix {
51            fixes = self.generate_fixes(&issues)?;
52        }
53
54        // Generate suggestions
55        let suggestions = self.generate_suggestions(&ast)?;
56
57        // Calculate statistics
58        let statistics = self.calculate_statistics(&issues);
59
60        Ok(LintResult {
61            issues,
62            fixes,
63            statistics,
64            suggestions,
65        })
66    }
67
68    /// Auto-fix CSS issues
69    pub fn fix_css(&self, css: &str, fixes: &[LintFix]) -> Result<String, AdvancedFeatureError> {
70        let mut fixed_css = css.to_string();
71
72        // Apply fixes in order
73        for fix in fixes {
74            fixed_css = self.apply_fix(&fixed_css, fix)?;
75        }
76
77        Ok(fixed_css)
78    }
79
80    /// Get linting suggestions
81    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    /// Parse CSS into AST
87    fn parse_css(&self, css: &str) -> Result<CSSAST, AdvancedFeatureError> {
88        // Simplified CSS parsing - would use proper CSS parser
89        Ok(CSSAST {
90            rules: self.parse_rules(css)?,
91            comments: self.parse_comments(css)?,
92        })
93    }
94
95    /// Parse CSS rules
96    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, // Simplified
108                column: 1,
109            });
110        }
111
112        Ok(rules)
113    }
114
115    /// Parse CSS properties
116    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    /// Parse CSS comments
135    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, // Simplified
143                column: 1,
144            });
145        }
146
147        Ok(comments)
148    }
149
150    /// Apply linting rule
151    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                // Apply custom rule - simplified implementation
173                // Custom rules would need to implement a trait
174            }
175        }
176
177        Ok(issues)
178    }
179
180    /// Check for duplicate selectors
181    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    /// Check for empty rules
224    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    /// Check for important declarations
252    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    /// Check selector specificity
289    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    /// Calculate selector specificity
324    fn calculate_specificity(&self, selector: &str) -> usize {
325        let mut specificity = 0;
326
327        // Count IDs
328        specificity += selector.matches('#').count() * 100;
329
330        // Count classes and attributes
331        specificity += selector.matches('.').count() * 10;
332        specificity += selector.matches('[').count() * 10;
333
334        // Count elements
335        specificity += selector.split_whitespace().count();
336
337        specificity
338    }
339
340    /// Generate fixes for issues
341    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    /// Generate suggestions
354    fn generate_suggestions(
355        &self,
356        ast: &CSSAST,
357    ) -> Result<Vec<LintSuggestion>, AdvancedFeatureError> {
358        let mut suggestions = Vec::new();
359
360        // Generate suggestions for optimization
361        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    /// Calculate linting statistics
378    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    /// Apply fix to CSS
407    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                // Simplified reordering
414                Ok(css.to_string())
415            }
416        }
417    }
418
419    /// Get custom rule
420    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    /// Get default linting rules
428    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/// Lint rule
470#[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
479/// Lint reporter
480pub struct LintReporter;
481
482impl LintReporter {
483    pub fn new() -> Self {
484        Self
485    }
486}
487
488/// Lint fixer
489pub struct LintFixer;
490
491impl LintFixer {
492    pub fn new() -> Self {
493        Self
494    }
495}
496
497/// CSS AST structures
498#[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; }"; // Duplicate selector
535        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 { }"; // Empty rule
551        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; }"; // Important declaration
567        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; }"; // High specificity
583        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}