dampen_cli/commands/check/
main_command.rs

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