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