dampen_cli/commands/check/
tree_view.rs1use crate::commands::check::errors::CheckError;
3use dampen_core::ir::node::{WidgetKind, WidgetNode};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7#[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#[derive(Debug, Default)]
19pub struct TreeViewValidator {
20 node_ids: HashMap<String, TreeNodeInfo>,
22 errors: Vec<CheckError>,
24 current_file: PathBuf,
26}
27
28impl TreeViewValidator {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn set_file(&mut self, file: PathBuf) {
35 self.current_file = file;
36 }
37
38 pub fn validate_tree_view(&mut self, node: &WidgetNode) {
40 self.node_ids.clear();
42 self.errors.clear();
43
44 for child in &node.children {
46 if child.kind == WidgetKind::TreeNode {
47 self.validate_tree_node(child);
48 }
49 }
50 }
51
52 fn validate_tree_node(&mut self, node: &WidgetNode) {
54 let span = &node.span;
55
56 let id_value = node.id.clone().unwrap_or_default();
59 let label = node.attributes.get("label");
60
61 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 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 if !id_value.is_empty() {
85 if let Some(existing) = self.node_ids.get(&id_value) {
86 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 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 for child in &node.children {
121 if child.kind == WidgetKind::TreeNode {
122 self.validate_tree_node(child);
123 }
124 }
125 }
126
127 pub fn errors(&self) -> &[CheckError] {
129 &self.errors
130 }
131
132 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}