use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::host::declarative::ProjectDeclarativeRegistry;
use crate::ts_syn::abi::ir::type_registry::FileImportEntry;
use crate::ts_syn::abi::ir::{ClassIR, EnumIR, InterfaceIR, TypeAliasIR};
use crate::ts_syn::declarative::MacroDef;
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub mtime_ns: u128,
pub size: u64,
pub classes: Vec<ClassIR>,
pub interfaces: Vec<InterfaceIR>,
pub enums: Vec<EnumIR>,
pub type_aliases: Vec<TypeAliasIR>,
pub declarative_macros: Vec<MacroDef>,
pub file_imports: Vec<FileImportEntry>,
pub exported_names: HashSet<String>,
}
#[derive(Debug, Default)]
pub struct ScanCache {
entries: HashMap<PathBuf, CacheEntry>,
}
impl ScanCache {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, path: &Path, mtime_ns: u128, size: u64) -> Option<&CacheEntry> {
let entry = self.entries.get(path)?;
if entry.mtime_ns == mtime_ns && entry.size == size {
Some(entry)
} else {
None
}
}
pub fn insert(&mut self, path: PathBuf, entry: CacheEntry) {
self.entries.insert(path, entry);
}
pub fn invalidate(&mut self, path: &Path) -> bool {
self.entries.remove(path).is_some()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &CacheEntry)> {
self.entries.iter()
}
}
pub fn file_stamp(path: &Path) -> Option<(u128, u64)> {
let meta = std::fs::metadata(path).ok()?;
let size = meta.len();
let mtime_ns = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
Some((mtime_ns, size))
}
pub fn splice_declarative(
declarative_registry: &mut ProjectDeclarativeRegistry,
file_name: &str,
entry: &CacheEntry,
) {
if !entry.declarative_macros.is_empty() {
declarative_registry.insert_file(file_name.to_string(), entry.declarative_macros.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_entry(mtime_ns: u128, size: u64) -> CacheEntry {
CacheEntry {
mtime_ns,
size,
classes: Vec::new(),
interfaces: Vec::new(),
enums: Vec::new(),
type_aliases: Vec::new(),
declarative_macros: Vec::new(),
file_imports: Vec::new(),
exported_names: HashSet::new(),
}
}
#[test]
fn get_returns_entry_when_stamp_matches() {
let mut cache = ScanCache::new();
let path = PathBuf::from("/tmp/foo.ts");
cache.insert(path.clone(), empty_entry(100, 50));
assert!(cache.get(&path, 100, 50).is_some());
}
#[test]
fn get_returns_none_when_mtime_differs() {
let mut cache = ScanCache::new();
let path = PathBuf::from("/tmp/foo.ts");
cache.insert(path.clone(), empty_entry(100, 50));
assert!(cache.get(&path, 200, 50).is_none());
}
#[test]
fn get_returns_none_when_size_differs() {
let mut cache = ScanCache::new();
let path = PathBuf::from("/tmp/foo.ts");
cache.insert(path.clone(), empty_entry(100, 50));
assert!(cache.get(&path, 100, 99).is_none());
}
#[test]
fn get_returns_none_for_unknown_path() {
let cache = ScanCache::new();
assert!(cache.get(Path::new("/tmp/unseen.ts"), 0, 0).is_none());
}
#[test]
fn invalidate_drops_entry() {
let mut cache = ScanCache::new();
let path = PathBuf::from("/tmp/foo.ts");
cache.insert(path.clone(), empty_entry(100, 50));
assert_eq!(cache.len(), 1);
assert!(cache.invalidate(&path));
assert_eq!(cache.len(), 0);
assert!(!cache.invalidate(&path));
}
#[test]
fn clear_drops_all_entries() {
let mut cache = ScanCache::new();
cache.insert(PathBuf::from("/tmp/a.ts"), empty_entry(1, 10));
cache.insert(PathBuf::from("/tmp/b.ts"), empty_entry(2, 20));
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn file_stamp_reads_real_file() {
let dir = std::env::temp_dir();
let path = dir.join(format!("macroforge_cache_test_{}.ts", std::process::id()));
std::fs::write(&path, b"// hi\n").expect("write");
let (mtime_ns, size) = file_stamp(&path).expect("stamp");
assert!(mtime_ns > 0);
assert_eq!(size, 6);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "oxc")]
#[test]
fn second_scan_reuses_cache_entries() {
use super::super::{ProjectScanner, ScanConfig};
let dir = std::env::temp_dir().join(format!(
"macroforge_scanner_cache_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.ts"), "export interface A { id: string; }\n").unwrap();
std::fs::write(dir.join("b.ts"), "export class B { name = \"\"; }\n").unwrap();
let mut scanner = ProjectScanner::new(ScanConfig {
root_dir: dir.clone(),
..Default::default()
});
scanner.enable_cache();
let out1 = scanner.scan().expect("scan 1");
assert_eq!(out1.files_scanned, 2);
assert_eq!(scanner.cache_len(), Some(2));
let out2 = scanner.scan().expect("scan 2");
assert_eq!(out2.files_scanned, 2);
assert!(out2.registry.get("A").is_some());
assert!(out2.registry.get("B").is_some());
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(feature = "oxc")]
#[test]
fn invalidate_forces_rescan_of_changed_file() {
use super::super::{ProjectScanner, ScanConfig};
let dir = std::env::temp_dir().join(format!(
"macroforge_scanner_invalidate_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let a_path = dir.join("a.ts");
std::fs::write(&a_path, "export interface A { id: string; }\n").unwrap();
let mut scanner = ProjectScanner::new(ScanConfig {
root_dir: dir.clone(),
..Default::default()
});
scanner.enable_cache();
let _ = scanner.scan().expect("scan 1");
assert_eq!(scanner.cache_len(), Some(1));
scanner.invalidate_cache_entry(&a_path);
assert_eq!(scanner.cache_len(), Some(0));
std::fs::write(
&a_path,
"export interface A { id: string; }\nexport class A2 {}\n",
)
.unwrap();
let out = scanner.scan().expect("scan 2");
assert!(out.registry.get("A2").is_some(), "A2 should be re-scanned");
assert_eq!(scanner.cache_len(), Some(1));
std::fs::remove_dir_all(&dir).ok();
}
}