1use std::path::Path;
9
10use fallow_types::discover::{DiscoveredFile, StableFileKey};
11use fallow_types::source_fingerprint::SourceFingerprint;
12
13mod store;
14
15pub use store::GraphCacheStore;
16
17pub const GRAPH_CACHE_VERSION: u32 = 1;
24
25pub(crate) mod span_serde {
33 use oxc_span::Span;
34 use serde::{Deserialize, Deserializer, Serialize, Serializer};
35
36 #[expect(
37 clippy::trivially_copy_pass_by_ref,
38 reason = "serde `serialize_with` / `with` requires a `&T` signature"
39 )]
40 pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
41 [span.start, span.end].serialize(serializer)
42 }
43
44 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
45 let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
46 Ok(Span::new(start, end))
47 }
48}
49
50pub(crate) mod member_serde {
59 use fallow_types::extract::{MemberInfo, MemberKind};
60 use oxc_span::Span;
61 use serde::{Deserialize, Deserializer, Serialize, Serializer};
62
63 #[derive(Serialize, Deserialize)]
64 struct CachedMemberInfo {
65 name: String,
66 kind: MemberKind,
67 span: [u32; 2],
68 has_decorator: bool,
69 decorator_names: Vec<String>,
70 is_instance_returning_static: bool,
71 is_self_returning: bool,
72 }
73
74 impl From<&MemberInfo> for CachedMemberInfo {
75 fn from(member: &MemberInfo) -> Self {
76 Self {
77 name: member.name.clone(),
78 kind: member.kind,
79 span: [member.span.start, member.span.end],
80 has_decorator: member.has_decorator,
81 decorator_names: member.decorator_names.clone(),
82 is_instance_returning_static: member.is_instance_returning_static,
83 is_self_returning: member.is_self_returning,
84 }
85 }
86 }
87
88 impl From<CachedMemberInfo> for MemberInfo {
89 fn from(cached: CachedMemberInfo) -> Self {
90 Self {
91 name: cached.name,
92 kind: cached.kind,
93 span: Span::new(cached.span[0], cached.span[1]),
94 has_decorator: cached.has_decorator,
95 decorator_names: cached.decorator_names,
96 is_instance_returning_static: cached.is_instance_returning_static,
97 is_self_returning: cached.is_self_returning,
98 }
99 }
100 }
101
102 pub fn serialize<S: Serializer>(
103 members: &[MemberInfo],
104 serializer: S,
105 ) -> Result<S::Ok, S::Error> {
106 let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
107 mirror.serialize(serializer)
108 }
109
110 pub fn deserialize<'de, D: Deserializer<'de>>(
111 deserializer: D,
112 ) -> Result<Vec<MemberInfo>, D::Error> {
113 let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
114 Ok(mirror.into_iter().map(MemberInfo::from).collect())
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
124pub struct GraphCacheMode {
125 pub resolver_options_hash: u64,
127 pub entry_points_hash: u64,
129 pub plugin_config_hash: u64,
131}
132
133impl GraphCacheMode {
134 #[must_use]
136 pub const fn new(
137 resolver_options_hash: u64,
138 entry_points_hash: u64,
139 plugin_config_hash: u64,
140 ) -> Self {
141 Self {
142 resolver_options_hash,
143 entry_points_hash,
144 plugin_config_hash,
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
151pub struct GraphCacheFile {
152 pub key: StableFileKey,
154 pub fingerprint: SourceFingerprint,
156}
157
158impl GraphCacheFile {
159 #[must_use]
161 pub fn from_discovered_file(
162 root: &Path,
163 file: &DiscoveredFile,
164 fingerprint: SourceFingerprint,
165 ) -> Self {
166 Self {
167 key: StableFileKey::from_root_relative(root, &file.path),
168 fingerprint,
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
175pub struct GraphCacheManifest {
176 pub version: u32,
178 pub mode: GraphCacheMode,
180 pub files: Vec<GraphCacheFile>,
182}
183
184impl GraphCacheManifest {
185 #[must_use]
187 pub fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
188 sort_files(&mut files);
189 Self {
190 version: GRAPH_CACHE_VERSION,
191 mode,
192 files,
193 }
194 }
195
196 pub fn from_discovered_files(
198 root: &Path,
199 files: &[DiscoveredFile],
200 mode: GraphCacheMode,
201 mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
202 ) -> Self {
203 let rows = files
204 .iter()
205 .map(|file| {
206 GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
207 })
208 .collect();
209 Self::new(mode, rows)
210 }
211
212 #[must_use]
214 pub fn matches_inputs(&self, current: &Self) -> bool {
215 self.version == GRAPH_CACHE_VERSION
216 && current.version == GRAPH_CACHE_VERSION
217 && self.mode == current.mode
218 && self.files == current.files
219 }
220}
221
222fn sort_files(files: &mut [GraphCacheFile]) {
223 files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
224}
225
226#[cfg(test)]
227mod tests {
228 use std::path::{Path, PathBuf};
229
230 use fallow_types::discover::FileId;
231 use rustc_hash::FxHashMap;
232
233 use super::*;
234
235 fn file(id: u32, path: &str) -> DiscoveredFile {
236 DiscoveredFile {
237 id: FileId(id),
238 path: PathBuf::from(path),
239 size_bytes: 1,
240 }
241 }
242
243 fn mode() -> GraphCacheMode {
244 GraphCacheMode::new(1, 2, 3)
245 }
246
247 fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
248 pairs
249 .iter()
250 .map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
251 .collect()
252 }
253
254 fn manifest(
255 files: &[DiscoveredFile],
256 mode: GraphCacheMode,
257 map: &FxHashMap<PathBuf, SourceFingerprint>,
258 ) -> GraphCacheManifest {
259 GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
260 *map.get(path).unwrap()
261 })
262 }
263
264 #[test]
265 fn manifest_sorts_by_stable_file_key() {
266 let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
267 let map = fingerprints(&[
268 ("/project/src/z.ts", SourceFingerprint::new(10, 1)),
269 ("/project/src/a.ts", SourceFingerprint::new(20, 1)),
270 ]);
271
272 let manifest = manifest(&files, mode(), &map);
273
274 let keys: Vec<&str> = manifest
275 .files
276 .iter()
277 .map(|file| file.key.as_str())
278 .collect();
279 assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
280 }
281
282 #[test]
283 fn manifest_matches_across_file_id_shift() {
284 let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
285 let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
286 let map = fingerprints(&[
287 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
288 ("/project/src/c.ts", SourceFingerprint::new(20, 1)),
289 ]);
290
291 let cached = manifest(&before, mode(), &map);
292 let current = manifest(&after, mode(), &map);
293
294 assert!(cached.matches_inputs(¤t));
295 }
296
297 #[test]
298 fn manifest_misses_on_fingerprint_change() {
299 let files = vec![file(0, "/project/src/a.ts")];
300 let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
301 let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
302
303 let cached = manifest(&files, mode(), &cached_map);
304 let current = manifest(&files, mode(), ¤t_map);
305
306 assert!(!cached.matches_inputs(¤t));
307 }
308
309 #[test]
310 fn manifest_misses_on_file_deletion() {
311 let before = vec![
312 file(0, "/project/src/a.ts"),
313 file(1, "/project/src/deleted.ts"),
314 ];
315 let after = vec![file(0, "/project/src/a.ts")];
316 let map = fingerprints(&[
317 ("/project/src/a.ts", SourceFingerprint::new(10, 1)),
318 ("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
319 ]);
320
321 let cached = manifest(&before, mode(), &map);
322 let current = manifest(&after, mode(), &map);
323
324 assert!(!cached.matches_inputs(¤t));
325 }
326
327 #[test]
328 fn manifest_misses_on_mode_change() {
329 let files = vec![file(0, "/project/src/a.ts")];
330 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
331
332 let cached = manifest(&files, mode(), &map);
333 let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
334
335 assert!(!cached.matches_inputs(¤t));
336 }
337
338 #[test]
339 fn manifest_misses_on_version_change() {
340 let files = vec![file(0, "/project/src/a.ts")];
341 let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
342 let mut cached = manifest(&files, mode(), &map);
343 let current = manifest(&files, mode(), &map);
344
345 cached.version = GRAPH_CACHE_VERSION + 1;
346
347 assert!(!cached.matches_inputs(¤t));
348 }
349}