Skip to main content

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 for a specific package
188///
189/// Searches for the UI directory in common package locations:
190/// - examples/{package}/src/ui
191/// - examples/{package}/ui
192/// - crates/{package}/src/ui
193/// - {package}/src/ui
194pub fn resolve_package_ui_path(package_name: &str) -> Option<PathBuf> {
195    let prefixes = ["examples", "crates", "."];
196    let suffixes = ["src/ui", "ui"];
197
198    for prefix in prefixes {
199        let package_root = Path::new(prefix).join(package_name);
200        if !package_root.exists() {
201            continue;
202        }
203
204        for suffix in suffixes {
205            let ui_path = package_root.join(suffix);
206            if ui_path.exists() {
207                return Some(ui_path);
208            }
209        }
210    }
211    None
212}
213
214/// Resolves the UI directory path using smart detection
215pub fn resolve_ui_directory(explicit_input: Option<&str>) -> Result<PathBuf, String> {
216    // If explicitly provided, use it
217    if let Some(path) = explicit_input {
218        let path_buf = PathBuf::from(path);
219        if path_buf.exists() {
220            return Ok(path_buf);
221        } else {
222            return Err(format!("Specified UI directory does not exist: {}", path));
223        }
224    }
225
226    // Try src/ui/ (Rust convention)
227    let src_ui = PathBuf::from("src/ui");
228    if src_ui.exists() && src_ui.is_dir() {
229        return Ok(src_ui);
230    }
231
232    // Try ui/ (fallback)
233    let ui = PathBuf::from("ui");
234    if ui.exists() && ui.is_dir() {
235        return Ok(ui);
236    }
237
238    // None found
239    Err("No UI directory found. Please create one of:\n\
240         - src/ui/ (recommended for Rust projects)\n\
241         - ui/ (general purpose)\n\n\
242         Or specify a custom path with --input:\n\
243         dampen check --input path/to/ui"
244        .to_string())
245}
246
247/// Resolves optional file paths with auto-discovery
248fn resolve_optional_file(explicit_path: Option<&str>, filename: &str) -> Option<PathBuf> {
249    // If explicitly provided, use it
250    if let Some(path) = explicit_path {
251        let path_buf = PathBuf::from(path);
252        if path_buf.exists() {
253            return Some(path_buf);
254        }
255        // Note: If explicit path doesn't exist, we'll let the caller handle the error
256        return Some(path_buf);
257    }
258
259    // Try project root
260    let root_file = PathBuf::from(filename);
261    if root_file.exists() {
262        return Some(root_file);
263    }
264
265    // Try src/ directory
266    let src_file = PathBuf::from("src").join(filename);
267    if src_file.exists() {
268        return Some(src_file);
269    }
270
271    // Not found - this is OK for optional files
272    None
273}
274
275/// Display a table of all widget types with their minimum schema version requirements
276fn display_widget_version_table() {
277    println!("Widget Version Requirements");
278    println!("===========================\n");
279    println!("{:<20} {:<10} Status", "Widget", "Min Version");
280    println!("{:-<20} {:-<10} {:-<30}", "", "", "");
281
282    let widgets = vec![
283        ("column", WidgetKind::Column),
284        ("row", WidgetKind::Row),
285        ("container", WidgetKind::Container),
286        ("scrollable", WidgetKind::Scrollable),
287        ("stack", WidgetKind::Stack),
288        ("text", WidgetKind::Text),
289        ("image", WidgetKind::Image),
290        ("svg", WidgetKind::Svg),
291        ("button", WidgetKind::Button),
292        ("text_input", WidgetKind::TextInput),
293        ("checkbox", WidgetKind::Checkbox),
294        ("slider", WidgetKind::Slider),
295        ("pick_list", WidgetKind::PickList),
296        ("toggler", WidgetKind::Toggler),
297        ("radio", WidgetKind::Radio),
298        ("space", WidgetKind::Space),
299        ("rule", WidgetKind::Rule),
300        ("progress_bar", WidgetKind::ProgressBar),
301        ("combobox", WidgetKind::ComboBox),
302        ("tooltip", WidgetKind::Tooltip),
303        ("grid", WidgetKind::Grid),
304        ("canvas", WidgetKind::Canvas),
305        ("date_picker", WidgetKind::DatePicker),
306        ("time_picker", WidgetKind::TimePicker),
307        ("color_picker", WidgetKind::ColorPicker),
308        ("menu", WidgetKind::Menu),
309        ("menu_item", WidgetKind::MenuItem),
310        ("menu_separator", WidgetKind::MenuSeparator),
311        ("context_menu", WidgetKind::ContextMenu),
312        ("float", WidgetKind::Float),
313        ("data_table", WidgetKind::DataTable),
314        ("data_column", WidgetKind::DataColumn),
315    ];
316
317    for (name, widget) in widgets {
318        let min_version = widget.minimum_version();
319        let version_str = format!("{}.{}", min_version.major, min_version.minor);
320        let status = if min_version.minor > 0 {
321            "Experimental (not fully functional)"
322        } else {
323            "Stable"
324        };
325        println!("{:<20} {:<10} {}", name, version_str, status);
326    }
327
328    println!("\nNote: Widgets requiring v1.1+ are experimental and may not be fully functional.");
329    println!("Use 'dampen check' to validate your .dampen files for version compatibility.");
330}
331
332pub fn run_checks(input: Option<String>, strict: bool, verbose: bool) -> Result<(), CheckError> {
333    use crate::commands::check::handlers::HandlerRegistry;
334
335    // Resolve UI directory
336    let input_path = resolve_ui_directory(input.as_deref())
337        .map_err(|msg| CheckError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, msg)))?;
338
339    if verbose {
340        eprintln!("Using UI directory: {}", input_path.display());
341    }
342
343    // Attempt auto-discovery for handlers and model
344    // Note: In library usage (run_checks), we don't support explicit paths for now
345    // to keep the API simple. If needed, we can add them later.
346    let handlers_path = resolve_optional_file(None, "handlers.json");
347    if verbose && let Some(ref path) = handlers_path {
348        eprintln!("Using handler registry: {}", path.display());
349    }
350
351    let model_path = resolve_optional_file(None, "model.json");
352    if verbose && let Some(ref path) = model_path {
353        eprintln!("Using model info: {}", path.display());
354    }
355
356    // Load handler registry if auto-discovered
357    let handler_registry = if let Some(path) = handlers_path {
358        let registry = HandlerRegistry::load_from_json(&path).map_err(|e| {
359            CheckError::HandlerRegistryLoadError {
360                path: path.clone(),
361                source: serde_json::Error::io(std::io::Error::other(e.to_string())),
362            }
363        })?;
364        Some(registry)
365    } else {
366        None
367    };
368
369    // Load model info if auto-discovered
370    let model_info = if let Some(path) = model_path {
371        let model =
372            crate::commands::check::model::ModelInfo::load_from_json(&path).map_err(|e| {
373                CheckError::ModelInfoLoadError {
374                    path: path.clone(),
375                    source: serde_json::Error::io(std::io::Error::other(e.to_string())),
376                }
377            })?;
378        Some(model)
379    } else {
380        None
381    };
382
383    let mut errors = Vec::new();
384    let mut files_checked = 0;
385
386    // Find all .dampen files
387    for entry in WalkDir::new(input_path)
388        .follow_links(true)
389        .into_iter()
390        .filter_map(|e| e.ok())
391        .filter(|e| {
392            e.path()
393                .extension()
394                .map(|ext| ext == "dampen")
395                .unwrap_or(false)
396        })
397    {
398        let file_path = entry.path();
399        files_checked += 1;
400
401        if verbose {
402            eprintln!("Checking: {}", file_path.display());
403        }
404
405        // Read and parse the file
406        let content = match fs::read_to_string(file_path) {
407            Ok(c) => c,
408            Err(e) => {
409                errors.push(CheckError::Io(e));
410                continue;
411            }
412        };
413
414        // First check for XML declaration
415        validate_xml_declaration(&content, file_path, &mut errors);
416
417        // Only proceed to parse if XML declaration is valid
418        if !errors.is_empty() {
419            // Keep going to check other files, but this file failed early
420            continue;
421        }
422
423        // Special handling for theme.dampen files
424        if file_path.file_name().is_some_and(|n| n == "theme.dampen") {
425            if let Err(theme_error) =
426                dampen_core::parser::theme_parser::parse_theme_document(&content)
427            {
428                errors.push(CheckError::XmlValidationError {
429                    file: file_path.to_path_buf(),
430                    line: 1,
431                    col: 1,
432                    message: format!("Theme validation error: {}", theme_error),
433                });
434            }
435            continue;
436        }
437
438        match parser::parse(&content) {
439            Ok(document) => {
440                // Validate the document structure
441                validate_document(
442                    &document,
443                    file_path,
444                    &handler_registry,
445                    &model_info,
446                    &mut errors,
447                );
448
449                // Validate references (themes, classes)
450                validate_references(&document, file_path, &mut errors);
451
452                // Validate widgets with styles, layout, breakpoints, and states
453                validate_widget_with_styles(&document.root, file_path, &document, &mut errors);
454
455                // Validate widget versions
456                let version_warnings = dampen_core::validate_widget_versions(&document);
457                if !version_warnings.is_empty() {
458                    for warning in version_warnings {
459                        eprintln!(
460                            "Warning: {} in {}:{}:{}",
461                            warning.format_message(),
462                            file_path.display(),
463                            warning.span.line,
464                            warning.span.column
465                        );
466                        eprintln!("  Suggestion: {}", warning.suggestion());
467                        eprintln!();
468                    }
469                }
470            }
471            Err(parse_error) => {
472                errors.push(CheckError::ParseError {
473                    file: file_path.to_path_buf(),
474                    line: parse_error.span.line,
475                    col: parse_error.span.column,
476                    message: parse_error.to_string(),
477                });
478            }
479        }
480    }
481
482    if verbose {
483        eprintln!("Checked {} files", files_checked);
484    }
485
486    // Report errors
487    if !errors.is_empty() {
488        let error_label = "error(s)";
489        eprintln!("Found {} {}:", errors.len(), error_label);
490
491        for error in &errors {
492            let prefix = "ERROR";
493            eprintln!("  [{}] {}", prefix, error);
494        }
495
496        Err(errors.remove(0))
497    } else {
498        if verbose {
499            let status = if strict {
500                "✓ All files passed validation (strict mode)"
501            } else {
502                "✓ All files passed validation"
503            };
504            eprintln!("{}", status);
505        }
506        Ok(())
507    }
508}
509
510pub fn execute(args: &CheckArgs) -> Result<(), CheckError> {
511    // If --show-widget-versions flag is set, display widget version table and exit
512    if args.show_widget_versions {
513        display_widget_version_table();
514        return Ok(());
515    }
516
517    // If custom paths provided, run full logic (legacy execute implementation)
518    // Otherwise delegate to run_checks
519    if args.handlers.is_some() || args.model.is_some() || args.custom_widgets.is_some() {
520        // ... (Original implementation needed here for full flexibility)
521        // For brevity in this refactor, we'll just inline the logic from run_checks
522        // but adapting it to use the explicit paths
523
524        // This is a partial duplication, but cleaner than refactoring everything at once
525        // Let's stick to the original plan: run_checks is a simplified entry point
526        // But execute needs to support all flags.
527        // So actually, execute should call a common internal function that takes all params.
528
529        return run_checks_internal(
530            args.input.clone(),
531            args.strict,
532            args.verbose,
533            args.handlers.clone(),
534            args.model.clone(),
535            args.custom_widgets.clone(),
536        );
537    }
538
539    run_checks(args.input.clone(), args.strict, args.verbose)
540}
541
542// Internal implementation that supports all options
543fn run_checks_internal(
544    input: Option<String>,
545    strict: bool,
546    verbose: bool,
547    handlers: Option<String>,
548    model: Option<String>,
549    _custom_widgets: Option<String>,
550) -> Result<(), CheckError> {
551    use crate::commands::check::handlers::HandlerRegistry;
552
553    // Resolve UI directory
554    let input_path = resolve_ui_directory(input.as_deref())
555        .map_err(|msg| CheckError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, msg)))?;
556
557    if verbose {
558        eprintln!("Using UI directory: {}", input_path.display());
559    }
560
561    // Resolve optional files
562    let handlers_path = resolve_optional_file(handlers.as_deref(), "handlers.json");
563    if verbose && let Some(ref path) = handlers_path {
564        eprintln!("Using handler registry: {}", path.display());
565    }
566
567    let model_path = resolve_optional_file(model.as_deref(), "model.json");
568    if verbose && let Some(ref path) = model_path {
569        eprintln!("Using model info: {}", path.display());
570    }
571
572    // Load handler registry if provided or auto-discovered (US2: Handler Registry Validation)
573    let handler_registry = if let Some(path) = handlers_path {
574        let registry = HandlerRegistry::load_from_json(&path).map_err(|e| {
575            CheckError::HandlerRegistryLoadError {
576                path: path.clone(),
577                source: serde_json::Error::io(std::io::Error::other(e.to_string())),
578            }
579        })?;
580        Some(registry)
581    } else {
582        None
583    };
584
585    // Load model info if provided or auto-discovered (US3: Binding Validation Against Model)
586    let model_info = if let Some(path) = model_path {
587        let model =
588            crate::commands::check::model::ModelInfo::load_from_json(&path).map_err(|e| {
589                CheckError::ModelInfoLoadError {
590                    path: path.clone(),
591                    source: serde_json::Error::io(std::io::Error::other(e.to_string())),
592                }
593            })?;
594        Some(model)
595    } else {
596        None
597    };
598
599    let mut errors = Vec::new();
600    let mut files_checked = 0;
601
602    // Find all .dampen files
603    for entry in WalkDir::new(input_path)
604        .follow_links(true)
605        .into_iter()
606        .filter_map(|e| e.ok())
607        .filter(|e| {
608            e.path()
609                .extension()
610                .map(|ext| ext == "dampen")
611                .unwrap_or(false)
612        })
613    {
614        let file_path = entry.path();
615        files_checked += 1;
616
617        if verbose {
618            eprintln!("Checking: {}", file_path.display());
619        }
620
621        // Read and parse the file
622        let content = fs::read_to_string(file_path)?;
623
624        // First check for XML declaration
625        validate_xml_declaration(&content, file_path, &mut errors);
626
627        // Only proceed to parse if XML declaration is valid
628        if !errors.is_empty() {
629            continue;
630        }
631
632        // Special handling for theme.dampen files
633        if file_path.file_name().is_some_and(|n| n == "theme.dampen") {
634            if let Err(theme_error) =
635                dampen_core::parser::theme_parser::parse_theme_document(&content)
636            {
637                errors.push(CheckError::XmlValidationError {
638                    file: file_path.to_path_buf(),
639                    line: 1, // theme_parser doesn't always provide spans yet
640                    col: 1,
641                    message: format!("Theme validation error: {}", theme_error),
642                });
643            }
644            continue;
645        }
646
647        match parser::parse(&content) {
648            Ok(document) => {
649                // Validate the document structure
650                validate_document(
651                    &document,
652                    file_path,
653                    &handler_registry,
654                    &model_info,
655                    &mut errors,
656                );
657
658                // Validate references (themes, classes)
659                validate_references(&document, file_path, &mut errors);
660
661                // Validate widgets with styles, layout, breakpoints, and states
662                validate_widget_with_styles(&document.root, file_path, &document, &mut errors);
663
664                // Validate widget versions (Phase 8: Widget Version Validation)
665                // This produces warnings for widgets requiring higher schema versions
666                let version_warnings = dampen_core::validate_widget_versions(&document);
667                if !version_warnings.is_empty() {
668                    for warning in version_warnings {
669                        eprintln!(
670                            "Warning: {} in {}:{}:{}",
671                            warning.format_message(),
672                            file_path.display(),
673                            warning.span.line,
674                            warning.span.column
675                        );
676                        eprintln!("  Suggestion: {}", warning.suggestion());
677                        eprintln!();
678                    }
679                }
680            }
681            Err(parse_error) => {
682                errors.push(CheckError::ParseError {
683                    file: file_path.to_path_buf(),
684                    line: parse_error.span.line,
685                    col: parse_error.span.column,
686                    message: parse_error.to_string(),
687                });
688            }
689        }
690    }
691
692    if verbose {
693        eprintln!("Checked {} files", files_checked);
694    }
695
696    // Report errors
697    if !errors.is_empty() {
698        // T048: Strict mode logic - in strict mode, all validation issues are errors
699        // (Currently all validation issues are already treated as errors, so this is
700        // primarily for future extensibility when we might add warnings)
701        let error_label = "error(s)"; // TODO: differentiate warnings in non-strict mode
702        eprintln!("Found {} {}:", errors.len(), error_label);
703
704        for error in &errors {
705            // T049: Error formatting - distinguish warnings from errors in strict mode
706            let prefix = "ERROR"; // TODO: use "WARNING" for warnings in non-strict mode
707            eprintln!("  [{}] {}", prefix, error);
708        }
709
710        // In strict mode, exit with code 1 on any error
711        // (This is already the default behavior)
712        Err(errors.remove(0))
713    } else {
714        if verbose {
715            let status = if strict {
716                "✓ All files passed validation (strict mode)"
717            } else {
718                "✓ All files passed validation"
719            };
720            eprintln!("{}", status);
721        }
722        Ok(())
723    }
724}
725
726fn validate_xml_declaration(content: &str, file_path: &Path, errors: &mut Vec<CheckError>) {
727    // XML declaration is now optional (since Dampen v0.2.9)
728    // If present, it must be valid. If absent, we assume UTF-8.
729    let trimmed = content.trim_start();
730    if trimmed.starts_with("<?xml") && !trimmed.starts_with("<?xml version=\"1.0\"") {
731        errors.push(CheckError::XmlValidationError {
732            file: file_path.to_path_buf(),
733            line: 1,
734            col: 1,
735            message:
736                "Invalid XML declaration. Expected: <?xml version=\"1.0\" ... ?> or no declaration"
737                    .to_string(),
738        });
739    }
740}
741
742fn validate_document(
743    document: &dampen_core::ir::DampenDocument,
744    file_path: &Path,
745    handler_registry: &Option<crate::commands::check::handlers::HandlerRegistry>,
746    model_info: &Option<crate::commands::check::model::ModelInfo>,
747    errors: &mut Vec<CheckError>,
748) {
749    use crate::commands::check::cross_widget::RadioGroupValidator;
750
751    // Get all valid widget names (using Display trait to get proper names like "tree_view")
752    let valid_widgets: HashSet<String> = WidgetKind::all_variants()
753        .iter()
754        .map(|w| format!("{}", w).to_lowercase())
755        .collect();
756
757    // Create radio group validator to collect radio buttons across the tree
758    let mut radio_validator = RadioGroupValidator::new();
759
760    // Validate each widget in the tree
761    validate_widget_node(
762        &document.root,
763        file_path,
764        &valid_widgets,
765        handler_registry,
766        model_info,
767        &mut radio_validator,
768        errors,
769    );
770
771    // After all widgets are validated, check radio groups for consistency
772    let radio_errors = radio_validator.validate();
773    for error in radio_errors {
774        // Convert cross_widget::CheckError to main_command::CheckError
775        match error {
776            crate::commands::check::errors::CheckError::DuplicateRadioValue {
777                value,
778                group,
779                file,
780                line,
781                col,
782                first_file,
783                first_line,
784                first_col,
785            } => {
786                errors.push(CheckError::XmlValidationError {
787                    file: file.clone(),
788                    line,
789                    col,
790                    message: format!(
791                        "Duplicate radio value '{}' in group '{}'. First occurrence: {}:{}:{}",
792                        value,
793                        group,
794                        first_file.display(),
795                        first_line,
796                        first_col
797                    ),
798                });
799            }
800            crate::commands::check::errors::CheckError::InconsistentRadioHandlers {
801                group,
802                file,
803                line,
804                col,
805                handlers,
806            } => {
807                errors.push(CheckError::XmlValidationError {
808                    file: file.clone(),
809                    line,
810                    col,
811                    message: format!(
812                        "Radio group '{}' has inconsistent on_select handlers. Found handlers: {}",
813                        group, handlers
814                    ),
815                });
816            }
817            _ => {}
818        }
819    }
820
821    // T057: Validate TreeView widgets for duplicate IDs and required attributes
822    validate_tree_views(&document.root, file_path, errors);
823}
824
825/// Recursively find and validate all TreeView widgets
826fn validate_tree_views(
827    node: &dampen_core::ir::WidgetNode,
828    file_path: &Path,
829    errors: &mut Vec<CheckError>,
830) {
831    use crate::commands::check::tree_view::TreeViewValidator;
832
833    // If this is a TreeView, validate it
834    if matches!(node.kind, WidgetKind::TreeView) {
835        let mut validator = TreeViewValidator::new();
836        validator.set_file(file_path.to_path_buf());
837        validator.validate_tree_view(node);
838
839        // Convert TreeView errors to CheckError
840        for error in validator.errors() {
841            match error {
842                crate::commands::check::errors::CheckError::DuplicateTreeNodeId {
843                    id,
844                    file,
845                    line,
846                    col,
847                    first_file,
848                    first_line,
849                    first_col,
850                } => {
851                    errors.push(CheckError::XmlValidationError {
852                        file: file.clone(),
853                        line: *line,
854                        col: *col,
855                        message: format!(
856                            "Duplicate tree node ID '{}'. First occurrence: {}:{}:{}",
857                            id,
858                            first_file.display(),
859                            first_line,
860                            first_col
861                        ),
862                    });
863                }
864                crate::commands::check::errors::CheckError::MissingRequiredAttribute {
865                    attr,
866                    widget,
867                    file,
868                    line,
869                    col,
870                } => {
871                    errors.push(CheckError::XmlValidationError {
872                        file: file.clone(),
873                        line: *line,
874                        col: *col,
875                        message: format!(
876                            "Missing required attribute '{}' for widget '{}'",
877                            attr, widget
878                        ),
879                    });
880                }
881                _ => {}
882            }
883        }
884    }
885
886    // Recursively check children
887    for child in &node.children {
888        validate_tree_views(child, file_path, errors);
889    }
890}
891
892fn validate_widget_node(
893    node: &dampen_core::ir::WidgetNode,
894    file_path: &Path,
895    valid_widgets: &HashSet<String>,
896    handler_registry: &Option<crate::commands::check::handlers::HandlerRegistry>,
897    model_info: &Option<crate::commands::check::model::ModelInfo>,
898    radio_validator: &mut crate::commands::check::cross_widget::RadioGroupValidator,
899    errors: &mut Vec<CheckError>,
900) {
901    use crate::commands::check::attributes;
902    use crate::commands::check::suggestions;
903
904    // Check if widget kind is valid
905    let widget_name = format!("{}", node.kind).to_lowercase();
906    if !valid_widgets.contains(&widget_name) && !matches!(node.kind, WidgetKind::Custom(_)) {
907        errors.push(CheckError::InvalidWidget {
908            widget: widget_name.clone(),
909            file: file_path.to_path_buf(),
910            line: node.span.line,
911            col: node.span.column,
912        });
913    }
914
915    // Validate widget attributes (US1: Unknown Attribute Detection)
916    let mut attr_names: Vec<String> = node.attributes.keys().map(|s| s.to_string()).collect();
917    if node.id.is_some() {
918        attr_names.push("id".to_string());
919    }
920    let unknown_attrs = attributes::validate_widget_attributes(&node.kind, &attr_names);
921
922    for (attr, _suggestion_opt) in unknown_attrs {
923        let schema = attributes::WidgetAttributeSchema::for_widget(&node.kind);
924        let all_valid = schema.all_valid_names();
925        let suggestion = suggestions::suggest(&attr, &all_valid, 3);
926
927        errors.push(CheckError::UnknownAttribute {
928            attr,
929            widget: widget_name.clone(),
930            file: file_path.to_path_buf(),
931            line: node.span.line,
932            col: node.span.column,
933            suggestion,
934        });
935    }
936
937    // Validate required attributes (US7: Required Attribute Validation)
938    let missing_required = attributes::validate_required_attributes(&node.kind, &attr_names);
939    for missing_attr in missing_required {
940        errors.push(CheckError::XmlValidationError {
941            file: file_path.to_path_buf(),
942            line: node.span.line,
943            col: node.span.column,
944            message: format!(
945                "Missing required attribute '{}' for widget '{}'",
946                missing_attr, widget_name
947            ),
948        });
949    }
950
951    // Validate event handlers (US2: Handler Registry Validation)
952    if let Some(registry) = handler_registry {
953        for event_binding in &node.events {
954            if !registry.contains(&event_binding.handler) {
955                // Generate suggestion using Levenshtein distance
956                let all_handler_names = registry.all_names();
957                let handler_refs: Vec<&str> =
958                    all_handler_names.iter().map(|s| s.as_str()).collect();
959                let suggestion = suggestions::suggest(&event_binding.handler, &handler_refs, 3);
960
961                errors.push(CheckError::UnknownHandler {
962                    handler: event_binding.handler.clone(),
963                    file: file_path.to_path_buf(),
964                    line: event_binding.span.line,
965                    col: event_binding.span.column,
966                    suggestion,
967                });
968            }
969        }
970    } else {
971        // If no registry provided, only check for empty handlers
972        for event_binding in &node.events {
973            if event_binding.handler.is_empty() {
974                errors.push(CheckError::UnknownHandler {
975                    handler: "<empty>".to_string(),
976                    file: file_path.to_path_buf(),
977                    line: event_binding.span.line,
978                    col: event_binding.span.column,
979                    suggestion: String::new(),
980                });
981            }
982        }
983    }
984
985    // Validate attribute bindings (US3: Binding Validation Against Model)
986    if let Some(model) = model_info {
987        for (attr_name, attr_value) in &node.attributes {
988            validate_attribute_bindings(
989                attr_name,
990                attr_value,
991                file_path,
992                node.span.line,
993                node.span.column,
994                model,
995                errors,
996            );
997        }
998    }
999
1000    // Validate attribute values (style, layout, etc.)
1001    for attr_value in node.attributes.values() {
1002        validate_attribute_value(
1003            attr_value,
1004            file_path,
1005            node.span.line,
1006            node.span.column,
1007            errors,
1008        );
1009    }
1010
1011    // Collect radio button information for cross-widget validation (US4: Radio Group Validation)
1012    if matches!(node.kind, WidgetKind::Radio) {
1013        // Extract radio button attributes
1014        // T096: Use node.id field instead of attributes map because parser special-cases 'id'
1015        let group_id = node.id.as_deref().unwrap_or("default");
1016
1017        let value = node
1018            .attributes
1019            .get("value")
1020            .and_then(|v| match v {
1021                AttributeValue::Static(s) => Some(s.as_str()),
1022                _ => None,
1023            })
1024            .unwrap_or("");
1025
1026        // Find on_select handler
1027        let handler = node
1028            .events
1029            .iter()
1030            .find(|e| e.event == EventKind::Select)
1031            .map(|e| e.handler.clone());
1032
1033        radio_validator.add_radio(
1034            group_id,
1035            value,
1036            file_path.to_str().unwrap_or("unknown"),
1037            node.span.line,
1038            node.span.column,
1039            handler,
1040        );
1041    }
1042
1043    // Recursively validate children
1044    for child in &node.children {
1045        validate_widget_node(
1046            child,
1047            file_path,
1048            valid_widgets,
1049            handler_registry,
1050            model_info,
1051            radio_validator,
1052            errors,
1053        );
1054    }
1055}
1056
1057fn validate_attribute_bindings(
1058    _attr_name: &str,
1059    value: &dampen_core::ir::AttributeValue,
1060    file_path: &Path,
1061    line: u32,
1062    col: u32,
1063    model: &crate::commands::check::model::ModelInfo,
1064    errors: &mut Vec<CheckError>,
1065) {
1066    // Only validate binding expressions
1067    if let dampen_core::ir::AttributeValue::Binding(binding_expr) = value {
1068        // Validate field access in the expression
1069        validate_expr_fields(&binding_expr.expr, file_path, line, col, model, errors);
1070    }
1071}
1072
1073fn validate_expr_fields(
1074    expr: &dampen_core::expr::Expr,
1075    file_path: &Path,
1076    line: u32,
1077    col: u32,
1078    model: &crate::commands::check::model::ModelInfo,
1079    errors: &mut Vec<CheckError>,
1080) {
1081    match expr {
1082        dampen_core::expr::Expr::FieldAccess(field_access) => {
1083            // Convert Vec<String> to Vec<&str>
1084            let field_parts: Vec<&str> = field_access.path.iter().map(|s| s.as_str()).collect();
1085
1086            if !model.contains_field(&field_parts) {
1087                // Generate available fields list
1088                let all_paths = model.all_field_paths();
1089                let available = if all_paths.len() > 5 {
1090                    format!("{} ({} total)", &all_paths[..5].join(", "), all_paths.len())
1091                } else {
1092                    all_paths.join(", ")
1093                };
1094
1095                let field_path = field_access.path.join(".");
1096
1097                errors.push(CheckError::InvalidBinding {
1098                    field: field_path,
1099                    file: file_path.to_path_buf(),
1100                    line,
1101                    col,
1102                });
1103
1104                // Add more detailed error with available fields
1105                eprintln!("  Available fields: {}", available);
1106            }
1107        }
1108        dampen_core::expr::Expr::MethodCall(method_call) => {
1109            // Validate the receiver expression
1110            validate_expr_fields(&method_call.receiver, file_path, line, col, model, errors);
1111            // Validate arguments
1112            for arg in &method_call.args {
1113                validate_expr_fields(arg, file_path, line, col, model, errors);
1114            }
1115        }
1116        dampen_core::expr::Expr::BinaryOp(binary_op) => {
1117            // Validate both sides of the binary operation
1118            validate_expr_fields(&binary_op.left, file_path, line, col, model, errors);
1119            validate_expr_fields(&binary_op.right, file_path, line, col, model, errors);
1120        }
1121        dampen_core::expr::Expr::UnaryOp(unary_op) => {
1122            // Validate the operand
1123            validate_expr_fields(&unary_op.operand, file_path, line, col, model, errors);
1124        }
1125        dampen_core::expr::Expr::Conditional(conditional) => {
1126            // Validate all parts of the conditional
1127            validate_expr_fields(&conditional.condition, file_path, line, col, model, errors);
1128            validate_expr_fields(
1129                &conditional.then_branch,
1130                file_path,
1131                line,
1132                col,
1133                model,
1134                errors,
1135            );
1136            validate_expr_fields(
1137                &conditional.else_branch,
1138                file_path,
1139                line,
1140                col,
1141                model,
1142                errors,
1143            );
1144        }
1145        dampen_core::expr::Expr::Literal(_) => {
1146            // Literals don't reference fields, nothing to validate
1147        }
1148        dampen_core::expr::Expr::SharedFieldAccess(shared_access) => {
1149            // Validate shared field paths similar to regular field access
1150            if shared_access.path.is_empty() || shared_access.path.iter().any(|f| f.is_empty()) {
1151                errors.push(CheckError::InvalidBinding {
1152                    field: "shared.<empty>".to_string(),
1153                    file: file_path.to_path_buf(),
1154                    line,
1155                    col,
1156                });
1157            }
1158        }
1159    }
1160}
1161
1162fn validate_attribute_value(
1163    value: &dampen_core::ir::AttributeValue,
1164    file_path: &Path,
1165    line: u32,
1166    col: u32,
1167    errors: &mut Vec<CheckError>,
1168) {
1169    match value {
1170        dampen_core::ir::AttributeValue::Static(_) => {
1171            // Static values are always valid
1172        }
1173        dampen_core::ir::AttributeValue::Binding(binding_expr) => {
1174            // For now, we'll do basic validation of the binding expression
1175            // In a real implementation, we'd check against the model fields
1176            validate_binding_expr(&binding_expr.expr, file_path, line, col, errors);
1177        }
1178        dampen_core::ir::AttributeValue::Interpolated(parts) => {
1179            for part in parts {
1180                match part {
1181                    dampen_core::ir::InterpolatedPart::Literal(_) => {
1182                        // Literals are always valid
1183                    }
1184                    dampen_core::ir::InterpolatedPart::Binding(binding_expr) => {
1185                        validate_binding_expr(&binding_expr.expr, file_path, line, col, errors);
1186                    }
1187                }
1188            }
1189        }
1190    }
1191}
1192
1193fn validate_binding_expr(
1194    expr: &dampen_core::expr::Expr,
1195    file_path: &Path,
1196    line: u32,
1197    col: u32,
1198    errors: &mut Vec<CheckError>,
1199) {
1200    match expr {
1201        dampen_core::expr::Expr::FieldAccess(field_access) => {
1202            // For now, we'll assume any field name is valid
1203            // In a real implementation, we'd check against the model fields
1204            if field_access.path.is_empty() || field_access.path.iter().any(|f| f.is_empty()) {
1205                errors.push(CheckError::InvalidBinding {
1206                    field: "<empty>".to_string(),
1207                    file: file_path.to_path_buf(),
1208                    line,
1209                    col,
1210                });
1211            }
1212        }
1213        dampen_core::expr::Expr::MethodCall(_) => {
1214            // Method calls are generally valid if the method exists
1215            // For now, we'll assume they're valid
1216        }
1217        dampen_core::expr::Expr::BinaryOp(_) => {
1218            // Binary operations are valid if both operands are valid
1219            // We'd need to recursively validate the operands
1220        }
1221        dampen_core::expr::Expr::UnaryOp(_) => {
1222            // Unary operations are valid if the operand is valid
1223        }
1224        dampen_core::expr::Expr::Conditional(_) => {
1225            // Conditionals are valid if all parts are valid
1226        }
1227        dampen_core::expr::Expr::Literal(_) => {
1228            // Literals are always valid
1229        }
1230        dampen_core::expr::Expr::SharedFieldAccess(_) => {
1231            // Shared field access is valid if the field exists in shared state
1232            // For now, we'll assume they're valid
1233        }
1234    }
1235}
1236
1237// Helper extension to get all widget variants
1238trait WidgetKindExt {
1239    fn all_variants() -> Vec<WidgetKind>;
1240}
1241
1242impl WidgetKindExt for WidgetKind {
1243    fn all_variants() -> Vec<WidgetKind> {
1244        vec![
1245            WidgetKind::Column,
1246            WidgetKind::Row,
1247            WidgetKind::Container,
1248            WidgetKind::Scrollable,
1249            WidgetKind::Stack,
1250            WidgetKind::Text,
1251            WidgetKind::Image,
1252            WidgetKind::Svg,
1253            WidgetKind::Button,
1254            WidgetKind::TextInput,
1255            WidgetKind::Checkbox,
1256            WidgetKind::Slider,
1257            WidgetKind::PickList,
1258            WidgetKind::Toggler,
1259            WidgetKind::Space,
1260            WidgetKind::Rule,
1261            WidgetKind::Radio,
1262            WidgetKind::ComboBox,
1263            WidgetKind::ProgressBar,
1264            WidgetKind::Tooltip,
1265            WidgetKind::Grid,
1266            WidgetKind::Canvas,
1267            WidgetKind::CanvasRect,
1268            WidgetKind::CanvasCircle,
1269            WidgetKind::CanvasLine,
1270            WidgetKind::CanvasText,
1271            WidgetKind::CanvasGroup,
1272            WidgetKind::DatePicker,
1273            WidgetKind::TimePicker,
1274            WidgetKind::ColorPicker,
1275            WidgetKind::Menu,
1276            WidgetKind::MenuItem,
1277            WidgetKind::MenuSeparator,
1278            WidgetKind::ContextMenu,
1279            WidgetKind::Float,
1280            WidgetKind::DataTable,
1281            WidgetKind::DataColumn,
1282            WidgetKind::TreeView,
1283            WidgetKind::TreeNode,
1284            WidgetKind::TabBar,
1285            WidgetKind::Tab,
1286            WidgetKind::For,
1287            WidgetKind::If,
1288        ]
1289    }
1290}
1291
1292/// Validate all references (themes, classes) in the document
1293fn validate_references(
1294    document: &dampen_core::ir::DampenDocument,
1295    file_path: &Path,
1296    errors: &mut Vec<CheckError>,
1297) {
1298    // Validate global theme reference
1299    if let Some(global_theme) = &document.global_theme
1300        && !document.themes.contains_key(global_theme)
1301    {
1302        errors.push(CheckError::UnknownTheme {
1303            theme: global_theme.clone(),
1304            file: file_path.to_path_buf(),
1305            line: 1,
1306            col: 1,
1307        });
1308    }
1309
1310    // Validate each theme definition (US5: Theme Property Validation)
1311    for (name, theme) in &document.themes {
1312        if let Err(msg) = theme.validate(false) {
1313            // Check if it's a circular dependency error
1314            if msg.contains("circular") || msg.contains("Circular") {
1315                errors.push(CheckError::XmlValidationError {
1316                    file: file_path.to_path_buf(),
1317                    line: 1,
1318                    col: 1,
1319                    message: format!("Theme '{}' validation error: {}", name, msg),
1320                });
1321            } else {
1322                errors.push(CheckError::InvalidStyleValue {
1323                    attr: format!("theme '{}'", name),
1324                    file: file_path.to_path_buf(),
1325                    line: 1,
1326                    col: 1,
1327                    message: msg,
1328                });
1329            }
1330        }
1331    }
1332
1333    // Validate each style class definition (US5: Circular Dependency Detection)
1334    for (name, class) in &document.style_classes {
1335        if let Err(msg) = class.validate(&document.style_classes) {
1336            // Check if it's a circular dependency error
1337            if msg.contains("circular") || msg.contains("Circular") {
1338                errors.push(CheckError::XmlValidationError {
1339                    file: file_path.to_path_buf(),
1340                    line: 1,
1341                    col: 1,
1342                    message: format!("Style class '{}' has circular dependency: {}", name, msg),
1343                });
1344            } else {
1345                errors.push(CheckError::InvalidStyleValue {
1346                    attr: format!("class '{}'", name),
1347                    file: file_path.to_path_buf(),
1348                    line: 1,
1349                    col: 1,
1350                    message: msg,
1351                });
1352            }
1353        }
1354    }
1355}
1356
1357/// Validate a widget node with all its styles, layout, and references
1358fn validate_widget_with_styles(
1359    node: &dampen_core::ir::WidgetNode,
1360    file_path: &Path,
1361    document: &dampen_core::ir::DampenDocument,
1362    errors: &mut Vec<CheckError>,
1363) {
1364    // Validate structured style properties
1365    if let Some(style) = &node.style
1366        && let Err(msg) = style.validate()
1367    {
1368        errors.push(CheckError::InvalidStyleValue {
1369            attr: "structured style".to_string(),
1370            file: file_path.to_path_buf(),
1371            line: node.span.line,
1372            col: node.span.column,
1373            message: msg,
1374        });
1375    }
1376
1377    // Validate structured layout constraints
1378    if let Some(layout) = &node.layout
1379        && let Err(msg) = layout.validate()
1380    {
1381        errors.push(CheckError::InvalidLayoutConstraint {
1382            file: file_path.to_path_buf(),
1383            line: node.span.line,
1384            col: node.span.column,
1385            message: msg,
1386        });
1387    }
1388
1389    // Validate style class references
1390    for class_name in &node.classes {
1391        if !document.style_classes.contains_key(class_name) {
1392            errors.push(CheckError::UnknownStyleClass {
1393                class: class_name.clone(),
1394                file: file_path.to_path_buf(),
1395                line: node.span.line,
1396                col: node.span.column,
1397            });
1398        }
1399    }
1400
1401    // Validate theme reference
1402    if let Some(theme_ref) = &node.theme_ref {
1403        match theme_ref {
1404            AttributeValue::Static(theme_name) => {
1405                if !document.themes.contains_key(theme_name) {
1406                    errors.push(CheckError::UnknownTheme {
1407                        theme: theme_name.clone(),
1408                        file: file_path.to_path_buf(),
1409                        line: node.span.line,
1410                        col: node.span.column,
1411                    });
1412                }
1413            }
1414            AttributeValue::Binding(_) | AttributeValue::Interpolated(_) => {
1415                // Binding expressions can't be validated at check time
1416                // They will be evaluated at runtime
1417            }
1418        }
1419    }
1420
1421    // Validate inline style attributes
1422    validate_style_attributes(node, file_path, errors);
1423
1424    // Validate inline layout attributes
1425    validate_layout_attributes(node, file_path, errors);
1426
1427    // Validate breakpoint attributes
1428    validate_breakpoint_attributes(node, file_path, errors);
1429
1430    // Validate state attributes
1431    validate_state_attributes(node, file_path, errors);
1432
1433    // Recursively validate children
1434    for child in &node.children {
1435        validate_widget_with_styles(child, file_path, document, errors);
1436    }
1437}
1438
1439/// Validate inline style attributes
1440fn validate_style_attributes(
1441    node: &dampen_core::ir::WidgetNode,
1442    file_path: &Path,
1443    errors: &mut Vec<CheckError>,
1444) {
1445    for (attr_name, attr_value) in &node.attributes {
1446        match attr_name.as_str() {
1447            "background" => {
1448                if let AttributeValue::Static(value) = attr_value
1449                    && let Err(msg) = style_parser::parse_background_attr(value)
1450                {
1451                    errors.push(CheckError::InvalidStyleValue {
1452                        attr: attr_name.clone(),
1453                        file: file_path.to_path_buf(),
1454                        line: node.span.line,
1455                        col: node.span.column,
1456                        message: msg,
1457                    });
1458                }
1459            }
1460            "color" | "border_color" => {
1461                if let AttributeValue::Static(value) = attr_value
1462                    && let Err(msg) = style_parser::parse_color_attr(value)
1463                {
1464                    errors.push(CheckError::InvalidStyleValue {
1465                        attr: attr_name.clone(),
1466                        file: file_path.to_path_buf(),
1467                        line: node.span.line,
1468                        col: node.span.column,
1469                        message: msg,
1470                    });
1471                }
1472            }
1473            "border_width" | "opacity" => {
1474                if let AttributeValue::Static(value) = attr_value
1475                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1476                {
1477                    errors.push(CheckError::InvalidStyleValue {
1478                        attr: attr_name.clone(),
1479                        file: file_path.to_path_buf(),
1480                        line: node.span.line,
1481                        col: node.span.column,
1482                        message: msg,
1483                    });
1484                }
1485            }
1486            "border_radius" => {
1487                if let AttributeValue::Static(value) = attr_value
1488                    && let Err(msg) = style_parser::parse_border_radius(value)
1489                {
1490                    errors.push(CheckError::InvalidStyleValue {
1491                        attr: attr_name.clone(),
1492                        file: file_path.to_path_buf(),
1493                        line: node.span.line,
1494                        col: node.span.column,
1495                        message: msg,
1496                    });
1497                }
1498            }
1499            "border_style" => {
1500                if let AttributeValue::Static(value) = attr_value
1501                    && let Err(msg) = style_parser::parse_border_style(value)
1502                {
1503                    errors.push(CheckError::InvalidStyleValue {
1504                        attr: attr_name.clone(),
1505                        file: file_path.to_path_buf(),
1506                        line: node.span.line,
1507                        col: node.span.column,
1508                        message: msg,
1509                    });
1510                }
1511            }
1512            "shadow" => {
1513                if let AttributeValue::Static(value) = attr_value
1514                    && let Err(msg) = style_parser::parse_shadow_attr(value)
1515                {
1516                    errors.push(CheckError::InvalidStyleValue {
1517                        attr: attr_name.clone(),
1518                        file: file_path.to_path_buf(),
1519                        line: node.span.line,
1520                        col: node.span.column,
1521                        message: msg,
1522                    });
1523                }
1524            }
1525            "transform" => {
1526                if let AttributeValue::Static(value) = attr_value
1527                    && let Err(msg) = style_parser::parse_transform(value)
1528                {
1529                    errors.push(CheckError::InvalidStyleValue {
1530                        attr: attr_name.clone(),
1531                        file: file_path.to_path_buf(),
1532                        line: node.span.line,
1533                        col: node.span.column,
1534                        message: msg,
1535                    });
1536                }
1537            }
1538            _ => {} // Autres attributs gérés ailleurs
1539        }
1540    }
1541}
1542
1543/// Validate inline layout attributes
1544fn validate_layout_attributes(
1545    node: &dampen_core::ir::WidgetNode,
1546    file_path: &Path,
1547    errors: &mut Vec<CheckError>,
1548) {
1549    for (attr_name, attr_value) in &node.attributes {
1550        match attr_name.as_str() {
1551            "width" | "height" | "min_width" | "max_width" | "min_height" | "max_height" => {
1552                if let AttributeValue::Static(value) = attr_value
1553                    && let Err(msg) = style_parser::parse_length_attr(value)
1554                {
1555                    errors.push(CheckError::InvalidStyleValue {
1556                        attr: attr_name.clone(),
1557                        file: file_path.to_path_buf(),
1558                        line: node.span.line,
1559                        col: node.span.column,
1560                        message: msg,
1561                    });
1562                }
1563            }
1564            "padding" => {
1565                if let AttributeValue::Static(value) = attr_value
1566                    && let Err(msg) = style_parser::parse_padding_attr(value)
1567                {
1568                    errors.push(CheckError::InvalidStyleValue {
1569                        attr: attr_name.clone(),
1570                        file: file_path.to_path_buf(),
1571                        line: node.span.line,
1572                        col: node.span.column,
1573                        message: msg,
1574                    });
1575                }
1576            }
1577            "spacing" => {
1578                if let AttributeValue::Static(value) = attr_value
1579                    && let Err(msg) = style_parser::parse_spacing(value)
1580                {
1581                    errors.push(CheckError::InvalidStyleValue {
1582                        attr: attr_name.clone(),
1583                        file: file_path.to_path_buf(),
1584                        line: node.span.line,
1585                        col: node.span.column,
1586                        message: msg,
1587                    });
1588                }
1589            }
1590            "align_items" => {
1591                if let AttributeValue::Static(value) = attr_value
1592                    && let Err(msg) = style_parser::parse_alignment(value)
1593                {
1594                    errors.push(CheckError::InvalidStyleValue {
1595                        attr: attr_name.clone(),
1596                        file: file_path.to_path_buf(),
1597                        line: node.span.line,
1598                        col: node.span.column,
1599                        message: msg,
1600                    });
1601                }
1602            }
1603            "justify_content" => {
1604                if let AttributeValue::Static(value) = attr_value
1605                    && let Err(msg) = style_parser::parse_justification(value)
1606                {
1607                    errors.push(CheckError::InvalidStyleValue {
1608                        attr: attr_name.clone(),
1609                        file: file_path.to_path_buf(),
1610                        line: node.span.line,
1611                        col: node.span.column,
1612                        message: msg,
1613                    });
1614                }
1615            }
1616            "direction" => {
1617                if let AttributeValue::Static(value) = attr_value
1618                    && let Err(msg) = Direction::parse(value)
1619                {
1620                    errors.push(CheckError::InvalidStyleValue {
1621                        attr: attr_name.clone(),
1622                        file: file_path.to_path_buf(),
1623                        line: node.span.line,
1624                        col: node.span.column,
1625                        message: msg,
1626                    });
1627                }
1628            }
1629            "position" => {
1630                if !matches!(node.kind, WidgetKind::Tooltip)
1631                    && let AttributeValue::Static(value) = attr_value
1632                    && let Err(msg) = Position::parse(value)
1633                {
1634                    errors.push(CheckError::InvalidStyleValue {
1635                        attr: attr_name.clone(),
1636                        file: file_path.to_path_buf(),
1637                        line: node.span.line,
1638                        col: node.span.column,
1639                        message: msg,
1640                    });
1641                }
1642            }
1643            "top" | "right" | "bottom" | "left" => {
1644                if let AttributeValue::Static(value) = attr_value
1645                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1646                {
1647                    errors.push(CheckError::InvalidStyleValue {
1648                        attr: attr_name.clone(),
1649                        file: file_path.to_path_buf(),
1650                        line: node.span.line,
1651                        col: node.span.column,
1652                        message: msg,
1653                    });
1654                }
1655            }
1656            "z_index" => {
1657                if let AttributeValue::Static(value) = attr_value
1658                    && let Err(msg) = style_parser::parse_int_attr(value, attr_name)
1659                {
1660                    errors.push(CheckError::InvalidStyleValue {
1661                        attr: attr_name.clone(),
1662                        file: file_path.to_path_buf(),
1663                        line: node.span.line,
1664                        col: node.span.column,
1665                        message: msg,
1666                    });
1667                }
1668            }
1669            _ => {} // Autres attributs gérés ailleurs
1670        }
1671    }
1672}
1673
1674/// Validate breakpoint attributes (mobile:, tablet:, desktop:)
1675fn validate_breakpoint_attributes(
1676    node: &dampen_core::ir::WidgetNode,
1677    file_path: &Path,
1678    errors: &mut Vec<CheckError>,
1679) {
1680    for (breakpoint, attrs) in &node.breakpoint_attributes {
1681        for (attr_name, attr_value) in attrs {
1682            // Valider que l'attribut de base est valide
1683            let base_attr = attr_name.as_str();
1684            let full_attr = format!("{:?}:{}", breakpoint, base_attr);
1685
1686            // Utiliser les mêmes validateurs que pour les attributs normaux
1687            let is_style_attr = matches!(
1688                base_attr,
1689                "background"
1690                    | "color"
1691                    | "border_width"
1692                    | "border_color"
1693                    | "border_radius"
1694                    | "border_style"
1695                    | "shadow"
1696                    | "opacity"
1697                    | "transform"
1698            );
1699
1700            let is_layout_attr = matches!(
1701                base_attr,
1702                "width"
1703                    | "height"
1704                    | "min_width"
1705                    | "max_width"
1706                    | "min_height"
1707                    | "max_height"
1708                    | "padding"
1709                    | "spacing"
1710                    | "align_items"
1711                    | "justify_content"
1712                    | "direction"
1713                    | "position"
1714                    | "top"
1715                    | "right"
1716                    | "bottom"
1717                    | "left"
1718                    | "z_index"
1719            );
1720
1721            if !is_style_attr && !is_layout_attr {
1722                errors.push(CheckError::InvalidBreakpoint {
1723                    attr: full_attr,
1724                    file: file_path.to_path_buf(),
1725                    line: node.span.line,
1726                    col: node.span.column,
1727                });
1728                continue;
1729            }
1730
1731            // Valider la valeur selon le type d'attribut
1732            if let AttributeValue::Static(value) = attr_value {
1733                let result: Result<(), String> = match base_attr {
1734                    "background" => style_parser::parse_background_attr(value).map(|_| ()),
1735                    "color" | "border_color" => style_parser::parse_color_attr(value).map(|_| ()),
1736                    "border_width" | "opacity" => {
1737                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1738                    }
1739                    "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1740                    "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1741                    "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1742                    "transform" => style_parser::parse_transform(value).map(|_| ()),
1743                    "width" | "height" | "min_width" | "max_width" | "min_height"
1744                    | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1745                    "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1746                    "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1747                    "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1748                    "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1749                    "direction" => Direction::parse(value).map(|_| ()),
1750                    "position" => Position::parse(value).map(|_| ()),
1751                    "top" | "right" | "bottom" | "left" => {
1752                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1753                    }
1754                    "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1755                    _ => Ok(()),
1756                };
1757
1758                if let Err(msg) = result {
1759                    errors.push(CheckError::InvalidStyleValue {
1760                        attr: full_attr,
1761                        file: file_path.to_path_buf(),
1762                        line: node.span.line,
1763                        col: node.span.column,
1764                        message: msg,
1765                    });
1766                }
1767            }
1768        }
1769    }
1770}
1771
1772/// Validate state attributes (hover:, focus:, active:, disabled:)
1773fn validate_state_attributes(
1774    node: &dampen_core::ir::WidgetNode,
1775    file_path: &Path,
1776    errors: &mut Vec<CheckError>,
1777) {
1778    for (attr_name, attr_value) in &node.attributes {
1779        if attr_name.contains(':') {
1780            let parts: Vec<&str> = attr_name.split(':').collect();
1781            if parts.len() >= 2 {
1782                let prefix = parts[0];
1783                let base_attr = parts[1];
1784
1785                // Valider le préfixe d'état
1786                if !["hover", "focus", "active", "disabled"].contains(&prefix) {
1787                    errors.push(CheckError::InvalidState {
1788                        attr: attr_name.clone(),
1789                        file: file_path.to_path_buf(),
1790                        line: node.span.line,
1791                        col: node.span.column,
1792                    });
1793                    continue;
1794                }
1795
1796                // Valider que l'attribut de base est valide
1797                let is_valid_attr = matches!(
1798                    base_attr,
1799                    "background"
1800                        | "color"
1801                        | "border_width"
1802                        | "border_color"
1803                        | "border_radius"
1804                        | "border_style"
1805                        | "shadow"
1806                        | "opacity"
1807                        | "transform"
1808                        | "width"
1809                        | "height"
1810                        | "min_width"
1811                        | "max_width"
1812                        | "min_height"
1813                        | "max_height"
1814                        | "padding"
1815                        | "spacing"
1816                        | "align_items"
1817                        | "justify_content"
1818                        | "direction"
1819                        | "position"
1820                        | "top"
1821                        | "right"
1822                        | "bottom"
1823                        | "left"
1824                        | "z_index"
1825                );
1826
1827                if !is_valid_attr {
1828                    errors.push(CheckError::InvalidState {
1829                        attr: attr_name.clone(),
1830                        file: file_path.to_path_buf(),
1831                        line: node.span.line,
1832                        col: node.span.column,
1833                    });
1834                    continue;
1835                }
1836
1837                // Valider la valeur
1838                if let AttributeValue::Static(value) = attr_value {
1839                    let result: Result<(), String> = match base_attr {
1840                        "background" => style_parser::parse_background_attr(value).map(|_| ()),
1841                        "color" | "border_color" => {
1842                            style_parser::parse_color_attr(value).map(|_| ())
1843                        }
1844                        "border_width" | "opacity" => {
1845                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1846                        }
1847                        "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1848                        "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1849                        "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1850                        "transform" => style_parser::parse_transform(value).map(|_| ()),
1851                        "width" | "height" | "min_width" | "max_width" | "min_height"
1852                        | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1853                        "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1854                        "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1855                        "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1856                        "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1857                        "direction" => Direction::parse(value).map(|_| ()),
1858                        "position" => Position::parse(value).map(|_| ()),
1859                        "top" | "right" | "bottom" | "left" => {
1860                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1861                        }
1862                        "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1863                        _ => Ok(()),
1864                    };
1865
1866                    if let Err(msg) = result {
1867                        errors.push(CheckError::InvalidStyleValue {
1868                            attr: attr_name.clone(),
1869                            file: file_path.to_path_buf(),
1870                            line: node.span.line,
1871                            col: node.span.column,
1872                            message: msg,
1873                        });
1874                    }
1875                }
1876            }
1877        }
1878    }
1879}