1use std::path::{Path, PathBuf};
9
10use fallow_types::discover::{DiscoveredFile, FileId, StableFileKey};
11use fallow_types::extract::{ImportInfo, ReExportInfo};
12use fallow_types::source_fingerprint::SourceFingerprint;
13use oxc_span::Span;
14
15use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
16
17mod store;
18
19pub use store::GraphCacheStore;
20
21pub const GRAPH_CACHE_VERSION: u32 = 4;
34
35#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
43pub enum CachedResolveResult {
44 InternalModule(StableFileKey),
46 SyntheticAutoImport(StableFileKey),
48 InternalPackageModule {
50 key: StableFileKey,
52 package_name: String,
54 },
55 ExternalFile(PathBuf),
57 NpmPackage(String),
59 Unresolvable(String),
61}
62
63impl CachedResolveResult {
64 fn from_resolve_result(
65 target: &ResolveResult,
66 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
67 ) -> Option<Self> {
68 Some(match target {
69 ResolveResult::InternalModule(file_id) => {
70 Self::InternalModule(key_by_file_id.get(file_id)?.clone())
71 }
72 ResolveResult::SyntheticAutoImport(file_id) => {
73 Self::SyntheticAutoImport(key_by_file_id.get(file_id)?.clone())
74 }
75 ResolveResult::InternalPackageModule {
76 file_id,
77 package_name,
78 } => Self::InternalPackageModule {
79 key: key_by_file_id.get(file_id)?.clone(),
80 package_name: package_name.clone(),
81 },
82 ResolveResult::ExternalFile(path) => Self::ExternalFile(path.clone()),
83 ResolveResult::NpmPackage(package_name) => Self::NpmPackage(package_name.clone()),
84 ResolveResult::Unresolvable(specifier) => Self::Unresolvable(specifier.clone()),
85 })
86 }
87
88 fn into_resolve_result(
89 self,
90 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
91 ) -> Option<ResolveResult> {
92 Some(match self {
93 Self::InternalModule(key) => ResolveResult::InternalModule(*id_by_key.get(&key)?),
94 Self::SyntheticAutoImport(key) => {
95 ResolveResult::SyntheticAutoImport(*id_by_key.get(&key)?)
96 }
97 Self::InternalPackageModule { key, package_name } => {
98 ResolveResult::InternalPackageModule {
99 file_id: *id_by_key.get(&key)?,
100 package_name,
101 }
102 }
103 Self::ExternalFile(path) => ResolveResult::ExternalFile(path),
104 Self::NpmPackage(package_name) => ResolveResult::NpmPackage(package_name),
105 Self::Unresolvable(specifier) => ResolveResult::Unresolvable(specifier),
106 })
107 }
108}
109
110#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
112pub struct CachedResolvedImport {
113 info: CachedImportInfo,
115 target: CachedResolveResult,
117}
118
119impl CachedResolvedImport {
120 fn from_resolved(
121 import: &ResolvedImport,
122 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
123 ) -> Option<Self> {
124 Some(Self {
125 info: CachedImportInfo::from(&import.info),
126 target: CachedResolveResult::from_resolve_result(&import.target, key_by_file_id)?,
127 })
128 }
129
130 fn into_resolved(
131 self,
132 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
133 ) -> Option<ResolvedImport> {
134 Some(ResolvedImport {
135 info: self.info.into(),
136 target: self.target.into_resolve_result(id_by_key)?,
137 })
138 }
139}
140
141#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
143pub struct CachedResolvedReExport {
144 info: CachedReExportInfo,
146 target: CachedResolveResult,
148}
149
150impl CachedResolvedReExport {
151 fn from_resolved(
152 re_export: &ResolvedReExport,
153 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
154 ) -> Option<Self> {
155 Some(Self {
156 info: CachedReExportInfo::from(&re_export.info),
157 target: CachedResolveResult::from_resolve_result(&re_export.target, key_by_file_id)?,
158 })
159 }
160
161 fn into_resolved(
162 self,
163 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
164 ) -> Option<ResolvedReExport> {
165 Some(ResolvedReExport {
166 info: self.info.into(),
167 target: self.target.into_resolve_result(id_by_key)?,
168 })
169 }
170}
171
172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
174pub struct CachedImportInfo {
175 source: String,
177 imported_name: fallow_types::extract::ImportedName,
179 local_name: String,
181 is_type_only: bool,
183 from_style: bool,
185 span: [u32; 2],
187 source_span: [u32; 2],
189}
190
191impl From<&ImportInfo> for CachedImportInfo {
192 fn from(info: &ImportInfo) -> Self {
193 Self {
194 source: info.source.clone(),
195 imported_name: info.imported_name.clone(),
196 local_name: info.local_name.clone(),
197 is_type_only: info.is_type_only,
198 from_style: info.from_style,
199 span: span_to_pair(info.span),
200 source_span: span_to_pair(info.source_span),
201 }
202 }
203}
204
205impl From<CachedImportInfo> for ImportInfo {
206 fn from(info: CachedImportInfo) -> Self {
207 Self {
208 source: info.source,
209 imported_name: info.imported_name,
210 local_name: info.local_name,
211 is_type_only: info.is_type_only,
212 from_style: info.from_style,
213 span: pair_to_span(info.span),
214 source_span: pair_to_span(info.source_span),
215 }
216 }
217}
218
219#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
221pub struct CachedReExportInfo {
222 source: String,
224 imported_name: String,
226 exported_name: String,
228 is_type_only: bool,
230 span: [u32; 2],
232}
233
234impl From<&ReExportInfo> for CachedReExportInfo {
235 fn from(info: &ReExportInfo) -> Self {
236 Self {
237 source: info.source.clone(),
238 imported_name: info.imported_name.clone(),
239 exported_name: info.exported_name.clone(),
240 is_type_only: info.is_type_only,
241 span: span_to_pair(info.span),
242 }
243 }
244}
245
246impl From<CachedReExportInfo> for ReExportInfo {
247 fn from(info: CachedReExportInfo) -> Self {
248 Self {
249 source: info.source,
250 imported_name: info.imported_name,
251 exported_name: info.exported_name,
252 is_type_only: info.is_type_only,
253 span: pair_to_span(info.span),
254 }
255 }
256}
257
258#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
260pub struct CachedResolvedModule {
261 key: StableFileKey,
263 resolved_imports: Vec<CachedResolvedImport>,
265 resolved_dynamic_imports: Vec<CachedResolvedImport>,
267 re_exports: Vec<CachedResolvedReExport>,
269 resolved_dynamic_pattern_targets: Vec<Vec<StableFileKey>>,
271}
272
273impl CachedResolvedModule {
274 fn from_resolved(
275 module: &ResolvedModule,
276 key_by_file_id: &rustc_hash::FxHashMap<FileId, StableFileKey>,
277 ) -> Option<Self> {
278 Some(Self {
279 key: key_by_file_id.get(&module.file_id)?.clone(),
280 resolved_imports: module
281 .resolved_imports
282 .iter()
283 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
284 .collect::<Option<Vec<_>>>()?,
285 resolved_dynamic_imports: module
286 .resolved_dynamic_imports
287 .iter()
288 .map(|import| CachedResolvedImport::from_resolved(import, key_by_file_id))
289 .collect::<Option<Vec<_>>>()?,
290 re_exports: module
291 .re_exports
292 .iter()
293 .map(|re_export| CachedResolvedReExport::from_resolved(re_export, key_by_file_id))
294 .collect::<Option<Vec<_>>>()?,
295 resolved_dynamic_pattern_targets: module
296 .resolved_dynamic_patterns
297 .iter()
298 .map(|(_, targets)| {
299 targets
300 .iter()
301 .map(|target| key_by_file_id.get(target).cloned())
302 .collect::<Option<Vec<_>>>()
303 })
304 .collect::<Option<Vec<_>>>()?,
305 })
306 }
307}
308
309#[must_use]
311pub fn cache_resolved_modules(
312 root: &Path,
313 files: &[DiscoveredFile],
314 resolved: &[ResolvedModule],
315) -> Option<Vec<CachedResolvedModule>> {
316 let key_by_file_id = stable_key_by_file_id(root, files);
317 resolved
318 .iter()
319 .map(|module| CachedResolvedModule::from_resolved(module, &key_by_file_id))
320 .collect()
321}
322
323#[must_use]
329pub fn restore_resolved_modules(
330 root: &Path,
331 modules: &[fallow_types::extract::ModuleInfo],
332 files: &[DiscoveredFile],
333 cached: &[CachedResolvedModule],
334) -> Option<Vec<ResolvedModule>> {
335 if modules.len() != cached.len() {
336 return None;
337 }
338
339 let mut indexes = RestoreResolvedModuleIndexes::new(root, modules, files);
340 cached
341 .iter()
342 .map(|entry| restore_cached_resolved_module(entry, &mut indexes))
343 .collect()
344}
345
346struct RestoreResolvedModuleIndexes<'a> {
347 file_ids: rustc_hash::FxHashMap<StableFileKey, FileId>,
348 modules: rustc_hash::FxHashMap<StableFileKey, &'a fallow_types::extract::ModuleInfo>,
349 paths: rustc_hash::FxHashMap<StableFileKey, std::path::PathBuf>,
350}
351
352impl<'a> RestoreResolvedModuleIndexes<'a> {
353 fn new(
354 root: &Path,
355 modules: &'a [fallow_types::extract::ModuleInfo],
356 files: &[DiscoveredFile],
357 ) -> Self {
358 let key_by_file_id = stable_key_by_file_id(root, files);
359 let id_by_key: rustc_hash::FxHashMap<_, _> = key_by_file_id
360 .iter()
361 .map(|(file_id, key)| (key.clone(), *file_id))
362 .collect();
363 let by_key: rustc_hash::FxHashMap<_, _> = modules
364 .iter()
365 .filter_map(|module| {
366 key_by_file_id
367 .get(&module.file_id)
368 .map(|key| (key.clone(), module))
369 })
370 .collect();
371 let path_by_key: rustc_hash::FxHashMap<_, _> = files
372 .iter()
373 .map(|file| {
374 (
375 StableFileKey::from_root_relative(root, &file.path),
376 file.path.clone(),
377 )
378 })
379 .collect();
380
381 Self {
382 file_ids: id_by_key,
383 modules: by_key,
384 paths: path_by_key,
385 }
386 }
387}
388
389fn restore_cached_resolved_module(
390 entry: &CachedResolvedModule,
391 indexes: &mut RestoreResolvedModuleIndexes<'_>,
392) -> Option<ResolvedModule> {
393 let module = indexes.modules.remove(&entry.key)?;
394 let path = indexes.paths.get(&entry.key)?.clone();
395 let resolved_dynamic_pattern_targets =
396 restore_dynamic_pattern_targets(entry, module, &indexes.file_ids)?;
397
398 Some(ResolvedModule {
399 file_id: module.file_id,
400 path,
401 exports: module.exports.clone(),
402 re_exports: entry
403 .re_exports
404 .iter()
405 .cloned()
406 .map(|re_export| re_export.into_resolved(&indexes.file_ids))
407 .collect::<Option<Vec<_>>>()?,
408 resolved_imports: entry
409 .resolved_imports
410 .iter()
411 .cloned()
412 .map(|import| import.into_resolved(&indexes.file_ids))
413 .collect::<Option<Vec<_>>>()?,
414 resolved_dynamic_imports: entry
415 .resolved_dynamic_imports
416 .iter()
417 .cloned()
418 .map(|import| import.into_resolved(&indexes.file_ids))
419 .collect::<Option<Vec<_>>>()?,
420 resolved_dynamic_patterns: module
421 .dynamic_import_patterns
422 .iter()
423 .cloned()
424 .zip(resolved_dynamic_pattern_targets)
425 .collect(),
426 member_accesses: module.member_accesses.clone(),
427 semantic_facts: module.semantic_facts.clone(),
428 whole_object_uses: module.whole_object_uses.clone(),
429 has_cjs_exports: module.has_cjs_exports,
430 has_angular_component_template_url: module.has_angular_component_template_url,
431 unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
432 type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
433 value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
434 namespace_object_aliases: module.namespace_object_aliases.clone(),
435 exported_factory_returns: module.exported_factory_returns.clone(),
436 exported_factory_return_object_shapes: module.exported_factory_return_object_shapes.clone(),
437 type_member_types: module.type_member_types.clone(),
438 })
439}
440
441fn restore_dynamic_pattern_targets(
442 entry: &CachedResolvedModule,
443 module: &fallow_types::extract::ModuleInfo,
444 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
445) -> Option<Vec<Vec<FileId>>> {
446 if entry.resolved_dynamic_pattern_targets.len() != module.dynamic_import_patterns.len() {
447 return None;
448 }
449 entry
450 .resolved_dynamic_pattern_targets
451 .iter()
452 .map(|targets| {
453 targets
454 .iter()
455 .map(|key| id_by_key.get(key).copied())
456 .collect::<Option<Vec<_>>>()
457 })
458 .collect()
459}
460
461fn stable_key_by_file_id(
462 root: &Path,
463 files: &[DiscoveredFile],
464) -> rustc_hash::FxHashMap<FileId, StableFileKey> {
465 files
466 .iter()
467 .map(|file| (file.id, StableFileKey::from_root_relative(root, &file.path)))
468 .collect()
469}
470
471fn span_to_pair(span: Span) -> [u32; 2] {
472 [span.start, span.end]
473}
474
475fn pair_to_span(pair: [u32; 2]) -> Span {
476 Span::new(pair[0], pair[1])
477}
478
479pub(crate) mod span_serde {
487 use oxc_span::Span;
488 use serde::{Deserialize, Deserializer, Serialize, Serializer};
489
490 #[expect(
491 clippy::trivially_copy_pass_by_ref,
492 reason = "serde `serialize_with` / `with` requires a `&T` signature"
493 )]
494 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
495 [span.start, span.end].serialize(serializer)
496 }
497
498 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
499 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
500 Ok(Span::new(start, end))
501 }
502}
503
504pub(crate) mod member_serde {
513 use fallow_types::extract::{MemberInfo, MemberKind};
514 use oxc_span::Span;
515 use serde::{Deserialize, Deserializer, Serialize, Serializer};
516
517 #[derive(Serialize, Deserialize)]
518 struct CachedMemberInfo {
519 name: String,
520 kind: MemberKind,
521 span: [u32; 2],
522 has_decorator: bool,
523 decorator_names: Vec<String>,
524 is_instance_returning_static: bool,
525 is_self_returning: bool,
526 }
527
528 impl From<&MemberInfo> for CachedMemberInfo {
529 fn from(member: &MemberInfo) -> Self {
530 Self {
531 name: member.name.clone(),
532 kind: member.kind,
533 span: [member.span.start, member.span.end],
534 has_decorator: member.has_decorator,
535 decorator_names: member.decorator_names.clone(),
536 is_instance_returning_static: member.is_instance_returning_static,
537 is_self_returning: member.is_self_returning,
538 }
539 }
540 }
541
542 impl From<CachedMemberInfo> for MemberInfo {
543 fn from(cached: CachedMemberInfo) -> Self {
544 Self {
545 name: cached.name,
546 kind: cached.kind,
547 span: Span::new(cached.span[0], cached.span[1]),
548 has_decorator: cached.has_decorator,
549 decorator_names: cached.decorator_names,
550 is_instance_returning_static: cached.is_instance_returning_static,
551 is_self_returning: cached.is_self_returning,
552 }
553 }
554 }
555
556 pub fn serialize<S: Serializer>(
557 members: &[MemberInfo],
558 serializer: S,
559 ) -> Result<S::Ok, S::Error> {
560 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
561 mirror.serialize(serializer)
562 }
563
564 pub fn deserialize<'de, D: Deserializer<'de>>(
565 deserializer: D,
566 ) -> Result<Vec<MemberInfo>, D::Error> {
567 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
568 Ok(mirror.into_iter().map(MemberInfo::from).collect())
569 }
570}
571
572#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
578pub struct GraphCacheMode {
579 pub resolver_options_hash: u64,
581 pub entry_points_hash: u64,
583 pub plugin_config_hash: u64,
585}
586
587impl GraphCacheMode {
588 #[must_use]
590 pub const fn new(
591 resolver_options_hash: u64,
592 entry_points_hash: u64,
593 plugin_config_hash: u64,
594 ) -> Self {
595 Self {
596 resolver_options_hash,
597 entry_points_hash,
598 plugin_config_hash,
599 }
600 }
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
605pub struct GraphCacheFile {
606 pub key: StableFileKey,
608 pub file_id: FileId,
615 pub fingerprint: SourceFingerprint,
617}
618
619impl GraphCacheFile {
620 #[must_use]
622 fn from_discovered_file(
623 root: &Path,
624 file: &DiscoveredFile,
625 fingerprint: SourceFingerprint,
626 ) -> Self {
627 Self {
628 key: StableFileKey::from_root_relative(root, &file.path),
629 file_id: file.id,
630 fingerprint,
631 }
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
637pub struct GraphCacheManifest {
638 pub version: u32,
640 pub mode: GraphCacheMode,
642 pub files: Vec<GraphCacheFile>,
644}
645
646impl GraphCacheManifest {
647 #[must_use]
649 fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
650 sort_files(&mut files);
651 Self {
652 version: GRAPH_CACHE_VERSION,
653 mode,
654 files,
655 }
656 }
657
658 pub fn from_discovered_files(
660 root: &Path,
661 files: &[DiscoveredFile],
662 mode: GraphCacheMode,
663 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
664 ) -> Self {
665 let rows = files
666 .iter()
667 .map(|file| {
668 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
669 })
670 .collect();
671 Self::new(mode, rows)
672 }
673
674 #[must_use]
676 pub fn matches_inputs(&self, current: &Self) -> bool {
677 self.version == GRAPH_CACHE_VERSION
678 && current.version == GRAPH_CACHE_VERSION
679 && self.mode == current.mode
680 && self.files == current.files
681 }
682
683 #[must_use]
690 pub fn matches_resolution_inputs(&self, current: &Self) -> bool {
691 self.version == GRAPH_CACHE_VERSION
692 && current.version == GRAPH_CACHE_VERSION
693 && self.mode == current.mode
694 && self.files.len() == current.files.len()
695 && self
696 .files
697 .iter()
698 .zip(current.files.iter())
699 .all(|(cached, current)| {
700 cached.key == current.key && cached.fingerprint == current.fingerprint
701 })
702 }
703}
704
705fn sort_files(files: &mut [GraphCacheFile]) {
706 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
707}
708
709#[cfg(test)]
710mod tests {
711 use std::path::{Path, PathBuf};
712
713 use fallow_types::discover::FileId;
714 use rustc_hash::FxHashMap;
715
716 use super::*;
717
718 fn file(id: u32, path: &str) -> DiscoveredFile {
719 DiscoveredFile {
720 id: FileId(id),
721 path: PathBuf::from(path),
722 size_bytes: 1,
723 }
724 }
725
726 fn mode() -> GraphCacheMode {
727 GraphCacheMode::new(1, 2, 3)
728 }
729
730 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
731 pairs
732 .iter()
733 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
734 .collect()
735 }
736
737 fn manifest(
738 files: &[DiscoveredFile],
739 mode: GraphCacheMode,
740 map: &FxHashMap<PathBuf, SourceFingerprint>,
741 ) -> GraphCacheManifest {
742 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
743 *map.get(path).unwrap()
744 })
745 }
746
747 fn import_info(source: &str) -> ImportInfo {
748 ImportInfo {
749 source: source.to_string(),
750 imported_name: fallow_types::extract::ImportedName::SideEffect,
751 local_name: String::new(),
752 is_type_only: false,
753 from_style: false,
754 span: Span::new(0, 0),
755 source_span: Span::new(0, 0),
756 }
757 }
758
759 #[test]
760 fn manifest_sorts_by_stable_file_key() {
761 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
762 let map = fingerprints(&[
763 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
764 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
765 ]);
766
767 let manifest = manifest(&files, mode(), &map);
768
769 let keys: Vec<&str> = manifest
770 .files
771 .iter()
772 .map(|file| file.key.as_str())
773 .collect();
774 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
775 }
776
777 #[test]
778 fn manifest_misses_on_file_id_shift_until_graph_remap_exists() {
779 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
780 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
781 let map = fingerprints(&[
782 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
783 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
784 ]);
785
786 let cached = manifest(&before, mode(), &map);
787 let current = manifest(&after, mode(), &map);
788
789 assert!(
790 !cached.matches_inputs(¤t),
791 "the persisted graph is still FileId-keyed, so FileId shifts cannot trust it"
792 );
793 assert!(
794 cached.matches_resolution_inputs(¤t),
795 "stable-keyed resolver payloads may be remapped across FileId shifts"
796 );
797 }
798
799 #[test]
800 fn cached_resolve_result_remaps_internal_targets_by_stable_key() {
801 let key_a = StableFileKey::from_root_relative(
802 Path::new("/project"),
803 Path::new("/project/src/a.ts"),
804 );
805 let key_b = StableFileKey::from_root_relative(
806 Path::new("/project"),
807 Path::new("/project/src/b.ts"),
808 );
809 let key_by_file_id =
810 FxHashMap::from_iter([(FileId(0), key_a.clone()), (FileId(1), key_b.clone())]);
811 let id_by_key = FxHashMap::from_iter([(key_a, FileId(7)), (key_b, FileId(9))]);
812
813 let cached = CachedResolveResult::from_resolve_result(
814 &ResolveResult::InternalPackageModule {
815 file_id: FileId(1),
816 package_name: "@scope/pkg".to_string(),
817 },
818 &key_by_file_id,
819 )
820 .expect("target file id should map to a stable key");
821
822 let restored = cached
823 .into_resolve_result(&id_by_key)
824 .expect("stable key should map to current FileId");
825
826 assert!(matches!(
827 restored,
828 ResolveResult::InternalPackageModule {
829 file_id: FileId(9),
830 ref package_name,
831 } if package_name == "@scope/pkg"
832 ));
833 }
834
835 #[test]
836 fn cache_resolved_modules_rejects_unknown_internal_targets() {
837 let files = vec![file(0, "/project/src/a.ts")];
838 let module = ResolvedModule {
839 file_id: FileId(0),
840 path: PathBuf::from("/project/src/a.ts"),
841 resolved_imports: vec![ResolvedImport {
842 info: import_info("./missing"),
843 target: ResolveResult::InternalModule(FileId(1)),
844 }],
845 ..ResolvedModule::default()
846 };
847
848 let cached = cache_resolved_modules(Path::new("/project"), &files, &[module]);
849
850 assert!(cached.is_none());
851 }
852
853 #[test]
854 fn manifest_misses_on_fingerprint_change() {
855 let files = vec![file(0, "/project/src/a.ts")];
856 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
857 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
858
859 let cached = manifest(&files, mode(), &cached_map);
860 let current = manifest(&files, mode(), ¤t_map);
861
862 assert!(!cached.matches_inputs(¤t));
863 }
864
865 #[test]
866 fn manifest_misses_on_file_deletion() {
867 let before = vec![
868 file(0, "/project/src/a.ts"),
869 file(1, "/project/src/deleted.ts"),
870 ];
871 let after = vec![file(0, "/project/src/a.ts")];
872 let map = fingerprints(&[
873 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
874 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
875 ]);
876
877 let cached = manifest(&before, mode(), &map);
878 let current = manifest(&after, mode(), &map);
879
880 assert!(!cached.matches_inputs(¤t));
881 }
882
883 #[test]
884 fn manifest_misses_on_file_rename_with_same_fingerprint() {
885 let before = vec![file(0, "/project/src/old.ts")];
886 let after = vec![file(0, "/project/src/new.ts")];
887 let map = fingerprints(&[
888 ("/project/src/old.ts", SourceFingerprint::new(10, 1)),
889 ("/project/src/new.ts", SourceFingerprint::new(10, 1)),
890 ]);
891
892 let cached = manifest(&before, mode(), &map);
893 let current = manifest(&after, mode(), &map);
894
895 assert!(!cached.matches_inputs(¤t));
896 }
897
898 #[test]
899 fn manifest_misses_on_workspace_scoped_file_set() {
900 let full_project = vec![
901 file(0, "/project/packages/app/src/index.ts"),
902 file(1, "/project/packages/shared/src/index.ts"),
903 ];
904 let workspace_scoped = vec![file(0, "/project/packages/app/src/index.ts")];
905 let map = fingerprints(&[
906 (
907 "/project/packages/app/src/index.ts",
908 SourceFingerprint::new(10, 1),
909 ),
910 (
911 "/project/packages/shared/src/index.ts",
912 SourceFingerprint::new(20, 1),
913 ),
914 ]);
915
916 let cached = manifest(&full_project, mode(), &map);
917 let current = manifest(&workspace_scoped, mode(), &map);
918
919 assert!(!cached.matches_inputs(¤t));
920 assert!(!cached.matches_resolution_inputs(¤t));
921 }
922
923 #[test]
924 fn manifest_misses_on_mode_change() {
925 let files = vec![file(0, "/project/src/a.ts")];
926 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
927
928 let cached = manifest(&files, mode(), &map);
929 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
930
931 assert!(!cached.matches_inputs(¤t));
932 }
933
934 #[test]
935 fn manifest_misses_on_version_change() {
936 let files = vec![file(0, "/project/src/a.ts")];
937 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
938 let mut cached = manifest(&files, mode(), &map);
939 let current = manifest(&files, mode(), &map);
940
941 cached.version = GRAPH_CACHE_VERSION + 1;
942
943 assert!(!cached.matches_inputs(¤t));
944 }
945}