tcss_core/
import.rs

1//! Import system for TCSS
2//!
3//! This module handles processing @import statements:
4//! - Loading imported files
5//! - Parsing imported content
6//! - Circular import detection
7//! - Import caching
8//! - Merging imported ASTs
9
10use crate::ast::{ASTNode, Program};
11use crate::lexer::Lexer;
12use crate::parser::Parser;
13use crate::resolver::{ImportType, Resolver};
14use std::collections::{HashMap, HashSet};
15use std::fs;
16use std::path::Path;
17
18/// Import cache entry
19#[derive(Debug, Clone)]
20struct CacheEntry {
21    /// Parsed program
22    program: Program,
23    
24    /// File modification time (for cache invalidation)
25    #[allow(dead_code)]
26    modified: Option<std::time::SystemTime>,
27}
28
29/// Import processor with caching and circular detection
30pub struct ImportProcessor {
31    /// Path resolver
32    resolver: Resolver,
33    
34    /// Cache of parsed imports
35    cache: HashMap<String, CacheEntry>,
36    
37    /// Currently processing imports (for circular detection)
38    processing: HashSet<String>,
39    
40    /// Enable caching
41    cache_enabled: bool,
42}
43
44impl ImportProcessor {
45    /// Create a new import processor
46    pub fn new<P: AsRef<Path>>(base_dir: P) -> Self {
47        Self {
48            resolver: Resolver::new(base_dir),
49            cache: HashMap::new(),
50            processing: HashSet::new(),
51            cache_enabled: true,
52        }
53    }
54    
55    /// Enable or disable caching
56    pub fn set_cache_enabled(&mut self, enabled: bool) {
57        self.cache_enabled = enabled;
58    }
59    
60    /// Clear the import cache
61    pub fn clear_cache(&mut self) {
62        self.cache.clear();
63    }
64    
65    /// Process all imports in a program
66    pub fn process_imports(&mut self, program: &mut Program) -> Result<(), String> {
67        // Find all import nodes
68        let mut imports_to_process = Vec::new();
69        
70        for (index, node) in program.nodes.iter().enumerate() {
71            if let ASTNode::Import { path } = node {
72                imports_to_process.push((index, path.clone()));
73            }
74        }
75        
76        // Process imports in reverse order to maintain correct indices
77        for (index, path) in imports_to_process.iter().rev() {
78            let imported_program = self.load_import(path)?;
79            
80            // Remove the import node
81            program.nodes.remove(*index);
82            
83            // Insert imported nodes at the same position
84            for (offset, node) in imported_program.nodes.iter().enumerate() {
85                program.nodes.insert(index + offset, node.clone());
86            }
87        }
88        
89        Ok(())
90    }
91    
92    /// Load and parse an import
93    fn load_import(&mut self, path: &str) -> Result<Program, String> {
94        // Resolve the import path
95        let resolved = self.resolver.resolve(path)?;
96        
97        // Check for circular imports
98        if self.processing.contains(&resolved.resolved_path) {
99            return Err(format!("Circular import detected: {}", path));
100        }
101        
102        // Check cache
103        if self.cache_enabled {
104            if let Some(entry) = self.cache.get(&resolved.resolved_path) {
105                return Ok(entry.program.clone());
106            }
107        }
108        
109        // Mark as processing
110        self.processing.insert(resolved.resolved_path.clone());
111        
112        // Load the file content
113        let content = match resolved.import_type {
114            ImportType::Url => self.load_url(&resolved.resolved_path)?,
115            _ => self.load_file(&resolved.resolved_path)?,
116        };
117        
118        // Parse the content
119        let mut lexer = Lexer::new(&content);
120        let tokens = lexer.tokenize()
121            .map_err(|e| format!("Error tokenizing {}: {}", path, e))?;
122        
123        let mut parser = Parser::new(tokens);
124        let mut program = parser.parse()
125            .map_err(|e| format!("Error parsing {}: {}", path, e))?;
126        
127        // Update resolver base directory for nested imports
128        let old_base = self.resolver.base_dir().to_path_buf();
129        if let ImportType::Relative | ImportType::Absolute = resolved.import_type {
130            if let Some(parent) = Path::new(&resolved.resolved_path).parent() {
131                self.resolver.set_base_dir(parent);
132            }
133        }
134        
135        // Recursively process imports in the imported file
136        self.process_imports(&mut program)?;
137        
138        // Restore base directory
139        self.resolver.set_base_dir(old_base);
140        
141        // Remove from processing set
142        self.processing.remove(&resolved.resolved_path);
143        
144        // Cache the result
145        if self.cache_enabled {
146            self.cache.insert(
147                resolved.resolved_path.clone(),
148                CacheEntry {
149                    program: program.clone(),
150                    modified: None,
151                },
152            );
153        }
154        
155        Ok(program)
156    }
157
158    /// Load content from a file
159    fn load_file(&self, path: &str) -> Result<String, String> {
160        fs::read_to_string(path)
161            .map_err(|e| format!("Failed to read file {}: {}", path, e))
162    }
163
164    /// Load content from a URL
165    fn load_url(&self, url: &str) -> Result<String, String> {
166        // For now, return an error as URL loading requires HTTP client
167        // In a real implementation, use reqwest or similar
168        Err(format!(
169            "URL imports not yet supported in this environment: {}. \
170            To enable URL imports, add an HTTP client dependency.",
171            url
172        ))
173    }
174
175    /// Get the resolver (for testing)
176    pub fn resolver(&self) -> &Resolver {
177        &self.resolver
178    }
179
180    /// Get mutable resolver
181    pub fn resolver_mut(&mut self) -> &mut Resolver {
182        &mut self.resolver
183    }
184}
185
186impl Default for ImportProcessor {
187    fn default() -> Self {
188        Self::new(".")
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::fs;
196    use std::io::Write;
197    use tempfile::TempDir;
198
199    #[test]
200    fn test_process_simple_import() {
201        let temp_dir = TempDir::new().unwrap();
202        let base_path = temp_dir.path();
203
204        // Create a file to import
205        let import_file = base_path.join("colors.tcss");
206        let mut file = fs::File::create(&import_file).unwrap();
207        writeln!(file, "@var primary: #3498db").unwrap();
208        writeln!(file, "@var secondary: #2ecc71").unwrap();
209
210        // Create main file with import
211        let main_content = "@import './colors.tcss'\n\n.button:\n    background: primary";
212
213        let mut lexer = Lexer::new(main_content);
214        let tokens = lexer.tokenize().unwrap();
215        let mut parser = Parser::new(tokens);
216        let mut program = parser.parse().unwrap();
217
218        // Process imports
219        let mut processor = ImportProcessor::new(base_path);
220        processor.process_imports(&mut program).unwrap();
221
222        // Should have 3 nodes: 2 variables + 1 CSS rule
223        assert_eq!(program.nodes.len(), 3);
224    }
225
226    #[test]
227    fn test_circular_import_detection() {
228        let temp_dir = TempDir::new().unwrap();
229        let base_path = temp_dir.path();
230
231        // Create file A that imports B
232        let file_a = base_path.join("a.tcss");
233        let mut f = fs::File::create(&file_a).unwrap();
234        writeln!(f, "@import './b.tcss'").unwrap();
235
236        // Create file B that imports A (circular)
237        let file_b = base_path.join("b.tcss");
238        let mut f = fs::File::create(&file_b).unwrap();
239        writeln!(f, "@import './a.tcss'").unwrap();
240
241        // Try to process
242        let mut lexer = Lexer::new("@import './a.tcss'");
243        let tokens = lexer.tokenize().unwrap();
244        let mut parser = Parser::new(tokens);
245        let mut program = parser.parse().unwrap();
246
247        let mut processor = ImportProcessor::new(base_path);
248        let result = processor.process_imports(&mut program);
249
250        assert!(result.is_err());
251        assert!(result.unwrap_err().contains("Circular import"));
252    }
253
254    #[test]
255    fn test_cache() {
256        let temp_dir = TempDir::new().unwrap();
257        let base_path = temp_dir.path();
258
259        // Create a file to import
260        let import_file = base_path.join("vars.tcss");
261        let mut file = fs::File::create(&import_file).unwrap();
262        writeln!(file, "@var x: 10").unwrap();
263
264        let mut processor = ImportProcessor::new(base_path);
265
266        // First load
267        let result1 = processor.load_import("./vars.tcss").unwrap();
268        assert_eq!(processor.cache.len(), 1);
269
270        // Second load (should use cache)
271        let result2 = processor.load_import("./vars.tcss").unwrap();
272        assert_eq!(result1.nodes.len(), result2.nodes.len());
273
274        // Clear cache
275        processor.clear_cache();
276        assert_eq!(processor.cache.len(), 0);
277    }
278}
279