dampen_cli/commands/check/
themes.rs1use crate::commands::check::errors::CheckError;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone)]
8struct StyleClassInfo {
9 _name: String,
10 extends: Vec<String>,
11 file: PathBuf,
12 line: u32,
13 col: u32,
14}
15
16#[derive(Debug, Clone)]
18struct ThemePropertyError {
19 theme_name: String,
20 property: String,
21 message: String,
22 file: PathBuf,
23 line: u32,
24 col: u32,
25}
26
27#[derive(Debug, Default)]
29pub struct ThemeValidator {
30 style_classes: HashMap<String, StyleClassInfo>,
31 property_errors: Vec<ThemePropertyError>,
32}
33
34impl ThemeValidator {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn add_style_class(
41 &mut self,
42 name: &str,
43 extends: Vec<String>,
44 file: &str,
45 line: u32,
46 col: u32,
47 ) {
48 let info = StyleClassInfo {
49 _name: name.to_string(),
50 extends,
51 file: PathBuf::from(file),
52 line,
53 col,
54 };
55 self.style_classes.insert(name.to_string(), info);
56 }
57
58 pub fn add_invalid_theme_property(
60 &mut self,
61 theme_name: &str,
62 property: &str,
63 value: &str,
64 file: &str,
65 line: u32,
66 col: u32,
67 ) -> Result<(), String> {
68 let error = ThemePropertyError {
70 theme_name: theme_name.to_string(),
71 property: property.to_string(),
72 message: format!("Invalid value '{}' for property '{}'", value, property),
73 file: PathBuf::from(file),
74 line,
75 col,
76 };
77 self.property_errors.push(error);
78 Err(format!("Invalid property: {}", property))
79 }
80
81 pub fn validate(&self) -> Vec<CheckError> {
83 let mut errors = Vec::new();
84
85 for prop_error in &self.property_errors {
87 errors.push(CheckError::InvalidThemeProperty {
88 property: prop_error.property.clone(),
89 theme: prop_error.theme_name.clone(),
90 file: prop_error.file.clone(),
91 line: prop_error.line,
92 col: prop_error.col,
93 message: prop_error.message.clone(),
94 valid_properties: String::new(),
95 });
96 }
97
98 for class_name in self.style_classes.keys() {
100 let mut path = Vec::new();
101 if let Err(cycle_error) = self.check_circular_dependency(class_name, &mut path) {
102 errors.push(cycle_error);
103 }
104 }
105
106 for (class_name, class_info) in &self.style_classes {
108 for parent in &class_info.extends {
109 if !self.style_classes.contains_key(parent) {
110 errors.push(CheckError::InvalidThemeProperty {
111 property: "extends".to_string(),
112 theme: class_name.clone(),
113 file: class_info.file.clone(),
114 line: class_info.line,
115 col: class_info.col,
116 message: format!("Parent class '{}' not found", parent),
117 valid_properties: self
118 .style_classes
119 .keys()
120 .cloned()
121 .collect::<Vec<_>>()
122 .join(", "),
123 });
124 }
125 }
126 }
127
128 errors
129 }
130
131 #[allow(clippy::result_large_err)]
133 fn check_circular_dependency(
134 &self,
135 class_name: &str,
136 path: &mut Vec<String>,
137 ) -> Result<(), CheckError> {
138 if path.contains(&class_name.to_string()) {
139 let cycle = format!("{} → {}", path.join(" → "), class_name);
141 return Err(CheckError::ThemeCircularDependency {
142 theme: class_name.to_string(),
143 cycle,
144 });
145 }
146
147 if let Some(class_info) = self.style_classes.get(class_name) {
148 path.push(class_name.to_string());
149
150 for parent in &class_info.extends {
151 self.check_circular_dependency(parent, path)?;
152 }
153
154 path.pop();
155 }
156
157 Ok(())
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_simple_circular_dependency() {
167 let mut validator = ThemeValidator::new();
168
169 validator.add_style_class("a", vec!["b".to_string()], "test.dampen", 1, 1);
171 validator.add_style_class("b", vec!["a".to_string()], "test.dampen", 2, 1);
172
173 let errors = validator.validate();
174 assert!(!errors.is_empty());
175
176 let has_circular = errors
177 .iter()
178 .any(|e| matches!(e, CheckError::ThemeCircularDependency { .. }));
179 assert!(has_circular);
180 }
181
182 #[test]
183 fn test_no_circular_dependency() {
184 let mut validator = ThemeValidator::new();
185
186 validator.add_style_class("a", vec!["b".to_string()], "test.dampen", 1, 1);
188 validator.add_style_class("b", vec!["c".to_string()], "test.dampen", 2, 1);
189 validator.add_style_class("c", vec![], "test.dampen", 3, 1);
190
191 let errors = validator.validate();
192
193 let has_circular = errors
194 .iter()
195 .any(|e| matches!(e, CheckError::ThemeCircularDependency { .. }));
196 assert!(!has_circular);
197 }
198
199 #[test]
200 fn test_missing_parent_class() {
201 let mut validator = ThemeValidator::new();
202
203 validator.add_style_class("a", vec!["nonexistent".to_string()], "test.dampen", 1, 1);
204
205 let errors = validator.validate();
206 assert!(!errors.is_empty());
207
208 let has_invalid_property = errors
209 .iter()
210 .any(|e| matches!(e, CheckError::InvalidThemeProperty { .. }));
211 assert!(has_invalid_property);
212 }
213}