use std::path::Path;
use fallow_types::discover::{DiscoveredFile, StableFileKey};
use fallow_types::source_fingerprint::SourceFingerprint;
mod store;
pub use store::GraphCacheStore;
pub const GRAPH_CACHE_VERSION: u32 = 1;
pub(crate) mod span_serde {
use oxc_span::Span;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde `serialize_with` / `with` requires a `&T` signature"
)]
pub fn serialize<S: Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
[span.start, span.end].serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Span, D::Error> {
let [start, end] = <[u32; 2]>::deserialize(deserializer)?;
Ok(Span::new(start, end))
}
}
pub(crate) mod member_serde {
use fallow_types::extract::{MemberInfo, MemberKind};
use oxc_span::Span;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Serialize, Deserialize)]
struct CachedMemberInfo {
name: String,
kind: MemberKind,
span: [u32; 2],
has_decorator: bool,
decorator_names: Vec<String>,
is_instance_returning_static: bool,
is_self_returning: bool,
}
impl From<&MemberInfo> for CachedMemberInfo {
fn from(member: &MemberInfo) -> Self {
Self {
name: member.name.clone(),
kind: member.kind,
span: [member.span.start, member.span.end],
has_decorator: member.has_decorator,
decorator_names: member.decorator_names.clone(),
is_instance_returning_static: member.is_instance_returning_static,
is_self_returning: member.is_self_returning,
}
}
}
impl From<CachedMemberInfo> for MemberInfo {
fn from(cached: CachedMemberInfo) -> Self {
Self {
name: cached.name,
kind: cached.kind,
span: Span::new(cached.span[0], cached.span[1]),
has_decorator: cached.has_decorator,
decorator_names: cached.decorator_names,
is_instance_returning_static: cached.is_instance_returning_static,
is_self_returning: cached.is_self_returning,
}
}
}
pub fn serialize<S: Serializer>(
members: &[MemberInfo],
serializer: S,
) -> Result<S::Ok, S::Error> {
let mirror: Vec<CachedMemberInfo> = members.iter().map(CachedMemberInfo::from).collect();
mirror.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<MemberInfo>, D::Error> {
let mirror = Vec::<CachedMemberInfo>::deserialize(deserializer)?;
Ok(mirror.into_iter().map(MemberInfo::from).collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct GraphCacheMode {
pub resolver_options_hash: u64,
pub entry_points_hash: u64,
pub plugin_config_hash: u64,
}
impl GraphCacheMode {
#[must_use]
pub const fn new(
resolver_options_hash: u64,
entry_points_hash: u64,
plugin_config_hash: u64,
) -> Self {
Self {
resolver_options_hash,
entry_points_hash,
plugin_config_hash,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct GraphCacheFile {
pub key: StableFileKey,
pub fingerprint: SourceFingerprint,
}
impl GraphCacheFile {
#[must_use]
pub fn from_discovered_file(
root: &Path,
file: &DiscoveredFile,
fingerprint: SourceFingerprint,
) -> Self {
Self {
key: StableFileKey::from_root_relative(root, &file.path),
fingerprint,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GraphCacheManifest {
pub version: u32,
pub mode: GraphCacheMode,
pub files: Vec<GraphCacheFile>,
}
impl GraphCacheManifest {
#[must_use]
pub fn new(mode: GraphCacheMode, mut files: Vec<GraphCacheFile>) -> Self {
sort_files(&mut files);
Self {
version: GRAPH_CACHE_VERSION,
mode,
files,
}
}
pub fn from_discovered_files(
root: &Path,
files: &[DiscoveredFile],
mode: GraphCacheMode,
mut fingerprint_for_path: impl FnMut(&Path) -> SourceFingerprint,
) -> Self {
let rows = files
.iter()
.map(|file| {
GraphCacheFile::from_discovered_file(root, file, fingerprint_for_path(&file.path))
})
.collect();
Self::new(mode, rows)
}
#[must_use]
pub fn matches_inputs(&self, current: &Self) -> bool {
self.version == GRAPH_CACHE_VERSION
&& current.version == GRAPH_CACHE_VERSION
&& self.mode == current.mode
&& self.files == current.files
}
}
fn sort_files(files: &mut [GraphCacheFile]) {
files.sort_unstable_by(|a, b| a.key.cmp(&b.key));
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use fallow_types::discover::FileId;
use rustc_hash::FxHashMap;
use super::*;
fn file(id: u32, path: &str) -> DiscoveredFile {
DiscoveredFile {
id: FileId(id),
path: PathBuf::from(path),
size_bytes: 1,
}
}
fn mode() -> GraphCacheMode {
GraphCacheMode::new(1, 2, 3)
}
fn fingerprints(pairs: &[(&str, SourceFingerprint)]) -> FxHashMap<PathBuf, SourceFingerprint> {
pairs
.iter()
.map(|(path, fingerprint)| (PathBuf::from(path), *fingerprint))
.collect()
}
fn manifest(
files: &[DiscoveredFile],
mode: GraphCacheMode,
map: &FxHashMap<PathBuf, SourceFingerprint>,
) -> GraphCacheManifest {
GraphCacheManifest::from_discovered_files(Path::new("/project"), files, mode, |path| {
*map.get(path).unwrap()
})
}
#[test]
fn manifest_sorts_by_stable_file_key() {
let files = vec![file(0, "/project/src/z.ts"), file(1, "/project/src/a.ts")];
let map = fingerprints(&[
("/project/src/z.ts", SourceFingerprint::new(10, 1)),
("/project/src/a.ts", SourceFingerprint::new(20, 1)),
]);
let manifest = manifest(&files, mode(), &map);
let keys: Vec<&str> = manifest
.files
.iter()
.map(|file| file.key.as_str())
.collect();
assert_eq!(keys, vec!["src/a.ts", "src/z.ts"]);
}
#[test]
fn manifest_matches_across_file_id_shift() {
let before = vec![file(0, "/project/src/a.ts"), file(1, "/project/src/c.ts")];
let after = vec![file(9, "/project/src/c.ts"), file(2, "/project/src/a.ts")];
let map = fingerprints(&[
("/project/src/a.ts", SourceFingerprint::new(10, 1)),
("/project/src/c.ts", SourceFingerprint::new(20, 1)),
]);
let cached = manifest(&before, mode(), &map);
let current = manifest(&after, mode(), &map);
assert!(cached.matches_inputs(¤t));
}
#[test]
fn manifest_misses_on_fingerprint_change() {
let files = vec![file(0, "/project/src/a.ts")];
let cached_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
let current_map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(11, 1))]);
let cached = manifest(&files, mode(), &cached_map);
let current = manifest(&files, mode(), ¤t_map);
assert!(!cached.matches_inputs(¤t));
}
#[test]
fn manifest_misses_on_file_deletion() {
let before = vec![
file(0, "/project/src/a.ts"),
file(1, "/project/src/deleted.ts"),
];
let after = vec![file(0, "/project/src/a.ts")];
let map = fingerprints(&[
("/project/src/a.ts", SourceFingerprint::new(10, 1)),
("/project/src/deleted.ts", SourceFingerprint::new(20, 1)),
]);
let cached = manifest(&before, mode(), &map);
let current = manifest(&after, mode(), &map);
assert!(!cached.matches_inputs(¤t));
}
#[test]
fn manifest_misses_on_mode_change() {
let files = vec![file(0, "/project/src/a.ts")];
let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
let cached = manifest(&files, mode(), &map);
let current = manifest(&files, GraphCacheMode::new(1, 99, 3), &map);
assert!(!cached.matches_inputs(¤t));
}
#[test]
fn manifest_misses_on_version_change() {
let files = vec![file(0, "/project/src/a.ts")];
let map = fingerprints(&[("/project/src/a.ts", SourceFingerprint::new(10, 1))]);
let mut cached = manifest(&files, mode(), &map);
let current = manifest(&files, mode(), &map);
cached.version = GRAPH_CACHE_VERSION + 1;
assert!(!cached.matches_inputs(¤t));
}
}