1use std::sync::Arc;
9
10use std::path::{Path, PathBuf};
11
12use fallow_types::discover::{DiscoveredFile, FileId, StableFileKey};
13use fallow_types::extract::{ImportInfo, ReExportInfo};
14use fallow_types::source_fingerprint::SourceFingerprint;
15use oxc_span::Span;
16
17use crate::resolve::{
18 ResolveResult, ResolvedImport, ResolvedModule, ResolvedProject, ResolvedReExport,
19 ResolvedReplacedModuleTarget,
20};
21
22mod store;
23
24pub use store::GraphCacheStore;
25
26pub const GRAPH_CACHE_VERSION: u32 = 48;
225
226#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
234pub enum CachedResolveResult {
235 InternalModule(StableFileKey),
237 CommonJsInternalModule(StableFileKey),
239 SyntheticAutoImport(StableFileKey),
241 InternalPackageModule {
243 key: StableFileKey,
245 package_name: String,
247 },
248 CommonJsInternalPackageModule {
250 key: StableFileKey,
252 package_name: String,
254 },
255 ExternalFile(PathBuf),
257 NpmPackage(String),
259 CommonJsNpmPackage(String),
261 Unresolvable(String),
263}
264
265impl CachedResolveResult {
266 fn from_resolve_result(
267 target: &ResolveResult,
268 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
269 ) -> Option<Self> {
270 Some(match target {
271 ResolveResult::InternalModule(file_id) => {
272 Self::InternalModule(key_by_file_id.get(file_id)?.clone())
273 }
274 ResolveResult::CommonJsInternalModule(file_id) => {
275 Self::CommonJsInternalModule(key_by_file_id.get(file_id)?.clone())
276 }
277 ResolveResult::SyntheticAutoImport(file_id) => {
278 Self::SyntheticAutoImport(key_by_file_id.get(file_id)?.clone())
279 }
280 ResolveResult::InternalPackageModule {
281 file_id,
282 package_name,
283 } => Self::InternalPackageModule {
284 key: key_by_file_id.get(file_id)?.clone(),
285 package_name: package_name.clone(),
286 },
287 ResolveResult::CommonJsInternalPackageModule {
288 file_id,
289 package_name,
290 } => Self::CommonJsInternalPackageModule {
291 key: key_by_file_id.get(file_id)?.clone(),
292 package_name: package_name.clone(),
293 },
294 ResolveResult::ExternalFile(path) => Self::ExternalFile(path.clone()),
295 ResolveResult::NpmPackage(package_name) => Self::NpmPackage(package_name.clone()),
296 ResolveResult::CommonJsNpmPackage(package_name) => {
297 Self::CommonJsNpmPackage(package_name.clone())
298 }
299 ResolveResult::Unresolvable(specifier) => Self::Unresolvable(specifier.clone()),
300 })
301 }
302
303 fn into_resolve_result(
304 self,
305 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
306 ) -> Option<ResolveResult> {
307 Some(match self {
308 Self::InternalModule(key) => ResolveResult::InternalModule(*id_by_key.get(&key)?),
309 Self::CommonJsInternalModule(key) => {
310 ResolveResult::CommonJsInternalModule(*id_by_key.get(&key)?)
311 }
312 Self::SyntheticAutoImport(key) => {
313 ResolveResult::SyntheticAutoImport(*id_by_key.get(&key)?)
314 }
315 Self::InternalPackageModule { key, package_name } => {
316 ResolveResult::InternalPackageModule {
317 file_id: *id_by_key.get(&key)?,
318 package_name,
319 }
320 }
321 Self::CommonJsInternalPackageModule { key, package_name } => {
322 ResolveResult::CommonJsInternalPackageModule {
323 file_id: *id_by_key.get(&key)?,
324 package_name,
325 }
326 }
327 Self::ExternalFile(path) => ResolveResult::ExternalFile(path),
328 Self::NpmPackage(package_name) => ResolveResult::NpmPackage(package_name),
329 Self::CommonJsNpmPackage(package_name) => {
330 ResolveResult::CommonJsNpmPackage(package_name)
331 }
332 Self::Unresolvable(specifier) => ResolveResult::Unresolvable(specifier),
333 })
334 }
335}
336
337#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
339pub struct CachedResolvedImport {
340 info: CachedImportInfo,
342 target: CachedResolveResult,
344}
345
346impl CachedResolvedImport {
347 fn from_resolved(
348 import: &ResolvedImport,
349 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
350 ) -> Option<Self> {
351 Some(Self {
352 info: CachedImportInfo::from(&import.info),
353 target: CachedResolveResult::from_resolve_result(&import.target, key_by_file_id)?,
354 })
355 }
356
357 fn into_resolved(
358 self,
359 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
360 ) -> Option<ResolvedImport> {
361 Some(ResolvedImport {
362 info: self.info.into(),
363 target: self.target.into_resolve_result(id_by_key)?,
364 })
365 }
366}
367
368#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
370pub struct CachedResolvedReExport {
371 info: CachedReExportInfo,
373 target: CachedResolveResult,
375}
376
377impl CachedResolvedReExport {
378 fn from_resolved(
379 re_export: &ResolvedReExport,
380 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
381 ) -> Option<Self> {
382 Some(Self {
383 info: CachedReExportInfo::from(&re_export.info),
384 target: CachedResolveResult::from_resolve_result(&re_export.target, key_by_file_id)?,
385 })
386 }
387
388 fn into_resolved(
389 self,
390 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
391 ) -> Option<ResolvedReExport> {
392 Some(ResolvedReExport {
393 info: self.info.into(),
394 target: self.target.into_resolve_result(id_by_key)?,
395 })
396 }
397}
398
399#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
401pub struct CachedImportInfo {
402 source: String,
404 imported_name: fallow_types::extract::ImportedName,
406 local_name: String,
408 is_type_only: bool,
410 is_type_only_star: bool,
412 from_style: bool,
414 span: [u32; 2],
416 source_span: [u32; 2],
418}
419
420impl From<&ImportInfo> for CachedImportInfo {
421 fn from(info: &ImportInfo) -> Self {
422 Self {
423 source: info.source.clone(),
424 imported_name: info.imported_name.clone(),
425 local_name: info.local_name.clone(),
426 is_type_only: info.is_type_only,
427 is_type_only_star: info.is_type_only_star,
428 from_style: info.from_style,
429 span: span_to_pair(info.span),
430 source_span: span_to_pair(info.source_span),
431 }
432 }
433}
434
435impl From<CachedImportInfo> for ImportInfo {
436 fn from(info: CachedImportInfo) -> Self {
437 Self {
438 source: info.source,
439 imported_name: info.imported_name,
440 local_name: info.local_name,
441 is_type_only: info.is_type_only,
442 is_type_only_star: info.is_type_only_star,
443 from_style: info.from_style,
444 span: pair_to_span(info.span),
445 source_span: pair_to_span(info.source_span),
446 }
447 }
448}
449
450#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
452pub struct CachedReExportInfo {
453 source: String,
455 imported_name: String,
457 exported_name: String,
459 is_type_only: bool,
461 span: [u32; 2],
463 statement_span: [u32; 2],
465 source_span: [u32; 2],
467}
468
469impl From<&ReExportInfo> for CachedReExportInfo {
470 fn from(info: &ReExportInfo) -> Self {
471 Self {
472 source: info.source.clone(),
473 imported_name: info.imported_name.clone(),
474 exported_name: info.exported_name.clone(),
475 is_type_only: info.is_type_only,
476 span: span_to_pair(info.span),
477 statement_span: span_to_pair(info.statement_span),
478 source_span: span_to_pair(info.source_span),
479 }
480 }
481}
482
483impl From<CachedReExportInfo> for ReExportInfo {
484 fn from(info: CachedReExportInfo) -> Self {
485 Self {
486 source: info.source,
487 imported_name: info.imported_name,
488 exported_name: info.exported_name,
489 is_type_only: info.is_type_only,
490 span: pair_to_span(info.span),
491 statement_span: pair_to_span(info.statement_span),
492 source_span: pair_to_span(info.source_span),
493 }
494 }
495}
496
497#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
499pub struct CachedResolvedModule {
500 key: StableFileKey,
502 resolved_imports: Vec<CachedResolvedImport>,
504 resolved_dynamic_imports: Vec<CachedResolvedImport>,
506 re_exports: Vec<CachedResolvedReExport>,
508 resolved_dynamic_pattern_targets: Vec<Vec<StableFileKey>>,
510}
511
512impl CachedResolvedModule {
513 fn from_resolved(
514 module: &ResolvedModule,
515 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
516 ) -> Option<Self> {
517 Some(Self {
518 key: key_by_file_id.get(&module.file_id)?.clone(),
519 resolved_imports: module
520 .resolved_imports
521 .iter()
522 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
523 .collect::<Option<Vec<_>>>()?,
524 resolved_dynamic_imports: module
525 .resolved_dynamic_imports
526 .iter()
527 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
528 .collect::<Option<Vec<_>>>()?,
529 re_exports: module
530 .re_exports
531 .iter()
532 .map(|re_export| CachedResolvedReExport::from_resolved(re_export, key_by_file_id))
533 .collect::<Option<Vec<_>>>()?,
534 resolved_dynamic_pattern_targets: module
535 .resolved_dynamic_patterns
536 .iter()
537 .map(|(_, targets)| {
538 targets
539 .iter()
540 .map(|target| key_by_file_id.get(target).cloned())
541 .collect::<Option<Vec<_>>>()
542 })
543 .collect::<Option<Vec<_>>>()?,
544 })
545 }
546}
547
548#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
550struct CachedResolvedReplacedModuleTarget {
551 source_key: StableFileKey,
552 target_key: StableFileKey,
553}
554
555impl CachedResolvedReplacedModuleTarget {
556 fn from_resolved(
557 target: ResolvedReplacedModuleTarget,
558 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
559 ) -> Option<Self> {
560 Some(Self {
561 source_key: key_by_file_id.get(&target.source_file)?.clone(),
562 target_key: key_by_file_id.get(&target.target_file)?.clone(),
563 })
564 }
565
566 fn into_resolved(
567 self,
568 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
569 ) -> Option<ResolvedReplacedModuleTarget> {
570 Some(ResolvedReplacedModuleTarget {
571 source_file: *id_by_key.get(&self.source_key)?,
572 target_file: *id_by_key.get(&self.target_key)?,
573 })
574 }
575}
576
577#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
579pub struct CachedResolvedProject {
580 modules: Vec<CachedResolvedModule>,
581 replaced_module_targets: Vec<CachedResolvedReplacedModuleTarget>,
582}
583
584#[must_use]
586pub fn cache_resolved_project(
587 root: &Path,
588 files: &[DiscoveredFile],
589 resolved: &ResolvedProject,
590) -> Option<CachedResolvedProject> {
591 let key_by_file_id = stable_key_by_file_id(root, files);
592 let modules = resolved
593 .modules
594 .iter()
595 .map(|module| CachedResolvedModule::from_resolved(module, &key_by_file_id))
596 .collect::<Option<Vec<_>>>()?;
597 let replaced_module_targets = resolved
598 .replaced_module_targets
599 .iter()
600 .copied()
601 .map(|target| CachedResolvedReplacedModuleTarget::from_resolved(target, &key_by_file_id))
602 .collect::<Option<Vec<_>>>()?;
603 Some(CachedResolvedProject {
604 modules,
605 replaced_module_targets,
606 })
607}
608
609#[must_use]
615pub fn restore_resolved_project(
616 root: &Path,
617 modules: &[fallow_types::extract::ModuleInfo],
618 files: &[DiscoveredFile],
619 cached: &CachedResolvedProject,
620) -> Option<ResolvedProject> {
621 if modules.len() != cached.modules.len() {
622 return None;
623 }
624
625 let mut indexes = RestoreResolvedModuleIndexes::new(root, modules, files);
626 let resolved_modules = cached
627 .modules
628 .iter()
629 .map(|entry| restore_cached_resolved_module(entry, &mut indexes))
630 .collect::<Option<Vec<_>>>()?;
631 let mut replaced_module_targets = cached
632 .replaced_module_targets
633 .iter()
634 .cloned()
635 .map(|target| target.into_resolved(&indexes.file_ids))
636 .collect::<Option<Vec<_>>>()?;
637 replaced_module_targets
638 .sort_unstable_by_key(|target| (target.source_file.0, target.target_file.0));
639 replaced_module_targets.dedup();
640 Some(ResolvedProject {
641 modules: resolved_modules,
642 replaced_module_targets,
643 })
644}
645
646struct RestoreResolvedModuleIndexes<'a> {
647 file_ids: rustc_hash::FxHashMap<StableFileKey, FileId>,
648 modules: rustc_hash::FxHashMap<StableFileKey, &'a fallow_types::extract::ModuleInfo>,
649 paths: rustc_hash::FxHashMap<StableFileKey, std::path::PathBuf>,
650}
651
652impl<'a> RestoreResolvedModuleIndexes<'a> {
653 fn new(
654 root: &Path,
655 modules: &'a [fallow_types::extract::ModuleInfo],
656 files: &[DiscoveredFile],
657 ) -> Self {
658 let key_by_file_id = stable_key_by_file_id(root, files);
659 let id_by_key: rustc_hash::FxHashMap<_, _> = key_by_file_id
660 .iter()
661 .map(|(file_id, key)| (key.clone(), *file_id))
662 .collect();
663 let by_key: rustc_hash::FxHashMap<_, _> = modules
664 .iter()
665 .filter_map(|module| {
666 key_by_file_id
667 .get(&module.file_id)
668 .map(|key| (key.clone(), module))
669 })
670 .collect();
671 let path_by_key: rustc_hash::FxHashMap<_, _> = files
672 .iter()
673 .map(|file| {
674 (
675 StableFileKey::from_root_relative(root, &file.path),
676 file.path.clone(),
677 )
678 })
679 .collect();
680
681 Self {
682 file_ids: id_by_key,
683 modules: by_key,
684 paths: path_by_key,
685 }
686 }
687}
688
689fn restore_cached_resolved_module(
690 entry: &CachedResolvedModule,
691 indexes: &mut RestoreResolvedModuleIndexes<'_>,
692) -> Option<ResolvedModule> {
693 let module = indexes.modules.remove(&entry.key)?;
694 let path = indexes.paths.get(&entry.key)?.clone();
695 let resolved_dynamic_pattern_targets =
696 restore_dynamic_pattern_targets(entry, module, &indexes.file_ids)?;
697
698 Some(ResolvedModule {
699 file_id: module.file_id,
700 path,
701 exports: Arc::clone(&module.exports),
702 re_exports: entry
703 .re_exports
704 .iter()
705 .cloned()
706 .map(|re_export| re_export.into_resolved(&indexes.file_ids))
707 .collect::<Option<Vec<_>>>()?,
708 resolved_imports: entry
709 .resolved_imports
710 .iter()
711 .cloned()
712 .map(|import| import.into_resolved(&indexes.file_ids))
713 .collect::<Option<Vec<_>>>()?,
714 resolved_dynamic_imports: entry
715 .resolved_dynamic_imports
716 .iter()
717 .cloned()
718 .map(|import| import.into_resolved(&indexes.file_ids))
719 .collect::<Option<Vec<_>>>()?,
720 resolved_dynamic_patterns: module
721 .dynamic_import_patterns
722 .iter()
723 .cloned()
724 .zip(resolved_dynamic_pattern_targets)
725 .collect(),
726 member_accesses: Arc::clone(&module.member_accesses),
727 semantic_facts: Arc::clone(&module.semantic_facts),
728 whole_object_uses: Arc::clone(&module.whole_object_uses),
729 has_cjs_exports: module.has_cjs_exports,
730 has_angular_component_template_url: module.has_angular_component_template_url,
731 unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
732 type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
733 value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
734 namespace_object_aliases: module.namespace_object_aliases.clone(),
735 exported_factory_returns: Arc::clone(&module.exported_factory_returns),
736 exported_factory_return_object_shapes: Arc::clone(
737 &module.exported_factory_return_object_shapes,
738 ),
739 type_member_types: Arc::clone(&module.type_member_types),
740 })
741}
742
743fn restore_dynamic_pattern_targets(
744 entry: &CachedResolvedModule,
745 module: &fallow_types::extract::ModuleInfo,
746 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
747) -> Option<Vec<Vec<FileId>>> {
748 if entry.resolved_dynamic_pattern_targets.len() != module.dynamic_import_patterns.len() {
749 return None;
750 }
751 entry
752 .resolved_dynamic_pattern_targets
753 .iter()
754 .map(|targets| {
755 targets
756 .iter()
757 .map(|key| id_by_key.get(key).copied())
758 .collect::<Option<Vec<_>>>()
759 })
760 .collect()
761}
762
763fn stable_key_by_file_id(
764 root: &Path,
765 files: &[DiscoveredFile],
766) -> rustc_hash::FxHashMap<FileId, StableFileKey> {
767 files
768 .iter()
769 .map(|file| (file.id, StableFileKey::from_root_relative(root, &file.path)))
770 .collect()
771}
772
773fn span_to_pair(span: Span) -> [u32; 2] {
774 [span.start, span.end]
775}
776
777fn pair_to_span(pair: [u32; 2]) -> Span {
778 Span::new(pair[0], pair[1])
779}
780
781pub(crate) mod span_serde {
789 use oxc_span::Span;
790 use serde::{Deserialize, Deserializer, Serialize, Serializer};
791
792 #[expect(
793 clippy::trivially_copy_pass_by_ref,
794 reason = "serde `serialize_with` / `with` requires a `&T` signature"
795 )]
796 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
797 [span.start, span.end].serialize(serializer)
798 }
799
800 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
801 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
802 Ok(Span::new(start, end))
803 }
804}
805
806pub(crate) mod member_serde {
815 use fallow_types::extract::{MemberInfo, MemberKind};
816 use oxc_span::Span;
817 use serde::{Deserialize, Deserializer, Serialize, Serializer};
818
819 #[derive(Serialize, Deserialize)]
820 struct CachedMemberInfo {
821 name: String,
822 kind: MemberKind,
823 span: [u32; 2],
824 has_decorator: bool,
825 decorator_names: Vec<String>,
826 is_instance_returning_static: bool,
827 is_self_returning: bool,
828 }
829
830 impl From<&MemberInfo> for CachedMemberInfo {
831 fn from(member: &MemberInfo) -> Self {
832 Self {
833 name: member.name.clone(),
834 kind: member.kind,
835 span: [member.span.start, member.span.end],
836 has_decorator: member.has_decorator,
837 decorator_names: member.decorator_names.clone(),
838 is_instance_returning_static: member.is_instance_returning_static,
839 is_self_returning: member.is_self_returning,
840 }
841 }
842 }
843
844 impl From<CachedMemberInfo> for MemberInfo {
845 fn from(cached: CachedMemberInfo) -> Self {
846 Self {
847 name: cached.name,
848 kind: cached.kind,
849 span: Span::new(cached.span[0], cached.span[1]),
850 has_decorator: cached.has_decorator,
851 decorator_names: cached.decorator_names,
852 is_instance_returning_static: cached.is_instance_returning_static,
853 is_self_returning: cached.is_self_returning,
854 }
855 }
856 }
857
858 pub fn serialize<S: Serializer>(
859 members: &[MemberInfo],
860 serializer: S,
861 ) -> Result<S::Ok, S::Error> {
862 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
863 mirror.serialize(serializer)
864 }
865
866 pub fn deserialize<'de, D: Deserializer<'de>>(
867 deserializer: D,
868 ) -> Result<Vec<MemberInfo>, D::Error> {
869 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
870 Ok(mirror.into_iter().map(MemberInfo::from).collect())
871 }
872}
873
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
880pub struct GraphCacheMode {
881 pub resolver_options_hash: u64,
883 pub entry_points_hash: u64,
885 pub plugin_config_hash: u64,
887}
888
889impl GraphCacheMode {
890 #[must_use]
892 pub const fn new(
893 resolver_options_hash: u64,
894 entry_points_hash: u64,
895 plugin_config_hash: u64,
896 ) -> Self {
897 Self {
898 resolver_options_hash,
899 entry_points_hash,
900 plugin_config_hash,
901 }
902 }
903}
904
905#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
907pub struct GraphCacheFile {
908 pub key: StableFileKey,
910 pub file_id: FileId,
917 pub fingerprint: SourceFingerprint,
919}
920
921impl GraphCacheFile {
922 #[must_use]
924 fn from_discovered_file(
925 root: &Path,
926 file: &DiscoveredFile,
927 fingerprint: SourceFingerprint,
928 ) -> Self {
929 Self {
930 key: StableFileKey::from_root_relative(root, &file.path),
931 file_id: file.id,
932 fingerprint,
933 }
934 }
935}
936
937#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
939pub struct GraphCacheManifest {
940 pub version: u32,
942 pub mode: GraphCacheMode,
944 pub files: Vec<GraphCacheFile>,
946}
947
948impl GraphCacheManifest {
949 #[must_use]
951 fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
952 sort_files(&mut files);
953 Self {
954 version: GRAPH_CACHE_VERSION,
955 mode,
956 files,
957 }
958 }
959
960 pub fn from_discovered_files(
962 root: &Path,
963 files: &[DiscoveredFile],
964 mode: GraphCacheMode,
965 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
966 ) -> Self {
967 let rows = files
968 .iter()
969 .map(|file| {
970 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
971 })
972 .collect();
973 Self::new(mode, rows)
974 }
975
976 #[must_use]
978 pub fn matches_inputs(&self, current: &Self) -> bool {
979 self.version == GRAPH_CACHE_VERSION
980 && current.version == GRAPH_CACHE_VERSION
981 && self.mode == current.mode
982 && self.files == current.files
983 }
984
985 #[must_use]
992 pub fn matches_resolution_inputs(&self, current: &Self) -> bool {
993 self.version == GRAPH_CACHE_VERSION
994 && current.version == GRAPH_CACHE_VERSION
995 && self.mode == current.mode
996 && self.files.len() == current.files.len()
997 && self
998 .files
999 .iter()
1000 .zip(current.files.iter())
1001 .all(|(cached, current)| {
1002 cached.key == current.key && cached.fingerprint == current.fingerprint
1003 })
1004 }
1005}
1006
1007fn sort_files(files: &mut [GraphCacheFile]) {
1008 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use std::path::{Path, PathBuf};
1014
1015 use fallow_types::discover::FileId;
1016 use rustc_hash::FxHashMap;
1017
1018 use super::*;
1019
1020 fn file(id: u32, path: &str) -> DiscoveredFile {
1021 DiscoveredFile {
1022 id: FileId(id),
1023 path: PathBuf::from(path),
1024 size_bytes: 1,
1025 }
1026 }
1027
1028 fn mode() -> GraphCacheMode {
1029 GraphCacheMode::new(1, 2, 3)
1030 }
1031
1032 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
1033 pairs
1034 .iter()
1035 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
1036 .collect()
1037 }
1038
1039 fn manifest(
1040 files: &[DiscoveredFile],
1041 mode: GraphCacheMode,
1042 map: &FxHashMap<PathBuf, SourceFingerprint>,
1043 ) -> GraphCacheManifest {
1044 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
1045 *map.get(path).unwrap()
1046 })
1047 }
1048
1049 fn import_info(source: &str) -> ImportInfo {
1050 ImportInfo {
1051 source: source.to_string(),
1052 imported_name: fallow_types::extract::ImportedName::SideEffect,
1053 local_name: String::new(),
1054 is_type_only: false,
1055 is_type_only_star: false,
1056 from_style: false,
1057 span: Span::new(0, 0),
1058 source_span: Span::new(0, 0),
1059 }
1060 }
1061
1062 #[test]
1063 fn cached_import_info_round_trip_keeps_every_field() {
1064 let original = ImportInfo {
1069 source: "./impl".to_string(),
1070 imported_name: fallow_types::extract::ImportedName::Namespace,
1071 local_name: "ns".to_string(),
1072 is_type_only: true,
1073 is_type_only_star: true,
1074 from_style: true,
1075 span: Span::new(3, 41),
1076 source_span: Span::new(17, 25),
1077 };
1078
1079 let restored = ImportInfo::from(CachedImportInfo::from(&original));
1080
1081 assert_eq!(format!("{restored:?}"), format!("{original:?}"));
1082 }
1083
1084 #[test]
1085 fn manifest_sorts_by_stable_file_key() {
1086 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
1087 let map = fingerprints(&[
1088 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
1089 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
1090 ]);
1091
1092 let manifest = manifest(&files, mode(), &map);
1093
1094 let keys: Vec<&str> = manifest
1095 .files
1096 .iter()
1097 .map(|file| file.key.as_str())
1098 .collect();
1099 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
1100 }
1101
1102 #[test]
1103 fn manifest_misses_on_file_id_shift_until_graph_remap_exists() {
1104 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
1105 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
1106 let map = fingerprints(&[
1107 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
1108 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
1109 ]);
1110
1111 let cached = manifest(&before, mode(), &map);
1112 let current = manifest(&after, mode(), &map);
1113
1114 assert!(
1115 !cached.matches_inputs(¤t),
1116 "the persisted graph is still FileId-keyed, so FileId shifts cannot trust it"
1117 );
1118 assert!(
1119 cached.matches_resolution_inputs(¤t),
1120 "stable-keyed resolver payloads may be remapped across FileId shifts"
1121 );
1122 }
1123
1124 #[test]
1125 fn cached_resolve_result_remaps_internal_targets_by_stable_key() {
1126 let key_a = StableFileKey::from_root_relative(
1127 Path::new("/project"),
1128 Path::new("/project/src/a.ts"),
1129 );
1130 let key_b = StableFileKey::from_root_relative(
1131 Path::new("/project"),
1132 Path::new("/project/src/b.ts"),
1133 );
1134 let key_by_file_id =
1135 FxHashMap::from_iter([(FileId(0), key_a.clone()), (FileId(1), key_b.clone())]);
1136 let id_by_key = FxHashMap::from_iter([(key_a, FileId(7)), (key_b, FileId(9))]);
1137
1138 let cached = CachedResolveResult::from_resolve_result(
1139 &ResolveResult::InternalPackageModule {
1140 file_id: FileId(1),
1141 package_name: "@scope/pkg".to_string(),
1142 },
1143 &key_by_file_id,
1144 )
1145 .expect("target file id should map to a stable key");
1146
1147 let restored = cached
1148 .into_resolve_result(&id_by_key)
1149 .expect("stable key should map to current FileId");
1150
1151 assert!(matches!(
1152 restored,
1153 ResolveResult::InternalPackageModule {
1154 file_id: FileId(9),
1155 ref package_name,
1156 } if package_name == "@scope/pkg"
1157 ));
1158 }
1159
1160 #[test]
1161 fn cached_resolve_result_preserves_commonjs_provenance() {
1162 let key = StableFileKey::from_root_relative(
1163 Path::new("/project"),
1164 Path::new("/project/src/dependency.ts"),
1165 );
1166 let key_by_file_id = FxHashMap::from_iter([(FileId(3), key.clone())]);
1167 let id_by_key = FxHashMap::from_iter([(key, FileId(8))]);
1168
1169 let cached = CachedResolveResult::from_resolve_result(
1170 &ResolveResult::CommonJsInternalModule(FileId(3)),
1171 &key_by_file_id,
1172 )
1173 .expect("CommonJS target should map to a stable key");
1174 let restored = cached
1175 .into_resolve_result(&id_by_key)
1176 .expect("stable key should map to the current FileId");
1177
1178 assert!(matches!(
1179 restored,
1180 ResolveResult::CommonJsInternalModule(FileId(8))
1181 ));
1182 }
1183
1184 #[test]
1185 fn cached_resolve_result_preserves_commonjs_bare_package_provenance() {
1186 let cached = CachedResolveResult::from_resolve_result(
1187 &ResolveResult::CommonJsNpmPackage("shared-package".to_string()),
1188 &FxHashMap::default(),
1189 )
1190 .expect("bare CommonJS package should not need a stable file key");
1191 let restored = cached
1192 .into_resolve_result(&FxHashMap::default())
1193 .expect("bare CommonJS package should restore without a file map");
1194
1195 assert!(matches!(
1196 restored,
1197 ResolveResult::CommonJsNpmPackage(package_name)
1198 if package_name == "shared-package"
1199 ));
1200 }
1201
1202 #[test]
1203 fn cache_resolved_project_rejects_unknown_internal_targets() {
1204 let files = vec![file(0, "/project/src/a.ts")];
1205 let module = ResolvedModule {
1206 file_id: FileId(0),
1207 path: PathBuf::from("/project/src/a.ts"),
1208 resolved_imports: vec![ResolvedImport {
1209 info: import_info("./missing"),
1210 target: ResolveResult::InternalModule(FileId(1)),
1211 }],
1212 ..ResolvedModule::default()
1213 };
1214 let project = ResolvedProject {
1215 modules: vec![module],
1216 replaced_module_targets: Vec::new(),
1217 };
1218
1219 let cached = cache_resolved_project(Path::new("/project"), &files, &project);
1220
1221 assert!(cached.is_none());
1222 }
1223
1224 #[test]
1225 fn cached_replaced_target_remaps_both_file_ids_by_stable_key() {
1226 let source_key = StableFileKey::from_root_relative(
1227 Path::new("/project"),
1228 Path::new("/project/src/example.test.ts"),
1229 );
1230 let target_key = StableFileKey::from_root_relative(
1231 Path::new("/project"),
1232 Path::new("/project/src/dependency.ts"),
1233 );
1234 let key_by_file_id = FxHashMap::from_iter([
1235 (FileId(2), source_key.clone()),
1236 (FileId(3), target_key.clone()),
1237 ]);
1238 let id_by_key = FxHashMap::from_iter([(source_key, FileId(8)), (target_key, FileId(9))]);
1239 let resolved = ResolvedReplacedModuleTarget {
1240 source_file: FileId(2),
1241 target_file: FileId(3),
1242 };
1243
1244 let cached = CachedResolvedReplacedModuleTarget::from_resolved(resolved, &key_by_file_id)
1245 .expect("both file ids should map to stable keys");
1246 let restored = cached
1247 .into_resolved(&id_by_key)
1248 .expect("both stable keys should map to current file ids");
1249
1250 assert_eq!(
1251 restored,
1252 ResolvedReplacedModuleTarget {
1253 source_file: FileId(8),
1254 target_file: FileId(9),
1255 }
1256 );
1257 }
1258
1259 #[test]
1260 fn cache_resolved_project_rejects_unknown_replacement_targets() {
1261 let files = vec![file(0, "/project/src/example.test.ts")];
1262 let project = ResolvedProject {
1263 modules: vec![ResolvedModule {
1264 file_id: FileId(0),
1265 path: PathBuf::from("/project/src/example.test.ts"),
1266 ..ResolvedModule::default()
1267 }],
1268 replaced_module_targets: vec![ResolvedReplacedModuleTarget {
1269 source_file: FileId(0),
1270 target_file: FileId(1),
1271 }],
1272 };
1273
1274 let cached = cache_resolved_project(Path::new("/project"), &files, &project);
1275
1276 assert!(cached.is_none());
1277 }
1278
1279 #[test]
1280 fn manifest_misses_on_fingerprint_change() {
1281 let files = vec![file(0, "/project/src/a.ts")];
1282 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1283 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
1284
1285 let cached = manifest(&files, mode(), &cached_map);
1286 let current = manifest(&files, mode(), ¤t_map);
1287
1288 assert!(!cached.matches_inputs(¤t));
1289 }
1290
1291 #[test]
1292 fn manifest_misses_on_file_deletion() {
1293 let before = vec![
1294 file(0, "/project/src/a.ts"),
1295 file(1, "/project/src/deleted.ts"),
1296 ];
1297 let after = vec![file(0, "/project/src/a.ts")];
1298 let map = fingerprints(&[
1299 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
1300 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
1301 ]);
1302
1303 let cached = manifest(&before, mode(), &map);
1304 let current = manifest(&after, mode(), &map);
1305
1306 assert!(!cached.matches_inputs(¤t));
1307 }
1308
1309 #[test]
1310 fn manifest_misses_on_file_rename_with_same_fingerprint() {
1311 let before = vec![file(0, "/project/src/old.ts")];
1312 let after = vec![file(0, "/project/src/new.ts")];
1313 let map = fingerprints(&[
1314 ("/project/src/old.ts", SourceFingerprint::new(10, 1)),
1315 ("/project/src/new.ts", SourceFingerprint::new(10, 1)),
1316 ]);
1317
1318 let cached = manifest(&before, mode(), &map);
1319 let current = manifest(&after, mode(), &map);
1320
1321 assert!(!cached.matches_inputs(¤t));
1322 }
1323
1324 #[test]
1325 fn manifest_misses_on_workspace_scoped_file_set() {
1326 let full_project = vec![
1327 file(0, "/project/packages/app/src/index.ts"),
1328 file(1, "/project/packages/shared/src/index.ts"),
1329 ];
1330 let workspace_scoped = vec![file(0, "/project/packages/app/src/index.ts")];
1331 let map = fingerprints(&[
1332 (
1333 "/project/packages/app/src/index.ts",
1334 SourceFingerprint::new(10, 1),
1335 ),
1336 (
1337 "/project/packages/shared/src/index.ts",
1338 SourceFingerprint::new(20, 1),
1339 ),
1340 ]);
1341
1342 let cached = manifest(&full_project, mode(), &map);
1343 let current = manifest(&workspace_scoped, mode(), &map);
1344
1345 assert!(!cached.matches_inputs(¤t));
1346 assert!(!cached.matches_resolution_inputs(¤t));
1347 }
1348
1349 #[test]
1350 fn manifest_misses_on_mode_change() {
1351 let files = vec![file(0, "/project/src/a.ts")];
1352 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1353
1354 let cached = manifest(&files, mode(), &map);
1355 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
1356
1357 assert!(!cached.matches_inputs(¤t));
1358 }
1359
1360 #[test]
1361 fn manifest_misses_on_version_change() {
1362 let files = vec![file(0, "/project/src/a.ts")];
1363 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1364 let mut cached = manifest(&files, mode(), &map);
1365 let current = manifest(&files, mode(), &map);
1366
1367 cached.version = GRAPH_CACHE_VERSION + 1;
1368
1369 assert!(!cached.matches_inputs(¤t));
1370 }
1371}