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