1use std::path::Path;
8
9use rustc_hash::FxHashMap;
10
11use bincode::{Decode, Encode};
12
13use oxc_span::Span;
14
15use crate::{ExportName, MemberAccess, MemberKind};
16
17const CACHE_VERSION: u32 = 12;
19
20const MAX_CACHE_SIZE: usize = 256 * 1024 * 1024;
22
23#[derive(Debug, Encode, Decode)]
25pub struct CacheStore {
26 version: u32,
27 entries: FxHashMap<String, CachedModule>,
29}
30
31#[derive(Debug, Clone, Encode, Decode)]
33pub struct CachedModule {
34 pub content_hash: u64,
36 pub mtime_secs: u64,
39 pub file_size: u64,
41 pub exports: Vec<CachedExport>,
43 pub imports: Vec<CachedImport>,
45 pub re_exports: Vec<CachedReExport>,
47 pub dynamic_imports: Vec<CachedDynamicImport>,
49 pub require_calls: Vec<CachedRequireCall>,
51 pub member_accesses: Vec<MemberAccess>,
53 pub whole_object_uses: Vec<String>,
55 pub dynamic_import_patterns: Vec<CachedDynamicImportPattern>,
57 pub has_cjs_exports: bool,
59 pub unused_import_bindings: Vec<String>,
61 pub suppressions: Vec<CachedSuppression>,
63 pub line_offsets: Vec<u32>,
65}
66
67#[derive(Debug, Clone, Encode, Decode)]
69pub struct CachedSuppression {
70 pub line: u32,
72 pub kind: u8,
74}
75
76#[derive(Debug, Clone, Encode, Decode)]
78pub struct CachedExport {
79 pub name: String,
81 pub is_default: bool,
83 pub is_type_only: bool,
85 pub local_name: Option<String>,
87 pub span_start: u32,
89 pub span_end: u32,
91 pub members: Vec<CachedMember>,
93}
94
95const IMPORT_KIND_NAMED: u8 = 0;
98const IMPORT_KIND_DEFAULT: u8 = 1;
99const IMPORT_KIND_NAMESPACE: u8 = 2;
100const IMPORT_KIND_SIDE_EFFECT: u8 = 3;
101
102#[derive(Debug, Clone, Encode, Decode)]
104pub struct CachedImport {
105 pub source: String,
107 pub imported_name: String,
109 pub local_name: String,
111 pub is_type_only: bool,
113 pub kind: u8,
115 pub span_start: u32,
117 pub span_end: u32,
119}
120
121#[derive(Debug, Clone, Encode, Decode)]
123pub struct CachedDynamicImport {
124 pub source: String,
126 pub span_start: u32,
128 pub span_end: u32,
130 pub destructured_names: Vec<String>,
132 pub local_name: Option<String>,
134}
135
136#[derive(Debug, Clone, Encode, Decode)]
138pub struct CachedRequireCall {
139 pub source: String,
141 pub span_start: u32,
143 pub span_end: u32,
145 pub destructured_names: Vec<String>,
147 pub local_name: Option<String>,
149}
150
151#[derive(Debug, Clone, Encode, Decode)]
153pub struct CachedReExport {
154 pub source: String,
156 pub imported_name: String,
158 pub exported_name: String,
160 pub is_type_only: bool,
162}
163
164#[derive(Debug, Clone, Encode, Decode)]
166pub struct CachedMember {
167 pub name: String,
169 pub kind: MemberKind,
171 pub span_start: u32,
173 pub span_end: u32,
175 pub has_decorator: bool,
177}
178
179#[derive(Debug, Clone, Encode, Decode)]
181pub struct CachedDynamicImportPattern {
182 pub prefix: String,
184 pub suffix: Option<String>,
186 pub span_start: u32,
188 pub span_end: u32,
190}
191
192impl CacheStore {
193 pub fn new() -> Self {
195 Self {
196 version: CACHE_VERSION,
197 entries: FxHashMap::default(),
198 }
199 }
200
201 pub fn load(cache_dir: &Path) -> Option<Self> {
203 let cache_file = cache_dir.join("cache.bin");
204 let data = std::fs::read(&cache_file).ok()?;
205 if data.len() > MAX_CACHE_SIZE {
206 tracing::warn!(
207 size_mb = data.len() / (1024 * 1024),
208 "Cache file exceeds size limit, ignoring"
209 );
210 return None;
211 }
212 let (store, _): (Self, usize) =
213 bincode::decode_from_slice(&data, bincode::config::standard()).ok()?;
214 if store.version != CACHE_VERSION {
215 return None;
216 }
217 Some(store)
218 }
219
220 pub fn save(&self, cache_dir: &Path) -> Result<(), String> {
222 std::fs::create_dir_all(cache_dir)
223 .map_err(|e| format!("Failed to create cache dir: {e}"))?;
224 let cache_file = cache_dir.join("cache.bin");
225 let data = bincode::encode_to_vec(self, bincode::config::standard())
226 .map_err(|e| format!("Failed to serialize cache: {e}"))?;
227 std::fs::write(&cache_file, data).map_err(|e| format!("Failed to write cache: {e}"))?;
228 Ok(())
229 }
230
231 pub fn get(&self, path: &Path, content_hash: u64) -> Option<&CachedModule> {
234 let key = path.to_string_lossy().to_string();
235 let entry = self.entries.get(&key)?;
236 if entry.content_hash == content_hash {
237 Some(entry)
238 } else {
239 None
240 }
241 }
242
243 pub fn insert(&mut self, path: &Path, module: CachedModule) {
245 let key = path.to_string_lossy().to_string();
246 self.entries.insert(key, module);
247 }
248
249 pub fn get_by_metadata(
256 &self,
257 path: &Path,
258 mtime_secs: u64,
259 file_size: u64,
260 ) -> Option<&CachedModule> {
261 let key = path.to_string_lossy().to_string();
262 let entry = self.entries.get(&key)?;
263 if entry.mtime_secs == mtime_secs && entry.file_size == file_size && mtime_secs > 0 {
264 Some(entry)
265 } else {
266 None
267 }
268 }
269
270 pub fn get_by_path_only(&self, path: &Path) -> Option<&CachedModule> {
274 let key = path.to_string_lossy().to_string();
275 self.entries.get(&key)
276 }
277
278 pub fn retain_paths(&mut self, files: &[fallow_types::discover::DiscoveredFile]) {
281 use rustc_hash::FxHashSet;
282 let current_paths: FxHashSet<String> = files
283 .iter()
284 .map(|f| f.path.to_string_lossy().to_string())
285 .collect();
286 self.entries.retain(|key, _| current_paths.contains(key));
287 }
288
289 pub fn len(&self) -> usize {
291 self.entries.len()
292 }
293
294 pub fn is_empty(&self) -> bool {
296 self.entries.is_empty()
297 }
298}
299
300impl Default for CacheStore {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306pub fn cached_to_module(
308 cached: &CachedModule,
309 file_id: fallow_types::discover::FileId,
310) -> crate::ModuleInfo {
311 use crate::*;
312
313 let exports = cached
314 .exports
315 .iter()
316 .map(|e| ExportInfo {
317 name: if e.is_default {
318 ExportName::Default
319 } else {
320 ExportName::Named(e.name.clone())
321 },
322 local_name: e.local_name.clone(),
323 is_type_only: e.is_type_only,
324 span: Span::new(e.span_start, e.span_end),
325 members: e
326 .members
327 .iter()
328 .map(|m| MemberInfo {
329 name: m.name.clone(),
330 kind: m.kind.clone(),
331 span: Span::new(m.span_start, m.span_end),
332 has_decorator: m.has_decorator,
333 })
334 .collect(),
335 })
336 .collect();
337
338 let imports = cached
339 .imports
340 .iter()
341 .map(|i| ImportInfo {
342 source: i.source.clone(),
343 imported_name: match i.kind {
344 IMPORT_KIND_DEFAULT => ImportedName::Default,
345 IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
346 IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
347 _ => ImportedName::Named(i.imported_name.clone()),
349 },
350 local_name: i.local_name.clone(),
351 is_type_only: i.is_type_only,
352 span: Span::new(i.span_start, i.span_end),
353 })
354 .collect();
355
356 let re_exports = cached
357 .re_exports
358 .iter()
359 .map(|r| ReExportInfo {
360 source: r.source.clone(),
361 imported_name: r.imported_name.clone(),
362 exported_name: r.exported_name.clone(),
363 is_type_only: r.is_type_only,
364 })
365 .collect();
366
367 let dynamic_imports = cached
368 .dynamic_imports
369 .iter()
370 .map(|d| DynamicImportInfo {
371 source: d.source.clone(),
372 span: Span::new(d.span_start, d.span_end),
373 destructured_names: d.destructured_names.clone(),
374 local_name: d.local_name.clone(),
375 })
376 .collect();
377
378 let require_calls = cached
379 .require_calls
380 .iter()
381 .map(|r| RequireCallInfo {
382 source: r.source.clone(),
383 span: Span::new(r.span_start, r.span_end),
384 destructured_names: r.destructured_names.clone(),
385 local_name: r.local_name.clone(),
386 })
387 .collect();
388
389 let dynamic_import_patterns = cached
390 .dynamic_import_patterns
391 .iter()
392 .map(|p| crate::DynamicImportPattern {
393 prefix: p.prefix.clone(),
394 suffix: p.suffix.clone(),
395 span: Span::new(p.span_start, p.span_end),
396 })
397 .collect();
398
399 let suppressions = cached
400 .suppressions
401 .iter()
402 .map(|s| crate::suppress::Suppression {
403 line: s.line,
404 kind: if s.kind == 0 {
405 None
406 } else {
407 crate::suppress::IssueKind::from_discriminant(s.kind)
408 },
409 })
410 .collect();
411
412 ModuleInfo {
413 file_id,
414 exports,
415 imports,
416 re_exports,
417 dynamic_imports,
418 dynamic_import_patterns,
419 require_calls,
420 member_accesses: cached.member_accesses.clone(),
421 whole_object_uses: cached.whole_object_uses.clone(),
422 has_cjs_exports: cached.has_cjs_exports,
423 content_hash: cached.content_hash,
424 suppressions,
425 unused_import_bindings: cached.unused_import_bindings.clone(),
426 line_offsets: cached.line_offsets.clone(),
427 }
428}
429
430pub fn module_to_cached(
436 module: &crate::ModuleInfo,
437 mtime_secs: u64,
438 file_size: u64,
439) -> CachedModule {
440 CachedModule {
441 content_hash: module.content_hash,
442 mtime_secs,
443 file_size,
444 exports: module
445 .exports
446 .iter()
447 .map(|e| CachedExport {
448 name: match &e.name {
449 ExportName::Named(n) => n.clone(),
450 ExportName::Default => String::new(),
451 },
452 is_default: matches!(e.name, ExportName::Default),
453 is_type_only: e.is_type_only,
454 local_name: e.local_name.clone(),
455 span_start: e.span.start,
456 span_end: e.span.end,
457 members: e
458 .members
459 .iter()
460 .map(|m| CachedMember {
461 name: m.name.clone(),
462 kind: m.kind.clone(),
463 span_start: m.span.start,
464 span_end: m.span.end,
465 has_decorator: m.has_decorator,
466 })
467 .collect(),
468 })
469 .collect(),
470 imports: module
471 .imports
472 .iter()
473 .map(|i| {
474 let (kind, imported_name) = match &i.imported_name {
475 crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
476 crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
477 crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
478 crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
479 };
480 CachedImport {
481 source: i.source.clone(),
482 imported_name,
483 local_name: i.local_name.clone(),
484 is_type_only: i.is_type_only,
485 kind,
486 span_start: i.span.start,
487 span_end: i.span.end,
488 }
489 })
490 .collect(),
491 re_exports: module
492 .re_exports
493 .iter()
494 .map(|r| CachedReExport {
495 source: r.source.clone(),
496 imported_name: r.imported_name.clone(),
497 exported_name: r.exported_name.clone(),
498 is_type_only: r.is_type_only,
499 })
500 .collect(),
501 dynamic_imports: module
502 .dynamic_imports
503 .iter()
504 .map(|d| CachedDynamicImport {
505 source: d.source.clone(),
506 span_start: d.span.start,
507 span_end: d.span.end,
508 destructured_names: d.destructured_names.clone(),
509 local_name: d.local_name.clone(),
510 })
511 .collect(),
512 require_calls: module
513 .require_calls
514 .iter()
515 .map(|r| CachedRequireCall {
516 source: r.source.clone(),
517 span_start: r.span.start,
518 span_end: r.span.end,
519 destructured_names: r.destructured_names.clone(),
520 local_name: r.local_name.clone(),
521 })
522 .collect(),
523 member_accesses: module.member_accesses.clone(),
524 whole_object_uses: module.whole_object_uses.clone(),
525 dynamic_import_patterns: module
526 .dynamic_import_patterns
527 .iter()
528 .map(|p| CachedDynamicImportPattern {
529 prefix: p.prefix.clone(),
530 suffix: p.suffix.clone(),
531 span_start: p.span.start,
532 span_end: p.span.end,
533 })
534 .collect(),
535 has_cjs_exports: module.has_cjs_exports,
536 unused_import_bindings: module.unused_import_bindings.clone(),
537 suppressions: module
538 .suppressions
539 .iter()
540 .map(|s| CachedSuppression {
541 line: s.line,
542 kind: s.kind.map_or(0, |k| k.to_discriminant()),
543 })
544 .collect(),
545 line_offsets: module.line_offsets.clone(),
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::*;
553 use fallow_types::discover::FileId;
554
555 #[test]
556 fn cache_store_new_is_empty() {
557 let store = CacheStore::new();
558 assert!(store.is_empty());
559 assert_eq!(store.len(), 0);
560 }
561
562 #[test]
563 fn cache_store_default_is_empty() {
564 let store = CacheStore::default();
565 assert!(store.is_empty());
566 }
567
568 #[test]
569 fn cache_store_insert_and_get() {
570 let mut store = CacheStore::new();
571 let module = CachedModule {
572 content_hash: 42,
573 mtime_secs: 0,
574 file_size: 0,
575 exports: vec![],
576 imports: vec![],
577 re_exports: vec![],
578 dynamic_imports: vec![],
579 require_calls: vec![],
580 member_accesses: vec![],
581 whole_object_uses: vec![],
582 dynamic_import_patterns: vec![],
583 has_cjs_exports: false,
584 unused_import_bindings: vec![],
585 suppressions: vec![],
586 line_offsets: vec![],
587 };
588 store.insert(Path::new("test.ts"), module);
589 assert_eq!(store.len(), 1);
590 assert!(!store.is_empty());
591 assert!(store.get(Path::new("test.ts"), 42).is_some());
592 }
593
594 #[test]
595 fn cache_store_hash_mismatch_returns_none() {
596 let mut store = CacheStore::new();
597 let module = CachedModule {
598 content_hash: 42,
599 mtime_secs: 0,
600 file_size: 0,
601 exports: vec![],
602 imports: vec![],
603 re_exports: vec![],
604 dynamic_imports: vec![],
605 require_calls: vec![],
606 member_accesses: vec![],
607 whole_object_uses: vec![],
608 dynamic_import_patterns: vec![],
609 has_cjs_exports: false,
610 unused_import_bindings: vec![],
611 suppressions: vec![],
612 line_offsets: vec![],
613 };
614 store.insert(Path::new("test.ts"), module);
615 assert!(store.get(Path::new("test.ts"), 99).is_none());
616 }
617
618 #[test]
619 fn cache_store_missing_key_returns_none() {
620 let store = CacheStore::new();
621 assert!(store.get(Path::new("nonexistent.ts"), 42).is_none());
622 }
623
624 #[test]
625 fn cache_store_overwrite_entry() {
626 let mut store = CacheStore::new();
627 let m1 = CachedModule {
628 content_hash: 1,
629 mtime_secs: 0,
630 file_size: 0,
631 exports: vec![],
632 imports: vec![],
633 re_exports: vec![],
634 dynamic_imports: vec![],
635 require_calls: vec![],
636 member_accesses: vec![],
637 whole_object_uses: vec![],
638 dynamic_import_patterns: vec![],
639 has_cjs_exports: false,
640 unused_import_bindings: vec![],
641 suppressions: vec![],
642 line_offsets: vec![],
643 };
644 let m2 = CachedModule {
645 content_hash: 2,
646 mtime_secs: 0,
647 file_size: 0,
648 exports: vec![],
649 imports: vec![],
650 re_exports: vec![],
651 dynamic_imports: vec![],
652 require_calls: vec![],
653 member_accesses: vec![],
654 whole_object_uses: vec![],
655 dynamic_import_patterns: vec![],
656 has_cjs_exports: false,
657 unused_import_bindings: vec![],
658 suppressions: vec![],
659 line_offsets: vec![],
660 };
661 store.insert(Path::new("test.ts"), m1);
662 store.insert(Path::new("test.ts"), m2);
663 assert_eq!(store.len(), 1);
664 assert!(store.get(Path::new("test.ts"), 1).is_none());
665 assert!(store.get(Path::new("test.ts"), 2).is_some());
666 }
667
668 #[test]
669 fn module_to_cached_roundtrip_named_export() {
670 let module = ModuleInfo {
671 file_id: FileId(0),
672 exports: vec![ExportInfo {
673 name: ExportName::Named("foo".to_string()),
674 local_name: Some("foo".to_string()),
675 is_type_only: false,
676 span: Span::new(10, 20),
677 members: vec![],
678 }],
679 imports: vec![],
680 re_exports: vec![],
681 dynamic_imports: vec![],
682 require_calls: vec![],
683 member_accesses: vec![],
684 whole_object_uses: vec![],
685 dynamic_import_patterns: vec![],
686 has_cjs_exports: false,
687 unused_import_bindings: vec![],
688 content_hash: 123,
689 suppressions: vec![],
690 line_offsets: vec![],
691 };
692
693 let cached = module_to_cached(&module, 0, 0);
694 let restored = cached_to_module(&cached, FileId(0));
695
696 assert_eq!(restored.exports.len(), 1);
697 assert_eq!(
698 restored.exports[0].name,
699 ExportName::Named("foo".to_string())
700 );
701 assert!(!restored.exports[0].is_type_only);
702 assert_eq!(restored.exports[0].span.start, 10);
703 assert_eq!(restored.exports[0].span.end, 20);
704 assert_eq!(restored.content_hash, 123);
705 }
706
707 #[test]
708 fn module_to_cached_roundtrip_default_export() {
709 let module = ModuleInfo {
710 file_id: FileId(0),
711 exports: vec![ExportInfo {
712 name: ExportName::Default,
713 local_name: None,
714 is_type_only: false,
715 span: Span::new(0, 10),
716 members: vec![],
717 }],
718 imports: vec![],
719 re_exports: vec![],
720 dynamic_imports: vec![],
721 require_calls: vec![],
722 member_accesses: vec![],
723 whole_object_uses: vec![],
724 dynamic_import_patterns: vec![],
725 has_cjs_exports: false,
726 unused_import_bindings: vec![],
727 content_hash: 456,
728 suppressions: vec![],
729 line_offsets: vec![],
730 };
731
732 let cached = module_to_cached(&module, 0, 0);
733 let restored = cached_to_module(&cached, FileId(0));
734
735 assert_eq!(restored.exports[0].name, ExportName::Default);
736 }
737
738 #[test]
739 fn module_to_cached_roundtrip_imports() {
740 let module = ModuleInfo {
741 file_id: FileId(0),
742 exports: vec![],
743 imports: vec![
744 ImportInfo {
745 source: "./utils".to_string(),
746 imported_name: ImportedName::Named("foo".to_string()),
747 local_name: "foo".to_string(),
748 is_type_only: false,
749 span: Span::new(0, 10),
750 },
751 ImportInfo {
752 source: "react".to_string(),
753 imported_name: ImportedName::Default,
754 local_name: "React".to_string(),
755 is_type_only: false,
756 span: Span::new(15, 30),
757 },
758 ImportInfo {
759 source: "./all".to_string(),
760 imported_name: ImportedName::Namespace,
761 local_name: "all".to_string(),
762 is_type_only: false,
763 span: Span::new(35, 50),
764 },
765 ImportInfo {
766 source: "./styles.css".to_string(),
767 imported_name: ImportedName::SideEffect,
768 local_name: String::new(),
769 is_type_only: false,
770 span: Span::new(55, 70),
771 },
772 ],
773 re_exports: vec![],
774 dynamic_imports: vec![],
775 require_calls: vec![],
776 member_accesses: vec![],
777 whole_object_uses: vec![],
778 dynamic_import_patterns: vec![],
779 has_cjs_exports: false,
780 unused_import_bindings: vec![],
781 content_hash: 789,
782 suppressions: vec![],
783 line_offsets: vec![],
784 };
785
786 let cached = module_to_cached(&module, 0, 0);
787 let restored = cached_to_module(&cached, FileId(0));
788
789 assert_eq!(restored.imports.len(), 4);
790 assert_eq!(
791 restored.imports[0].imported_name,
792 ImportedName::Named("foo".to_string())
793 );
794 assert_eq!(restored.imports[0].span.start, 0);
795 assert_eq!(restored.imports[0].span.end, 10);
796 assert_eq!(restored.imports[1].imported_name, ImportedName::Default);
797 assert_eq!(restored.imports[1].span.start, 15);
798 assert_eq!(restored.imports[1].span.end, 30);
799 assert_eq!(restored.imports[2].imported_name, ImportedName::Namespace);
800 assert_eq!(restored.imports[2].span.start, 35);
801 assert_eq!(restored.imports[2].span.end, 50);
802 assert_eq!(restored.imports[3].imported_name, ImportedName::SideEffect);
803 assert_eq!(restored.imports[3].span.start, 55);
804 assert_eq!(restored.imports[3].span.end, 70);
805 }
806
807 #[test]
808 fn module_to_cached_roundtrip_re_exports() {
809 let module = ModuleInfo {
810 file_id: FileId(0),
811 exports: vec![],
812 imports: vec![],
813 re_exports: vec![ReExportInfo {
814 source: "./module".to_string(),
815 imported_name: "foo".to_string(),
816 exported_name: "bar".to_string(),
817 is_type_only: true,
818 }],
819 dynamic_imports: vec![],
820 require_calls: vec![],
821 member_accesses: vec![],
822 whole_object_uses: vec![],
823 dynamic_import_patterns: vec![],
824 has_cjs_exports: false,
825 unused_import_bindings: vec![],
826 content_hash: 0,
827 suppressions: vec![],
828 line_offsets: vec![],
829 };
830
831 let cached = module_to_cached(&module, 0, 0);
832 let restored = cached_to_module(&cached, FileId(0));
833
834 assert_eq!(restored.re_exports.len(), 1);
835 assert_eq!(restored.re_exports[0].source, "./module");
836 assert_eq!(restored.re_exports[0].imported_name, "foo");
837 assert_eq!(restored.re_exports[0].exported_name, "bar");
838 assert!(restored.re_exports[0].is_type_only);
839 }
840
841 #[test]
842 fn module_to_cached_roundtrip_dynamic_imports() {
843 let module = ModuleInfo {
844 file_id: FileId(0),
845 exports: vec![],
846 imports: vec![],
847 re_exports: vec![],
848 dynamic_imports: vec![DynamicImportInfo {
849 source: "./lazy".to_string(),
850 span: Span::new(0, 10),
851 destructured_names: Vec::new(),
852 local_name: None,
853 }],
854 require_calls: vec![RequireCallInfo {
855 source: "fs".to_string(),
856 span: Span::new(15, 25),
857 destructured_names: Vec::new(),
858 local_name: None,
859 }],
860 member_accesses: vec![MemberAccess {
861 object: "Status".to_string(),
862 member: "Active".to_string(),
863 }],
864 whole_object_uses: vec![],
865 dynamic_import_patterns: vec![],
866 has_cjs_exports: true,
867 content_hash: 0,
868 suppressions: vec![],
869 unused_import_bindings: vec![],
870 line_offsets: vec![],
871 };
872
873 let cached = module_to_cached(&module, 0, 0);
874 let restored = cached_to_module(&cached, FileId(0));
875
876 assert_eq!(restored.dynamic_imports.len(), 1);
877 assert_eq!(restored.dynamic_imports[0].source, "./lazy");
878 assert_eq!(restored.dynamic_imports[0].span.start, 0);
879 assert_eq!(restored.dynamic_imports[0].span.end, 10);
880 assert_eq!(restored.require_calls.len(), 1);
881 assert_eq!(restored.require_calls[0].source, "fs");
882 assert_eq!(restored.require_calls[0].span.start, 15);
883 assert_eq!(restored.require_calls[0].span.end, 25);
884 assert_eq!(restored.member_accesses.len(), 1);
885 assert_eq!(restored.member_accesses[0].object, "Status");
886 assert_eq!(restored.member_accesses[0].member, "Active");
887 assert!(restored.has_cjs_exports);
888 }
889
890 #[test]
891 fn module_to_cached_roundtrip_members() {
892 let module = ModuleInfo {
893 file_id: FileId(0),
894 exports: vec![ExportInfo {
895 name: ExportName::Named("Color".to_string()),
896 local_name: Some("Color".to_string()),
897 is_type_only: false,
898 span: Span::new(0, 50),
899 members: vec![
900 MemberInfo {
901 name: "Red".to_string(),
902 kind: MemberKind::EnumMember,
903 span: Span::new(10, 15),
904 has_decorator: false,
905 },
906 MemberInfo {
907 name: "greet".to_string(),
908 kind: MemberKind::ClassMethod,
909 span: Span::new(20, 30),
910 has_decorator: false,
911 },
912 MemberInfo {
913 name: "name".to_string(),
914 kind: MemberKind::ClassProperty,
915 span: Span::new(35, 45),
916 has_decorator: false,
917 },
918 ],
919 }],
920 imports: vec![],
921 re_exports: vec![],
922 dynamic_imports: vec![],
923 require_calls: vec![],
924 member_accesses: vec![],
925 whole_object_uses: vec![],
926 dynamic_import_patterns: vec![],
927 has_cjs_exports: false,
928 unused_import_bindings: vec![],
929 content_hash: 0,
930 suppressions: vec![],
931 line_offsets: vec![],
932 };
933
934 let cached = module_to_cached(&module, 0, 0);
935 let restored = cached_to_module(&cached, FileId(0));
936
937 assert_eq!(restored.exports[0].members.len(), 3);
938 assert_eq!(restored.exports[0].members[0].kind, MemberKind::EnumMember);
939 assert_eq!(restored.exports[0].members[1].kind, MemberKind::ClassMethod);
940 assert_eq!(
941 restored.exports[0].members[2].kind,
942 MemberKind::ClassProperty
943 );
944 }
945
946 #[test]
947 fn cache_load_nonexistent_returns_none() {
948 let result = CacheStore::load(Path::new("/nonexistent/path"));
949 assert!(result.is_none());
950 }
951
952 fn test_cache_dir(name: &str) -> std::path::PathBuf {
954 let dir = std::env::temp_dir()
955 .join("fallow_cache_tests")
956 .join(name)
957 .join(format!("{}", std::process::id()));
958 let _ = std::fs::remove_dir_all(&dir);
960 std::fs::create_dir_all(&dir).unwrap();
961 dir
962 }
963
964 #[test]
965 fn cache_save_and_load_roundtrip() {
966 let dir = test_cache_dir("roundtrip");
967 let mut store = CacheStore::new();
968 let module = CachedModule {
969 content_hash: 42,
970 mtime_secs: 0,
971 file_size: 0,
972 exports: vec![],
973 imports: vec![],
974 re_exports: vec![],
975 dynamic_imports: vec![],
976 require_calls: vec![],
977 member_accesses: vec![],
978 whole_object_uses: vec![],
979 dynamic_import_patterns: vec![],
980 has_cjs_exports: false,
981 unused_import_bindings: vec![],
982 suppressions: vec![],
983 line_offsets: vec![],
984 };
985 store.insert(Path::new("test.ts"), module);
986 store.save(&dir).unwrap();
987
988 let loaded = CacheStore::load(&dir);
989 assert!(loaded.is_some());
990 let loaded = loaded.unwrap();
991 assert_eq!(loaded.len(), 1);
992 assert!(loaded.get(Path::new("test.ts"), 42).is_some());
993
994 let _ = std::fs::remove_dir_all(&dir);
995 }
996
997 #[test]
998 fn cache_version_mismatch_returns_none() {
999 let dir = test_cache_dir("version_mismatch");
1000 let mut store = CacheStore::new();
1001 let module = CachedModule {
1002 content_hash: 42,
1003 mtime_secs: 0,
1004 file_size: 0,
1005 exports: vec![],
1006 imports: vec![],
1007 re_exports: vec![],
1008 dynamic_imports: vec![],
1009 require_calls: vec![],
1010 member_accesses: vec![],
1011 whole_object_uses: vec![],
1012 dynamic_import_patterns: vec![],
1013 has_cjs_exports: false,
1014 unused_import_bindings: vec![],
1015 suppressions: vec![],
1016 line_offsets: vec![],
1017 };
1018 store.insert(Path::new("test.ts"), module);
1019 store.save(&dir).unwrap();
1020
1021 assert!(CacheStore::load(&dir).is_some());
1023
1024 let cache_file = dir.join("cache.bin");
1030 let mut data = std::fs::read(&cache_file).unwrap();
1031 assert!(!data.is_empty());
1032 data[0] = 255; std::fs::write(&cache_file, &data).unwrap();
1034
1035 let result = CacheStore::load(&dir);
1037 assert!(result.is_none());
1038
1039 let _ = std::fs::remove_dir_all(&dir);
1040 }
1041
1042 #[test]
1043 fn module_to_cached_roundtrip_type_only_import() {
1044 let module = ModuleInfo {
1045 file_id: FileId(0),
1046 exports: vec![],
1047 imports: vec![ImportInfo {
1048 source: "./types".to_string(),
1049 imported_name: ImportedName::Named("Foo".to_string()),
1050 local_name: "Foo".to_string(),
1051 is_type_only: true,
1052 span: Span::new(0, 10),
1053 }],
1054 re_exports: vec![],
1055 dynamic_imports: vec![],
1056 require_calls: vec![],
1057 member_accesses: vec![],
1058 whole_object_uses: vec![],
1059 dynamic_import_patterns: vec![],
1060 has_cjs_exports: false,
1061 unused_import_bindings: vec![],
1062 content_hash: 0,
1063 suppressions: vec![],
1064 line_offsets: vec![],
1065 };
1066
1067 let cached = module_to_cached(&module, 0, 0);
1068 let restored = cached_to_module(&cached, FileId(0));
1069
1070 assert!(restored.imports[0].is_type_only);
1071 assert_eq!(restored.imports[0].span.start, 0);
1072 assert_eq!(restored.imports[0].span.end, 10);
1073 }
1074
1075 #[test]
1076 fn get_by_path_only_returns_entry_regardless_of_hash() {
1077 let mut store = CacheStore::new();
1078 let module = CachedModule {
1079 content_hash: 42,
1080 mtime_secs: 0,
1081 file_size: 0,
1082 exports: vec![],
1083 imports: vec![],
1084 re_exports: vec![],
1085 dynamic_imports: vec![],
1086 require_calls: vec![],
1087 member_accesses: vec![],
1088 whole_object_uses: vec![],
1089 dynamic_import_patterns: vec![],
1090 has_cjs_exports: false,
1091 unused_import_bindings: vec![],
1092 suppressions: vec![],
1093 line_offsets: vec![],
1094 };
1095 store.insert(Path::new("test.ts"), module);
1096
1097 let result = store.get_by_path_only(Path::new("test.ts"));
1099 assert!(result.is_some());
1100 assert_eq!(result.unwrap().content_hash, 42);
1101 }
1102
1103 #[test]
1104 fn get_by_path_only_returns_none_for_missing() {
1105 let store = CacheStore::new();
1106 assert!(
1107 store
1108 .get_by_path_only(Path::new("nonexistent.ts"))
1109 .is_none()
1110 );
1111 }
1112
1113 #[test]
1114 fn retain_paths_removes_stale_entries() {
1115 use fallow_types::discover::DiscoveredFile;
1116 use std::path::PathBuf;
1117
1118 let mut store = CacheStore::new();
1119 let m = || CachedModule {
1120 content_hash: 1,
1121 mtime_secs: 0,
1122 file_size: 0,
1123 exports: vec![],
1124 imports: vec![],
1125 re_exports: vec![],
1126 dynamic_imports: vec![],
1127 require_calls: vec![],
1128 member_accesses: vec![],
1129 whole_object_uses: vec![],
1130 dynamic_import_patterns: vec![],
1131 has_cjs_exports: false,
1132 unused_import_bindings: vec![],
1133 suppressions: vec![],
1134 line_offsets: vec![],
1135 };
1136
1137 store.insert(Path::new("/project/a.ts"), m());
1138 store.insert(Path::new("/project/b.ts"), m());
1139 store.insert(Path::new("/project/c.ts"), m());
1140 assert_eq!(store.len(), 3);
1141
1142 let files = vec![
1144 DiscoveredFile {
1145 id: FileId(0),
1146 path: PathBuf::from("/project/a.ts"),
1147 size_bytes: 100,
1148 },
1149 DiscoveredFile {
1150 id: FileId(1),
1151 path: PathBuf::from("/project/c.ts"),
1152 size_bytes: 50,
1153 },
1154 ];
1155
1156 store.retain_paths(&files);
1157 assert_eq!(store.len(), 2);
1158 assert!(store.get_by_path_only(Path::new("/project/a.ts")).is_some());
1159 assert!(store.get_by_path_only(Path::new("/project/b.ts")).is_none());
1160 assert!(store.get_by_path_only(Path::new("/project/c.ts")).is_some());
1161 }
1162
1163 #[test]
1164 fn retain_paths_with_empty_files_clears_cache() {
1165 let mut store = CacheStore::new();
1166 let m = CachedModule {
1167 content_hash: 1,
1168 mtime_secs: 0,
1169 file_size: 0,
1170 exports: vec![],
1171 imports: vec![],
1172 re_exports: vec![],
1173 dynamic_imports: vec![],
1174 require_calls: vec![],
1175 member_accesses: vec![],
1176 whole_object_uses: vec![],
1177 dynamic_import_patterns: vec![],
1178 has_cjs_exports: false,
1179 unused_import_bindings: vec![],
1180 suppressions: vec![],
1181 line_offsets: vec![],
1182 };
1183 store.insert(Path::new("a.ts"), m);
1184 assert_eq!(store.len(), 1);
1185
1186 store.retain_paths(&[]);
1187 assert!(store.is_empty());
1188 }
1189
1190 #[test]
1191 fn get_by_metadata_returns_entry_on_match() {
1192 let mut store = CacheStore::new();
1193 let module = CachedModule {
1194 content_hash: 42,
1195 mtime_secs: 1000,
1196 file_size: 500,
1197 exports: vec![],
1198 imports: vec![],
1199 re_exports: vec![],
1200 dynamic_imports: vec![],
1201 require_calls: vec![],
1202 member_accesses: vec![],
1203 whole_object_uses: vec![],
1204 dynamic_import_patterns: vec![],
1205 has_cjs_exports: false,
1206 unused_import_bindings: vec![],
1207 suppressions: vec![],
1208 line_offsets: vec![],
1209 };
1210 store.insert(Path::new("test.ts"), module);
1211
1212 let result = store.get_by_metadata(Path::new("test.ts"), 1000, 500);
1213 assert!(result.is_some());
1214 assert_eq!(result.unwrap().content_hash, 42);
1215 }
1216
1217 #[test]
1218 fn get_by_metadata_returns_none_on_mtime_mismatch() {
1219 let mut store = CacheStore::new();
1220 let module = CachedModule {
1221 content_hash: 42,
1222 mtime_secs: 1000,
1223 file_size: 500,
1224 exports: vec![],
1225 imports: vec![],
1226 re_exports: vec![],
1227 dynamic_imports: vec![],
1228 require_calls: vec![],
1229 member_accesses: vec![],
1230 whole_object_uses: vec![],
1231 dynamic_import_patterns: vec![],
1232 has_cjs_exports: false,
1233 unused_import_bindings: vec![],
1234 suppressions: vec![],
1235 line_offsets: vec![],
1236 };
1237 store.insert(Path::new("test.ts"), module);
1238
1239 assert!(
1240 store
1241 .get_by_metadata(Path::new("test.ts"), 2000, 500)
1242 .is_none()
1243 );
1244 }
1245
1246 #[test]
1247 fn get_by_metadata_returns_none_on_size_mismatch() {
1248 let mut store = CacheStore::new();
1249 let module = CachedModule {
1250 content_hash: 42,
1251 mtime_secs: 1000,
1252 file_size: 500,
1253 exports: vec![],
1254 imports: vec![],
1255 re_exports: vec![],
1256 dynamic_imports: vec![],
1257 require_calls: vec![],
1258 member_accesses: vec![],
1259 whole_object_uses: vec![],
1260 dynamic_import_patterns: vec![],
1261 has_cjs_exports: false,
1262 unused_import_bindings: vec![],
1263 suppressions: vec![],
1264 line_offsets: vec![],
1265 };
1266 store.insert(Path::new("test.ts"), module);
1267
1268 assert!(
1269 store
1270 .get_by_metadata(Path::new("test.ts"), 1000, 999)
1271 .is_none()
1272 );
1273 }
1274
1275 #[test]
1276 fn get_by_metadata_returns_none_for_zero_mtime() {
1277 let mut store = CacheStore::new();
1278 let module = CachedModule {
1279 content_hash: 42,
1280 mtime_secs: 0,
1281 file_size: 500,
1282 exports: vec![],
1283 imports: vec![],
1284 re_exports: vec![],
1285 dynamic_imports: vec![],
1286 require_calls: vec![],
1287 member_accesses: vec![],
1288 whole_object_uses: vec![],
1289 dynamic_import_patterns: vec![],
1290 has_cjs_exports: false,
1291 unused_import_bindings: vec![],
1292 suppressions: vec![],
1293 line_offsets: vec![],
1294 };
1295 store.insert(Path::new("test.ts"), module);
1296
1297 assert!(
1299 store
1300 .get_by_metadata(Path::new("test.ts"), 0, 500)
1301 .is_none()
1302 );
1303 }
1304
1305 #[test]
1306 fn get_by_metadata_returns_none_for_missing_file() {
1307 let store = CacheStore::new();
1308 assert!(
1309 store
1310 .get_by_metadata(Path::new("nonexistent.ts"), 1000, 500)
1311 .is_none()
1312 );
1313 }
1314
1315 #[test]
1316 fn module_to_cached_stores_mtime_and_size() {
1317 let module = ModuleInfo {
1318 file_id: FileId(0),
1319 exports: vec![],
1320 imports: vec![],
1321 re_exports: vec![],
1322 dynamic_imports: vec![],
1323 require_calls: vec![],
1324 member_accesses: vec![],
1325 whole_object_uses: vec![],
1326 dynamic_import_patterns: vec![],
1327 has_cjs_exports: false,
1328 unused_import_bindings: vec![],
1329 content_hash: 42,
1330 suppressions: vec![],
1331 line_offsets: vec![],
1332 };
1333
1334 let cached = module_to_cached(&module, 12345, 6789);
1335 assert_eq!(cached.mtime_secs, 12345);
1336 assert_eq!(cached.file_size, 6789);
1337 assert_eq!(cached.content_hash, 42);
1338 }
1339
1340 #[test]
1341 fn module_to_cached_roundtrip_line_offsets() {
1342 let module = ModuleInfo {
1343 file_id: FileId(0),
1344 exports: vec![],
1345 imports: vec![],
1346 re_exports: vec![],
1347 dynamic_imports: vec![],
1348 require_calls: vec![],
1349 member_accesses: vec![],
1350 whole_object_uses: vec![],
1351 dynamic_import_patterns: vec![],
1352 has_cjs_exports: false,
1353 unused_import_bindings: vec![],
1354 content_hash: 0,
1355 suppressions: vec![],
1356 line_offsets: vec![0, 15, 30, 45],
1357 };
1358 let cached = module_to_cached(&module, 0, 0);
1359 let restored = cached_to_module(&cached, FileId(0));
1360 assert_eq!(restored.line_offsets, vec![0, 15, 30, 45]);
1361 }
1362}