use crate::Finding;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
struct CacheEntry {
content_hash: u64,
findings: Vec<Finding>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct CacheData {
env_hash: u64,
entries: HashMap<String, CacheEntry>,
}
pub struct AnalysisCache {
path: PathBuf,
data: CacheData,
dirty: bool,
}
fn hash_all_configs(dir: &Path, h: &mut impl std::hash::Hasher) {
use std::hash::Hash;
let cfg = dir.join(".cha.toml");
if let Ok(content) = std::fs::read_to_string(&cfg) {
content.hash(h);
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = entry.file_name();
let s = name.to_string_lossy();
if !s.starts_with('.') && !matches!(s.as_ref(), "target" | "node_modules" | "dist") {
hash_all_configs(&path, h);
}
}
}
}
impl AnalysisCache {
pub fn open(project_root: &Path, env_hash: u64) -> Self {
let path = project_root.join(".cha/cache/analysis.json");
let data = std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str::<CacheData>(&s).ok())
.unwrap_or_default();
let data = if data.env_hash != env_hash {
CacheData {
env_hash,
entries: HashMap::new(),
}
} else {
data
};
Self {
path,
data,
dirty: false,
}
}
pub fn get(&self, rel_path: &str, content_hash: u64) -> Option<&[Finding]> {
let entry = self.data.entries.get(rel_path)?;
if entry.content_hash == content_hash {
Some(&entry.findings)
} else {
None
}
}
pub fn put(&mut self, rel_path: String, content_hash: u64, findings: Vec<Finding>) {
self.data.entries.insert(
rel_path,
CacheEntry {
content_hash,
findings,
},
);
self.dirty = true;
}
pub fn flush(&self) {
if !self.dirty {
return;
}
if let Some(dir) = self.path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(json) = serde_json::to_string(&self.data) {
let _ = std::fs::write(&self.path, json);
}
}
pub fn hash_content(content: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut h);
h.finish()
}
pub fn env_hash(project_root: &Path, plugin_dirs: &[PathBuf]) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
hash_all_configs(project_root, &mut h);
for dir in plugin_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Ok(mtime) = entry.metadata().and_then(|m| m.modified()) {
mtime.hash(&mut h);
}
entry.file_name().hash(&mut h);
}
}
}
h.finish()
}
}