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