Skip to main content

dampen_cli/commands/check/
themes.rs

1// Theme property validation and circular dependency detection
2use crate::commands::check::errors::CheckError;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Information about a style class for validation
7#[derive(Debug, Clone)]
8struct StyleClassInfo {
9    _name: String,
10    extends: Vec<String>,
11    file: PathBuf,
12    line: u32,
13    col: u32,
14}
15
16/// Information about a theme property error
17#[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/// Validator for theme properties and style class dependencies
28#[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    /// Add a style class for validation
40    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    /// Add an invalid theme property error
59    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        // For now, just record the error
69        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    /// Validate all themes and style classes
82    pub fn validate(&self) -> Vec<CheckError> {
83        let mut errors = Vec::new();
84
85        // Add property errors
86        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        // Check for circular dependencies
99        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        // Check for missing parent classes
107        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    /// Check for circular dependencies in style class inheritance
132    #[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            // Found a cycle
140            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        // a -> b -> a
170        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        // a -> b -> c (no cycle)
187        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}