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 std::collections::HashMap;
7use regex::Regex;
8use super::types::*;
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(&self, css: &str, options: &LintOptions) -> Result<LintResult, AdvancedFeatureError> {
31        
32        // Parse CSS
33        let ast = self.parse_css(css)?;
34        
35        // Apply linting rules
36        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        // Generate fixes if requested
47        if options.auto_fix {
48            fixes = self.generate_fixes(&issues)?;
49        }
50        
51        // Generate suggestions
52        let suggestions = self.generate_suggestions(&ast)?;
53        
54        // Calculate statistics
55        let statistics = self.calculate_statistics(&issues);
56        
57        Ok(LintResult {
58            issues,
59            fixes,
60            statistics,
61            suggestions,
62        })
63    }
64    
65    /// Auto-fix CSS issues
66    pub fn fix_css(&self, css: &str, fixes: &[LintFix]) -> Result<String, AdvancedFeatureError> {
67        let mut fixed_css = css.to_string();
68        
69        // Apply fixes in order
70        for fix in fixes {
71            fixed_css = self.apply_fix(&fixed_css, fix)?;
72        }
73        
74        Ok(fixed_css)
75    }
76    
77    /// Get linting suggestions
78    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    /// Parse CSS into AST
84    fn parse_css(&self, css: &str) -> Result<CSSAST, AdvancedFeatureError> {
85        // Simplified CSS parsing - would use proper CSS parser
86        Ok(CSSAST {
87            rules: self.parse_rules(css)?,
88            comments: self.parse_comments(css)?,
89        })
90    }
91    
92    /// Parse CSS rules
93    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, // Simplified
105                column: 1,
106            });
107        }
108        
109        Ok(rules)
110    }
111    
112    /// Parse CSS properties
113    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    /// Parse CSS comments
129    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, // Simplified
137                column: 1,
138            });
139        }
140        
141        Ok(comments)
142    }
143    
144    /// Apply linting rule
145    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                // Apply custom rule - simplified implementation
163                // Custom rules would need to implement a trait
164            }
165        }
166        
167        Ok(issues)
168    }
169    
170    /// Check for duplicate selectors
171    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    /// Check for empty rules
208    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    /// Check for important declarations
236    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    /// Check selector specificity
266    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    /// Calculate selector specificity
292    fn calculate_specificity(&self, selector: &str) -> usize {
293        let mut specificity = 0;
294        
295        // Count IDs
296        specificity += selector.matches('#').count() * 100;
297        
298        // Count classes and attributes
299        specificity += selector.matches('.').count() * 10;
300        specificity += selector.matches('[').count() * 10;
301        
302        // Count elements
303        specificity += selector.split_whitespace().count();
304        
305        specificity
306    }
307    
308    /// Generate fixes for issues
309    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    /// Generate suggestions
322    fn generate_suggestions(&self, ast: &CSSAST) -> Result<Vec<LintSuggestion>, AdvancedFeatureError> {
323        let mut suggestions = Vec::new();
324        
325        // Generate suggestions for optimization
326        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    /// Calculate linting statistics
343    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    /// Apply fix to CSS
372    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                // Simplified reordering
385                Ok(css.to_string())
386            }
387        }
388    }
389    
390    /// Get custom rule
391    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    /// Get default linting rules
396    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/// Lint rule
435#[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
444/// Lint reporter
445pub struct LintReporter;
446
447impl LintReporter {
448    pub fn new() -> Self {
449        Self
450    }
451}
452
453/// Lint fixer
454pub struct LintFixer;
455
456impl LintFixer {
457    pub fn new() -> Self {
458        Self
459    }
460}
461
462/// CSS AST structures
463#[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; }"; // Duplicate selector
500        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 { }"; // Empty rule
513        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; }"; // Important declaration
526        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; }"; // High specificity
539        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}