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