rustyle_css/
errors.rs

1//! Error types and utilities for Rustyle
2//!
3//! Provides comprehensive error handling with helpful error messages.
4
5use std::fmt;
6
7/// Rustyle error types with enhanced context
8#[derive(Debug, Clone)]
9pub enum RustyleError {
10    /// CSS parsing error with detailed location
11    CssParseError {
12        message: String,
13        line: Option<usize>,
14        column: Option<usize>,
15        source_snippet: Option<String>,
16        file: Option<String>,
17    },
18    /// Invalid CSS property with suggestions
19    InvalidProperty {
20        property: String,
21        suggestions: Vec<String>,
22        context: Option<String>,
23        line: Option<usize>,
24        column: Option<usize>,
25    },
26    /// Invalid CSS value with suggestions
27    InvalidValue {
28        property: String,
29        value: String,
30        suggestions: Vec<String>,
31        expected_types: Vec<String>,
32        line: Option<usize>,
33        column: Option<usize>,
34    },
35    /// Missing required property
36    MissingProperty {
37        property: String,
38        context: Option<String>,
39        suggestions: Vec<String>,
40    },
41    /// Style registration error
42    RegistrationError {
43        message: String,
44        class_name: Option<String>,
45    },
46    /// Invalid design tokens
47    InvalidTokens { errors: Vec<String> },
48    /// Browser compatibility warning
49    BrowserCompatibility {
50        property: String,
51        value: Option<String>,
52        unsupported_browsers: Vec<String>,
53        suggestion: Option<String>,
54    },
55    /// Selector validation error
56    InvalidSelector {
57        selector: String,
58        message: String,
59        suggestion: Option<String>,
60    },
61}
62
63impl fmt::Display for RustyleError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            RustyleError::CssParseError {
67                message,
68                line,
69                column,
70                source_snippet,
71                file,
72            } => {
73                if let Some(file_name) = file {
74                    write!(f, "Error in {}: ", file_name)?;
75                }
76                write!(f, "CSS parsing error: {}", message)?;
77                if let Some(l) = line {
78                    write!(f, " (line {})", l)?;
79                }
80                if let Some(c) = column {
81                    write!(f, " (column {})", c)?;
82                }
83                if let Some(snippet) = source_snippet {
84                    write!(f, "\n\nCode snippet:\n{}", snippet)?;
85                }
86                Ok(())
87            }
88            RustyleError::InvalidProperty {
89                property,
90                suggestions,
91                context,
92                line,
93                column,
94            } => {
95                write!(f, "Invalid CSS property: '{}'", property)?;
96                if let Some(l) = line {
97                    write!(f, " (line {})", l)?;
98                }
99                if let Some(c) = column {
100                    write!(f, " (column {})", c)?;
101                }
102                if let Some(ctx) = context {
103                    write!(f, "\nContext: {}", ctx)?;
104                }
105                if !suggestions.is_empty() {
106                    write!(f, "\n\nDid you mean one of these?")?;
107                    for (i, sug) in suggestions.iter().take(3).enumerate() {
108                        write!(f, "\n  {}. {}", i + 1, sug)?;
109                    }
110                }
111                Ok(())
112            }
113            RustyleError::InvalidValue {
114                property,
115                value,
116                suggestions,
117                expected_types,
118                line,
119                column,
120            } => {
121                write!(f, "Invalid value '{}' for property '{}'", value, property)?;
122                if let Some(l) = line {
123                    write!(f, " (line {})", l)?;
124                }
125                if let Some(c) = column {
126                    write!(f, " (column {})", c)?;
127                }
128                if !expected_types.is_empty() {
129                    write!(f, "\n\nExpected one of: {}", expected_types.join(", "))?;
130                }
131                if !suggestions.is_empty() {
132                    write!(f, "\n\nDid you mean one of these?")?;
133                    for (i, sug) in suggestions.iter().take(3).enumerate() {
134                        write!(f, "\n  {}. {}", i + 1, sug)?;
135                    }
136                }
137                Ok(())
138            }
139            RustyleError::MissingProperty {
140                property,
141                context,
142                suggestions,
143            } => {
144                write!(f, "Missing required property: '{}'", property)?;
145                if let Some(c) = context {
146                    write!(f, " in context: {}", c)?;
147                }
148                if !suggestions.is_empty() {
149                    write!(f, "\n\nSimilar properties: {}", suggestions.join(", "))?;
150                }
151                Ok(())
152            }
153            RustyleError::RegistrationError {
154                message,
155                class_name,
156            } => {
157                write!(f, "Style registration error: {}", message)?;
158                if let Some(class) = class_name {
159                    write!(f, " (class: {})", class)?;
160                }
161                Ok(())
162            }
163            RustyleError::InvalidTokens { errors } => {
164                write!(f, "Invalid design tokens:")?;
165                for error in errors {
166                    write!(f, "\n  - {}", error)?;
167                }
168                Ok(())
169            }
170            RustyleError::BrowserCompatibility {
171                property,
172                value,
173                unsupported_browsers,
174                suggestion,
175            } => {
176                write!(
177                    f,
178                    "Browser compatibility warning for property '{}'",
179                    property
180                )?;
181                if let Some(val) = value {
182                    write!(f, " with value '{}'", val)?;
183                }
184                write!(f, "\nNot supported in: {}", unsupported_browsers.join(", "))?;
185                if let Some(sug) = suggestion {
186                    write!(f, "\nSuggestion: {}", sug)?;
187                }
188                Ok(())
189            }
190            RustyleError::InvalidSelector {
191                selector,
192                message,
193                suggestion,
194            } => {
195                write!(f, "Invalid CSS selector '{}': {}", selector, message)?;
196                if let Some(sug) = suggestion {
197                    write!(f, "\nSuggestion: {}", sug)?;
198                }
199                Ok(())
200            }
201        }
202    }
203}
204
205impl std::error::Error for RustyleError {}
206
207/// Helper to suggest similar property names (returns multiple suggestions)
208pub fn suggest_property(property: &str) -> Vec<String> {
209    let common_properties = vec![
210        "background-color",
211        "background",
212        "color",
213        "padding",
214        "margin",
215        "border",
216        "border-radius",
217        "border-width",
218        "border-style",
219        "border-color",
220        "font-size",
221        "font-weight",
222        "font-family",
223        "font-style",
224        "line-height",
225        "display",
226        "flex-direction",
227        "justify-content",
228        "align-items",
229        "flex-wrap",
230        "width",
231        "height",
232        "max-width",
233        "min-width",
234        "max-height",
235        "min-height",
236        "opacity",
237        "transform",
238        "transition",
239        "animation",
240        "box-shadow",
241        "text-align",
242        "text-decoration",
243        "text-transform",
244        "overflow",
245        "position",
246        "top",
247        "right",
248        "bottom",
249        "left",
250        "z-index",
251        "cursor",
252        "pointer-events",
253    ];
254
255    // Find all similar properties within threshold
256    let mut suggestions: Vec<(&str, usize)> = Vec::new();
257
258    for prop in &common_properties {
259        let distance = levenshtein_distance(property, prop);
260        if distance <= 3 {
261            suggestions.push((prop, distance));
262        }
263    }
264
265    // Sort by distance and return top suggestions
266    suggestions.sort_by_key(|(_, dist)| *dist);
267    suggestions
268        .into_iter()
269        .take(5)
270        .map(|(prop, _)| prop.to_string())
271        .collect()
272}
273
274/// Get all valid CSS properties (comprehensive list)
275pub fn get_all_css_properties() -> Vec<&'static str> {
276    vec![
277        // Layout
278        "display",
279        "position",
280        "top",
281        "right",
282        "bottom",
283        "left",
284        "z-index",
285        "float",
286        "clear",
287        "overflow",
288        "overflow-x",
289        "overflow-y",
290        // Flexbox
291        "flex",
292        "flex-direction",
293        "flex-wrap",
294        "flex-flow",
295        "justify-content",
296        "align-items",
297        "align-content",
298        "align-self",
299        "flex-grow",
300        "flex-shrink",
301        "flex-basis",
302        // Grid
303        "grid",
304        "grid-template",
305        "grid-template-rows",
306        "grid-template-columns",
307        "grid-template-areas",
308        "grid-auto-rows",
309        "grid-auto-columns",
310        "grid-auto-flow",
311        "grid-gap",
312        "grid-row-gap",
313        "grid-column-gap",
314        "grid-row",
315        "grid-column",
316        // Box Model
317        "width",
318        "height",
319        "min-width",
320        "max-width",
321        "min-height",
322        "max-height",
323        "margin",
324        "margin-top",
325        "margin-right",
326        "margin-bottom",
327        "margin-left",
328        "padding",
329        "padding-top",
330        "padding-right",
331        "padding-bottom",
332        "padding-left",
333        "box-sizing",
334        "border",
335        "border-width",
336        "border-style",
337        "border-color",
338        "border-top",
339        "border-right",
340        "border-bottom",
341        "border-left",
342        "border-radius",
343        "border-top-left-radius",
344        "border-top-right-radius",
345        "border-bottom-left-radius",
346        "border-bottom-right-radius",
347        // Typography
348        "font",
349        "font-family",
350        "font-size",
351        "font-weight",
352        "font-style",
353        "font-variant",
354        "line-height",
355        "text-align",
356        "text-decoration",
357        "text-transform",
358        "text-indent",
359        "letter-spacing",
360        "word-spacing",
361        "white-space",
362        "word-wrap",
363        "text-overflow",
364        // Colors
365        "color",
366        "background",
367        "background-color",
368        "background-image",
369        "background-repeat",
370        "background-position",
371        "background-size",
372        "background-attachment",
373        // Visual
374        "opacity",
375        "visibility",
376        "cursor",
377        "pointer-events",
378        "user-select",
379        "box-shadow",
380        "text-shadow",
381        "outline",
382        "outline-width",
383        "outline-style",
384        "outline-color",
385        // Transform & Animation
386        "transform",
387        "transform-origin",
388        "transition",
389        "transition-property",
390        "transition-duration",
391        "transition-timing-function",
392        "transition-delay",
393        "animation",
394        "animation-name",
395        "animation-duration",
396        "animation-timing-function",
397        "animation-delay",
398        "animation-iteration-count",
399        "animation-direction",
400        "animation-fill-mode",
401        // Other
402        "content",
403        "quotes",
404        "counter-reset",
405        "counter-increment",
406        "resize",
407        "clip",
408    ]
409}
410
411/// Simple Levenshtein distance calculation
412fn levenshtein_distance(s1: &str, s2: &str) -> usize {
413    let s1_chars: Vec<char> = s1.chars().collect();
414    let s2_chars: Vec<char> = s2.chars().collect();
415    let s1_len = s1_chars.len();
416    let s2_len = s2_chars.len();
417
418    if s1_len == 0 {
419        return s2_len;
420    }
421    if s2_len == 0 {
422        return s1_len;
423    }
424
425    let mut matrix = vec![vec![0; s2_len + 1]; s1_len + 1];
426
427    for i in 0..=s1_len {
428        matrix[i][0] = i;
429    }
430    for j in 0..=s2_len {
431        matrix[0][j] = j;
432    }
433
434    for i in 1..=s1_len {
435        for j in 1..=s2_len {
436            let cost = if s1_chars[i - 1] == s2_chars[j - 1] {
437                0
438            } else {
439                1
440            };
441            matrix[i][j] = (matrix[i - 1][j] + 1)
442                .min(matrix[i][j - 1] + 1)
443                .min(matrix[i - 1][j - 1] + cost);
444        }
445    }
446
447    matrix[s1_len][s2_len]
448}
449
450/// Create a helpful error message with context and visual formatting
451pub fn create_error_message(
452    error: &RustyleError,
453    file: Option<&str>,
454    line: Option<usize>,
455) -> String {
456    let mut msg = String::new();
457
458    // Add file and line context if not already in error
459    match error {
460        RustyleError::CssParseError {
461            file: err_file,
462            line: err_line,
463            ..
464        } => {
465            if let Some(f) = file.or(err_file.as_ref().map(|s| s.as_str())) {
466                msg.push_str(&format!("Error in {}: ", f));
467            }
468            if let Some(l) = line.or(*err_line) {
469                msg.push_str(&format!("line {}: ", l));
470            }
471        }
472        _ => {
473            if let Some(f) = file {
474                msg.push_str(&format!("Error in {}: ", f));
475            }
476            if let Some(l) = line {
477                msg.push_str(&format!("line {}: ", l));
478            }
479        }
480    }
481
482    msg.push_str(&error.to_string());
483
484    // Add helpful links and documentation
485    msg.push_str("\n\nFor more information:");
486    msg.push_str("\n  - Documentation: https://github.com/usvx/rustyle");
487    msg.push_str("\n  - CSS Reference: https://developer.mozilla.org/en-US/docs/Web/CSS");
488
489    // Add quick fix suggestions for common errors
490    if let RustyleError::InvalidProperty { property, .. } = error {
491        if property.contains("colour") {
492            msg.push_str("\n\n💡 Tip: Use 'color' instead of 'colour' (American spelling)");
493        }
494    }
495
496    msg
497}
498
499/// Extract code snippet around an error location
500pub fn extract_code_snippet(
501    source: &str,
502    line: usize,
503    column: Option<usize>,
504    context_lines: usize,
505) -> String {
506    let lines: Vec<&str> = source.lines().collect();
507    let line_num = line.saturating_sub(1);
508
509    if line_num >= lines.len() {
510        return String::new();
511    }
512
513    let start = line_num.saturating_sub(context_lines);
514    let end = (line_num + context_lines + 1).min(lines.len());
515
516    let mut snippet = String::new();
517    for i in start..end {
518        let line_content = lines[i];
519        let line_no = i + 1;
520
521        // Add line number
522        snippet.push_str(&format!("{:4} | ", line_no));
523        snippet.push_str(line_content);
524        snippet.push('\n');
525
526        // Add caret pointing to error column
527        if i == line_num {
528            if let Some(col) = column {
529                let spaces = "     | ".len() + col.saturating_sub(1);
530                snippet.push_str(&" ".repeat(spaces));
531                snippet.push_str("^\n");
532            }
533        }
534    }
535
536    snippet
537}
538
539/// Validate CSS property name
540pub fn validate_property(property: &str) -> Result<(), RustyleError> {
541    let valid_properties = get_all_css_properties();
542
543    if valid_properties.contains(&property) {
544        return Ok(());
545    }
546
547    let suggestions = suggest_property(property);
548    Err(RustyleError::InvalidProperty {
549        property: property.to_string(),
550        suggestions,
551        context: None,
552        line: None,
553        column: None,
554    })
555}
556
557/// Validate CSS value for a property
558pub fn validate_value(property: &str, value: &str) -> Result<(), RustyleError> {
559    // Basic validation - can be extended with more sophisticated checks
560    if value.trim().is_empty() {
561        return Err(RustyleError::InvalidValue {
562            property: property.to_string(),
563            value: value.to_string(),
564            suggestions: vec![],
565            expected_types: vec!["non-empty value".to_string()],
566            line: None,
567            column: None,
568        });
569    }
570
571    Ok(())
572}