dampen_cli/commands/check/
main_command.rs

1#![allow(clippy::print_stderr, clippy::print_stdout)]
2
3//! Check command - validates Dampen UI files
4
5use clap::Args;
6use dampen_core::ir::layout::{Direction, Position};
7use dampen_core::{
8    ir::{AttributeValue, EventKind, WidgetKind},
9    parser,
10    parser::style_parser,
11};
12use std::collections::HashSet;
13use std::fs;
14use std::path::{Path, PathBuf};
15use thiserror::Error;
16use walkdir::WalkDir;
17
18#[derive(Error, Debug)]
19pub enum CheckError {
20    #[error("Directory not found: {0}")]
21    DirectoryNotFound(PathBuf),
22
23    #[error("Parse error in {file}:{line}:{col}: {message}")]
24    ParseError {
25        file: PathBuf,
26        line: u32,
27        col: u32,
28        message: String,
29    },
30
31    #[error("XML validation error in {file}:{line}:{col}: {message}")]
32    XmlValidationError {
33        file: PathBuf,
34        line: u32,
35        col: u32,
36        message: String,
37    },
38
39    #[error("Invalid widget '{widget}' in {file}:{line}:{col}")]
40    InvalidWidget {
41        widget: String,
42        file: PathBuf,
43        line: u32,
44        col: u32,
45    },
46
47    #[error("Unknown attribute '{attr}' for widget '{widget}' in {file}:{line}:{col}{suggestion}")]
48    UnknownAttribute {
49        attr: String,
50        widget: String,
51        file: PathBuf,
52        line: u32,
53        col: u32,
54        suggestion: String,
55    },
56
57    #[error("Unknown handler '{handler}' in {file}:{line}:{col}{suggestion}")]
58    UnknownHandler {
59        handler: String,
60        file: PathBuf,
61        line: u32,
62        col: u32,
63        suggestion: String,
64    },
65
66    #[error("Invalid binding field '{field}' in {file}:{line}:{col}")]
67    InvalidBinding {
68        field: String,
69        file: PathBuf,
70        line: u32,
71        col: u32,
72    },
73
74    #[error("Invalid style attribute '{attr}' in {file}:{line}:{col}: {message}")]
75    InvalidStyleAttribute {
76        attr: String,
77        file: PathBuf,
78        line: u32,
79        col: u32,
80        message: String,
81    },
82
83    #[error("Invalid state prefix '{prefix}' in {file}:{line}:{col}")]
84    InvalidStatePrefix {
85        prefix: String,
86        file: PathBuf,
87        line: u32,
88        col: u32,
89    },
90
91    #[error("Invalid style value for '{attr}' in {file}:{line}:{col}: {message}")]
92    InvalidStyleValue {
93        attr: String,
94        file: PathBuf,
95        line: u32,
96        col: u32,
97        message: String,
98    },
99
100    #[error("Invalid layout constraint in {file}:{line}:{col}: {message}")]
101    InvalidLayoutConstraint {
102        file: PathBuf,
103        line: u32,
104        col: u32,
105        message: String,
106    },
107
108    #[error("Unknown theme '{theme}' referenced in {file}:{line}:{col}")]
109    UnknownTheme {
110        theme: String,
111        file: PathBuf,
112        line: u32,
113        col: u32,
114    },
115
116    #[error("Unknown style class '{class}' referenced in {file}:{line}:{col}")]
117    UnknownStyleClass {
118        class: String,
119        file: PathBuf,
120        line: u32,
121        col: u32,
122    },
123
124    #[error("Invalid breakpoint attribute '{attr}' in {file}:{line}:{col}")]
125    InvalidBreakpoint {
126        attr: String,
127        file: PathBuf,
128        line: u32,
129        col: u32,
130    },
131
132    #[error("Invalid state attribute '{attr}' in {file}:{line}:{col}")]
133    InvalidState {
134        attr: String,
135        file: PathBuf,
136        line: u32,
137        col: u32,
138    },
139
140    #[error("Failed to load handler registry from {path}: {source}")]
141    HandlerRegistryLoadError {
142        path: PathBuf,
143        source: serde_json::Error,
144    },
145
146    #[error("Failed to load model info from {path}: {source}")]
147    ModelInfoLoadError {
148        path: PathBuf,
149        source: serde_json::Error,
150    },
151
152    #[error("IO error: {0}")]
153    Io(#[from] std::io::Error),
154}
155
156#[derive(Args)]
157pub struct CheckArgs {
158    /// Directory containing .dampen files (default: auto-detect src/ui or ui)
159    #[arg(short, long)]
160    pub input: Option<String>,
161
162    /// Enable verbose output
163    #[arg(short, long)]
164    pub verbose: bool,
165
166    /// Path to handler registry JSON (default: auto-discover handlers.json)
167    #[arg(long)]
168    pub handlers: Option<String>,
169
170    /// Path to model info JSON (default: auto-discover model.json)
171    #[arg(long)]
172    pub model: Option<String>,
173
174    /// Path to custom widget configuration JSON file
175    #[arg(long)]
176    pub custom_widgets: Option<String>,
177
178    /// Treat warnings as errors (strict mode for CI/CD)
179    #[arg(long)]
180    pub strict: bool,
181
182    /// Show minimum required schema version for each widget type
183    #[arg(long)]
184    pub show_widget_versions: bool,
185}
186
187/// Resolves the UI directory path using smart detection
188fn resolve_ui_directory(explicit_input: Option<&str>) -> Result<PathBuf, String> {
189    // If explicitly provided, use it
190    if let Some(path) = explicit_input {
191        let path_buf = PathBuf::from(path);
192        if path_buf.exists() {
193            return Ok(path_buf);
194        } else {
195            return Err(format!("Specified UI directory does not exist: {}", path));
196        }
197    }
198
199    // Try src/ui/ (Rust convention)
200    let src_ui = PathBuf::from("src/ui");
201    if src_ui.exists() && src_ui.is_dir() {
202        return Ok(src_ui);
203    }
204
205    // Try ui/ (fallback)
206    let ui = PathBuf::from("ui");
207    if ui.exists() && ui.is_dir() {
208        return Ok(ui);
209    }
210
211    // None found
212    Err("No UI directory found. Please create one of:\n\
213         - src/ui/ (recommended for Rust projects)\n\
214         - ui/ (general purpose)\n\n\
215         Or specify a custom path with --input:\n\
216         dampen check --input path/to/ui"
217        .to_string())
218}
219
220/// Resolves optional file paths with auto-discovery
221fn resolve_optional_file(explicit_path: Option<&str>, filename: &str) -> Option<PathBuf> {
222    // If explicitly provided, use it
223    if let Some(path) = explicit_path {
224        let path_buf = PathBuf::from(path);
225        if path_buf.exists() {
226            return Some(path_buf);
227        }
228        // Note: If explicit path doesn't exist, we'll let the caller handle the error
229        return Some(path_buf);
230    }
231
232    // Try project root
233    let root_file = PathBuf::from(filename);
234    if root_file.exists() {
235        return Some(root_file);
236    }
237
238    // Try src/ directory
239    let src_file = PathBuf::from("src").join(filename);
240    if src_file.exists() {
241        return Some(src_file);
242    }
243
244    // Not found - this is OK for optional files
245    None
246}
247
248/// Display a table of all widget types with their minimum schema version requirements
249fn display_widget_version_table() {
250    println!("Widget Version Requirements");
251    println!("===========================\n");
252    println!("{:<20} {:<10} Status", "Widget", "Min Version");
253    println!("{:-<20} {:-<10} {:-<30}", "", "", "");
254
255    let widgets = vec![
256        ("column", WidgetKind::Column),
257        ("row", WidgetKind::Row),
258        ("container", WidgetKind::Container),
259        ("scrollable", WidgetKind::Scrollable),
260        ("stack", WidgetKind::Stack),
261        ("text", WidgetKind::Text),
262        ("image", WidgetKind::Image),
263        ("svg", WidgetKind::Svg),
264        ("button", WidgetKind::Button),
265        ("text_input", WidgetKind::TextInput),
266        ("checkbox", WidgetKind::Checkbox),
267        ("slider", WidgetKind::Slider),
268        ("pick_list", WidgetKind::PickList),
269        ("toggler", WidgetKind::Toggler),
270        ("radio", WidgetKind::Radio),
271        ("space", WidgetKind::Space),
272        ("rule", WidgetKind::Rule),
273        ("progress_bar", WidgetKind::ProgressBar),
274        ("combobox", WidgetKind::ComboBox),
275        ("tooltip", WidgetKind::Tooltip),
276        ("grid", WidgetKind::Grid),
277        ("canvas", WidgetKind::Canvas),
278        ("float", WidgetKind::Float),
279    ];
280
281    for (name, widget) in widgets {
282        let min_version = widget.minimum_version();
283        let version_str = format!("{}.{}", min_version.major, min_version.minor);
284        let status = if min_version.minor > 0 {
285            "Experimental (not fully functional)"
286        } else {
287            "Stable"
288        };
289        println!("{:<20} {:<10} {}", name, version_str, status);
290    }
291
292    println!("\nNote: Widgets requiring v1.1+ are experimental and may not be fully functional.");
293    println!("Use 'dampen check' to validate your .dampen files for version compatibility.");
294}
295
296pub fn execute(args: &CheckArgs) -> Result<(), CheckError> {
297    use crate::commands::check::handlers::HandlerRegistry;
298
299    // If --show-widget-versions flag is set, display widget version table and exit
300    if args.show_widget_versions {
301        display_widget_version_table();
302        return Ok(());
303    }
304
305    // Resolve UI directory
306    let input_path = resolve_ui_directory(args.input.as_deref())
307        .map_err(|msg| CheckError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, msg)))?;
308
309    if args.verbose {
310        eprintln!("Using UI directory: {}", input_path.display());
311    }
312
313    // Resolve optional files
314    let handlers_path = resolve_optional_file(args.handlers.as_deref(), "handlers.json");
315    if args.verbose
316        && let Some(ref path) = handlers_path
317    {
318        eprintln!("Using handler registry: {}", path.display());
319    }
320
321    let model_path = resolve_optional_file(args.model.as_deref(), "model.json");
322    if args.verbose
323        && let Some(ref path) = model_path
324    {
325        eprintln!("Using model info: {}", path.display());
326    }
327
328    // Load handler registry if provided or auto-discovered (US2: Handler Registry Validation)
329    let handler_registry = if let Some(path) = handlers_path {
330        let registry = HandlerRegistry::load_from_json(&path).map_err(|e| {
331            CheckError::HandlerRegistryLoadError {
332                path: path.clone(),
333                source: serde_json::Error::io(std::io::Error::other(e.to_string())),
334            }
335        })?;
336        Some(registry)
337    } else {
338        None
339    };
340
341    // Load model info if provided or auto-discovered (US3: Binding Validation Against Model)
342    let model_info = if let Some(path) = model_path {
343        let model =
344            crate::commands::check::model::ModelInfo::load_from_json(&path).map_err(|e| {
345                CheckError::ModelInfoLoadError {
346                    path: path.clone(),
347                    source: serde_json::Error::io(std::io::Error::other(e.to_string())),
348                }
349            })?;
350        Some(model)
351    } else {
352        None
353    };
354
355    let mut errors = Vec::new();
356    let mut files_checked = 0;
357
358    // Find all .dampen files
359    for entry in WalkDir::new(input_path)
360        .follow_links(true)
361        .into_iter()
362        .filter_map(|e| e.ok())
363        .filter(|e| {
364            e.path()
365                .extension()
366                .map(|ext| ext == "dampen")
367                .unwrap_or(false)
368        })
369    {
370        let file_path = entry.path();
371        files_checked += 1;
372
373        if args.verbose {
374            eprintln!("Checking: {}", file_path.display());
375        }
376
377        // Read and parse the file
378        let content = fs::read_to_string(file_path)?;
379
380        // First check for XML declaration
381        validate_xml_declaration(&content, file_path, &mut errors);
382
383        // Only proceed to parse if XML declaration is valid
384        if !errors.is_empty() {
385            continue;
386        }
387
388        // Special handling for theme.dampen files
389        if file_path.file_name().is_some_and(|n| n == "theme.dampen") {
390            if let Err(theme_error) =
391                dampen_core::parser::theme_parser::parse_theme_document(&content)
392            {
393                errors.push(CheckError::XmlValidationError {
394                    file: file_path.to_path_buf(),
395                    line: 1, // theme_parser doesn't always provide spans yet
396                    col: 1,
397                    message: format!("Theme validation error: {}", theme_error),
398                });
399            }
400            continue;
401        }
402
403        match parser::parse(&content) {
404            Ok(document) => {
405                // Validate the document structure
406                validate_document(
407                    &document,
408                    file_path,
409                    &handler_registry,
410                    &model_info,
411                    &mut errors,
412                );
413
414                // Validate references (themes, classes)
415                validate_references(&document, file_path, &mut errors);
416
417                // Validate widgets with styles, layout, breakpoints, and states
418                validate_widget_with_styles(&document.root, file_path, &document, &mut errors);
419
420                // Validate widget versions (Phase 8: Widget Version Validation)
421                // This produces warnings for widgets requiring higher schema versions
422                let version_warnings = dampen_core::validate_widget_versions(&document);
423                if !version_warnings.is_empty() {
424                    for warning in version_warnings {
425                        eprintln!(
426                            "Warning: {} in {}:{}:{}",
427                            warning.format_message(),
428                            file_path.display(),
429                            warning.span.line,
430                            warning.span.column
431                        );
432                        eprintln!("  Suggestion: {}", warning.suggestion());
433                        eprintln!();
434                    }
435                }
436            }
437            Err(parse_error) => {
438                errors.push(CheckError::ParseError {
439                    file: file_path.to_path_buf(),
440                    line: parse_error.span.line,
441                    col: parse_error.span.column,
442                    message: parse_error.to_string(),
443                });
444            }
445        }
446    }
447
448    if args.verbose {
449        eprintln!("Checked {} files", files_checked);
450    }
451
452    // Report errors
453    if !errors.is_empty() {
454        // T048: Strict mode logic - in strict mode, all validation issues are errors
455        // (Currently all validation issues are already treated as errors, so this is
456        // primarily for future extensibility when we might add warnings)
457        let error_label = "error(s)"; // TODO: differentiate warnings in non-strict mode
458        eprintln!("Found {} {}:", errors.len(), error_label);
459
460        for error in &errors {
461            // T049: Error formatting - distinguish warnings from errors in strict mode
462            let prefix = "ERROR"; // TODO: use "WARNING" for warnings in non-strict mode
463            eprintln!("  [{}] {}", prefix, error);
464        }
465
466        // In strict mode, exit with code 1 on any error
467        // (This is already the default behavior)
468        Err(errors.remove(0))
469    } else {
470        if args.verbose {
471            let status = if args.strict {
472                "✓ All files passed validation (strict mode)"
473            } else {
474                "✓ All files passed validation"
475            };
476            eprintln!("{}", status);
477        }
478        Ok(())
479    }
480}
481
482fn validate_xml_declaration(content: &str, file_path: &Path, errors: &mut Vec<CheckError>) {
483    // Check if content starts with proper XML declaration
484    let trimmed = content.trim_start();
485    if !trimmed.starts_with("<?xml version=\"1.0\"") {
486        errors.push(CheckError::XmlValidationError {
487            file: file_path.to_path_buf(),
488            line: 1,
489            col: 1,
490            message: "Missing or invalid XML declaration. Expected: <?xml version=\"1.0\" encoding=\"UTF-8\"?>".to_string(),
491        });
492    }
493}
494
495fn validate_document(
496    document: &dampen_core::ir::DampenDocument,
497    file_path: &Path,
498    handler_registry: &Option<crate::commands::check::handlers::HandlerRegistry>,
499    model_info: &Option<crate::commands::check::model::ModelInfo>,
500    errors: &mut Vec<CheckError>,
501) {
502    use crate::commands::check::cross_widget::RadioGroupValidator;
503
504    // Get all valid widget names
505    let valid_widgets: HashSet<String> = WidgetKind::all_variants()
506        .iter()
507        .map(|w| format!("{:?}", w).to_lowercase())
508        .collect();
509
510    // Create radio group validator to collect radio buttons across the tree
511    let mut radio_validator = RadioGroupValidator::new();
512
513    // Validate each widget in the tree
514    validate_widget_node(
515        &document.root,
516        file_path,
517        &valid_widgets,
518        handler_registry,
519        model_info,
520        &mut radio_validator,
521        errors,
522    );
523
524    // After all widgets are validated, check radio groups for consistency
525    let radio_errors = radio_validator.validate();
526    for error in radio_errors {
527        // Convert cross_widget::CheckError to main_command::CheckError
528        match error {
529            crate::commands::check::errors::CheckError::DuplicateRadioValue {
530                value,
531                group,
532                file,
533                line,
534                col,
535                first_file,
536                first_line,
537                first_col,
538            } => {
539                errors.push(CheckError::XmlValidationError {
540                    file: file.clone(),
541                    line,
542                    col,
543                    message: format!(
544                        "Duplicate radio value '{}' in group '{}'. First occurrence: {}:{}:{}",
545                        value,
546                        group,
547                        first_file.display(),
548                        first_line,
549                        first_col
550                    ),
551                });
552            }
553            crate::commands::check::errors::CheckError::InconsistentRadioHandlers {
554                group,
555                file,
556                line,
557                col,
558                handlers,
559            } => {
560                errors.push(CheckError::XmlValidationError {
561                    file: file.clone(),
562                    line,
563                    col,
564                    message: format!(
565                        "Radio group '{}' has inconsistent on_select handlers. Found handlers: {}",
566                        group, handlers
567                    ),
568                });
569            }
570            _ => {}
571        }
572    }
573}
574
575fn validate_widget_node(
576    node: &dampen_core::ir::WidgetNode,
577    file_path: &Path,
578    valid_widgets: &HashSet<String>,
579    handler_registry: &Option<crate::commands::check::handlers::HandlerRegistry>,
580    model_info: &Option<crate::commands::check::model::ModelInfo>,
581    radio_validator: &mut crate::commands::check::cross_widget::RadioGroupValidator,
582    errors: &mut Vec<CheckError>,
583) {
584    use crate::commands::check::attributes;
585    use crate::commands::check::suggestions;
586
587    // Check if widget kind is valid
588    let widget_name = format!("{:?}", node.kind).to_lowercase();
589    if !valid_widgets.contains(&widget_name) && !matches!(node.kind, WidgetKind::Custom(_)) {
590        errors.push(CheckError::InvalidWidget {
591            widget: widget_name.clone(),
592            file: file_path.to_path_buf(),
593            line: node.span.line,
594            col: node.span.column,
595        });
596    }
597
598    // Validate widget attributes (US1: Unknown Attribute Detection)
599    let attr_names: Vec<String> = node.attributes.keys().map(|s| s.to_string()).collect();
600    let unknown_attrs = attributes::validate_widget_attributes(&node.kind, &attr_names);
601
602    for (attr, _suggestion_opt) in unknown_attrs {
603        let schema = attributes::WidgetAttributeSchema::for_widget(&node.kind);
604        let all_valid = schema.all_valid_names();
605        let suggestion = suggestions::suggest(&attr, &all_valid, 3);
606
607        errors.push(CheckError::UnknownAttribute {
608            attr,
609            widget: widget_name.clone(),
610            file: file_path.to_path_buf(),
611            line: node.span.line,
612            col: node.span.column,
613            suggestion,
614        });
615    }
616
617    // Validate required attributes (US7: Required Attribute Validation)
618    let missing_required = attributes::validate_required_attributes(&node.kind, &attr_names);
619    for missing_attr in missing_required {
620        errors.push(CheckError::XmlValidationError {
621            file: file_path.to_path_buf(),
622            line: node.span.line,
623            col: node.span.column,
624            message: format!(
625                "Missing required attribute '{}' for widget '{}'",
626                missing_attr, widget_name
627            ),
628        });
629    }
630
631    // Validate event handlers (US2: Handler Registry Validation)
632    if let Some(registry) = handler_registry {
633        for event_binding in &node.events {
634            if !registry.contains(&event_binding.handler) {
635                // Generate suggestion using Levenshtein distance
636                let all_handler_names = registry.all_names();
637                let handler_refs: Vec<&str> =
638                    all_handler_names.iter().map(|s| s.as_str()).collect();
639                let suggestion = suggestions::suggest(&event_binding.handler, &handler_refs, 3);
640
641                errors.push(CheckError::UnknownHandler {
642                    handler: event_binding.handler.clone(),
643                    file: file_path.to_path_buf(),
644                    line: event_binding.span.line,
645                    col: event_binding.span.column,
646                    suggestion,
647                });
648            }
649        }
650    } else {
651        // If no registry provided, only check for empty handlers
652        for event_binding in &node.events {
653            if event_binding.handler.is_empty() {
654                errors.push(CheckError::UnknownHandler {
655                    handler: "<empty>".to_string(),
656                    file: file_path.to_path_buf(),
657                    line: event_binding.span.line,
658                    col: event_binding.span.column,
659                    suggestion: String::new(),
660                });
661            }
662        }
663    }
664
665    // Validate attribute bindings (US3: Binding Validation Against Model)
666    if let Some(model) = model_info {
667        for (attr_name, attr_value) in &node.attributes {
668            validate_attribute_bindings(
669                attr_name,
670                attr_value,
671                file_path,
672                node.span.line,
673                node.span.column,
674                model,
675                errors,
676            );
677        }
678    }
679
680    // Validate attribute values (style, layout, etc.)
681    for attr_value in node.attributes.values() {
682        validate_attribute_value(
683            attr_value,
684            file_path,
685            node.span.line,
686            node.span.column,
687            errors,
688        );
689    }
690
691    // Collect radio button information for cross-widget validation (US4: Radio Group Validation)
692    if matches!(node.kind, WidgetKind::Radio) {
693        // Extract radio button attributes
694        let group_id = node
695            .attributes
696            .get("id")
697            .and_then(|v| match v {
698                AttributeValue::Static(s) => Some(s.as_str()),
699                _ => None,
700            })
701            .unwrap_or("default");
702
703        let value = node
704            .attributes
705            .get("value")
706            .and_then(|v| match v {
707                AttributeValue::Static(s) => Some(s.as_str()),
708                _ => None,
709            })
710            .unwrap_or("");
711
712        // Find on_select handler
713        let handler = node
714            .events
715            .iter()
716            .find(|e| e.event == EventKind::Select)
717            .map(|e| e.handler.clone());
718
719        radio_validator.add_radio(
720            group_id,
721            value,
722            file_path.to_str().unwrap_or("unknown"),
723            node.span.line,
724            node.span.column,
725            handler,
726        );
727    }
728
729    // Recursively validate children
730    for child in &node.children {
731        validate_widget_node(
732            child,
733            file_path,
734            valid_widgets,
735            handler_registry,
736            model_info,
737            radio_validator,
738            errors,
739        );
740    }
741}
742
743fn validate_attribute_bindings(
744    _attr_name: &str,
745    value: &dampen_core::ir::AttributeValue,
746    file_path: &Path,
747    line: u32,
748    col: u32,
749    model: &crate::commands::check::model::ModelInfo,
750    errors: &mut Vec<CheckError>,
751) {
752    // Only validate binding expressions
753    if let dampen_core::ir::AttributeValue::Binding(binding_expr) = value {
754        // Validate field access in the expression
755        validate_expr_fields(&binding_expr.expr, file_path, line, col, model, errors);
756    }
757}
758
759fn validate_expr_fields(
760    expr: &dampen_core::expr::Expr,
761    file_path: &Path,
762    line: u32,
763    col: u32,
764    model: &crate::commands::check::model::ModelInfo,
765    errors: &mut Vec<CheckError>,
766) {
767    match expr {
768        dampen_core::expr::Expr::FieldAccess(field_access) => {
769            // Convert Vec<String> to Vec<&str>
770            let field_parts: Vec<&str> = field_access.path.iter().map(|s| s.as_str()).collect();
771
772            if !model.contains_field(&field_parts) {
773                // Generate available fields list
774                let all_paths = model.all_field_paths();
775                let available = if all_paths.len() > 5 {
776                    format!("{} ({} total)", &all_paths[..5].join(", "), all_paths.len())
777                } else {
778                    all_paths.join(", ")
779                };
780
781                let field_path = field_access.path.join(".");
782
783                errors.push(CheckError::InvalidBinding {
784                    field: field_path,
785                    file: file_path.to_path_buf(),
786                    line,
787                    col,
788                });
789
790                // Add more detailed error with available fields
791                eprintln!("  Available fields: {}", available);
792            }
793        }
794        dampen_core::expr::Expr::MethodCall(method_call) => {
795            // Validate the receiver expression
796            validate_expr_fields(&method_call.receiver, file_path, line, col, model, errors);
797            // Validate arguments
798            for arg in &method_call.args {
799                validate_expr_fields(arg, file_path, line, col, model, errors);
800            }
801        }
802        dampen_core::expr::Expr::BinaryOp(binary_op) => {
803            // Validate both sides of the binary operation
804            validate_expr_fields(&binary_op.left, file_path, line, col, model, errors);
805            validate_expr_fields(&binary_op.right, file_path, line, col, model, errors);
806        }
807        dampen_core::expr::Expr::UnaryOp(unary_op) => {
808            // Validate the operand
809            validate_expr_fields(&unary_op.operand, file_path, line, col, model, errors);
810        }
811        dampen_core::expr::Expr::Conditional(conditional) => {
812            // Validate all parts of the conditional
813            validate_expr_fields(&conditional.condition, file_path, line, col, model, errors);
814            validate_expr_fields(
815                &conditional.then_branch,
816                file_path,
817                line,
818                col,
819                model,
820                errors,
821            );
822            validate_expr_fields(
823                &conditional.else_branch,
824                file_path,
825                line,
826                col,
827                model,
828                errors,
829            );
830        }
831        dampen_core::expr::Expr::Literal(_) => {
832            // Literals don't reference fields, nothing to validate
833        }
834        dampen_core::expr::Expr::SharedFieldAccess(shared_access) => {
835            // Validate shared field paths similar to regular field access
836            if shared_access.path.is_empty() || shared_access.path.iter().any(|f| f.is_empty()) {
837                errors.push(CheckError::InvalidBinding {
838                    field: "shared.<empty>".to_string(),
839                    file: file_path.to_path_buf(),
840                    line,
841                    col,
842                });
843            }
844        }
845    }
846}
847
848fn validate_attribute_value(
849    value: &dampen_core::ir::AttributeValue,
850    file_path: &Path,
851    line: u32,
852    col: u32,
853    errors: &mut Vec<CheckError>,
854) {
855    match value {
856        dampen_core::ir::AttributeValue::Static(_) => {
857            // Static values are always valid
858        }
859        dampen_core::ir::AttributeValue::Binding(binding_expr) => {
860            // For now, we'll do basic validation of the binding expression
861            // In a real implementation, we'd check against the model fields
862            validate_binding_expr(&binding_expr.expr, file_path, line, col, errors);
863        }
864        dampen_core::ir::AttributeValue::Interpolated(parts) => {
865            for part in parts {
866                match part {
867                    dampen_core::ir::InterpolatedPart::Literal(_) => {
868                        // Literals are always valid
869                    }
870                    dampen_core::ir::InterpolatedPart::Binding(binding_expr) => {
871                        validate_binding_expr(&binding_expr.expr, file_path, line, col, errors);
872                    }
873                }
874            }
875        }
876    }
877}
878
879fn validate_binding_expr(
880    expr: &dampen_core::expr::Expr,
881    file_path: &Path,
882    line: u32,
883    col: u32,
884    errors: &mut Vec<CheckError>,
885) {
886    match expr {
887        dampen_core::expr::Expr::FieldAccess(field_access) => {
888            // For now, we'll assume any field name is valid
889            // In a real implementation, we'd check against the model fields
890            if field_access.path.is_empty() || field_access.path.iter().any(|f| f.is_empty()) {
891                errors.push(CheckError::InvalidBinding {
892                    field: "<empty>".to_string(),
893                    file: file_path.to_path_buf(),
894                    line,
895                    col,
896                });
897            }
898        }
899        dampen_core::expr::Expr::MethodCall(_) => {
900            // Method calls are generally valid if the method exists
901            // For now, we'll assume they're valid
902        }
903        dampen_core::expr::Expr::BinaryOp(_) => {
904            // Binary operations are valid if both operands are valid
905            // We'd need to recursively validate the operands
906        }
907        dampen_core::expr::Expr::UnaryOp(_) => {
908            // Unary operations are valid if the operand is valid
909        }
910        dampen_core::expr::Expr::Conditional(_) => {
911            // Conditionals are valid if all parts are valid
912        }
913        dampen_core::expr::Expr::Literal(_) => {
914            // Literals are always valid
915        }
916        dampen_core::expr::Expr::SharedFieldAccess(_) => {
917            // Shared field access is valid if the field exists in shared state
918            // For now, we'll assume they're valid
919        }
920    }
921}
922
923// Helper extension to get all widget variants
924trait WidgetKindExt {
925    fn all_variants() -> Vec<WidgetKind>;
926}
927
928impl WidgetKindExt for WidgetKind {
929    fn all_variants() -> Vec<WidgetKind> {
930        vec![
931            WidgetKind::Column,
932            WidgetKind::Row,
933            WidgetKind::Container,
934            WidgetKind::Scrollable,
935            WidgetKind::Stack,
936            WidgetKind::Text,
937            WidgetKind::Image,
938            WidgetKind::Svg,
939            WidgetKind::Button,
940            WidgetKind::TextInput,
941            WidgetKind::Checkbox,
942            WidgetKind::Slider,
943            WidgetKind::PickList,
944            WidgetKind::Toggler,
945            WidgetKind::Space,
946            WidgetKind::Rule,
947            WidgetKind::Radio,
948            WidgetKind::ComboBox,
949            WidgetKind::ProgressBar,
950            WidgetKind::Tooltip,
951            WidgetKind::Grid,
952            WidgetKind::Canvas,
953            WidgetKind::Float,
954            WidgetKind::For,
955            WidgetKind::If,
956        ]
957    }
958}
959
960/// Validate all references (themes, classes) in the document
961fn validate_references(
962    document: &dampen_core::ir::DampenDocument,
963    file_path: &Path,
964    errors: &mut Vec<CheckError>,
965) {
966    // Validate global theme reference
967    if let Some(global_theme) = &document.global_theme
968        && !document.themes.contains_key(global_theme)
969    {
970        errors.push(CheckError::UnknownTheme {
971            theme: global_theme.clone(),
972            file: file_path.to_path_buf(),
973            line: 1,
974            col: 1,
975        });
976    }
977
978    // Validate each theme definition (US5: Theme Property Validation)
979    for (name, theme) in &document.themes {
980        if let Err(msg) = theme.validate(false) {
981            // Check if it's a circular dependency error
982            if msg.contains("circular") || msg.contains("Circular") {
983                errors.push(CheckError::XmlValidationError {
984                    file: file_path.to_path_buf(),
985                    line: 1,
986                    col: 1,
987                    message: format!("Theme '{}' validation error: {}", name, msg),
988                });
989            } else {
990                errors.push(CheckError::InvalidStyleValue {
991                    attr: format!("theme '{}'", name),
992                    file: file_path.to_path_buf(),
993                    line: 1,
994                    col: 1,
995                    message: msg,
996                });
997            }
998        }
999    }
1000
1001    // Validate each style class definition (US5: Circular Dependency Detection)
1002    for (name, class) in &document.style_classes {
1003        if let Err(msg) = class.validate(&document.style_classes) {
1004            // Check if it's a circular dependency error
1005            if msg.contains("circular") || msg.contains("Circular") {
1006                errors.push(CheckError::XmlValidationError {
1007                    file: file_path.to_path_buf(),
1008                    line: 1,
1009                    col: 1,
1010                    message: format!("Style class '{}' has circular dependency: {}", name, msg),
1011                });
1012            } else {
1013                errors.push(CheckError::InvalidStyleValue {
1014                    attr: format!("class '{}'", name),
1015                    file: file_path.to_path_buf(),
1016                    line: 1,
1017                    col: 1,
1018                    message: msg,
1019                });
1020            }
1021        }
1022    }
1023}
1024
1025/// Validate a widget node with all its styles, layout, and references
1026fn validate_widget_with_styles(
1027    node: &dampen_core::ir::WidgetNode,
1028    file_path: &Path,
1029    document: &dampen_core::ir::DampenDocument,
1030    errors: &mut Vec<CheckError>,
1031) {
1032    // Validate structured style properties
1033    if let Some(style) = &node.style
1034        && let Err(msg) = style.validate()
1035    {
1036        errors.push(CheckError::InvalidStyleValue {
1037            attr: "structured style".to_string(),
1038            file: file_path.to_path_buf(),
1039            line: node.span.line,
1040            col: node.span.column,
1041            message: msg,
1042        });
1043    }
1044
1045    // Validate structured layout constraints
1046    if let Some(layout) = &node.layout
1047        && let Err(msg) = layout.validate()
1048    {
1049        errors.push(CheckError::InvalidLayoutConstraint {
1050            file: file_path.to_path_buf(),
1051            line: node.span.line,
1052            col: node.span.column,
1053            message: msg,
1054        });
1055    }
1056
1057    // Validate style class references
1058    for class_name in &node.classes {
1059        if !document.style_classes.contains_key(class_name) {
1060            errors.push(CheckError::UnknownStyleClass {
1061                class: class_name.clone(),
1062                file: file_path.to_path_buf(),
1063                line: node.span.line,
1064                col: node.span.column,
1065            });
1066        }
1067    }
1068
1069    // Validate theme reference
1070    if let Some(theme_ref) = &node.theme_ref {
1071        match theme_ref {
1072            AttributeValue::Static(theme_name) => {
1073                if !document.themes.contains_key(theme_name) {
1074                    errors.push(CheckError::UnknownTheme {
1075                        theme: theme_name.clone(),
1076                        file: file_path.to_path_buf(),
1077                        line: node.span.line,
1078                        col: node.span.column,
1079                    });
1080                }
1081            }
1082            AttributeValue::Binding(_) | AttributeValue::Interpolated(_) => {
1083                // Binding expressions can't be validated at check time
1084                // They will be evaluated at runtime
1085            }
1086        }
1087    }
1088
1089    // Validate inline style attributes
1090    validate_style_attributes(node, file_path, errors);
1091
1092    // Validate inline layout attributes
1093    validate_layout_attributes(node, file_path, errors);
1094
1095    // Validate breakpoint attributes
1096    validate_breakpoint_attributes(node, file_path, errors);
1097
1098    // Validate state attributes
1099    validate_state_attributes(node, file_path, errors);
1100
1101    // Recursively validate children
1102    for child in &node.children {
1103        validate_widget_with_styles(child, file_path, document, errors);
1104    }
1105}
1106
1107/// Validate inline style attributes
1108fn validate_style_attributes(
1109    node: &dampen_core::ir::WidgetNode,
1110    file_path: &Path,
1111    errors: &mut Vec<CheckError>,
1112) {
1113    for (attr_name, attr_value) in &node.attributes {
1114        match attr_name.as_str() {
1115            "background" => {
1116                if let AttributeValue::Static(value) = attr_value
1117                    && let Err(msg) = style_parser::parse_background_attr(value)
1118                {
1119                    errors.push(CheckError::InvalidStyleValue {
1120                        attr: attr_name.clone(),
1121                        file: file_path.to_path_buf(),
1122                        line: node.span.line,
1123                        col: node.span.column,
1124                        message: msg,
1125                    });
1126                }
1127            }
1128            "color" | "border_color" => {
1129                if let AttributeValue::Static(value) = attr_value
1130                    && let Err(msg) = style_parser::parse_color_attr(value)
1131                {
1132                    errors.push(CheckError::InvalidStyleValue {
1133                        attr: attr_name.clone(),
1134                        file: file_path.to_path_buf(),
1135                        line: node.span.line,
1136                        col: node.span.column,
1137                        message: msg,
1138                    });
1139                }
1140            }
1141            "border_width" | "opacity" => {
1142                if let AttributeValue::Static(value) = attr_value
1143                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1144                {
1145                    errors.push(CheckError::InvalidStyleValue {
1146                        attr: attr_name.clone(),
1147                        file: file_path.to_path_buf(),
1148                        line: node.span.line,
1149                        col: node.span.column,
1150                        message: msg,
1151                    });
1152                }
1153            }
1154            "border_radius" => {
1155                if let AttributeValue::Static(value) = attr_value
1156                    && let Err(msg) = style_parser::parse_border_radius(value)
1157                {
1158                    errors.push(CheckError::InvalidStyleValue {
1159                        attr: attr_name.clone(),
1160                        file: file_path.to_path_buf(),
1161                        line: node.span.line,
1162                        col: node.span.column,
1163                        message: msg,
1164                    });
1165                }
1166            }
1167            "border_style" => {
1168                if let AttributeValue::Static(value) = attr_value
1169                    && let Err(msg) = style_parser::parse_border_style(value)
1170                {
1171                    errors.push(CheckError::InvalidStyleValue {
1172                        attr: attr_name.clone(),
1173                        file: file_path.to_path_buf(),
1174                        line: node.span.line,
1175                        col: node.span.column,
1176                        message: msg,
1177                    });
1178                }
1179            }
1180            "shadow" => {
1181                if let AttributeValue::Static(value) = attr_value
1182                    && let Err(msg) = style_parser::parse_shadow_attr(value)
1183                {
1184                    errors.push(CheckError::InvalidStyleValue {
1185                        attr: attr_name.clone(),
1186                        file: file_path.to_path_buf(),
1187                        line: node.span.line,
1188                        col: node.span.column,
1189                        message: msg,
1190                    });
1191                }
1192            }
1193            "transform" => {
1194                if let AttributeValue::Static(value) = attr_value
1195                    && let Err(msg) = style_parser::parse_transform(value)
1196                {
1197                    errors.push(CheckError::InvalidStyleValue {
1198                        attr: attr_name.clone(),
1199                        file: file_path.to_path_buf(),
1200                        line: node.span.line,
1201                        col: node.span.column,
1202                        message: msg,
1203                    });
1204                }
1205            }
1206            _ => {} // Autres attributs gérés ailleurs
1207        }
1208    }
1209}
1210
1211/// Validate inline layout attributes
1212fn validate_layout_attributes(
1213    node: &dampen_core::ir::WidgetNode,
1214    file_path: &Path,
1215    errors: &mut Vec<CheckError>,
1216) {
1217    for (attr_name, attr_value) in &node.attributes {
1218        match attr_name.as_str() {
1219            "width" | "height" | "min_width" | "max_width" | "min_height" | "max_height" => {
1220                if let AttributeValue::Static(value) = attr_value
1221                    && let Err(msg) = style_parser::parse_length_attr(value)
1222                {
1223                    errors.push(CheckError::InvalidStyleValue {
1224                        attr: attr_name.clone(),
1225                        file: file_path.to_path_buf(),
1226                        line: node.span.line,
1227                        col: node.span.column,
1228                        message: msg,
1229                    });
1230                }
1231            }
1232            "padding" => {
1233                if let AttributeValue::Static(value) = attr_value
1234                    && let Err(msg) = style_parser::parse_padding_attr(value)
1235                {
1236                    errors.push(CheckError::InvalidStyleValue {
1237                        attr: attr_name.clone(),
1238                        file: file_path.to_path_buf(),
1239                        line: node.span.line,
1240                        col: node.span.column,
1241                        message: msg,
1242                    });
1243                }
1244            }
1245            "spacing" => {
1246                if let AttributeValue::Static(value) = attr_value
1247                    && let Err(msg) = style_parser::parse_spacing(value)
1248                {
1249                    errors.push(CheckError::InvalidStyleValue {
1250                        attr: attr_name.clone(),
1251                        file: file_path.to_path_buf(),
1252                        line: node.span.line,
1253                        col: node.span.column,
1254                        message: msg,
1255                    });
1256                }
1257            }
1258            "align_items" => {
1259                if let AttributeValue::Static(value) = attr_value
1260                    && let Err(msg) = style_parser::parse_alignment(value)
1261                {
1262                    errors.push(CheckError::InvalidStyleValue {
1263                        attr: attr_name.clone(),
1264                        file: file_path.to_path_buf(),
1265                        line: node.span.line,
1266                        col: node.span.column,
1267                        message: msg,
1268                    });
1269                }
1270            }
1271            "justify_content" => {
1272                if let AttributeValue::Static(value) = attr_value
1273                    && let Err(msg) = style_parser::parse_justification(value)
1274                {
1275                    errors.push(CheckError::InvalidStyleValue {
1276                        attr: attr_name.clone(),
1277                        file: file_path.to_path_buf(),
1278                        line: node.span.line,
1279                        col: node.span.column,
1280                        message: msg,
1281                    });
1282                }
1283            }
1284            "direction" => {
1285                if let AttributeValue::Static(value) = attr_value
1286                    && let Err(msg) = Direction::parse(value)
1287                {
1288                    errors.push(CheckError::InvalidStyleValue {
1289                        attr: attr_name.clone(),
1290                        file: file_path.to_path_buf(),
1291                        line: node.span.line,
1292                        col: node.span.column,
1293                        message: msg,
1294                    });
1295                }
1296            }
1297            "position" => {
1298                if !matches!(node.kind, WidgetKind::Tooltip)
1299                    && let AttributeValue::Static(value) = attr_value
1300                    && let Err(msg) = Position::parse(value)
1301                {
1302                    errors.push(CheckError::InvalidStyleValue {
1303                        attr: attr_name.clone(),
1304                        file: file_path.to_path_buf(),
1305                        line: node.span.line,
1306                        col: node.span.column,
1307                        message: msg,
1308                    });
1309                }
1310            }
1311            "top" | "right" | "bottom" | "left" => {
1312                if let AttributeValue::Static(value) = attr_value
1313                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1314                {
1315                    errors.push(CheckError::InvalidStyleValue {
1316                        attr: attr_name.clone(),
1317                        file: file_path.to_path_buf(),
1318                        line: node.span.line,
1319                        col: node.span.column,
1320                        message: msg,
1321                    });
1322                }
1323            }
1324            "z_index" => {
1325                if let AttributeValue::Static(value) = attr_value
1326                    && let Err(msg) = style_parser::parse_int_attr(value, attr_name)
1327                {
1328                    errors.push(CheckError::InvalidStyleValue {
1329                        attr: attr_name.clone(),
1330                        file: file_path.to_path_buf(),
1331                        line: node.span.line,
1332                        col: node.span.column,
1333                        message: msg,
1334                    });
1335                }
1336            }
1337            _ => {} // Autres attributs gérés ailleurs
1338        }
1339    }
1340}
1341
1342/// Validate breakpoint attributes (mobile:, tablet:, desktop:)
1343fn validate_breakpoint_attributes(
1344    node: &dampen_core::ir::WidgetNode,
1345    file_path: &Path,
1346    errors: &mut Vec<CheckError>,
1347) {
1348    for (breakpoint, attrs) in &node.breakpoint_attributes {
1349        for (attr_name, attr_value) in attrs {
1350            // Valider que l'attribut de base est valide
1351            let base_attr = attr_name.as_str();
1352            let full_attr = format!("{:?}:{}", breakpoint, base_attr);
1353
1354            // Utiliser les mêmes validateurs que pour les attributs normaux
1355            let is_style_attr = matches!(
1356                base_attr,
1357                "background"
1358                    | "color"
1359                    | "border_width"
1360                    | "border_color"
1361                    | "border_radius"
1362                    | "border_style"
1363                    | "shadow"
1364                    | "opacity"
1365                    | "transform"
1366            );
1367
1368            let is_layout_attr = matches!(
1369                base_attr,
1370                "width"
1371                    | "height"
1372                    | "min_width"
1373                    | "max_width"
1374                    | "min_height"
1375                    | "max_height"
1376                    | "padding"
1377                    | "spacing"
1378                    | "align_items"
1379                    | "justify_content"
1380                    | "direction"
1381                    | "position"
1382                    | "top"
1383                    | "right"
1384                    | "bottom"
1385                    | "left"
1386                    | "z_index"
1387            );
1388
1389            if !is_style_attr && !is_layout_attr {
1390                errors.push(CheckError::InvalidBreakpoint {
1391                    attr: full_attr,
1392                    file: file_path.to_path_buf(),
1393                    line: node.span.line,
1394                    col: node.span.column,
1395                });
1396                continue;
1397            }
1398
1399            // Valider la valeur selon le type d'attribut
1400            if let AttributeValue::Static(value) = attr_value {
1401                let result: Result<(), String> = match base_attr {
1402                    "background" => style_parser::parse_background_attr(value).map(|_| ()),
1403                    "color" | "border_color" => style_parser::parse_color_attr(value).map(|_| ()),
1404                    "border_width" | "opacity" => {
1405                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1406                    }
1407                    "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1408                    "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1409                    "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1410                    "transform" => style_parser::parse_transform(value).map(|_| ()),
1411                    "width" | "height" | "min_width" | "max_width" | "min_height"
1412                    | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1413                    "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1414                    "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1415                    "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1416                    "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1417                    "direction" => Direction::parse(value).map(|_| ()),
1418                    "position" => Position::parse(value).map(|_| ()),
1419                    "top" | "right" | "bottom" | "left" => {
1420                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1421                    }
1422                    "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1423                    _ => Ok(()),
1424                };
1425
1426                if let Err(msg) = result {
1427                    errors.push(CheckError::InvalidStyleValue {
1428                        attr: full_attr,
1429                        file: file_path.to_path_buf(),
1430                        line: node.span.line,
1431                        col: node.span.column,
1432                        message: msg,
1433                    });
1434                }
1435            }
1436        }
1437    }
1438}
1439
1440/// Validate state attributes (hover:, focus:, active:, disabled:)
1441fn validate_state_attributes(
1442    node: &dampen_core::ir::WidgetNode,
1443    file_path: &Path,
1444    errors: &mut Vec<CheckError>,
1445) {
1446    for (attr_name, attr_value) in &node.attributes {
1447        if attr_name.contains(':') {
1448            let parts: Vec<&str> = attr_name.split(':').collect();
1449            if parts.len() >= 2 {
1450                let prefix = parts[0];
1451                let base_attr = parts[1];
1452
1453                // Valider le préfixe d'état
1454                if !["hover", "focus", "active", "disabled"].contains(&prefix) {
1455                    errors.push(CheckError::InvalidState {
1456                        attr: attr_name.clone(),
1457                        file: file_path.to_path_buf(),
1458                        line: node.span.line,
1459                        col: node.span.column,
1460                    });
1461                    continue;
1462                }
1463
1464                // Valider que l'attribut de base est valide
1465                let is_valid_attr = matches!(
1466                    base_attr,
1467                    "background"
1468                        | "color"
1469                        | "border_width"
1470                        | "border_color"
1471                        | "border_radius"
1472                        | "border_style"
1473                        | "shadow"
1474                        | "opacity"
1475                        | "transform"
1476                        | "width"
1477                        | "height"
1478                        | "min_width"
1479                        | "max_width"
1480                        | "min_height"
1481                        | "max_height"
1482                        | "padding"
1483                        | "spacing"
1484                        | "align_items"
1485                        | "justify_content"
1486                        | "direction"
1487                        | "position"
1488                        | "top"
1489                        | "right"
1490                        | "bottom"
1491                        | "left"
1492                        | "z_index"
1493                );
1494
1495                if !is_valid_attr {
1496                    errors.push(CheckError::InvalidState {
1497                        attr: attr_name.clone(),
1498                        file: file_path.to_path_buf(),
1499                        line: node.span.line,
1500                        col: node.span.column,
1501                    });
1502                    continue;
1503                }
1504
1505                // Valider la valeur
1506                if let AttributeValue::Static(value) = attr_value {
1507                    let result: Result<(), String> = match base_attr {
1508                        "background" => style_parser::parse_background_attr(value).map(|_| ()),
1509                        "color" | "border_color" => {
1510                            style_parser::parse_color_attr(value).map(|_| ())
1511                        }
1512                        "border_width" | "opacity" => {
1513                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1514                        }
1515                        "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1516                        "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1517                        "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1518                        "transform" => style_parser::parse_transform(value).map(|_| ()),
1519                        "width" | "height" | "min_width" | "max_width" | "min_height"
1520                        | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1521                        "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1522                        "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1523                        "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1524                        "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1525                        "direction" => Direction::parse(value).map(|_| ()),
1526                        "position" => Position::parse(value).map(|_| ()),
1527                        "top" | "right" | "bottom" | "left" => {
1528                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1529                        }
1530                        "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1531                        _ => Ok(()),
1532                    };
1533
1534                    if let Err(msg) = result {
1535                        errors.push(CheckError::InvalidStyleValue {
1536                            attr: attr_name.clone(),
1537                            file: file_path.to_path_buf(),
1538                            line: node.span.line,
1539                            col: node.span.column,
1540                            message: msg,
1541                        });
1542                    }
1543                }
1544            }
1545        }
1546    }
1547}