Skip to main content

dampen_cli/commands/check/
tree_view.rs

1// TreeView validation for duplicate node IDs and required attributes
2use crate::commands::check::errors::CheckError;
3use dampen_core::ir::node::{WidgetKind, WidgetNode};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7/// Represents a tree node for validation purposes
8#[derive(Debug, Clone)]
9pub struct TreeNodeInfo {
10    pub id: String,
11    pub label: String,
12    pub file: PathBuf,
13    pub line: u32,
14    pub col: u32,
15}
16
17/// Validator for TreeView widgets
18#[derive(Debug, Default)]
19pub struct TreeViewValidator {
20    /// Map of node IDs to their first occurrence (for duplicate detection)
21    node_ids: HashMap<String, TreeNodeInfo>,
22    /// List of validation errors
23    errors: Vec<CheckError>,
24    /// Current file being validated
25    current_file: PathBuf,
26}
27
28impl TreeViewValidator {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set the current file being validated
34    pub fn set_file(&mut self, file: PathBuf) {
35        self.current_file = file;
36    }
37
38    /// Validate a TreeView widget and all its child nodes
39    pub fn validate_tree_view(&mut self, node: &WidgetNode) {
40        // Clear previous state for new tree
41        self.node_ids.clear();
42        self.errors.clear();
43
44        // Validate all tree_node children recursively
45        for child in &node.children {
46            if child.kind == WidgetKind::TreeNode {
47                self.validate_tree_node(child);
48            }
49        }
50    }
51
52    /// Validate a single tree node and its children
53    fn validate_tree_node(&mut self, node: &WidgetNode) {
54        let span = &node.span;
55
56        // T059: Check required attributes (id, label)
57        // Use node.id field since parser extracts it
58        let id_value = node.id.clone().unwrap_or_default();
59        let label = node.attributes.get("label");
60
61        // Check for missing id
62        if node.id.is_none() {
63            self.errors.push(CheckError::MissingRequiredAttribute {
64                attr: "id".to_string(),
65                widget: "tree_node".to_string(),
66                file: self.current_file.clone(),
67                line: span.line,
68                col: span.column,
69            });
70        }
71
72        // Check for missing label
73        if label.is_none() {
74            self.errors.push(CheckError::MissingRequiredAttribute {
75                attr: "label".to_string(),
76                widget: "tree_node".to_string(),
77                file: self.current_file.clone(),
78                line: span.line,
79                col: span.column,
80            });
81        }
82
83        // T058: Check for duplicate node IDs
84        if !id_value.is_empty() {
85            if let Some(existing) = self.node_ids.get(&id_value) {
86                // Found duplicate
87                self.errors.push(CheckError::DuplicateTreeNodeId {
88                    id: id_value.clone(),
89                    file: self.current_file.clone(),
90                    line: span.line,
91                    col: span.column,
92                    first_file: existing.file.clone(),
93                    first_line: existing.line,
94                    first_col: existing.col,
95                });
96            } else {
97                // Store first occurrence
98                let label_value = label.map_or_else(
99                    || id_value.clone(),
100                    |attr| match attr {
101                        dampen_core::ir::node::AttributeValue::Static(s) => s.clone(),
102                        _ => id_value.clone(),
103                    },
104                );
105
106                self.node_ids.insert(
107                    id_value.clone(),
108                    TreeNodeInfo {
109                        id: id_value.clone(),
110                        label: label_value,
111                        file: self.current_file.clone(),
112                        line: span.line,
113                        col: span.column,
114                    },
115                );
116            }
117        }
118
119        // Recursively validate children
120        for child in &node.children {
121            if child.kind == WidgetKind::TreeNode {
122                self.validate_tree_node(child);
123            }
124        }
125    }
126
127    /// Get all validation errors
128    pub fn errors(&self) -> &[CheckError] {
129        &self.errors
130    }
131
132    /// Check if validation found any errors
133    pub fn has_errors(&self) -> bool {
134        !self.errors.is_empty()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use dampen_core::ir::Span;
142    use dampen_core::ir::node::AttributeValue;
143    use std::collections::HashMap;
144
145    fn create_test_node(id: &str, label: &str, line: u32) -> WidgetNode {
146        let mut attributes = HashMap::new();
147        attributes.insert(
148            "label".to_string(),
149            AttributeValue::Static(label.to_string()),
150        );
151
152        WidgetNode {
153            kind: WidgetKind::TreeNode,
154            id: Some(id.to_string()),
155            attributes,
156            events: vec![],
157            children: vec![],
158            span: Span::new(0, 0, line, 1),
159            style: None,
160            layout: None,
161            theme_ref: None,
162            classes: vec![],
163            breakpoint_attributes: HashMap::new(),
164            inline_state_variants: HashMap::new(),
165        }
166    }
167
168    fn create_node_without_id(line: u32) -> WidgetNode {
169        let mut attributes = HashMap::new();
170        attributes.insert(
171            "label".to_string(),
172            AttributeValue::Static("Test".to_string()),
173        );
174
175        WidgetNode {
176            kind: WidgetKind::TreeNode,
177            id: None,
178            attributes,
179            events: vec![],
180            children: vec![],
181            span: Span::new(0, 0, line, 1),
182            style: None,
183            layout: None,
184            theme_ref: None,
185            classes: vec![],
186            breakpoint_attributes: HashMap::new(),
187            inline_state_variants: HashMap::new(),
188        }
189    }
190
191    fn create_node_without_label(line: u32) -> WidgetNode {
192        WidgetNode {
193            kind: WidgetKind::TreeNode,
194            id: Some("test".to_string()),
195            attributes: HashMap::new(),
196            events: vec![],
197            children: vec![],
198            span: Span::new(0, 0, line, 1),
199            style: None,
200            layout: None,
201            theme_ref: None,
202            classes: vec![],
203            breakpoint_attributes: HashMap::new(),
204            inline_state_variants: HashMap::new(),
205        }
206    }
207
208    #[test]
209    fn test_unique_node_ids() {
210        let mut validator = TreeViewValidator::new();
211        validator.set_file(PathBuf::from("test.dampen"));
212
213        let tree_view = WidgetNode {
214            kind: WidgetKind::TreeView,
215            id: None,
216            attributes: HashMap::new(),
217            events: vec![],
218            children: vec![
219                create_test_node("node1", "Node 1", 10),
220                create_test_node("node2", "Node 2", 15),
221                create_test_node("node3", "Node 3", 20),
222            ],
223            span: Span::new(0, 0, 1, 1),
224            style: None,
225            layout: None,
226            theme_ref: None,
227            classes: vec![],
228            breakpoint_attributes: HashMap::new(),
229            inline_state_variants: HashMap::new(),
230        };
231
232        validator.validate_tree_view(&tree_view);
233        assert!(!validator.has_errors());
234    }
235
236    #[test]
237    fn test_duplicate_node_ids() {
238        let mut validator = TreeViewValidator::new();
239        validator.set_file(PathBuf::from("test.dampen"));
240
241        let tree_view = WidgetNode {
242            kind: WidgetKind::TreeView,
243            id: None,
244            attributes: HashMap::new(),
245            events: vec![],
246            children: vec![
247                create_test_node("node1", "Node 1", 10),
248                create_test_node("node1", "Node 1 Duplicate", 15),
249            ],
250            span: Span::new(0, 0, 1, 1),
251            style: None,
252            layout: None,
253            theme_ref: None,
254            classes: vec![],
255            breakpoint_attributes: HashMap::new(),
256            inline_state_variants: HashMap::new(),
257        };
258
259        validator.validate_tree_view(&tree_view);
260        assert!(validator.has_errors());
261        assert_eq!(validator.errors().len(), 1);
262    }
263
264    #[test]
265    fn test_missing_required_attributes() {
266        let mut validator = TreeViewValidator::new();
267        validator.set_file(PathBuf::from("test.dampen"));
268
269        let tree_view = WidgetNode {
270            kind: WidgetKind::TreeView,
271            id: None,
272            attributes: HashMap::new(),
273            events: vec![],
274            children: vec![create_node_without_id(10), create_node_without_label(15)],
275            span: Span::new(0, 0, 1, 1),
276            style: None,
277            layout: None,
278            theme_ref: None,
279            classes: vec![],
280            breakpoint_attributes: HashMap::new(),
281            inline_state_variants: HashMap::new(),
282        };
283
284        validator.validate_tree_view(&tree_view);
285        assert!(validator.has_errors());
286        assert_eq!(validator.errors().len(), 2);
287    }
288}