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