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