1use std::collections::HashMap;
2use std::path::PathBuf;
3use syn::File as SynFile;
4use walkdir::WalkDir;
5
6#[derive(Debug, Clone)]
8pub struct ParsedFile {
9 pub ast: SynFile,
11 pub path: PathBuf,
13 }
16
17impl ParsedFile {
18 pub fn new(ast: SynFile, path: PathBuf) -> Self {
19 Self { ast, path }
20 }
21}
22
23#[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 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 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 }
75 }
76 }
77 }
78
79 if verbose {
80 println!("📊 Cached {} Rust files", self.cache.len());
81 }
82 Ok(())
83 }
84
85 pub fn get(&self, path: &PathBuf) -> Option<&ParsedFile> {
87 self.cache.get(path)
88 }
89
90 pub fn get_cloned(&self, path: &PathBuf) -> Option<ParsedFile> {
92 self.cache.get(path).cloned()
93 }
94
95 pub fn keys(&self) -> std::collections::hash_map::Keys<'_, PathBuf, ParsedFile> {
97 self.cache.keys()
98 }
99
100 pub fn iter(&self) -> std::collections::hash_map::Iter<'_, PathBuf, ParsedFile> {
102 self.cache.iter()
103 }
104
105 pub fn contains(&self, path: &PathBuf) -> bool {
107 self.cache.contains_key(path)
108 }
109
110 pub fn len(&self) -> usize {
112 self.cache.len()
113 }
114
115 pub fn is_empty(&self) -> bool {
117 self.cache.is_empty()
118 }
119
120 pub fn clear(&mut self) {
122 self.cache.clear();
123 }
124
125 pub fn insert(&mut self, path: PathBuf, parsed_file: ParsedFile) -> Option<ParsedFile> {
127 self.cache.insert(path, parsed_file)
128 }
129
130 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 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 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 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 assert_eq!(cache.len(), 1);
521
522 cleanup_dir(&dir);
523 }
524 }
525}