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 })
431}
432
433fn restore_dynamic_pattern_targets(
434 entry: &CachedResolvedModule,
435 module: &fallow_types::extract::ModuleInfo,
436 id_by_key: &rustc_hash::FxHashMap<StableFileKey, FileId>,
437) -> Option<Vec<Vec<FileId>>> {
438 if entry.resolved_dynamic_pattern_targets.len() != module.dynamic_import_patterns.len() {
439 return None;
440 }
441 entry
442 .resolved_dynamic_pattern_targets
443 .iter()
444 .map(|targets| {
445 targets
446 .iter()
447 .map(|key| id_by_key.get(key).copied())
448 .collect::<Option<Vec<_>>>()
449 })
450 .collect()
451}
452
453fn stable_key_by_file_id(
454 root: &Path,
455 files: &[DiscoveredFile],
456) -> rustc_hash::FxHashMap<FileId, StableFileKey> {
457 files
458 .iter()
459 .map(|file| (file.id, StableFileKey::from_root_relative(root, &file.path)))
460 .collect()
461}
462
463fn span_to_pair(span: Span) -> [u32; 2] {
464 [span.start, span.end]
465}
466
467fn pair_to_span(pair: [u32; 2]) -> Span {
468 Span::new(pair[0], pair[1])
469}
470
471pub(crate) mod span_serde {
479 use oxc_span::Span;
480 use serde::{Deserialize, Deserializer, Serialize, Serializer};
481
482 #[expect(
483 clippy::trivially_copy_pass_by_ref,
484 reason = "serde `serialize_with` / `with` requires a `&T` signature"
485 )]
486 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
487 [span.start, span.end].serialize(serializer)
488 }
489
490 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
491 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
492 Ok(Span::new(start, end))
493 }
494}
495
496pub(crate) mod member_serde {
505 use fallow_types::extract::{MemberInfo, MemberKind};
506 use oxc_span::Span;
507 use serde::{Deserialize, Deserializer, Serialize, Serializer};
508
509 #[derive(Serialize, Deserialize)]
510 struct CachedMemberInfo {
511 name: String,
512 kind: MemberKind,
513 span: [u32; 2],
514 has_decorator: bool,
515 decorator_names: Vec<String>,
516 is_instance_returning_static: bool,
517 is_self_returning: bool,
518 }
519
520 impl From<&MemberInfo> for CachedMemberInfo {
521 fn from(member: &MemberInfo) -> Self {
522 Self {
523 name: member.name.clone(),
524 kind: member.kind,
525 span: [member.span.start, member.span.end],
526 has_decorator: member.has_decorator,
527 decorator_names: member.decorator_names.clone(),
528 is_instance_returning_static: member.is_instance_returning_static,
529 is_self_returning: member.is_self_returning,
530 }
531 }
532 }
533
534 impl From<CachedMemberInfo> for MemberInfo {
535 fn from(cached: CachedMemberInfo) -> Self {
536 Self {
537 name: cached.name,
538 kind: cached.kind,
539 span: Span::new(cached.span[0], cached.span[1]),
540 has_decorator: cached.has_decorator,
541 decorator_names: cached.decorator_names,
542 is_instance_returning_static: cached.is_instance_returning_static,
543 is_self_returning: cached.is_self_returning,
544 }
545 }
546 }
547
548 pub fn serialize<S: Serializer>(
549 members: &[MemberInfo],
550 serializer: S,
551 ) -> Result<S::Ok, S::Error> {
552 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
553 mirror.serialize(serializer)
554 }
555
556 pub fn deserialize<'de, D: Deserializer<'de>>(
557 deserializer: D,
558 ) -> Result<Vec<MemberInfo>, D::Error> {
559 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
560 Ok(mirror.into_iter().map(MemberInfo::from).collect())
561 }
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
570pub struct GraphCacheMode {
571 pub resolver_options_hash: u64,
573 pub entry_points_hash: u64,
575 pub plugin_config_hash: u64,
577}
578
579impl GraphCacheMode {
580 #[must_use]
582 pub const fn new(
583 resolver_options_hash: u64,
584 entry_points_hash: u64,
585 plugin_config_hash: u64,
586 ) -> Self {
587 Self {
588 resolver_options_hash,
589 entry_points_hash,
590 plugin_config_hash,
591 }
592 }
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
597pub struct GraphCacheFile {
598 pub key: StableFileKey,
600 pub file_id: FileId,
607 pub fingerprint: SourceFingerprint,
609}
610
611impl GraphCacheFile {
612 #[must_use]
614 pub fn from_discovered_file(
615 root: &Path,
616 file: &DiscoveredFile,
617 fingerprint: SourceFingerprint,
618 ) -> Self {
619 Self {
620 key: StableFileKey::from_root_relative(root, &file.path),
621 file_id: file.id,
622 fingerprint,
623 }
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
629pub struct GraphCacheManifest {
630 pub version: u32,
632 pub mode: GraphCacheMode,
634 pub files: Vec<GraphCacheFile>,
636}
637
638impl GraphCacheManifest {
639 #[must_use]
641 pub fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
642 sort_files(&mut files);
643 Self {
644 version: GRAPH_CACHE_VERSION,
645 mode,
646 files,
647 }
648 }
649
650 pub fn from_discovered_files(
652 root: &Path,
653 files: &[DiscoveredFile],
654 mode: GraphCacheMode,
655 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
656 ) -> Self {
657 let rows = files
658 .iter()
659 .map(|file| {
660 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
661 })
662 .collect();
663 Self::new(mode, rows)
664 }
665
666 #[must_use]
668 pub fn matches_inputs(&self, current: &Self) -> bool {
669 self.version == GRAPH_CACHE_VERSION
670 && current.version == GRAPH_CACHE_VERSION
671 && self.mode == current.mode
672 && self.files == current.files
673 }
674
675 #[must_use]
682 pub fn matches_resolution_inputs(&self, current: &Self) -> bool {
683 self.version == GRAPH_CACHE_VERSION
684 && current.version == GRAPH_CACHE_VERSION
685 && self.mode == current.mode
686 && self.files.len() == current.files.len()
687 && self
688 .files
689 .iter()
690 .zip(current.files.iter())
691 .all(|(cached, current)| {
692 cached.key == current.key && cached.fingerprint == current.fingerprint
693 })
694 }
695}
696
697fn sort_files(files: &mut [GraphCacheFile]) {
698 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
699}
700
701#[cfg(test)]
702mod tests {
703 use std::path::{Path, PathBuf};
704
705 use fallow_types::discover::FileId;
706 use rustc_hash::FxHashMap;
707
708 use super::*;
709
710 fn file(id: u32, path: &str) -> DiscoveredFile {
711 DiscoveredFile {
712 id: FileId(id),
713 path: PathBuf::from(path),
714 size_bytes: 1,
715 }
716 }
717
718 fn mode() -> GraphCacheMode {
719 GraphCacheMode::new(1, 2, 3)
720 }
721
722 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
723 pairs
724 .iter()
725 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
726 .collect()
727 }
728
729 fn manifest(
730 files: &[DiscoveredFile],
731 mode: GraphCacheMode,
732 map: &FxHashMap<PathBuf, SourceFingerprint>,
733 ) -> GraphCacheManifest {
734 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
735 *map.get(path).unwrap()
736 })
737 }
738
739 fn import_info(source: &str) -> ImportInfo {
740 ImportInfo {
741 source: source.to_string(),
742 imported_name: fallow_types::extract::ImportedName::SideEffect,
743 local_name: String::new(),
744 is_type_only: false,
745 from_style: false,
746 span: Span::new(0, 0),
747 source_span: Span::new(0, 0),
748 }
749 }
750
751 #[test]
752 fn manifest_sorts_by_stable_file_key() {
753 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
754 let map = fingerprints(&[
755 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
756 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
757 ]);
758
759 let manifest = manifest(&files, mode(), &map);
760
761 let keys: Vec<&str> = manifest
762 .files
763 .iter()
764 .map(|file| file.key.as_str())
765 .collect();
766 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
767 }
768
769 #[test]
770 fn manifest_misses_on_file_id_shift_until_graph_remap_exists() {
771 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
772 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
773 let map = fingerprints(&[
774 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
775 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
776 ]);
777
778 let cached = manifest(&before, mode(), &map);
779 let current = manifest(&after, mode(), &map);
780
781 assert!(
782 !cached.matches_inputs(¤t),
783 "the persisted graph is still FileId-keyed, so FileId shifts cannot trust it"
784 );
785 assert!(
786 cached.matches_resolution_inputs(¤t),
787 "stable-keyed resolver payloads may be remapped across FileId shifts"
788 );
789 }
790
791 #[test]
792 fn cached_resolve_result_remaps_internal_targets_by_stable_key() {
793 let key_a = StableFileKey::from_root_relative(
794 Path::new("/project"),
795 Path::new("/project/src/a.ts"),
796 );
797 let key_b = StableFileKey::from_root_relative(
798 Path::new("/project"),
799 Path::new("/project/src/b.ts"),
800 );
801 let key_by_file_id =
802 FxHashMap::from_iter([(FileId(0), key_a.clone()), (FileId(1), key_b.clone())]);
803 let id_by_key = FxHashMap::from_iter([(key_a, FileId(7)), (key_b, FileId(9))]);
804
805 let cached = CachedResolveResult::from_resolve_result(
806 &ResolveResult::InternalPackageModule {
807 file_id: FileId(1),
808 package_name: "@scope/pkg".to_string(),
809 },
810 &key_by_file_id,
811 )
812 .expect("target file id should map to a stable key");
813
814 let restored = cached
815 .into_resolve_result(&id_by_key)
816 .expect("stable key should map to current FileId");
817
818 assert!(matches!(
819 restored,
820 ResolveResult::InternalPackageModule {
821 file_id: FileId(9),
822 ref package_name,
823 } if package_name == "@scope/pkg"
824 ));
825 }
826
827 #[test]
828 fn cache_resolved_modules_rejects_unknown_internal_targets() {
829 let files = vec![file(0, "/project/src/a.ts")];
830 let module = ResolvedModule {
831 file_id: FileId(0),
832 path: PathBuf::from("/project/src/a.ts"),
833 resolved_imports: vec![ResolvedImport {
834 info: import_info("./missing"),
835 target: ResolveResult::InternalModule(FileId(1)),
836 }],
837 ..ResolvedModule::default()
838 };
839
840 let cached = cache_resolved_modules(Path::new("/project"), &files, &[module]);
841
842 assert!(cached.is_none());
843 }
844
845 #[test]
846 fn manifest_misses_on_fingerprint_change() {
847 let files = vec![file(0, "/project/src/a.ts")];
848 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
849 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
850
851 let cached = manifest(&files, mode(), &cached_map);
852 let current = manifest(&files, mode(), ¤t_map);
853
854 assert!(!cached.matches_inputs(¤t));
855 }
856
857 #[test]
858 fn manifest_misses_on_file_deletion() {
859 let before = vec![
860 file(0, "/project/src/a.ts"),
861 file(1, "/project/src/deleted.ts"),
862 ];
863 let after = vec![file(0, "/project/src/a.ts")];
864 let map = fingerprints(&[
865 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
866 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
867 ]);
868
869 let cached = manifest(&before, mode(), &map);
870 let current = manifest(&after, mode(), &map);
871
872 assert!(!cached.matches_inputs(¤t));
873 }
874
875 #[test]
876 fn manifest_misses_on_file_rename_with_same_fingerprint() {
877 let before = vec![file(0, "/project/src/old.ts")];
878 let after = vec![file(0, "/project/src/new.ts")];
879 let map = fingerprints(&[
880 ("/project/src/old.ts", SourceFingerprint::new(10, 1)),
881 ("/project/src/new.ts", SourceFingerprint::new(10, 1)),
882 ]);
883
884 let cached = manifest(&before, mode(), &map);
885 let current = manifest(&after, mode(), &map);
886
887 assert!(!cached.matches_inputs(¤t));
888 }
889
890 #[test]
891 fn manifest_misses_on_workspace_scoped_file_set() {
892 let full_project = vec![
893 file(0, "/project/packages/app/src/index.ts"),
894 file(1, "/project/packages/shared/src/index.ts"),
895 ];
896 let workspace_scoped = vec![file(0, "/project/packages/app/src/index.ts")];
897 let map = fingerprints(&[
898 (
899 "/project/packages/app/src/index.ts",
900 SourceFingerprint::new(10, 1),
901 ),
902 (
903 "/project/packages/shared/src/index.ts",
904 SourceFingerprint::new(20, 1),
905 ),
906 ]);
907
908 let cached = manifest(&full_project, mode(), &map);
909 let current = manifest(&workspace_scoped, mode(), &map);
910
911 assert!(!cached.matches_inputs(¤t));
912 assert!(!cached.matches_resolution_inputs(¤t));
913 }
914
915 #[test]
916 fn manifest_misses_on_mode_change() {
917 let files = vec![file(0, "/project/src/a.ts")];
918 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
919
920 let cached = manifest(&files, mode(), &map);
921 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
922
923 assert!(!cached.matches_inputs(¤t));
924 }
925
926 #[test]
927 fn manifest_misses_on_version_change() {
928 let files = vec![file(0, "/project/src/a.ts")];
929 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
930 let mut cached = manifest(&files, mode(), &map);
931 let current = manifest(&files, mode(), &map);
932
933 cached.version = GRAPH_CACHE_VERSION + 1;
934
935 assert!(!cached.matches_inputs(¤t));
936 }
937}