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 key_by_file_id = stable_key_by_file_id(root, files);
334 let id_by_key: rustc_hash::FxHashMap<_, _> = key_by_file_id
335 .iter()
336 .map(|(file_id, key)| (key.clone(), *file_id))
337 .collect();
338 let mut by_key: rustc_hash::FxHashMap<_, _> = modules
339 .iter()
340 .filter_map(|module| {
341 key_by_file_id
342 .get(&module.file_id)
343 .map(|key| (key.clone(), module))
344 })
345 .collect();
346 let path_by_key: rustc_hash::FxHashMap<_, _> = files
347 .iter()
348 .map(|file| {
349 (
350 StableFileKey::from_root_relative(root, &file.path),
351 file.path.clone(),
352 )
353 })
354 .collect();
355
356 cached
357 .iter()
358 .map(|entry| {
359 let module = by_key.remove(&entry.key)?;
360 let path = path_by_key.get(&entry.key)?.clone();
361 if entry.resolved_dynamic_pattern_targets.len() != module.dynamic_import_patterns.len()
362 {
363 return None;
364 }
365 let resolved_dynamic_pattern_targets = entry
366 .resolved_dynamic_pattern_targets
367 .iter()
368 .map(|targets| {
369 targets
370 .iter()
371 .map(|key| id_by_key.get(key).copied())
372 .collect::<Option<Vec<_>>>()
373 })
374 .collect::<Option<Vec<_>>>()?;
375
376 Some(ResolvedModule {
377 file_id: module.file_id,
378 path,
379 exports: module.exports.clone(),
380 re_exports: entry
381 .re_exports
382 .iter()
383 .cloned()
384 .map(|re_export| re_export.into_resolved(&id_by_key))
385 .collect::<Option<Vec<_>>>()?,
386 resolved_imports: entry
387 .resolved_imports
388 .iter()
389 .cloned()
390 .map(|import| import.into_resolved(&id_by_key))
391 .collect::<Option<Vec<_>>>()?,
392 resolved_dynamic_imports: entry
393 .resolved_dynamic_imports
394 .iter()
395 .cloned()
396 .map(|import| import.into_resolved(&id_by_key))
397 .collect::<Option<Vec<_>>>()?,
398 resolved_dynamic_patterns: module
399 .dynamic_import_patterns
400 .iter()
401 .cloned()
402 .zip(resolved_dynamic_pattern_targets)
403 .collect(),
404 member_accesses: module.member_accesses.clone(),
405 semantic_facts: module.semantic_facts.clone(),
406 whole_object_uses: module.whole_object_uses.clone(),
407 has_cjs_exports: module.has_cjs_exports,
408 has_angular_component_template_url: module.has_angular_component_template_url,
409 unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
410 type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
411 value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
412 namespace_object_aliases: module.namespace_object_aliases.clone(),
413 exported_factory_returns: module.exported_factory_returns.clone(),
414 })
415 })
416 .collect()
417}
418
419fn stable_key_by_file_id(
420 root: &Path,
421 files: &[DiscoveredFile],
422) -> rustc_hash::FxHashMap<FileId, StableFileKey> {
423 files
424 .iter()
425 .map(|file| (file.id, StableFileKey::from_root_relative(root, &file.path)))
426 .collect()
427}
428
429fn span_to_pair(span: Span) -> [u32; 2] {
430 [span.start, span.end]
431}
432
433fn pair_to_span(pair: [u32; 2]) -> Span {
434 Span::new(pair[0], pair[1])
435}
436
437pub(crate) mod span_serde {
445 use oxc_span::Span;
446 use serde::{Deserialize, Deserializer, Serialize, Serializer};
447
448 #[expect(
449 clippy::trivially_copy_pass_by_ref,
450 reason = "serde `serialize_with` / `with` requires a `&T` signature"
451 )]
452 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
453 [span.start, span.end].serialize(serializer)
454 }
455
456 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
457 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
458 Ok(Span::new(start, end))
459 }
460}
461
462pub(crate) mod member_serde {
471 use fallow_types::extract::{MemberInfo, MemberKind};
472 use oxc_span::Span;
473 use serde::{Deserialize, Deserializer, Serialize, Serializer};
474
475 #[derive(Serialize, Deserialize)]
476 struct CachedMemberInfo {
477 name: String,
478 kind: MemberKind,
479 span: [u32; 2],
480 has_decorator: bool,
481 decorator_names: Vec<String>,
482 is_instance_returning_static: bool,
483 is_self_returning: bool,
484 }
485
486 impl From<&MemberInfo> for CachedMemberInfo {
487 fn from(member: &MemberInfo) -> Self {
488 Self {
489 name: member.name.clone(),
490 kind: member.kind,
491 span: [member.span.start, member.span.end],
492 has_decorator: member.has_decorator,
493 decorator_names: member.decorator_names.clone(),
494 is_instance_returning_static: member.is_instance_returning_static,
495 is_self_returning: member.is_self_returning,
496 }
497 }
498 }
499
500 impl From<CachedMemberInfo> for MemberInfo {
501 fn from(cached: CachedMemberInfo) -> Self {
502 Self {
503 name: cached.name,
504 kind: cached.kind,
505 span: Span::new(cached.span[0], cached.span[1]),
506 has_decorator: cached.has_decorator,
507 decorator_names: cached.decorator_names,
508 is_instance_returning_static: cached.is_instance_returning_static,
509 is_self_returning: cached.is_self_returning,
510 }
511 }
512 }
513
514 pub fn serialize<S: Serializer>(
515 members: &[MemberInfo],
516 serializer: S,
517 ) -> Result<S::Ok, S::Error> {
518 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
519 mirror.serialize(serializer)
520 }
521
522 pub fn deserialize<'de, D: Deserializer<'de>>(
523 deserializer: D,
524 ) -> Result<Vec<MemberInfo>, D::Error> {
525 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
526 Ok(mirror.into_iter().map(MemberInfo::from).collect())
527 }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
536pub struct GraphCacheMode {
537 pub resolver_options_hash: u64,
539 pub entry_points_hash: u64,
541 pub plugin_config_hash: u64,
543}
544
545impl GraphCacheMode {
546 #[must_use]
548 pub const fn new(
549 resolver_options_hash: u64,
550 entry_points_hash: u64,
551 plugin_config_hash: u64,
552 ) -> Self {
553 Self {
554 resolver_options_hash,
555 entry_points_hash,
556 plugin_config_hash,
557 }
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
563pub struct GraphCacheFile {
564 pub key: StableFileKey,
566 pub file_id: FileId,
573 pub fingerprint: SourceFingerprint,
575}
576
577impl GraphCacheFile {
578 #[must_use]
580 pub fn from_discovered_file(
581 root: &Path,
582 file: &DiscoveredFile,
583 fingerprint: SourceFingerprint,
584 ) -> Self {
585 Self {
586 key: StableFileKey::from_root_relative(root, &file.path),
587 file_id: file.id,
588 fingerprint,
589 }
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
595pub struct GraphCacheManifest {
596 pub version: u32,
598 pub mode: GraphCacheMode,
600 pub files: Vec<GraphCacheFile>,
602}
603
604impl GraphCacheManifest {
605 #[must_use]
607 pub fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
608 sort_files(&mut files);
609 Self {
610 version: GRAPH_CACHE_VERSION,
611 mode,
612 files,
613 }
614 }
615
616 pub fn from_discovered_files(
618 root: &Path,
619 files: &[DiscoveredFile],
620 mode: GraphCacheMode,
621 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
622 ) -> Self {
623 let rows = files
624 .iter()
625 .map(|file| {
626 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
627 })
628 .collect();
629 Self::new(mode, rows)
630 }
631
632 #[must_use]
634 pub fn matches_inputs(&self, current: &Self) -> bool {
635 self.version == GRAPH_CACHE_VERSION
636 && current.version == GRAPH_CACHE_VERSION
637 && self.mode == current.mode
638 && self.files == current.files
639 }
640
641 #[must_use]
648 pub fn matches_resolution_inputs(&self, current: &Self) -> bool {
649 self.version == GRAPH_CACHE_VERSION
650 && current.version == GRAPH_CACHE_VERSION
651 && self.mode == current.mode
652 && self.files.len() == current.files.len()
653 && self
654 .files
655 .iter()
656 .zip(current.files.iter())
657 .all(|(cached, current)| {
658 cached.key == current.key && cached.fingerprint == current.fingerprint
659 })
660 }
661}
662
663fn sort_files(files: &mut [GraphCacheFile]) {
664 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
665}
666
667#[cfg(test)]
668mod tests {
669 use std::path::{Path, PathBuf};
670
671 use fallow_types::discover::FileId;
672 use rustc_hash::FxHashMap;
673
674 use super::*;
675
676 fn file(id: u32, path: &str) -> DiscoveredFile {
677 DiscoveredFile {
678 id: FileId(id),
679 path: PathBuf::from(path),
680 size_bytes: 1,
681 }
682 }
683
684 fn mode() -> GraphCacheMode {
685 GraphCacheMode::new(1, 2, 3)
686 }
687
688 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
689 pairs
690 .iter()
691 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
692 .collect()
693 }
694
695 fn manifest(
696 files: &[DiscoveredFile],
697 mode: GraphCacheMode,
698 map: &FxHashMap<PathBuf, SourceFingerprint>,
699 ) -> GraphCacheManifest {
700 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
701 *map.get(path).unwrap()
702 })
703 }
704
705 fn import_info(source: &str) -> ImportInfo {
706 ImportInfo {
707 source: source.to_string(),
708 imported_name: fallow_types::extract::ImportedName::SideEffect,
709 local_name: String::new(),
710 is_type_only: false,
711 from_style: false,
712 span: Span::new(0, 0),
713 source_span: Span::new(0, 0),
714 }
715 }
716
717 #[test]
718 fn manifest_sorts_by_stable_file_key() {
719 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
720 let map = fingerprints(&[
721 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
722 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
723 ]);
724
725 let manifest = manifest(&files, mode(), &map);
726
727 let keys: Vec<&str> = manifest
728 .files
729 .iter()
730 .map(|file| file.key.as_str())
731 .collect();
732 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
733 }
734
735 #[test]
736 fn manifest_misses_on_file_id_shift_until_graph_remap_exists() {
737 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
738 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
739 let map = fingerprints(&[
740 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
741 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
742 ]);
743
744 let cached = manifest(&before, mode(), &map);
745 let current = manifest(&after, mode(), &map);
746
747 assert!(
748 !cached.matches_inputs(¤t),
749 "the persisted graph is still FileId-keyed, so FileId shifts cannot trust it"
750 );
751 assert!(
752 cached.matches_resolution_inputs(¤t),
753 "stable-keyed resolver payloads may be remapped across FileId shifts"
754 );
755 }
756
757 #[test]
758 fn cached_resolve_result_remaps_internal_targets_by_stable_key() {
759 let key_a = StableFileKey::from_root_relative(
760 Path::new("/project"),
761 Path::new("/project/src/a.ts"),
762 );
763 let key_b = StableFileKey::from_root_relative(
764 Path::new("/project"),
765 Path::new("/project/src/b.ts"),
766 );
767 let key_by_file_id =
768 FxHashMap::from_iter([(FileId(0), key_a.clone()), (FileId(1), key_b.clone())]);
769 let id_by_key = FxHashMap::from_iter([(key_a, FileId(7)), (key_b, FileId(9))]);
770
771 let cached = CachedResolveResult::from_resolve_result(
772 &ResolveResult::InternalPackageModule {
773 file_id: FileId(1),
774 package_name: "@scope/pkg".to_string(),
775 },
776 &key_by_file_id,
777 )
778 .expect("target file id should map to a stable key");
779
780 let restored = cached
781 .into_resolve_result(&id_by_key)
782 .expect("stable key should map to current FileId");
783
784 assert!(matches!(
785 restored,
786 ResolveResult::InternalPackageModule {
787 file_id: FileId(9),
788 ref package_name,
789 } if package_name == "@scope/pkg"
790 ));
791 }
792
793 #[test]
794 fn cache_resolved_modules_rejects_unknown_internal_targets() {
795 let files = vec![file(0, "/project/src/a.ts")];
796 let module = ResolvedModule {
797 file_id: FileId(0),
798 path: PathBuf::from("/project/src/a.ts"),
799 resolved_imports: vec![ResolvedImport {
800 info: import_info("./missing"),
801 target: ResolveResult::InternalModule(FileId(1)),
802 }],
803 ..ResolvedModule::default()
804 };
805
806 let cached = cache_resolved_modules(Path::new("/project"), &files, &[module]);
807
808 assert!(cached.is_none());
809 }
810
811 #[test]
812 fn manifest_misses_on_fingerprint_change() {
813 let files = vec![file(0, "/project/src/a.ts")];
814 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
815 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
816
817 let cached = manifest(&files, mode(), &cached_map);
818 let current = manifest(&files, mode(), ¤t_map);
819
820 assert!(!cached.matches_inputs(¤t));
821 }
822
823 #[test]
824 fn manifest_misses_on_file_deletion() {
825 let before = vec![
826 file(0, "/project/src/a.ts"),
827 file(1, "/project/src/deleted.ts"),
828 ];
829 let after = vec![file(0, "/project/src/a.ts")];
830 let map = fingerprints(&[
831 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
832 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
833 ]);
834
835 let cached = manifest(&before, mode(), &map);
836 let current = manifest(&after, mode(), &map);
837
838 assert!(!cached.matches_inputs(¤t));
839 }
840
841 #[test]
842 fn manifest_misses_on_file_rename_with_same_fingerprint() {
843 let before = vec![file(0, "/project/src/old.ts")];
844 let after = vec![file(0, "/project/src/new.ts")];
845 let map = fingerprints(&[
846 ("/project/src/old.ts", SourceFingerprint::new(10, 1)),
847 ("/project/src/new.ts", SourceFingerprint::new(10, 1)),
848 ]);
849
850 let cached = manifest(&before, mode(), &map);
851 let current = manifest(&after, mode(), &map);
852
853 assert!(!cached.matches_inputs(¤t));
854 }
855
856 #[test]
857 fn manifest_misses_on_workspace_scoped_file_set() {
858 let full_project = vec![
859 file(0, "/project/packages/app/src/index.ts"),
860 file(1, "/project/packages/shared/src/index.ts"),
861 ];
862 let workspace_scoped = vec![file(0, "/project/packages/app/src/index.ts")];
863 let map = fingerprints(&[
864 (
865 "/project/packages/app/src/index.ts",
866 SourceFingerprint::new(10, 1),
867 ),
868 (
869 "/project/packages/shared/src/index.ts",
870 SourceFingerprint::new(20, 1),
871 ),
872 ]);
873
874 let cached = manifest(&full_project, mode(), &map);
875 let current = manifest(&workspace_scoped, mode(), &map);
876
877 assert!(!cached.matches_inputs(¤t));
878 assert!(!cached.matches_resolution_inputs(¤t));
879 }
880
881 #[test]
882 fn manifest_misses_on_mode_change() {
883 let files = vec![file(0, "/project/src/a.ts")];
884 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
885
886 let cached = manifest(&files, mode(), &map);
887 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
888
889 assert!(!cached.matches_inputs(¤t));
890 }
891
892 #[test]
893 fn manifest_misses_on_version_change() {
894 let files = vec![file(0, "/project/src/a.ts")];
895 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
896 let mut cached = manifest(&files, mode(), &map);
897 let current = manifest(&files, mode(), &map);
898
899 cached.version = GRAPH_CACHE_VERSION + 1;
900
901 assert!(!cached.matches_inputs(¤t));
902 }
903}