use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use tracing::debug;
#[derive(Clone, Default)]
pub(crate) struct ParseCache(HashMap<PathBuf, Rc<syn::File>>);
impl ParseCache {
#[must_use]
pub(crate) fn new() -> Self {
Self(HashMap::new())
}
#[must_use]
pub(crate) fn get(&self, path: &Path) -> Option<Rc<syn::File>> {
self.0.get(path).map(Rc::clone)
}
pub(crate) fn insert(&mut self, path: PathBuf, file: Rc<syn::File>) {
self.0.insert(path, file);
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.0.len()
}
#[must_use]
#[allow(dead_code)]
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn get_or_parse<E, F>(
&mut self,
path: &Path,
read_and_parse: F,
) -> Result<Rc<syn::File>, E>
where
F: FnOnce(&Path) -> Result<syn::File, E>,
{
if let Some(cached) = self.0.get(path) {
debug!("Cache hit: {}", path.display());
return Ok(Rc::clone(cached));
}
debug!("Cache miss: {}", path.display());
let file = read_and_parse(path)?;
let rc = Rc::new(file);
self.0.insert(path.to_path_buf(), Rc::clone(&rc));
Ok(rc)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::path::Path;
#[test]
fn new_cache_is_empty() {
let cache = ParseCache::new();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn len_and_is_empty_reflect_inserts() {
let mut cache = ParseCache::new();
let path = PathBuf::from("/fake/a.rs");
let file = Rc::new(syn::parse_str::<syn::File>("").expect("parses"));
cache.insert(path, file);
assert_eq!(cache.len(), 1);
assert!(!cache.is_empty());
}
#[test]
fn get_or_parse_calls_closure_exactly_once_for_same_path() {
let mut cache = ParseCache::new();
let call_count = Cell::new(0u32);
let path = Path::new("/fake/path.rs");
let first = cache
.get_or_parse(
path,
|_: &Path| -> Result<syn::File, std::convert::Infallible> {
call_count.set(call_count.get() + 1);
Ok(syn::parse_str("").expect("empty source parses"))
},
)
.expect("first call succeeds");
assert_eq!(call_count.get(), 1, "closure must run on first call");
let second = cache
.get_or_parse(
path,
|_: &Path| -> Result<syn::File, std::convert::Infallible> {
call_count.set(call_count.get() + 1);
Ok(syn::parse_str("").expect("empty source parses"))
},
)
.expect("second call succeeds");
assert_eq!(call_count.get(), 1, "closure must not run on second call");
assert!(
Rc::ptr_eq(&first, &second),
"both calls must return same Rc allocation"
);
}
}