1use std::collections::HashMap;
2use std::path::Path;
3
4use bincode::{Decode, Encode};
5
6use oxc_span::Span;
7
8use crate::extract::{ExportName, MemberAccess, MemberKind};
9
10const CACHE_VERSION: u32 = 7;
12
13const MAX_CACHE_SIZE: usize = 256 * 1024 * 1024;
15
16#[derive(Debug, Encode, Decode)]
18pub struct CacheStore {
19 version: u32,
20 entries: HashMap<String, CachedModule>,
22}
23
24#[derive(Debug, Clone, Encode, Decode)]
26pub struct CachedModule {
27 pub content_hash: u64,
29 pub exports: Vec<CachedExport>,
31 pub imports: Vec<CachedImport>,
33 pub re_exports: Vec<CachedReExport>,
35 pub dynamic_imports: Vec<CachedDynamicImport>,
37 pub require_calls: Vec<CachedRequireCall>,
39 pub member_accesses: Vec<MemberAccess>,
41 pub whole_object_uses: Vec<String>,
43 pub dynamic_import_patterns: Vec<CachedDynamicImportPattern>,
45 pub has_cjs_exports: bool,
47 pub suppressions: Vec<CachedSuppression>,
49}
50
51#[derive(Debug, Clone, Encode, Decode)]
53pub struct CachedSuppression {
54 pub line: u32,
56 pub kind: u8,
58}
59
60#[derive(Debug, Clone, Encode, Decode)]
61pub struct CachedExport {
62 pub name: String,
63 pub is_default: bool,
64 pub is_type_only: bool,
65 pub local_name: Option<String>,
66 pub span_start: u32,
67 pub span_end: u32,
68 pub members: Vec<CachedMember>,
69}
70
71const IMPORT_KIND_NAMED: u8 = 0;
74const IMPORT_KIND_DEFAULT: u8 = 1;
75const IMPORT_KIND_NAMESPACE: u8 = 2;
76const IMPORT_KIND_SIDE_EFFECT: u8 = 3;
77
78#[derive(Debug, Clone, Encode, Decode)]
79pub struct CachedImport {
80 pub source: String,
81 pub imported_name: String,
83 pub local_name: String,
84 pub is_type_only: bool,
85 pub kind: u8,
87 pub span_start: u32,
88 pub span_end: u32,
89}
90
91#[derive(Debug, Clone, Encode, Decode)]
92pub struct CachedDynamicImport {
93 pub source: String,
94 pub span_start: u32,
95 pub span_end: u32,
96 pub destructured_names: Vec<String>,
97 pub local_name: Option<String>,
98}
99
100#[derive(Debug, Clone, Encode, Decode)]
101pub struct CachedRequireCall {
102 pub source: String,
103 pub span_start: u32,
104 pub span_end: u32,
105 pub destructured_names: Vec<String>,
106 pub local_name: Option<String>,
107}
108
109#[derive(Debug, Clone, Encode, Decode)]
110pub struct CachedReExport {
111 pub source: String,
112 pub imported_name: String,
113 pub exported_name: String,
114 pub is_type_only: bool,
115}
116
117#[derive(Debug, Clone, Encode, Decode)]
118pub struct CachedMember {
119 pub name: String,
120 pub kind: String,
121 pub span_start: u32,
122 pub span_end: u32,
123}
124
125#[derive(Debug, Clone, Encode, Decode)]
126pub struct CachedDynamicImportPattern {
127 pub prefix: String,
128 pub suffix: Option<String>,
129 pub span_start: u32,
130 pub span_end: u32,
131}
132
133impl CacheStore {
134 pub fn new() -> Self {
136 Self {
137 version: CACHE_VERSION,
138 entries: HashMap::new(),
139 }
140 }
141
142 pub fn load(cache_dir: &Path) -> Option<Self> {
144 let cache_file = cache_dir.join("cache.bin");
145 let data = std::fs::read(&cache_file).ok()?;
146 if data.len() > MAX_CACHE_SIZE {
147 tracing::warn!(
148 size_mb = data.len() / (1024 * 1024),
149 "Cache file exceeds size limit, ignoring"
150 );
151 return None;
152 }
153 let (store, _): (Self, usize) =
154 bincode::decode_from_slice(&data, bincode::config::standard()).ok()?;
155 if store.version != CACHE_VERSION {
156 return None;
157 }
158 Some(store)
159 }
160
161 pub fn save(&self, cache_dir: &Path) -> Result<(), String> {
163 std::fs::create_dir_all(cache_dir)
164 .map_err(|e| format!("Failed to create cache dir: {e}"))?;
165 let cache_file = cache_dir.join("cache.bin");
166 let data = bincode::encode_to_vec(self, bincode::config::standard())
167 .map_err(|e| format!("Failed to serialize cache: {e}"))?;
168 std::fs::write(&cache_file, data).map_err(|e| format!("Failed to write cache: {e}"))?;
169 Ok(())
170 }
171
172 pub fn get(&self, path: &Path, content_hash: u64) -> Option<&CachedModule> {
175 let key = path.to_string_lossy().to_string();
176 let entry = self.entries.get(&key)?;
177 if entry.content_hash == content_hash {
178 Some(entry)
179 } else {
180 None
181 }
182 }
183
184 pub fn insert(&mut self, path: &Path, module: CachedModule) {
186 let key = path.to_string_lossy().to_string();
187 self.entries.insert(key, module);
188 }
189
190 pub fn len(&self) -> usize {
192 self.entries.len()
193 }
194
195 pub fn is_empty(&self) -> bool {
197 self.entries.is_empty()
198 }
199}
200
201impl Default for CacheStore {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207pub fn cached_to_module(
209 cached: &CachedModule,
210 file_id: crate::discover::FileId,
211) -> crate::extract::ModuleInfo {
212 use crate::extract::*;
213
214 let exports = cached
215 .exports
216 .iter()
217 .map(|e| ExportInfo {
218 name: if e.is_default {
219 ExportName::Default
220 } else {
221 ExportName::Named(e.name.clone())
222 },
223 local_name: e.local_name.clone(),
224 is_type_only: e.is_type_only,
225 span: Span::new(e.span_start, e.span_end),
226 members: e
227 .members
228 .iter()
229 .map(|m| MemberInfo {
230 name: m.name.clone(),
231 kind: match m.kind.as_str() {
232 "enum" => MemberKind::EnumMember,
233 "method" => MemberKind::ClassMethod,
234 "property" => MemberKind::ClassProperty,
235 other => {
236 tracing::warn!(
237 kind = other,
238 "Unknown cached member kind, defaulting to ClassProperty"
239 );
240 MemberKind::ClassProperty
241 }
242 },
243 span: Span::new(m.span_start, m.span_end),
244 })
245 .collect(),
246 })
247 .collect();
248
249 let imports = cached
250 .imports
251 .iter()
252 .map(|i| ImportInfo {
253 source: i.source.clone(),
254 imported_name: match i.kind {
255 IMPORT_KIND_DEFAULT => ImportedName::Default,
256 IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
257 IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
258 _ => ImportedName::Named(i.imported_name.clone()),
260 },
261 local_name: i.local_name.clone(),
262 is_type_only: i.is_type_only,
263 span: Span::new(i.span_start, i.span_end),
264 })
265 .collect();
266
267 let re_exports = cached
268 .re_exports
269 .iter()
270 .map(|r| ReExportInfo {
271 source: r.source.clone(),
272 imported_name: r.imported_name.clone(),
273 exported_name: r.exported_name.clone(),
274 is_type_only: r.is_type_only,
275 })
276 .collect();
277
278 let dynamic_imports = cached
279 .dynamic_imports
280 .iter()
281 .map(|d| DynamicImportInfo {
282 source: d.source.clone(),
283 span: Span::new(d.span_start, d.span_end),
284 destructured_names: d.destructured_names.clone(),
285 local_name: d.local_name.clone(),
286 })
287 .collect();
288
289 let require_calls = cached
290 .require_calls
291 .iter()
292 .map(|r| RequireCallInfo {
293 source: r.source.clone(),
294 span: Span::new(r.span_start, r.span_end),
295 destructured_names: r.destructured_names.clone(),
296 local_name: r.local_name.clone(),
297 })
298 .collect();
299
300 let dynamic_import_patterns = cached
301 .dynamic_import_patterns
302 .iter()
303 .map(|p| crate::extract::DynamicImportPattern {
304 prefix: p.prefix.clone(),
305 suffix: p.suffix.clone(),
306 span: Span::new(p.span_start, p.span_end),
307 })
308 .collect();
309
310 let suppressions = cached
311 .suppressions
312 .iter()
313 .map(|s| crate::suppress::Suppression {
314 line: s.line,
315 kind: if s.kind == 0 {
316 None
317 } else {
318 crate::suppress::IssueKind::from_discriminant(s.kind)
319 },
320 })
321 .collect();
322
323 ModuleInfo {
324 file_id,
325 exports,
326 imports,
327 re_exports,
328 dynamic_imports,
329 dynamic_import_patterns,
330 require_calls,
331 member_accesses: cached.member_accesses.clone(),
332 whole_object_uses: cached.whole_object_uses.clone(),
333 has_cjs_exports: cached.has_cjs_exports,
334 content_hash: cached.content_hash,
335 suppressions,
336 }
337}
338
339pub fn module_to_cached(module: &crate::extract::ModuleInfo) -> CachedModule {
341 CachedModule {
342 content_hash: module.content_hash,
343 exports: module
344 .exports
345 .iter()
346 .map(|e| CachedExport {
347 name: match &e.name {
348 ExportName::Named(n) => n.clone(),
349 ExportName::Default => "default".to_string(),
350 },
351 is_default: matches!(e.name, ExportName::Default),
352 is_type_only: e.is_type_only,
353 local_name: e.local_name.clone(),
354 span_start: e.span.start,
355 span_end: e.span.end,
356 members: e
357 .members
358 .iter()
359 .map(|m| CachedMember {
360 name: m.name.clone(),
361 kind: match m.kind {
362 MemberKind::EnumMember => "enum".to_string(),
363 MemberKind::ClassMethod => "method".to_string(),
364 MemberKind::ClassProperty => "property".to_string(),
365 },
366 span_start: m.span.start,
367 span_end: m.span.end,
368 })
369 .collect(),
370 })
371 .collect(),
372 imports: module
373 .imports
374 .iter()
375 .map(|i| {
376 let (kind, imported_name) = match &i.imported_name {
377 crate::extract::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
378 crate::extract::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
379 crate::extract::ImportedName::Namespace => {
380 (IMPORT_KIND_NAMESPACE, String::new())
381 }
382 crate::extract::ImportedName::SideEffect => {
383 (IMPORT_KIND_SIDE_EFFECT, String::new())
384 }
385 };
386 CachedImport {
387 source: i.source.clone(),
388 imported_name,
389 local_name: i.local_name.clone(),
390 is_type_only: i.is_type_only,
391 kind,
392 span_start: i.span.start,
393 span_end: i.span.end,
394 }
395 })
396 .collect(),
397 re_exports: module
398 .re_exports
399 .iter()
400 .map(|r| CachedReExport {
401 source: r.source.clone(),
402 imported_name: r.imported_name.clone(),
403 exported_name: r.exported_name.clone(),
404 is_type_only: r.is_type_only,
405 })
406 .collect(),
407 dynamic_imports: module
408 .dynamic_imports
409 .iter()
410 .map(|d| CachedDynamicImport {
411 source: d.source.clone(),
412 span_start: d.span.start,
413 span_end: d.span.end,
414 destructured_names: d.destructured_names.clone(),
415 local_name: d.local_name.clone(),
416 })
417 .collect(),
418 require_calls: module
419 .require_calls
420 .iter()
421 .map(|r| CachedRequireCall {
422 source: r.source.clone(),
423 span_start: r.span.start,
424 span_end: r.span.end,
425 destructured_names: r.destructured_names.clone(),
426 local_name: r.local_name.clone(),
427 })
428 .collect(),
429 member_accesses: module.member_accesses.clone(),
430 whole_object_uses: module.whole_object_uses.clone(),
431 dynamic_import_patterns: module
432 .dynamic_import_patterns
433 .iter()
434 .map(|p| CachedDynamicImportPattern {
435 prefix: p.prefix.clone(),
436 suffix: p.suffix.clone(),
437 span_start: p.span.start,
438 span_end: p.span.end,
439 })
440 .collect(),
441 has_cjs_exports: module.has_cjs_exports,
442 suppressions: module
443 .suppressions
444 .iter()
445 .map(|s| CachedSuppression {
446 line: s.line,
447 kind: s.kind.map_or(0, |k| k.to_discriminant()),
448 })
449 .collect(),
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::discover::FileId;
457 use crate::extract::*;
458
459 #[test]
460 fn cache_store_new_is_empty() {
461 let store = CacheStore::new();
462 assert!(store.is_empty());
463 assert_eq!(store.len(), 0);
464 }
465
466 #[test]
467 fn cache_store_default_is_empty() {
468 let store = CacheStore::default();
469 assert!(store.is_empty());
470 }
471
472 #[test]
473 fn cache_store_insert_and_get() {
474 let mut store = CacheStore::new();
475 let module = CachedModule {
476 content_hash: 42,
477 exports: vec![],
478 imports: vec![],
479 re_exports: vec![],
480 dynamic_imports: vec![],
481 require_calls: vec![],
482 member_accesses: vec![],
483 whole_object_uses: vec![],
484 dynamic_import_patterns: vec![],
485 has_cjs_exports: false,
486 suppressions: vec![],
487 };
488 store.insert(Path::new("test.ts"), module);
489 assert_eq!(store.len(), 1);
490 assert!(!store.is_empty());
491 assert!(store.get(Path::new("test.ts"), 42).is_some());
492 }
493
494 #[test]
495 fn cache_store_hash_mismatch_returns_none() {
496 let mut store = CacheStore::new();
497 let module = CachedModule {
498 content_hash: 42,
499 exports: vec![],
500 imports: vec![],
501 re_exports: vec![],
502 dynamic_imports: vec![],
503 require_calls: vec![],
504 member_accesses: vec![],
505 whole_object_uses: vec![],
506 dynamic_import_patterns: vec![],
507 has_cjs_exports: false,
508 suppressions: vec![],
509 };
510 store.insert(Path::new("test.ts"), module);
511 assert!(store.get(Path::new("test.ts"), 99).is_none());
512 }
513
514 #[test]
515 fn cache_store_missing_key_returns_none() {
516 let store = CacheStore::new();
517 assert!(store.get(Path::new("nonexistent.ts"), 42).is_none());
518 }
519
520 #[test]
521 fn cache_store_overwrite_entry() {
522 let mut store = CacheStore::new();
523 let m1 = CachedModule {
524 content_hash: 1,
525 exports: vec![],
526 imports: vec![],
527 re_exports: vec![],
528 dynamic_imports: vec![],
529 require_calls: vec![],
530 member_accesses: vec![],
531 whole_object_uses: vec![],
532 dynamic_import_patterns: vec![],
533 has_cjs_exports: false,
534 suppressions: vec![],
535 };
536 let m2 = CachedModule {
537 content_hash: 2,
538 exports: vec![],
539 imports: vec![],
540 re_exports: vec![],
541 dynamic_imports: vec![],
542 require_calls: vec![],
543 member_accesses: vec![],
544 whole_object_uses: vec![],
545 dynamic_import_patterns: vec![],
546 has_cjs_exports: false,
547 suppressions: vec![],
548 };
549 store.insert(Path::new("test.ts"), m1);
550 store.insert(Path::new("test.ts"), m2);
551 assert_eq!(store.len(), 1);
552 assert!(store.get(Path::new("test.ts"), 1).is_none());
553 assert!(store.get(Path::new("test.ts"), 2).is_some());
554 }
555
556 #[test]
557 fn module_to_cached_roundtrip_named_export() {
558 let module = ModuleInfo {
559 file_id: FileId(0),
560 exports: vec![ExportInfo {
561 name: ExportName::Named("foo".to_string()),
562 local_name: Some("foo".to_string()),
563 is_type_only: false,
564 span: Span::new(10, 20),
565 members: vec![],
566 }],
567 imports: vec![],
568 re_exports: vec![],
569 dynamic_imports: vec![],
570 require_calls: vec![],
571 member_accesses: vec![],
572 whole_object_uses: vec![],
573 dynamic_import_patterns: vec![],
574 has_cjs_exports: false,
575 content_hash: 123,
576 suppressions: vec![],
577 };
578
579 let cached = module_to_cached(&module);
580 let restored = cached_to_module(&cached, FileId(0));
581
582 assert_eq!(restored.exports.len(), 1);
583 assert_eq!(
584 restored.exports[0].name,
585 ExportName::Named("foo".to_string())
586 );
587 assert!(!restored.exports[0].is_type_only);
588 assert_eq!(restored.exports[0].span.start, 10);
589 assert_eq!(restored.exports[0].span.end, 20);
590 assert_eq!(restored.content_hash, 123);
591 }
592
593 #[test]
594 fn module_to_cached_roundtrip_default_export() {
595 let module = ModuleInfo {
596 file_id: FileId(0),
597 exports: vec![ExportInfo {
598 name: ExportName::Default,
599 local_name: None,
600 is_type_only: false,
601 span: Span::new(0, 10),
602 members: vec![],
603 }],
604 imports: vec![],
605 re_exports: vec![],
606 dynamic_imports: vec![],
607 require_calls: vec![],
608 member_accesses: vec![],
609 whole_object_uses: vec![],
610 dynamic_import_patterns: vec![],
611 has_cjs_exports: false,
612 content_hash: 456,
613 suppressions: vec![],
614 };
615
616 let cached = module_to_cached(&module);
617 let restored = cached_to_module(&cached, FileId(0));
618
619 assert_eq!(restored.exports[0].name, ExportName::Default);
620 }
621
622 #[test]
623 fn module_to_cached_roundtrip_imports() {
624 let module = ModuleInfo {
625 file_id: FileId(0),
626 exports: vec![],
627 imports: vec![
628 ImportInfo {
629 source: "./utils".to_string(),
630 imported_name: ImportedName::Named("foo".to_string()),
631 local_name: "foo".to_string(),
632 is_type_only: false,
633 span: Span::new(0, 10),
634 },
635 ImportInfo {
636 source: "react".to_string(),
637 imported_name: ImportedName::Default,
638 local_name: "React".to_string(),
639 is_type_only: false,
640 span: Span::new(15, 30),
641 },
642 ImportInfo {
643 source: "./all".to_string(),
644 imported_name: ImportedName::Namespace,
645 local_name: "all".to_string(),
646 is_type_only: false,
647 span: Span::new(35, 50),
648 },
649 ImportInfo {
650 source: "./styles.css".to_string(),
651 imported_name: ImportedName::SideEffect,
652 local_name: String::new(),
653 is_type_only: false,
654 span: Span::new(55, 70),
655 },
656 ],
657 re_exports: vec![],
658 dynamic_imports: vec![],
659 require_calls: vec![],
660 member_accesses: vec![],
661 whole_object_uses: vec![],
662 dynamic_import_patterns: vec![],
663 has_cjs_exports: false,
664 content_hash: 789,
665 suppressions: vec![],
666 };
667
668 let cached = module_to_cached(&module);
669 let restored = cached_to_module(&cached, FileId(0));
670
671 assert_eq!(restored.imports.len(), 4);
672 assert_eq!(
673 restored.imports[0].imported_name,
674 ImportedName::Named("foo".to_string())
675 );
676 assert_eq!(restored.imports[0].span.start, 0);
677 assert_eq!(restored.imports[0].span.end, 10);
678 assert_eq!(restored.imports[1].imported_name, ImportedName::Default);
679 assert_eq!(restored.imports[1].span.start, 15);
680 assert_eq!(restored.imports[1].span.end, 30);
681 assert_eq!(restored.imports[2].imported_name, ImportedName::Namespace);
682 assert_eq!(restored.imports[2].span.start, 35);
683 assert_eq!(restored.imports[2].span.end, 50);
684 assert_eq!(restored.imports[3].imported_name, ImportedName::SideEffect);
685 assert_eq!(restored.imports[3].span.start, 55);
686 assert_eq!(restored.imports[3].span.end, 70);
687 }
688
689 #[test]
690 fn module_to_cached_roundtrip_re_exports() {
691 let module = ModuleInfo {
692 file_id: FileId(0),
693 exports: vec![],
694 imports: vec![],
695 re_exports: vec![ReExportInfo {
696 source: "./module".to_string(),
697 imported_name: "foo".to_string(),
698 exported_name: "bar".to_string(),
699 is_type_only: true,
700 }],
701 dynamic_imports: vec![],
702 require_calls: vec![],
703 member_accesses: vec![],
704 whole_object_uses: vec![],
705 dynamic_import_patterns: vec![],
706 has_cjs_exports: false,
707 content_hash: 0,
708 suppressions: vec![],
709 };
710
711 let cached = module_to_cached(&module);
712 let restored = cached_to_module(&cached, FileId(0));
713
714 assert_eq!(restored.re_exports.len(), 1);
715 assert_eq!(restored.re_exports[0].source, "./module");
716 assert_eq!(restored.re_exports[0].imported_name, "foo");
717 assert_eq!(restored.re_exports[0].exported_name, "bar");
718 assert!(restored.re_exports[0].is_type_only);
719 }
720
721 #[test]
722 fn module_to_cached_roundtrip_dynamic_imports() {
723 let module = ModuleInfo {
724 file_id: FileId(0),
725 exports: vec![],
726 imports: vec![],
727 re_exports: vec![],
728 dynamic_imports: vec![DynamicImportInfo {
729 source: "./lazy".to_string(),
730 span: Span::new(0, 10),
731 destructured_names: Vec::new(),
732 local_name: None,
733 }],
734 require_calls: vec![RequireCallInfo {
735 source: "fs".to_string(),
736 span: Span::new(15, 25),
737 destructured_names: Vec::new(),
738 local_name: None,
739 }],
740 member_accesses: vec![MemberAccess {
741 object: "Status".to_string(),
742 member: "Active".to_string(),
743 }],
744 whole_object_uses: vec![],
745 dynamic_import_patterns: vec![],
746 has_cjs_exports: true,
747 content_hash: 0,
748 suppressions: vec![],
749 };
750
751 let cached = module_to_cached(&module);
752 let restored = cached_to_module(&cached, FileId(0));
753
754 assert_eq!(restored.dynamic_imports.len(), 1);
755 assert_eq!(restored.dynamic_imports[0].source, "./lazy");
756 assert_eq!(restored.dynamic_imports[0].span.start, 0);
757 assert_eq!(restored.dynamic_imports[0].span.end, 10);
758 assert_eq!(restored.require_calls.len(), 1);
759 assert_eq!(restored.require_calls[0].source, "fs");
760 assert_eq!(restored.require_calls[0].span.start, 15);
761 assert_eq!(restored.require_calls[0].span.end, 25);
762 assert_eq!(restored.member_accesses.len(), 1);
763 assert_eq!(restored.member_accesses[0].object, "Status");
764 assert_eq!(restored.member_accesses[0].member, "Active");
765 assert!(restored.has_cjs_exports);
766 }
767
768 #[test]
769 fn module_to_cached_roundtrip_members() {
770 let module = ModuleInfo {
771 file_id: FileId(0),
772 exports: vec![ExportInfo {
773 name: ExportName::Named("Color".to_string()),
774 local_name: Some("Color".to_string()),
775 is_type_only: false,
776 span: Span::new(0, 50),
777 members: vec![
778 MemberInfo {
779 name: "Red".to_string(),
780 kind: MemberKind::EnumMember,
781 span: Span::new(10, 15),
782 },
783 MemberInfo {
784 name: "greet".to_string(),
785 kind: MemberKind::ClassMethod,
786 span: Span::new(20, 30),
787 },
788 MemberInfo {
789 name: "name".to_string(),
790 kind: MemberKind::ClassProperty,
791 span: Span::new(35, 45),
792 },
793 ],
794 }],
795 imports: vec![],
796 re_exports: vec![],
797 dynamic_imports: vec![],
798 require_calls: vec![],
799 member_accesses: vec![],
800 whole_object_uses: vec![],
801 dynamic_import_patterns: vec![],
802 has_cjs_exports: false,
803 content_hash: 0,
804 suppressions: vec![],
805 };
806
807 let cached = module_to_cached(&module);
808 let restored = cached_to_module(&cached, FileId(0));
809
810 assert_eq!(restored.exports[0].members.len(), 3);
811 assert_eq!(restored.exports[0].members[0].kind, MemberKind::EnumMember);
812 assert_eq!(restored.exports[0].members[1].kind, MemberKind::ClassMethod);
813 assert_eq!(
814 restored.exports[0].members[2].kind,
815 MemberKind::ClassProperty
816 );
817 }
818
819 #[test]
820 fn cache_load_nonexistent_returns_none() {
821 let result = CacheStore::load(Path::new("/nonexistent/path"));
822 assert!(result.is_none());
823 }
824
825 fn test_cache_dir(name: &str) -> std::path::PathBuf {
827 let dir = std::env::temp_dir()
828 .join("fallow_cache_tests")
829 .join(name)
830 .join(format!("{}", std::process::id()));
831 let _ = std::fs::remove_dir_all(&dir);
833 std::fs::create_dir_all(&dir).unwrap();
834 dir
835 }
836
837 #[test]
838 fn cache_save_and_load_roundtrip() {
839 let dir = test_cache_dir("roundtrip");
840 let mut store = CacheStore::new();
841 let module = CachedModule {
842 content_hash: 42,
843 exports: vec![],
844 imports: vec![],
845 re_exports: vec![],
846 dynamic_imports: vec![],
847 require_calls: vec![],
848 member_accesses: vec![],
849 whole_object_uses: vec![],
850 dynamic_import_patterns: vec![],
851 has_cjs_exports: false,
852 suppressions: vec![],
853 };
854 store.insert(Path::new("test.ts"), module);
855 store.save(&dir).unwrap();
856
857 let loaded = CacheStore::load(&dir);
858 assert!(loaded.is_some());
859 let loaded = loaded.unwrap();
860 assert_eq!(loaded.len(), 1);
861 assert!(loaded.get(Path::new("test.ts"), 42).is_some());
862
863 let _ = std::fs::remove_dir_all(&dir);
864 }
865
866 #[test]
867 fn cache_version_mismatch_returns_none() {
868 let dir = test_cache_dir("version_mismatch");
869 let mut store = CacheStore::new();
870 let module = CachedModule {
871 content_hash: 42,
872 exports: vec![],
873 imports: vec![],
874 re_exports: vec![],
875 dynamic_imports: vec![],
876 require_calls: vec![],
877 member_accesses: vec![],
878 whole_object_uses: vec![],
879 dynamic_import_patterns: vec![],
880 has_cjs_exports: false,
881 suppressions: vec![],
882 };
883 store.insert(Path::new("test.ts"), module);
884 store.save(&dir).unwrap();
885
886 assert!(CacheStore::load(&dir).is_some());
888
889 let cache_file = dir.join("cache.bin");
895 let mut data = std::fs::read(&cache_file).unwrap();
896 assert!(!data.is_empty());
897 data[0] = 255; std::fs::write(&cache_file, &data).unwrap();
899
900 let result = CacheStore::load(&dir);
902 assert!(result.is_none());
903
904 let _ = std::fs::remove_dir_all(&dir);
905 }
906
907 #[test]
908 fn module_to_cached_roundtrip_type_only_import() {
909 let module = ModuleInfo {
910 file_id: FileId(0),
911 exports: vec![],
912 imports: vec![ImportInfo {
913 source: "./types".to_string(),
914 imported_name: ImportedName::Named("Foo".to_string()),
915 local_name: "Foo".to_string(),
916 is_type_only: true,
917 span: Span::new(0, 10),
918 }],
919 re_exports: vec![],
920 dynamic_imports: vec![],
921 require_calls: vec![],
922 member_accesses: vec![],
923 whole_object_uses: vec![],
924 dynamic_import_patterns: vec![],
925 has_cjs_exports: false,
926 content_hash: 0,
927 suppressions: vec![],
928 };
929
930 let cached = module_to_cached(&module);
931 let restored = cached_to_module(&cached, FileId(0));
932
933 assert!(restored.imports[0].is_type_only);
934 assert_eq!(restored.imports[0].span.start, 0);
935 assert_eq!(restored.imports[0].span.end, 10);
936 }
937}