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::For,
1285            WidgetKind::If,
1286        ]
1287    }
1288}
1289
1290/// Validate all references (themes, classes) in the document
1291fn validate_references(
1292    document: &dampen_core::ir::DampenDocument,
1293    file_path: &Path,
1294    errors: &mut Vec<CheckError>,
1295) {
1296    // Validate global theme reference
1297    if let Some(global_theme) = &document.global_theme
1298        && !document.themes.contains_key(global_theme)
1299    {
1300        errors.push(CheckError::UnknownTheme {
1301            theme: global_theme.clone(),
1302            file: file_path.to_path_buf(),
1303            line: 1,
1304            col: 1,
1305        });
1306    }
1307
1308    // Validate each theme definition (US5: Theme Property Validation)
1309    for (name, theme) in &document.themes {
1310        if let Err(msg) = theme.validate(false) {
1311            // Check if it's a circular dependency error
1312            if msg.contains("circular") || msg.contains("Circular") {
1313                errors.push(CheckError::XmlValidationError {
1314                    file: file_path.to_path_buf(),
1315                    line: 1,
1316                    col: 1,
1317                    message: format!("Theme '{}' validation error: {}", name, msg),
1318                });
1319            } else {
1320                errors.push(CheckError::InvalidStyleValue {
1321                    attr: format!("theme '{}'", name),
1322                    file: file_path.to_path_buf(),
1323                    line: 1,
1324                    col: 1,
1325                    message: msg,
1326                });
1327            }
1328        }
1329    }
1330
1331    // Validate each style class definition (US5: Circular Dependency Detection)
1332    for (name, class) in &document.style_classes {
1333        if let Err(msg) = class.validate(&document.style_classes) {
1334            // Check if it's a circular dependency error
1335            if msg.contains("circular") || msg.contains("Circular") {
1336                errors.push(CheckError::XmlValidationError {
1337                    file: file_path.to_path_buf(),
1338                    line: 1,
1339                    col: 1,
1340                    message: format!("Style class '{}' has circular dependency: {}", name, msg),
1341                });
1342            } else {
1343                errors.push(CheckError::InvalidStyleValue {
1344                    attr: format!("class '{}'", name),
1345                    file: file_path.to_path_buf(),
1346                    line: 1,
1347                    col: 1,
1348                    message: msg,
1349                });
1350            }
1351        }
1352    }
1353}
1354
1355/// Validate a widget node with all its styles, layout, and references
1356fn validate_widget_with_styles(
1357    node: &dampen_core::ir::WidgetNode,
1358    file_path: &Path,
1359    document: &dampen_core::ir::DampenDocument,
1360    errors: &mut Vec<CheckError>,
1361) {
1362    // Validate structured style properties
1363    if let Some(style) = &node.style
1364        && let Err(msg) = style.validate()
1365    {
1366        errors.push(CheckError::InvalidStyleValue {
1367            attr: "structured style".to_string(),
1368            file: file_path.to_path_buf(),
1369            line: node.span.line,
1370            col: node.span.column,
1371            message: msg,
1372        });
1373    }
1374
1375    // Validate structured layout constraints
1376    if let Some(layout) = &node.layout
1377        && let Err(msg) = layout.validate()
1378    {
1379        errors.push(CheckError::InvalidLayoutConstraint {
1380            file: file_path.to_path_buf(),
1381            line: node.span.line,
1382            col: node.span.column,
1383            message: msg,
1384        });
1385    }
1386
1387    // Validate style class references
1388    for class_name in &node.classes {
1389        if !document.style_classes.contains_key(class_name) {
1390            errors.push(CheckError::UnknownStyleClass {
1391                class: class_name.clone(),
1392                file: file_path.to_path_buf(),
1393                line: node.span.line,
1394                col: node.span.column,
1395            });
1396        }
1397    }
1398
1399    // Validate theme reference
1400    if let Some(theme_ref) = &node.theme_ref {
1401        match theme_ref {
1402            AttributeValue::Static(theme_name) => {
1403                if !document.themes.contains_key(theme_name) {
1404                    errors.push(CheckError::UnknownTheme {
1405                        theme: theme_name.clone(),
1406                        file: file_path.to_path_buf(),
1407                        line: node.span.line,
1408                        col: node.span.column,
1409                    });
1410                }
1411            }
1412            AttributeValue::Binding(_) | AttributeValue::Interpolated(_) => {
1413                // Binding expressions can't be validated at check time
1414                // They will be evaluated at runtime
1415            }
1416        }
1417    }
1418
1419    // Validate inline style attributes
1420    validate_style_attributes(node, file_path, errors);
1421
1422    // Validate inline layout attributes
1423    validate_layout_attributes(node, file_path, errors);
1424
1425    // Validate breakpoint attributes
1426    validate_breakpoint_attributes(node, file_path, errors);
1427
1428    // Validate state attributes
1429    validate_state_attributes(node, file_path, errors);
1430
1431    // Recursively validate children
1432    for child in &node.children {
1433        validate_widget_with_styles(child, file_path, document, errors);
1434    }
1435}
1436
1437/// Validate inline style attributes
1438fn validate_style_attributes(
1439    node: &dampen_core::ir::WidgetNode,
1440    file_path: &Path,
1441    errors: &mut Vec<CheckError>,
1442) {
1443    for (attr_name, attr_value) in &node.attributes {
1444        match attr_name.as_str() {
1445            "background" => {
1446                if let AttributeValue::Static(value) = attr_value
1447                    && let Err(msg) = style_parser::parse_background_attr(value)
1448                {
1449                    errors.push(CheckError::InvalidStyleValue {
1450                        attr: attr_name.clone(),
1451                        file: file_path.to_path_buf(),
1452                        line: node.span.line,
1453                        col: node.span.column,
1454                        message: msg,
1455                    });
1456                }
1457            }
1458            "color" | "border_color" => {
1459                if let AttributeValue::Static(value) = attr_value
1460                    && let Err(msg) = style_parser::parse_color_attr(value)
1461                {
1462                    errors.push(CheckError::InvalidStyleValue {
1463                        attr: attr_name.clone(),
1464                        file: file_path.to_path_buf(),
1465                        line: node.span.line,
1466                        col: node.span.column,
1467                        message: msg,
1468                    });
1469                }
1470            }
1471            "border_width" | "opacity" => {
1472                if let AttributeValue::Static(value) = attr_value
1473                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1474                {
1475                    errors.push(CheckError::InvalidStyleValue {
1476                        attr: attr_name.clone(),
1477                        file: file_path.to_path_buf(),
1478                        line: node.span.line,
1479                        col: node.span.column,
1480                        message: msg,
1481                    });
1482                }
1483            }
1484            "border_radius" => {
1485                if let AttributeValue::Static(value) = attr_value
1486                    && let Err(msg) = style_parser::parse_border_radius(value)
1487                {
1488                    errors.push(CheckError::InvalidStyleValue {
1489                        attr: attr_name.clone(),
1490                        file: file_path.to_path_buf(),
1491                        line: node.span.line,
1492                        col: node.span.column,
1493                        message: msg,
1494                    });
1495                }
1496            }
1497            "border_style" => {
1498                if let AttributeValue::Static(value) = attr_value
1499                    && let Err(msg) = style_parser::parse_border_style(value)
1500                {
1501                    errors.push(CheckError::InvalidStyleValue {
1502                        attr: attr_name.clone(),
1503                        file: file_path.to_path_buf(),
1504                        line: node.span.line,
1505                        col: node.span.column,
1506                        message: msg,
1507                    });
1508                }
1509            }
1510            "shadow" => {
1511                if let AttributeValue::Static(value) = attr_value
1512                    && let Err(msg) = style_parser::parse_shadow_attr(value)
1513                {
1514                    errors.push(CheckError::InvalidStyleValue {
1515                        attr: attr_name.clone(),
1516                        file: file_path.to_path_buf(),
1517                        line: node.span.line,
1518                        col: node.span.column,
1519                        message: msg,
1520                    });
1521                }
1522            }
1523            "transform" => {
1524                if let AttributeValue::Static(value) = attr_value
1525                    && let Err(msg) = style_parser::parse_transform(value)
1526                {
1527                    errors.push(CheckError::InvalidStyleValue {
1528                        attr: attr_name.clone(),
1529                        file: file_path.to_path_buf(),
1530                        line: node.span.line,
1531                        col: node.span.column,
1532                        message: msg,
1533                    });
1534                }
1535            }
1536            _ => {} // Autres attributs gérés ailleurs
1537        }
1538    }
1539}
1540
1541/// Validate inline layout attributes
1542fn validate_layout_attributes(
1543    node: &dampen_core::ir::WidgetNode,
1544    file_path: &Path,
1545    errors: &mut Vec<CheckError>,
1546) {
1547    for (attr_name, attr_value) in &node.attributes {
1548        match attr_name.as_str() {
1549            "width" | "height" | "min_width" | "max_width" | "min_height" | "max_height" => {
1550                if let AttributeValue::Static(value) = attr_value
1551                    && let Err(msg) = style_parser::parse_length_attr(value)
1552                {
1553                    errors.push(CheckError::InvalidStyleValue {
1554                        attr: attr_name.clone(),
1555                        file: file_path.to_path_buf(),
1556                        line: node.span.line,
1557                        col: node.span.column,
1558                        message: msg,
1559                    });
1560                }
1561            }
1562            "padding" => {
1563                if let AttributeValue::Static(value) = attr_value
1564                    && let Err(msg) = style_parser::parse_padding_attr(value)
1565                {
1566                    errors.push(CheckError::InvalidStyleValue {
1567                        attr: attr_name.clone(),
1568                        file: file_path.to_path_buf(),
1569                        line: node.span.line,
1570                        col: node.span.column,
1571                        message: msg,
1572                    });
1573                }
1574            }
1575            "spacing" => {
1576                if let AttributeValue::Static(value) = attr_value
1577                    && let Err(msg) = style_parser::parse_spacing(value)
1578                {
1579                    errors.push(CheckError::InvalidStyleValue {
1580                        attr: attr_name.clone(),
1581                        file: file_path.to_path_buf(),
1582                        line: node.span.line,
1583                        col: node.span.column,
1584                        message: msg,
1585                    });
1586                }
1587            }
1588            "align_items" => {
1589                if let AttributeValue::Static(value) = attr_value
1590                    && let Err(msg) = style_parser::parse_alignment(value)
1591                {
1592                    errors.push(CheckError::InvalidStyleValue {
1593                        attr: attr_name.clone(),
1594                        file: file_path.to_path_buf(),
1595                        line: node.span.line,
1596                        col: node.span.column,
1597                        message: msg,
1598                    });
1599                }
1600            }
1601            "justify_content" => {
1602                if let AttributeValue::Static(value) = attr_value
1603                    && let Err(msg) = style_parser::parse_justification(value)
1604                {
1605                    errors.push(CheckError::InvalidStyleValue {
1606                        attr: attr_name.clone(),
1607                        file: file_path.to_path_buf(),
1608                        line: node.span.line,
1609                        col: node.span.column,
1610                        message: msg,
1611                    });
1612                }
1613            }
1614            "direction" => {
1615                if let AttributeValue::Static(value) = attr_value
1616                    && let Err(msg) = Direction::parse(value)
1617                {
1618                    errors.push(CheckError::InvalidStyleValue {
1619                        attr: attr_name.clone(),
1620                        file: file_path.to_path_buf(),
1621                        line: node.span.line,
1622                        col: node.span.column,
1623                        message: msg,
1624                    });
1625                }
1626            }
1627            "position" => {
1628                if !matches!(node.kind, WidgetKind::Tooltip)
1629                    && let AttributeValue::Static(value) = attr_value
1630                    && let Err(msg) = Position::parse(value)
1631                {
1632                    errors.push(CheckError::InvalidStyleValue {
1633                        attr: attr_name.clone(),
1634                        file: file_path.to_path_buf(),
1635                        line: node.span.line,
1636                        col: node.span.column,
1637                        message: msg,
1638                    });
1639                }
1640            }
1641            "top" | "right" | "bottom" | "left" => {
1642                if let AttributeValue::Static(value) = attr_value
1643                    && let Err(msg) = style_parser::parse_float_attr(value, attr_name)
1644                {
1645                    errors.push(CheckError::InvalidStyleValue {
1646                        attr: attr_name.clone(),
1647                        file: file_path.to_path_buf(),
1648                        line: node.span.line,
1649                        col: node.span.column,
1650                        message: msg,
1651                    });
1652                }
1653            }
1654            "z_index" => {
1655                if let AttributeValue::Static(value) = attr_value
1656                    && let Err(msg) = style_parser::parse_int_attr(value, attr_name)
1657                {
1658                    errors.push(CheckError::InvalidStyleValue {
1659                        attr: attr_name.clone(),
1660                        file: file_path.to_path_buf(),
1661                        line: node.span.line,
1662                        col: node.span.column,
1663                        message: msg,
1664                    });
1665                }
1666            }
1667            _ => {} // Autres attributs gérés ailleurs
1668        }
1669    }
1670}
1671
1672/// Validate breakpoint attributes (mobile:, tablet:, desktop:)
1673fn validate_breakpoint_attributes(
1674    node: &dampen_core::ir::WidgetNode,
1675    file_path: &Path,
1676    errors: &mut Vec<CheckError>,
1677) {
1678    for (breakpoint, attrs) in &node.breakpoint_attributes {
1679        for (attr_name, attr_value) in attrs {
1680            // Valider que l'attribut de base est valide
1681            let base_attr = attr_name.as_str();
1682            let full_attr = format!("{:?}:{}", breakpoint, base_attr);
1683
1684            // Utiliser les mêmes validateurs que pour les attributs normaux
1685            let is_style_attr = matches!(
1686                base_attr,
1687                "background"
1688                    | "color"
1689                    | "border_width"
1690                    | "border_color"
1691                    | "border_radius"
1692                    | "border_style"
1693                    | "shadow"
1694                    | "opacity"
1695                    | "transform"
1696            );
1697
1698            let is_layout_attr = matches!(
1699                base_attr,
1700                "width"
1701                    | "height"
1702                    | "min_width"
1703                    | "max_width"
1704                    | "min_height"
1705                    | "max_height"
1706                    | "padding"
1707                    | "spacing"
1708                    | "align_items"
1709                    | "justify_content"
1710                    | "direction"
1711                    | "position"
1712                    | "top"
1713                    | "right"
1714                    | "bottom"
1715                    | "left"
1716                    | "z_index"
1717            );
1718
1719            if !is_style_attr && !is_layout_attr {
1720                errors.push(CheckError::InvalidBreakpoint {
1721                    attr: full_attr,
1722                    file: file_path.to_path_buf(),
1723                    line: node.span.line,
1724                    col: node.span.column,
1725                });
1726                continue;
1727            }
1728
1729            // Valider la valeur selon le type d'attribut
1730            if let AttributeValue::Static(value) = attr_value {
1731                let result: Result<(), String> = match base_attr {
1732                    "background" => style_parser::parse_background_attr(value).map(|_| ()),
1733                    "color" | "border_color" => style_parser::parse_color_attr(value).map(|_| ()),
1734                    "border_width" | "opacity" => {
1735                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1736                    }
1737                    "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1738                    "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1739                    "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1740                    "transform" => style_parser::parse_transform(value).map(|_| ()),
1741                    "width" | "height" | "min_width" | "max_width" | "min_height"
1742                    | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1743                    "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1744                    "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1745                    "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1746                    "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1747                    "direction" => Direction::parse(value).map(|_| ()),
1748                    "position" => Position::parse(value).map(|_| ()),
1749                    "top" | "right" | "bottom" | "left" => {
1750                        style_parser::parse_float_attr(value, base_attr).map(|_| ())
1751                    }
1752                    "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1753                    _ => Ok(()),
1754                };
1755
1756                if let Err(msg) = result {
1757                    errors.push(CheckError::InvalidStyleValue {
1758                        attr: full_attr,
1759                        file: file_path.to_path_buf(),
1760                        line: node.span.line,
1761                        col: node.span.column,
1762                        message: msg,
1763                    });
1764                }
1765            }
1766        }
1767    }
1768}
1769
1770/// Validate state attributes (hover:, focus:, active:, disabled:)
1771fn validate_state_attributes(
1772    node: &dampen_core::ir::WidgetNode,
1773    file_path: &Path,
1774    errors: &mut Vec<CheckError>,
1775) {
1776    for (attr_name, attr_value) in &node.attributes {
1777        if attr_name.contains(':') {
1778            let parts: Vec<&str> = attr_name.split(':').collect();
1779            if parts.len() >= 2 {
1780                let prefix = parts[0];
1781                let base_attr = parts[1];
1782
1783                // Valider le préfixe d'état
1784                if !["hover", "focus", "active", "disabled"].contains(&prefix) {
1785                    errors.push(CheckError::InvalidState {
1786                        attr: attr_name.clone(),
1787                        file: file_path.to_path_buf(),
1788                        line: node.span.line,
1789                        col: node.span.column,
1790                    });
1791                    continue;
1792                }
1793
1794                // Valider que l'attribut de base est valide
1795                let is_valid_attr = matches!(
1796                    base_attr,
1797                    "background"
1798                        | "color"
1799                        | "border_width"
1800                        | "border_color"
1801                        | "border_radius"
1802                        | "border_style"
1803                        | "shadow"
1804                        | "opacity"
1805                        | "transform"
1806                        | "width"
1807                        | "height"
1808                        | "min_width"
1809                        | "max_width"
1810                        | "min_height"
1811                        | "max_height"
1812                        | "padding"
1813                        | "spacing"
1814                        | "align_items"
1815                        | "justify_content"
1816                        | "direction"
1817                        | "position"
1818                        | "top"
1819                        | "right"
1820                        | "bottom"
1821                        | "left"
1822                        | "z_index"
1823                );
1824
1825                if !is_valid_attr {
1826                    errors.push(CheckError::InvalidState {
1827                        attr: attr_name.clone(),
1828                        file: file_path.to_path_buf(),
1829                        line: node.span.line,
1830                        col: node.span.column,
1831                    });
1832                    continue;
1833                }
1834
1835                // Valider la valeur
1836                if let AttributeValue::Static(value) = attr_value {
1837                    let result: Result<(), String> = match base_attr {
1838                        "background" => style_parser::parse_background_attr(value).map(|_| ()),
1839                        "color" | "border_color" => {
1840                            style_parser::parse_color_attr(value).map(|_| ())
1841                        }
1842                        "border_width" | "opacity" => {
1843                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1844                        }
1845                        "border_radius" => style_parser::parse_border_radius(value).map(|_| ()),
1846                        "border_style" => style_parser::parse_border_style(value).map(|_| ()),
1847                        "shadow" => style_parser::parse_shadow_attr(value).map(|_| ()),
1848                        "transform" => style_parser::parse_transform(value).map(|_| ()),
1849                        "width" | "height" | "min_width" | "max_width" | "min_height"
1850                        | "max_height" => style_parser::parse_length_attr(value).map(|_| ()),
1851                        "padding" => style_parser::parse_padding_attr(value).map(|_| ()),
1852                        "spacing" => style_parser::parse_spacing(value).map(|_| ()),
1853                        "align_items" => style_parser::parse_alignment(value).map(|_| ()),
1854                        "justify_content" => style_parser::parse_justification(value).map(|_| ()),
1855                        "direction" => Direction::parse(value).map(|_| ()),
1856                        "position" => Position::parse(value).map(|_| ()),
1857                        "top" | "right" | "bottom" | "left" => {
1858                            style_parser::parse_float_attr(value, base_attr).map(|_| ())
1859                        }
1860                        "z_index" => style_parser::parse_int_attr(value, base_attr).map(|_| ()),
1861                        _ => Ok(()),
1862                    };
1863
1864                    if let Err(msg) = result {
1865                        errors.push(CheckError::InvalidStyleValue {
1866                            attr: attr_name.clone(),
1867                            file: file_path.to_path_buf(),
1868                            line: node.span.line,
1869                            col: node.span.column,
1870                            message: msg,
1871                        });
1872                    }
1873                }
1874            }
1875        }
1876    }
1877}