tauri_typegen/analysis/
ast_cache.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use syn::File as SynFile;
4use walkdir::WalkDir;
5
6/// Cache entry for a parsed Rust file
7#[derive(Debug, Clone)]
8pub struct ParsedFile {
9    /// The parsed AST
10    pub ast: SynFile,
11    /// File path for reference
12    pub path: PathBuf,
13    // Last modified time for cache invalidation (if needed later)
14    // modified: std::time::SystemTime,
15}
16
17impl ParsedFile {
18    pub fn new(ast: SynFile, path: PathBuf) -> Self {
19        Self { ast, path }
20    }
21}
22
23/// AST cache for parsed Rust files
24#[derive(Debug, Default)]
25pub struct AstCache {
26    cache: HashMap<PathBuf, ParsedFile>,
27}
28
29impl AstCache {
30    pub fn new() -> Self {
31        Self {
32            cache: HashMap::new(),
33        }
34    }
35
36    /// Parse and cache all Rust files in the given project path
37    pub fn parse_and_cache_all_files(
38        &mut self,
39        project_path: &str,
40        verbose: bool,
41    ) -> Result<(), Box<dyn std::error::Error>> {
42        if verbose {
43            println!("🔄 Parsing and caching all Rust files in: {}", project_path);
44        }
45
46        for entry in WalkDir::new(project_path) {
47            let entry = entry?;
48            let path = entry.path();
49
50            if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
51                // Skip target directory and other build artifacts
52                if path.to_string_lossy().contains("/target/")
53                    || path.to_string_lossy().contains("/.git/")
54                {
55                    continue;
56                }
57
58                if verbose {
59                    println!("📄 Parsing file: {}", path.display());
60                }
61
62                let content = std::fs::read_to_string(path)?;
63                match syn::parse_file(&content) {
64                    Ok(ast) => {
65                        let parsed_file = ParsedFile::new(ast, path.to_path_buf());
66                        self.cache.insert(path.to_path_buf(), parsed_file);
67                        if verbose {
68                            println!("✅ Successfully parsed: {}", path.display());
69                        }
70                    }
71                    Err(e) => {
72                        eprintln!("❌ Failed to parse {}: {}", path.display(), e);
73                        // Continue processing other files even if one fails
74                    }
75                }
76            }
77        }
78
79        if verbose {
80            println!("📊 Cached {} Rust files", self.cache.len());
81        }
82        Ok(())
83    }
84
85    /// Get a parsed file from the cache
86    pub fn get(&self, path: &PathBuf) -> Option<&ParsedFile> {
87        self.cache.get(path)
88    }
89
90    /// Get a cloned parsed file from the cache
91    pub fn get_cloned(&self, path: &PathBuf) -> Option<ParsedFile> {
92        self.cache.get(path).cloned()
93    }
94
95    /// Get all cached file paths
96    pub fn keys(&self) -> std::collections::hash_map::Keys<'_, PathBuf, ParsedFile> {
97        self.cache.keys()
98    }
99
100    /// Get all cached files as an iterator
101    pub fn iter(&self) -> std::collections::hash_map::Iter<'_, PathBuf, ParsedFile> {
102        self.cache.iter()
103    }
104
105    /// Check if a file is cached
106    pub fn contains(&self, path: &PathBuf) -> bool {
107        self.cache.contains_key(path)
108    }
109
110    /// Get the number of cached files
111    pub fn len(&self) -> usize {
112        self.cache.len()
113    }
114
115    /// Check if the cache is empty
116    pub fn is_empty(&self) -> bool {
117        self.cache.is_empty()
118    }
119
120    /// Clear the cache
121    pub fn clear(&mut self) {
122        self.cache.clear();
123    }
124
125    /// Insert a parsed file into the cache
126    pub fn insert(&mut self, path: PathBuf, parsed_file: ParsedFile) -> Option<ParsedFile> {
127        self.cache.insert(path, parsed_file)
128    }
129
130    /// Parse a single file and add it to the cache
131    pub fn parse_and_cache_file(
132        &mut self,
133        file_path: &std::path::Path,
134    ) -> Result<(), Box<dyn std::error::Error>> {
135        let content = std::fs::read_to_string(file_path)?;
136        let ast = syn::parse_file(&content)?;
137        let parsed_file = ParsedFile::new(ast, file_path.to_path_buf());
138        self.cache.insert(file_path.to_path_buf(), parsed_file);
139        Ok(())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::fs;
147    use std::io::Write;
148
149    fn temp_dir() -> String {
150        use std::time::{SystemTime, UNIX_EPOCH};
151        let timestamp = SystemTime::now()
152            .duration_since(UNIX_EPOCH)
153            .unwrap()
154            .as_nanos();
155        format!("./test_ast_cache_{}_{}", std::process::id(), timestamp)
156    }
157
158    fn cleanup_dir(dir: &str) {
159        let _ = fs::remove_dir_all(dir);
160    }
161
162    fn create_rust_file(dir: &str, name: &str, content: &str) -> PathBuf {
163        let path = PathBuf::from(format!("{}/{}", dir, name));
164        if let Some(parent) = path.parent() {
165            fs::create_dir_all(parent).unwrap();
166        }
167        let mut file = fs::File::create(&path).unwrap();
168        file.write_all(content.as_bytes()).unwrap();
169        path
170    }
171
172    mod parsed_file {
173        use super::*;
174
175        #[test]
176        fn test_new_creates_parsed_file() {
177            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
178            let path = PathBuf::from("test.rs");
179            let parsed = ParsedFile::new(ast, path.clone());
180            assert_eq!(parsed.path, path);
181        }
182
183        #[test]
184        fn test_clone_works() {
185            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
186            let path = PathBuf::from("test.rs");
187            let parsed1 = ParsedFile::new(ast, path.clone());
188            let parsed2 = parsed1.clone();
189            assert_eq!(parsed1.path, parsed2.path);
190        }
191    }
192
193    mod initialization {
194        use super::*;
195
196        #[test]
197        fn test_new_creates_empty_cache() {
198            let cache = AstCache::new();
199            assert!(cache.is_empty());
200            assert_eq!(cache.len(), 0);
201        }
202
203        #[test]
204        fn test_default_creates_empty_cache() {
205            let cache = AstCache::default();
206            assert!(cache.is_empty());
207        }
208    }
209
210    mod single_file_operations {
211        use super::*;
212
213        #[test]
214        fn test_parse_and_cache_single_file() {
215            let dir = temp_dir();
216            fs::create_dir_all(&dir).unwrap();
217            let path = create_rust_file(&dir, "test.rs", "fn main() {}");
218
219            let mut cache = AstCache::new();
220            let result = cache.parse_and_cache_file(&path);
221            assert!(result.is_ok());
222            assert_eq!(cache.len(), 1);
223            assert!(cache.contains(&path));
224
225            cleanup_dir(&dir);
226        }
227
228        #[test]
229        fn test_parse_invalid_syntax_errors() {
230            let dir = temp_dir();
231            fs::create_dir_all(&dir).unwrap();
232            let path = create_rust_file(&dir, "invalid.rs", "fn main( {");
233
234            let mut cache = AstCache::new();
235            let result = cache.parse_and_cache_file(&path);
236            assert!(result.is_err());
237            assert_eq!(cache.len(), 0);
238
239            cleanup_dir(&dir);
240        }
241
242        #[test]
243        fn test_parse_nonexistent_file_errors() {
244            let mut cache = AstCache::new();
245            let path = PathBuf::from("nonexistent.rs");
246            let result = cache.parse_and_cache_file(&path);
247            assert!(result.is_err());
248        }
249    }
250
251    mod multi_file_operations {
252        use super::*;
253
254        #[test]
255        fn test_parse_and_cache_all_files() {
256            let dir = temp_dir();
257            fs::create_dir_all(&dir).unwrap();
258
259            create_rust_file(&dir, "lib.rs", "pub fn hello() {}");
260            create_rust_file(&dir, "main.rs", "fn main() {}");
261            create_rust_file(&dir, "mod/types.rs", "struct User {}");
262
263            let mut cache = AstCache::new();
264            let result = cache.parse_and_cache_all_files(&dir, false);
265            assert!(result.is_ok());
266            assert_eq!(cache.len(), 3);
267
268            cleanup_dir(&dir);
269        }
270
271        #[test]
272        fn test_parse_skips_target_directory() {
273            let dir = temp_dir();
274            fs::create_dir_all(format!("{}/target", dir)).unwrap();
275
276            create_rust_file(&dir, "lib.rs", "pub fn hello() {}");
277            create_rust_file(&dir, "target/debug.rs", "fn debug() {}");
278
279            let mut cache = AstCache::new();
280            cache.parse_and_cache_all_files(&dir, false).unwrap();
281
282            // Should only have lib.rs, not target/debug.rs
283            assert_eq!(cache.len(), 1);
284
285            cleanup_dir(&dir);
286        }
287
288        #[test]
289        fn test_parse_skips_git_directory() {
290            let dir = temp_dir();
291            fs::create_dir_all(format!("{}/.git", dir)).unwrap();
292
293            create_rust_file(&dir, "lib.rs", "pub fn hello() {}");
294            create_rust_file(&dir, ".git/hooks.rs", "fn hook() {}");
295
296            let mut cache = AstCache::new();
297            cache.parse_and_cache_all_files(&dir, false).unwrap();
298
299            assert_eq!(cache.len(), 1);
300
301            cleanup_dir(&dir);
302        }
303
304        #[test]
305        fn test_parse_continues_on_syntax_error() {
306            let dir = temp_dir();
307            fs::create_dir_all(&dir).unwrap();
308
309            create_rust_file(&dir, "valid.rs", "fn main() {}");
310            create_rust_file(&dir, "invalid.rs", "fn main( {");
311            create_rust_file(&dir, "valid2.rs", "struct User {}");
312
313            let mut cache = AstCache::new();
314            let result = cache.parse_and_cache_all_files(&dir, false);
315            assert!(result.is_ok());
316            // Should have 2 valid files, skip the invalid one
317            assert_eq!(cache.len(), 2);
318
319            cleanup_dir(&dir);
320        }
321
322        #[test]
323        fn test_parse_with_verbose_output() {
324            let dir = temp_dir();
325            fs::create_dir_all(&dir).unwrap();
326            create_rust_file(&dir, "lib.rs", "pub fn hello() {}");
327
328            let mut cache = AstCache::new();
329            // Just verify it doesn't panic with verbose=true
330            let result = cache.parse_and_cache_all_files(&dir, true);
331            assert!(result.is_ok());
332
333            cleanup_dir(&dir);
334        }
335    }
336
337    mod cache_operations {
338        use super::*;
339
340        #[test]
341        fn test_get_returns_reference() {
342            let mut cache = AstCache::new();
343            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
344            let path = PathBuf::from("test.rs");
345            let parsed = ParsedFile::new(ast, path.clone());
346            cache.insert(path.clone(), parsed);
347
348            let result = cache.get(&path);
349            assert!(result.is_some());
350            assert_eq!(result.unwrap().path, path);
351        }
352
353        #[test]
354        fn test_get_returns_none_for_missing() {
355            let cache = AstCache::new();
356            let path = PathBuf::from("missing.rs");
357            assert!(cache.get(&path).is_none());
358        }
359
360        #[test]
361        fn test_get_cloned_returns_owned() {
362            let mut cache = AstCache::new();
363            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
364            let path = PathBuf::from("test.rs");
365            let parsed = ParsedFile::new(ast, path.clone());
366            cache.insert(path.clone(), parsed);
367
368            let result = cache.get_cloned(&path);
369            assert!(result.is_some());
370            assert_eq!(result.unwrap().path, path);
371        }
372
373        #[test]
374        fn test_contains_checks_presence() {
375            let mut cache = AstCache::new();
376            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
377            let path = PathBuf::from("test.rs");
378            let parsed = ParsedFile::new(ast, path.clone());
379            cache.insert(path.clone(), parsed);
380
381            assert!(cache.contains(&path));
382            assert!(!cache.contains(&PathBuf::from("other.rs")));
383        }
384
385        #[test]
386        fn test_len_returns_count() {
387            let mut cache = AstCache::new();
388            assert_eq!(cache.len(), 0);
389
390            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
391            cache.insert(
392                PathBuf::from("test.rs"),
393                ParsedFile::new(ast, PathBuf::from("test.rs")),
394            );
395            assert_eq!(cache.len(), 1);
396        }
397
398        #[test]
399        fn test_is_empty_checks_emptiness() {
400            let mut cache = AstCache::new();
401            assert!(cache.is_empty());
402
403            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
404            cache.insert(
405                PathBuf::from("test.rs"),
406                ParsedFile::new(ast, PathBuf::from("test.rs")),
407            );
408            assert!(!cache.is_empty());
409        }
410
411        #[test]
412        fn test_clear_empties_cache() {
413            let mut cache = AstCache::new();
414            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
415            cache.insert(
416                PathBuf::from("test.rs"),
417                ParsedFile::new(ast, PathBuf::from("test.rs")),
418            );
419
420            assert!(!cache.is_empty());
421            cache.clear();
422            assert!(cache.is_empty());
423        }
424
425        #[test]
426        fn test_insert_returns_old_value() {
427            let mut cache = AstCache::new();
428            let ast1: SynFile = syn::parse_str("fn main() {}").unwrap();
429            let ast2: SynFile = syn::parse_str("fn test() {}").unwrap();
430            let path = PathBuf::from("test.rs");
431
432            let old = cache.insert(path.clone(), ParsedFile::new(ast1, path.clone()));
433            assert!(old.is_none());
434
435            let old = cache.insert(path.clone(), ParsedFile::new(ast2, path.clone()));
436            assert!(old.is_some());
437        }
438
439        #[test]
440        fn test_keys_returns_iterator() {
441            let mut cache = AstCache::new();
442            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
443            let path1 = PathBuf::from("test1.rs");
444            let path2 = PathBuf::from("test2.rs");
445
446            cache.insert(path1.clone(), ParsedFile::new(ast.clone(), path1.clone()));
447            cache.insert(path2.clone(), ParsedFile::new(ast.clone(), path2.clone()));
448
449            let keys: Vec<_> = cache.keys().collect();
450            assert_eq!(keys.len(), 2);
451        }
452
453        #[test]
454        fn test_iter_returns_iterator() {
455            let mut cache = AstCache::new();
456            let ast: SynFile = syn::parse_str("fn main() {}").unwrap();
457            let path = PathBuf::from("test.rs");
458            cache.insert(path.clone(), ParsedFile::new(ast, path.clone()));
459
460            let count = cache.iter().count();
461            assert_eq!(count, 1);
462        }
463    }
464
465    mod edge_cases {
466        use super::*;
467
468        #[test]
469        fn test_empty_directory() {
470            let dir = temp_dir();
471            fs::create_dir_all(&dir).unwrap();
472
473            let mut cache = AstCache::new();
474            let result = cache.parse_and_cache_all_files(&dir, false);
475            assert!(result.is_ok());
476            assert_eq!(cache.len(), 0);
477
478            cleanup_dir(&dir);
479        }
480
481        #[test]
482        fn test_directory_with_only_non_rust_files() {
483            let dir = temp_dir();
484            fs::create_dir_all(&dir).unwrap();
485            create_rust_file(&dir, "readme.txt", "Hello");
486            create_rust_file(&dir, "config.json", "{}");
487
488            let mut cache = AstCache::new();
489            cache.parse_and_cache_all_files(&dir, false).unwrap();
490            assert_eq!(cache.len(), 0);
491
492            cleanup_dir(&dir);
493        }
494
495        #[test]
496        fn test_parse_empty_rust_file() {
497            let dir = temp_dir();
498            fs::create_dir_all(&dir).unwrap();
499            let path = create_rust_file(&dir, "empty.rs", "");
500
501            let mut cache = AstCache::new();
502            let result = cache.parse_and_cache_file(&path);
503            assert!(result.is_ok());
504            assert_eq!(cache.len(), 1);
505
506            cleanup_dir(&dir);
507        }
508
509        #[test]
510        fn test_cache_same_file_twice() {
511            let dir = temp_dir();
512            fs::create_dir_all(&dir).unwrap();
513            let path = create_rust_file(&dir, "test.rs", "fn main() {}");
514
515            let mut cache = AstCache::new();
516            cache.parse_and_cache_file(&path).unwrap();
517            cache.parse_and_cache_file(&path).unwrap();
518
519            // Should still be 1 (overwritten)
520            assert_eq!(cache.len(), 1);
521
522            cleanup_dir(&dir);
523        }
524    }
525}