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