1use std::path::{Path, PathBuf};
9
10use fallow_types::discover::{DiscoveredFile, FileId, StableFileKey};
11use fallow_types::extract::{ImportInfo, ReExportInfo};
12use fallow_types::source_fingerprint::SourceFingerprint;
13use oxc_span::Span;
14
15use crate::resolve::{
16 ResolveResult, ResolvedImport, ResolvedModule, ResolvedProject, ResolvedReExport,
17 ResolvedReplacedModuleTarget,
18};
19
20mod store;
21
22pub use store::GraphCacheStore;
23
24pub const GRAPH_CACHE_VERSION: u32 = 19;
55
56#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
64pub enum CachedResolveResult {
65 InternalModule(StableFileKey),
67 CommonJsInternalModule(StableFileKey),
69 SyntheticAutoImport(StableFileKey),
71 InternalPackageModule {
73 key: StableFileKey,
75 package_name: String,
77 },
78 CommonJsInternalPackageModule {
80 key: StableFileKey,
82 package_name: String,
84 },
85 ExternalFile(PathBuf),
87 NpmPackage(String),
89 CommonJsNpmPackage(String),
91 Unresolvable(String),
93}
94
95impl CachedResolveResult {
96 fn from_resolve_result(
97 target: &ResolveResult,
98 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
99 ) -> Option<Self> {
100 Some(match target {
101 ResolveResult::InternalModule(file_id) => {
102 Self::InternalModule(key_by_file_id.get(file_id)?.clone())
103 }
104 ResolveResult::CommonJsInternalModule(file_id) => {
105 Self::CommonJsInternalModule(key_by_file_id.get(file_id)?.clone())
106 }
107 ResolveResult::SyntheticAutoImport(file_id) => {
108 Self::SyntheticAutoImport(key_by_file_id.get(file_id)?.clone())
109 }
110 ResolveResult::InternalPackageModule {
111 file_id,
112 package_name,
113 } => Self::InternalPackageModule {
114 key: key_by_file_id.get(file_id)?.clone(),
115 package_name: package_name.clone(),
116 },
117 ResolveResult::CommonJsInternalPackageModule {
118 file_id,
119 package_name,
120 } => Self::CommonJsInternalPackageModule {
121 key: key_by_file_id.get(file_id)?.clone(),
122 package_name: package_name.clone(),
123 },
124 ResolveResult::ExternalFile(path) => Self::ExternalFile(path.clone()),
125 ResolveResult::NpmPackage(package_name) => Self::NpmPackage(package_name.clone()),
126 ResolveResult::CommonJsNpmPackage(package_name) => {
127 Self::CommonJsNpmPackage(package_name.clone())
128 }
129 ResolveResult::Unresolvable(specifier) => Self::Unresolvable(specifier.clone()),
130 })
131 }
132
133 fn into_resolve_result(
134 self,
135 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
136 ) -> Option<ResolveResult> {
137 Some(match self {
138 Self::InternalModule(key) => ResolveResult::InternalModule(*id_by_key.get(&key)?),
139 Self::CommonJsInternalModule(key) => {
140 ResolveResult::CommonJsInternalModule(*id_by_key.get(&key)?)
141 }
142 Self::SyntheticAutoImport(key) => {
143 ResolveResult::SyntheticAutoImport(*id_by_key.get(&key)?)
144 }
145 Self::InternalPackageModule { key, package_name } => {
146 ResolveResult::InternalPackageModule {
147 file_id: *id_by_key.get(&key)?,
148 package_name,
149 }
150 }
151 Self::CommonJsInternalPackageModule { key, package_name } => {
152 ResolveResult::CommonJsInternalPackageModule {
153 file_id: *id_by_key.get(&key)?,
154 package_name,
155 }
156 }
157 Self::ExternalFile(path) => ResolveResult::ExternalFile(path),
158 Self::NpmPackage(package_name) => ResolveResult::NpmPackage(package_name),
159 Self::CommonJsNpmPackage(package_name) => {
160 ResolveResult::CommonJsNpmPackage(package_name)
161 }
162 Self::Unresolvable(specifier) => ResolveResult::Unresolvable(specifier),
163 })
164 }
165}
166
167#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
169pub struct CachedResolvedImport {
170 info: CachedImportInfo,
172 target: CachedResolveResult,
174}
175
176impl CachedResolvedImport {
177 fn from_resolved(
178 import: &ResolvedImport,
179 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
180 ) -> Option<Self> {
181 Some(Self {
182 info: CachedImportInfo::from(&import.info),
183 target: CachedResolveResult::from_resolve_result(&import.target, key_by_file_id)?,
184 })
185 }
186
187 fn into_resolved(
188 self,
189 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
190 ) -> Option<ResolvedImport> {
191 Some(ResolvedImport {
192 info: self.info.into(),
193 target: self.target.into_resolve_result(id_by_key)?,
194 })
195 }
196}
197
198#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
200pub struct CachedResolvedReExport {
201 info: CachedReExportInfo,
203 target: CachedResolveResult,
205}
206
207impl CachedResolvedReExport {
208 fn from_resolved(
209 re_export: &ResolvedReExport,
210 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
211 ) -> Option<Self> {
212 Some(Self {
213 info: CachedReExportInfo::from(&re_export.info),
214 target: CachedResolveResult::from_resolve_result(&re_export.target, key_by_file_id)?,
215 })
216 }
217
218 fn into_resolved(
219 self,
220 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
221 ) -> Option<ResolvedReExport> {
222 Some(ResolvedReExport {
223 info: self.info.into(),
224 target: self.target.into_resolve_result(id_by_key)?,
225 })
226 }
227}
228
229#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
231pub struct CachedImportInfo {
232 source: String,
234 imported_name: fallow_types::extract::ImportedName,
236 local_name: String,
238 is_type_only: bool,
240 from_style: bool,
242 span: [u32; 2],
244 source_span: [u32; 2],
246}
247
248impl From<&ImportInfo> for CachedImportInfo {
249 fn from(info: &ImportInfo) -> Self {
250 Self {
251 source: info.source.clone(),
252 imported_name: info.imported_name.clone(),
253 local_name: info.local_name.clone(),
254 is_type_only: info.is_type_only,
255 from_style: info.from_style,
256 span: span_to_pair(info.span),
257 source_span: span_to_pair(info.source_span),
258 }
259 }
260}
261
262impl From<CachedImportInfo> for ImportInfo {
263 fn from(info: CachedImportInfo) -> Self {
264 Self {
265 source: info.source,
266 imported_name: info.imported_name,
267 local_name: info.local_name,
268 is_type_only: info.is_type_only,
269 from_style: info.from_style,
270 span: pair_to_span(info.span),
271 source_span: pair_to_span(info.source_span),
272 }
273 }
274}
275
276#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
278pub struct CachedReExportInfo {
279 source: String,
281 imported_name: String,
283 exported_name: String,
285 is_type_only: bool,
287 span: [u32; 2],
289}
290
291impl From<&ReExportInfo> for CachedReExportInfo {
292 fn from(info: &ReExportInfo) -> Self {
293 Self {
294 source: info.source.clone(),
295 imported_name: info.imported_name.clone(),
296 exported_name: info.exported_name.clone(),
297 is_type_only: info.is_type_only,
298 span: span_to_pair(info.span),
299 }
300 }
301}
302
303impl From<CachedReExportInfo> for ReExportInfo {
304 fn from(info: CachedReExportInfo) -> Self {
305 Self {
306 source: info.source,
307 imported_name: info.imported_name,
308 exported_name: info.exported_name,
309 is_type_only: info.is_type_only,
310 span: pair_to_span(info.span),
311 }
312 }
313}
314
315#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
317pub struct CachedResolvedModule {
318 key: StableFileKey,
320 resolved_imports: Vec<CachedResolvedImport>,
322 resolved_dynamic_imports: Vec<CachedResolvedImport>,
324 re_exports: Vec<CachedResolvedReExport>,
326 resolved_dynamic_pattern_targets: Vec<Vec<StableFileKey>>,
328}
329
330impl CachedResolvedModule {
331 fn from_resolved(
332 module: &ResolvedModule,
333 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
334 ) -> Option<Self> {
335 Some(Self {
336 key: key_by_file_id.get(&module.file_id)?.clone(),
337 resolved_imports: module
338 .resolved_imports
339 .iter()
340 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
341 .collect::<Option<Vec<_>>>()?,
342 resolved_dynamic_imports: module
343 .resolved_dynamic_imports
344 .iter()
345 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
346 .collect::<Option<Vec<_>>>()?,
347 re_exports: module
348 .re_exports
349 .iter()
350 .map(|re_export| CachedResolvedReExport::from_resolved(re_export, key_by_file_id))
351 .collect::<Option<Vec<_>>>()?,
352 resolved_dynamic_pattern_targets: module
353 .resolved_dynamic_patterns
354 .iter()
355 .map(|(_, targets)| {
356 targets
357 .iter()
358 .map(|target| key_by_file_id.get(target).cloned())
359 .collect::<Option<Vec<_>>>()
360 })
361 .collect::<Option<Vec<_>>>()?,
362 })
363 }
364}
365
366#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
368struct CachedResolvedReplacedModuleTarget {
369 source_key: StableFileKey,
370 target_key: StableFileKey,
371}
372
373impl CachedResolvedReplacedModuleTarget {
374 fn from_resolved(
375 target: ResolvedReplacedModuleTarget,
376 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
377 ) -> Option<Self> {
378 Some(Self {
379 source_key: key_by_file_id.get(&target.source_file)?.clone(),
380 target_key: key_by_file_id.get(&target.target_file)?.clone(),
381 })
382 }
383
384 fn into_resolved(
385 self,
386 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
387 ) -> Option<ResolvedReplacedModuleTarget> {
388 Some(ResolvedReplacedModuleTarget {
389 source_file: *id_by_key.get(&self.source_key)?,
390 target_file: *id_by_key.get(&self.target_key)?,
391 })
392 }
393}
394
395#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
397pub struct CachedResolvedProject {
398 modules: Vec<CachedResolvedModule>,
399 replaced_module_targets: Vec<CachedResolvedReplacedModuleTarget>,
400}
401
402#[must_use]
404pub fn cache_resolved_project(
405 root: &Path,
406 files: &[DiscoveredFile],
407 resolved: &ResolvedProject,
408) -> Option<CachedResolvedProject> {
409 let key_by_file_id = stable_key_by_file_id(root, files);
410 let modules = resolved
411 .modules
412 .iter()
413 .map(|module| CachedResolvedModule::from_resolved(module, &key_by_file_id))
414 .collect::<Option<Vec<_>>>()?;
415 let replaced_module_targets = resolved
416 .replaced_module_targets
417 .iter()
418 .copied()
419 .map(|target| CachedResolvedReplacedModuleTarget::from_resolved(target, &key_by_file_id))
420 .collect::<Option<Vec<_>>>()?;
421 Some(CachedResolvedProject {
422 modules,
423 replaced_module_targets,
424 })
425}
426
427#[must_use]
433pub fn restore_resolved_project(
434 root: &Path,
435 modules: &[fallow_types::extract::ModuleInfo],
436 files: &[DiscoveredFile],
437 cached: &CachedResolvedProject,
438) -> Option<ResolvedProject> {
439 if modules.len() != cached.modules.len() {
440 return None;
441 }
442
443 let mut indexes = RestoreResolvedModuleIndexes::new(root, modules, files);
444 let resolved_modules = cached
445 .modules
446 .iter()
447 .map(|entry| restore_cached_resolved_module(entry, &mut indexes))
448 .collect::<Option<Vec<_>>>()?;
449 let mut replaced_module_targets = cached
450 .replaced_module_targets
451 .iter()
452 .cloned()
453 .map(|target| target.into_resolved(&indexes.file_ids))
454 .collect::<Option<Vec<_>>>()?;
455 replaced_module_targets
456 .sort_unstable_by_key(|target| (target.source_file.0, target.target_file.0));
457 replaced_module_targets.dedup();
458 Some(ResolvedProject {
459 modules: resolved_modules,
460 replaced_module_targets,
461 })
462}
463
464struct RestoreResolvedModuleIndexes<'a> {
465 file_ids: rustc_hash::FxHashMap<StableFileKey, FileId>,
466 modules: rustc_hash::FxHashMap<StableFileKey, &'a fallow_types::extract::ModuleInfo>,
467 paths: rustc_hash::FxHashMap<StableFileKey, std::path::PathBuf>,
468}
469
470impl<'a> RestoreResolvedModuleIndexes<'a> {
471 fn new(
472 root: &Path,
473 modules: &'a [fallow_types::extract::ModuleInfo],
474 files: &[DiscoveredFile],
475 ) -> Self {
476 let key_by_file_id = stable_key_by_file_id(root, files);
477 let id_by_key: rustc_hash::FxHashMap<_, _> = key_by_file_id
478 .iter()
479 .map(|(file_id, key)| (key.clone(), *file_id))
480 .collect();
481 let by_key: rustc_hash::FxHashMap<_, _> = modules
482 .iter()
483 .filter_map(|module| {
484 key_by_file_id
485 .get(&module.file_id)
486 .map(|key| (key.clone(), module))
487 })
488 .collect();
489 let path_by_key: rustc_hash::FxHashMap<_, _> = files
490 .iter()
491 .map(|file| {
492 (
493 StableFileKey::from_root_relative(root, &file.path),
494 file.path.clone(),
495 )
496 })
497 .collect();
498
499 Self {
500 file_ids: id_by_key,
501 modules: by_key,
502 paths: path_by_key,
503 }
504 }
505}
506
507fn restore_cached_resolved_module(
508 entry: &CachedResolvedModule,
509 indexes: &mut RestoreResolvedModuleIndexes<'_>,
510) -> Option<ResolvedModule> {
511 let module = indexes.modules.remove(&entry.key)?;
512 let path = indexes.paths.get(&entry.key)?.clone();
513 let resolved_dynamic_pattern_targets =
514 restore_dynamic_pattern_targets(entry, module, &indexes.file_ids)?;
515
516 Some(ResolvedModule {
517 file_id: module.file_id,
518 path,
519 exports: module.exports.clone(),
520 re_exports: entry
521 .re_exports
522 .iter()
523 .cloned()
524 .map(|re_export| re_export.into_resolved(&indexes.file_ids))
525 .collect::<Option<Vec<_>>>()?,
526 resolved_imports: entry
527 .resolved_imports
528 .iter()
529 .cloned()
530 .map(|import| import.into_resolved(&indexes.file_ids))
531 .collect::<Option<Vec<_>>>()?,
532 resolved_dynamic_imports: entry
533 .resolved_dynamic_imports
534 .iter()
535 .cloned()
536 .map(|import| import.into_resolved(&indexes.file_ids))
537 .collect::<Option<Vec<_>>>()?,
538 resolved_dynamic_patterns: module
539 .dynamic_import_patterns
540 .iter()
541 .cloned()
542 .zip(resolved_dynamic_pattern_targets)
543 .collect(),
544 member_accesses: module.member_accesses.clone(),
545 semantic_facts: module.semantic_facts.clone(),
546 whole_object_uses: module.whole_object_uses.clone(),
547 has_cjs_exports: module.has_cjs_exports,
548 has_angular_component_template_url: module.has_angular_component_template_url,
549 unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
550 type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
551 value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
552 namespace_object_aliases: module.namespace_object_aliases.clone(),
553 exported_factory_returns: module.exported_factory_returns.clone(),
554 exported_factory_return_object_shapes: module.exported_factory_return_object_shapes.clone(),
555 type_member_types: module.type_member_types.clone(),
556 })
557}
558
559fn restore_dynamic_pattern_targets(
560 entry: &CachedResolvedModule,
561 module: &fallow_types::extract::ModuleInfo,
562 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
563) -> Option<Vec<Vec<FileId>>> {
564 if entry.resolved_dynamic_pattern_targets.len() != module.dynamic_import_patterns.len() {
565 return None;
566 }
567 entry
568 .resolved_dynamic_pattern_targets
569 .iter()
570 .map(|targets| {
571 targets
572 .iter()
573 .map(|key| id_by_key.get(key).copied())
574 .collect::<Option<Vec<_>>>()
575 })
576 .collect()
577}
578
579fn stable_key_by_file_id(
580 root: &Path,
581 files: &[DiscoveredFile],
582) -> rustc_hash::FxHashMap<FileId, StableFileKey> {
583 files
584 .iter()
585 .map(|file| (file.id, StableFileKey::from_root_relative(root, &file.path)))
586 .collect()
587}
588
589fn span_to_pair(span: Span) -> [u32; 2] {
590 [span.start, span.end]
591}
592
593fn pair_to_span(pair: [u32; 2]) -> Span {
594 Span::new(pair[0], pair[1])
595}
596
597pub(crate) mod span_serde {
605 use oxc_span::Span;
606 use serde::{Deserialize, Deserializer, Serialize, Serializer};
607
608 #[expect(
609 clippy::trivially_copy_pass_by_ref,
610 reason = "serde `serialize_with` / `with` requires a `&T` signature"
611 )]
612 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
613 [span.start, span.end].serialize(serializer)
614 }
615
616 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
617 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
618 Ok(Span::new(start, end))
619 }
620}
621
622pub(crate) mod member_serde {
631 use fallow_types::extract::{MemberInfo, MemberKind};
632 use oxc_span::Span;
633 use serde::{Deserialize, Deserializer, Serialize, Serializer};
634
635 #[derive(Serialize, Deserialize)]
636 struct CachedMemberInfo {
637 name: String,
638 kind: MemberKind,
639 span: [u32; 2],
640 has_decorator: bool,
641 decorator_names: Vec<String>,
642 is_instance_returning_static: bool,
643 is_self_returning: bool,
644 }
645
646 impl From<&MemberInfo> for CachedMemberInfo {
647 fn from(member: &MemberInfo) -> Self {
648 Self {
649 name: member.name.clone(),
650 kind: member.kind,
651 span: [member.span.start, member.span.end],
652 has_decorator: member.has_decorator,
653 decorator_names: member.decorator_names.clone(),
654 is_instance_returning_static: member.is_instance_returning_static,
655 is_self_returning: member.is_self_returning,
656 }
657 }
658 }
659
660 impl From<CachedMemberInfo> for MemberInfo {
661 fn from(cached: CachedMemberInfo) -> Self {
662 Self {
663 name: cached.name,
664 kind: cached.kind,
665 span: Span::new(cached.span[0], cached.span[1]),
666 has_decorator: cached.has_decorator,
667 decorator_names: cached.decorator_names,
668 is_instance_returning_static: cached.is_instance_returning_static,
669 is_self_returning: cached.is_self_returning,
670 }
671 }
672 }
673
674 pub fn serialize<S: Serializer>(
675 members: &[MemberInfo],
676 serializer: S,
677 ) -> Result<S::Ok, S::Error> {
678 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
679 mirror.serialize(serializer)
680 }
681
682 pub fn deserialize<'de, D: Deserializer<'de>>(
683 deserializer: D,
684 ) -> Result<Vec<MemberInfo>, D::Error> {
685 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
686 Ok(mirror.into_iter().map(MemberInfo::from).collect())
687 }
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
696pub struct GraphCacheMode {
697 pub resolver_options_hash: u64,
699 pub entry_points_hash: u64,
701 pub plugin_config_hash: u64,
703}
704
705impl GraphCacheMode {
706 #[must_use]
708 pub const fn new(
709 resolver_options_hash: u64,
710 entry_points_hash: u64,
711 plugin_config_hash: u64,
712 ) -> Self {
713 Self {
714 resolver_options_hash,
715 entry_points_hash,
716 plugin_config_hash,
717 }
718 }
719}
720
721#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
723pub struct GraphCacheFile {
724 pub key: StableFileKey,
726 pub file_id: FileId,
733 pub fingerprint: SourceFingerprint,
735}
736
737impl GraphCacheFile {
738 #[must_use]
740 fn from_discovered_file(
741 root: &Path,
742 file: &DiscoveredFile,
743 fingerprint: SourceFingerprint,
744 ) -> Self {
745 Self {
746 key: StableFileKey::from_root_relative(root, &file.path),
747 file_id: file.id,
748 fingerprint,
749 }
750 }
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
755pub struct GraphCacheManifest {
756 pub version: u32,
758 pub mode: GraphCacheMode,
760 pub files: Vec<GraphCacheFile>,
762}
763
764impl GraphCacheManifest {
765 #[must_use]
767 fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
768 sort_files(&mut files);
769 Self {
770 version: GRAPH_CACHE_VERSION,
771 mode,
772 files,
773 }
774 }
775
776 pub fn from_discovered_files(
778 root: &Path,
779 files: &[DiscoveredFile],
780 mode: GraphCacheMode,
781 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
782 ) -> Self {
783 let rows = files
784 .iter()
785 .map(|file| {
786 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
787 })
788 .collect();
789 Self::new(mode, rows)
790 }
791
792 #[must_use]
794 pub fn matches_inputs(&self, current: &Self) -> bool {
795 self.version == GRAPH_CACHE_VERSION
796 && current.version == GRAPH_CACHE_VERSION
797 && self.mode == current.mode
798 && self.files == current.files
799 }
800
801 #[must_use]
808 pub fn matches_resolution_inputs(&self, current: &Self) -> bool {
809 self.version == GRAPH_CACHE_VERSION
810 && current.version == GRAPH_CACHE_VERSION
811 && self.mode == current.mode
812 && self.files.len() == current.files.len()
813 && self
814 .files
815 .iter()
816 .zip(current.files.iter())
817 .all(|(cached, current)| {
818 cached.key == current.key && cached.fingerprint == current.fingerprint
819 })
820 }
821}
822
823fn sort_files(files: &mut [GraphCacheFile]) {
824 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
825}
826
827#[cfg(test)]
828mod tests {
829 use std::path::{Path, PathBuf};
830
831 use fallow_types::discover::FileId;
832 use rustc_hash::FxHashMap;
833
834 use super::*;
835
836 fn file(id: u32, path: &str) -> DiscoveredFile {
837 DiscoveredFile {
838 id: FileId(id),
839 path: PathBuf::from(path),
840 size_bytes: 1,
841 }
842 }
843
844 fn mode() -> GraphCacheMode {
845 GraphCacheMode::new(1, 2, 3)
846 }
847
848 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
849 pairs
850 .iter()
851 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
852 .collect()
853 }
854
855 fn manifest(
856 files: &[DiscoveredFile],
857 mode: GraphCacheMode,
858 map: &FxHashMap<PathBuf, SourceFingerprint>,
859 ) -> GraphCacheManifest {
860 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
861 *map.get(path).unwrap()
862 })
863 }
864
865 fn import_info(source: &str) -> ImportInfo {
866 ImportInfo {
867 source: source.to_string(),
868 imported_name: fallow_types::extract::ImportedName::SideEffect,
869 local_name: String::new(),
870 is_type_only: false,
871 from_style: false,
872 span: Span::new(0, 0),
873 source_span: Span::new(0, 0),
874 }
875 }
876
877 #[test]
878 fn manifest_sorts_by_stable_file_key() {
879 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
880 let map = fingerprints(&[
881 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
882 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
883 ]);
884
885 let manifest = manifest(&files, mode(), &map);
886
887 let keys: Vec<&str> = manifest
888 .files
889 .iter()
890 .map(|file| file.key.as_str())
891 .collect();
892 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
893 }
894
895 #[test]
896 fn manifest_misses_on_file_id_shift_until_graph_remap_exists() {
897 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
898 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
899 let map = fingerprints(&[
900 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
901 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
902 ]);
903
904 let cached = manifest(&before, mode(), &map);
905 let current = manifest(&after, mode(), &map);
906
907 assert!(
908 !cached.matches_inputs(¤t),
909 "the persisted graph is still FileId-keyed, so FileId shifts cannot trust it"
910 );
911 assert!(
912 cached.matches_resolution_inputs(¤t),
913 "stable-keyed resolver payloads may be remapped across FileId shifts"
914 );
915 }
916
917 #[test]
918 fn cached_resolve_result_remaps_internal_targets_by_stable_key() {
919 let key_a = StableFileKey::from_root_relative(
920 Path::new("/project"),
921 Path::new("/project/src/a.ts"),
922 );
923 let key_b = StableFileKey::from_root_relative(
924 Path::new("/project"),
925 Path::new("/project/src/b.ts"),
926 );
927 let key_by_file_id =
928 FxHashMap::from_iter([(FileId(0), key_a.clone()), (FileId(1), key_b.clone())]);
929 let id_by_key = FxHashMap::from_iter([(key_a, FileId(7)), (key_b, FileId(9))]);
930
931 let cached = CachedResolveResult::from_resolve_result(
932 &ResolveResult::InternalPackageModule {
933 file_id: FileId(1),
934 package_name: "@scope/pkg".to_string(),
935 },
936 &key_by_file_id,
937 )
938 .expect("target file id should map to a stable key");
939
940 let restored = cached
941 .into_resolve_result(&id_by_key)
942 .expect("stable key should map to current FileId");
943
944 assert!(matches!(
945 restored,
946 ResolveResult::InternalPackageModule {
947 file_id: FileId(9),
948 ref package_name,
949 } if package_name == "@scope/pkg"
950 ));
951 }
952
953 #[test]
954 fn cached_resolve_result_preserves_commonjs_provenance() {
955 let key = StableFileKey::from_root_relative(
956 Path::new("/project"),
957 Path::new("/project/src/dependency.ts"),
958 );
959 let key_by_file_id = FxHashMap::from_iter([(FileId(3), key.clone())]);
960 let id_by_key = FxHashMap::from_iter([(key, FileId(8))]);
961
962 let cached = CachedResolveResult::from_resolve_result(
963 &ResolveResult::CommonJsInternalModule(FileId(3)),
964 &key_by_file_id,
965 )
966 .expect("CommonJS target should map to a stable key");
967 let restored = cached
968 .into_resolve_result(&id_by_key)
969 .expect("stable key should map to the current FileId");
970
971 assert!(matches!(
972 restored,
973 ResolveResult::CommonJsInternalModule(FileId(8))
974 ));
975 }
976
977 #[test]
978 fn cached_resolve_result_preserves_commonjs_bare_package_provenance() {
979 let cached = CachedResolveResult::from_resolve_result(
980 &ResolveResult::CommonJsNpmPackage("shared-package".to_string()),
981 &FxHashMap::default(),
982 )
983 .expect("bare CommonJS package should not need a stable file key");
984 let restored = cached
985 .into_resolve_result(&FxHashMap::default())
986 .expect("bare CommonJS package should restore without a file map");
987
988 assert!(matches!(
989 restored,
990 ResolveResult::CommonJsNpmPackage(package_name)
991 if package_name == "shared-package"
992 ));
993 }
994
995 #[test]
996 fn cache_resolved_project_rejects_unknown_internal_targets() {
997 let files = vec![file(0, "/project/src/a.ts")];
998 let module = ResolvedModule {
999 file_id: FileId(0),
1000 path: PathBuf::from("/project/src/a.ts"),
1001 resolved_imports: vec![ResolvedImport {
1002 info: import_info("./missing"),
1003 target: ResolveResult::InternalModule(FileId(1)),
1004 }],
1005 ..ResolvedModule::default()
1006 };
1007 let project = ResolvedProject {
1008 modules: vec![module],
1009 replaced_module_targets: Vec::new(),
1010 };
1011
1012 let cached = cache_resolved_project(Path::new("/project"), &files, &project);
1013
1014 assert!(cached.is_none());
1015 }
1016
1017 #[test]
1018 fn cached_replaced_target_remaps_both_file_ids_by_stable_key() {
1019 let source_key = StableFileKey::from_root_relative(
1020 Path::new("/project"),
1021 Path::new("/project/src/example.test.ts"),
1022 );
1023 let target_key = StableFileKey::from_root_relative(
1024 Path::new("/project"),
1025 Path::new("/project/src/dependency.ts"),
1026 );
1027 let key_by_file_id = FxHashMap::from_iter([
1028 (FileId(2), source_key.clone()),
1029 (FileId(3), target_key.clone()),
1030 ]);
1031 let id_by_key = FxHashMap::from_iter([(source_key, FileId(8)), (target_key, FileId(9))]);
1032 let resolved = ResolvedReplacedModuleTarget {
1033 source_file: FileId(2),
1034 target_file: FileId(3),
1035 };
1036
1037 let cached = CachedResolvedReplacedModuleTarget::from_resolved(resolved, &key_by_file_id)
1038 .expect("both file ids should map to stable keys");
1039 let restored = cached
1040 .into_resolved(&id_by_key)
1041 .expect("both stable keys should map to current file ids");
1042
1043 assert_eq!(
1044 restored,
1045 ResolvedReplacedModuleTarget {
1046 source_file: FileId(8),
1047 target_file: FileId(9),
1048 }
1049 );
1050 }
1051
1052 #[test]
1053 fn cache_resolved_project_rejects_unknown_replacement_targets() {
1054 let files = vec![file(0, "/project/src/example.test.ts")];
1055 let project = ResolvedProject {
1056 modules: vec![ResolvedModule {
1057 file_id: FileId(0),
1058 path: PathBuf::from("/project/src/example.test.ts"),
1059 ..ResolvedModule::default()
1060 }],
1061 replaced_module_targets: vec![ResolvedReplacedModuleTarget {
1062 source_file: FileId(0),
1063 target_file: FileId(1),
1064 }],
1065 };
1066
1067 let cached = cache_resolved_project(Path::new("/project"), &files, &project);
1068
1069 assert!(cached.is_none());
1070 }
1071
1072 #[test]
1073 fn manifest_misses_on_fingerprint_change() {
1074 let files = vec![file(0, "/project/src/a.ts")];
1075 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1076 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
1077
1078 let cached = manifest(&files, mode(), &cached_map);
1079 let current = manifest(&files, mode(), ¤t_map);
1080
1081 assert!(!cached.matches_inputs(¤t));
1082 }
1083
1084 #[test]
1085 fn manifest_misses_on_file_deletion() {
1086 let before = vec![
1087 file(0, "/project/src/a.ts"),
1088 file(1, "/project/src/deleted.ts"),
1089 ];
1090 let after = vec![file(0, "/project/src/a.ts")];
1091 let map = fingerprints(&[
1092 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
1093 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
1094 ]);
1095
1096 let cached = manifest(&before, mode(), &map);
1097 let current = manifest(&after, mode(), &map);
1098
1099 assert!(!cached.matches_inputs(¤t));
1100 }
1101
1102 #[test]
1103 fn manifest_misses_on_file_rename_with_same_fingerprint() {
1104 let before = vec![file(0, "/project/src/old.ts")];
1105 let after = vec![file(0, "/project/src/new.ts")];
1106 let map = fingerprints(&[
1107 ("/project/src/old.ts", SourceFingerprint::new(10, 1)),
1108 ("/project/src/new.ts", SourceFingerprint::new(10, 1)),
1109 ]);
1110
1111 let cached = manifest(&before, mode(), &map);
1112 let current = manifest(&after, mode(), &map);
1113
1114 assert!(!cached.matches_inputs(¤t));
1115 }
1116
1117 #[test]
1118 fn manifest_misses_on_workspace_scoped_file_set() {
1119 let full_project = vec![
1120 file(0, "/project/packages/app/src/index.ts"),
1121 file(1, "/project/packages/shared/src/index.ts"),
1122 ];
1123 let workspace_scoped = vec![file(0, "/project/packages/app/src/index.ts")];
1124 let map = fingerprints(&[
1125 (
1126 "/project/packages/app/src/index.ts",
1127 SourceFingerprint::new(10, 1),
1128 ),
1129 (
1130 "/project/packages/shared/src/index.ts",
1131 SourceFingerprint::new(20, 1),
1132 ),
1133 ]);
1134
1135 let cached = manifest(&full_project, mode(), &map);
1136 let current = manifest(&workspace_scoped, mode(), &map);
1137
1138 assert!(!cached.matches_inputs(¤t));
1139 assert!(!cached.matches_resolution_inputs(¤t));
1140 }
1141
1142 #[test]
1143 fn manifest_misses_on_mode_change() {
1144 let files = vec![file(0, "/project/src/a.ts")];
1145 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1146
1147 let cached = manifest(&files, mode(), &map);
1148 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
1149
1150 assert!(!cached.matches_inputs(¤t));
1151 }
1152
1153 #[test]
1154 fn manifest_misses_on_version_change() {
1155 let files = vec![file(0, "/project/src/a.ts")];
1156 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
1157 let mut cached = manifest(&files, mode(), &map);
1158 let current = manifest(&files, mode(), &map);
1159
1160 cached.version = GRAPH_CACHE_VERSION + 1;
1161
1162 assert!(!cached.matches_inputs(¤t));
1163 }
1164}