1use crate::Finding;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Serialize, Deserialize)]
8struct CacheEntry {
9 content_hash: u64,
10 findings: Vec<Finding>,
11}
12
13#[derive(Debug, Serialize, Deserialize, Default)]
15struct CacheData {
16 env_hash: u64,
18 entries: HashMap<String, CacheEntry>,
19}
20
21pub struct AnalysisCache {
27 path: PathBuf,
28 data: CacheData,
29 dirty: bool,
30}
31
32fn hash_all_configs(dir: &Path, h: &mut impl std::hash::Hasher) {
33 use std::hash::Hash;
34 let cfg = dir.join(".cha.toml");
35 if let Ok(content) = std::fs::read_to_string(&cfg) {
36 content.hash(h);
37 }
38 let Ok(entries) = std::fs::read_dir(dir) else {
39 return;
40 };
41 for entry in entries.flatten() {
42 let path = entry.path();
43 if path.is_dir() {
44 let name = entry.file_name();
45 let s = name.to_string_lossy();
46 if !s.starts_with('.') && !matches!(s.as_ref(), "target" | "node_modules" | "dist") {
47 hash_all_configs(&path, h);
48 }
49 }
50 }
51}
52
53impl AnalysisCache {
54 pub fn open(project_root: &Path, env_hash: u64) -> Self {
56 let path = project_root.join(".cha/cache/analysis.json");
57 let data = std::fs::read_to_string(&path)
58 .ok()
59 .and_then(|s| serde_json::from_str::<CacheData>(&s).ok())
60 .unwrap_or_default();
61 let data = if data.env_hash != env_hash {
63 CacheData {
64 env_hash,
65 entries: HashMap::new(),
66 }
67 } else {
68 data
69 };
70 Self {
71 path,
72 data,
73 dirty: false,
74 }
75 }
76
77 pub fn get(&self, rel_path: &str, content_hash: u64) -> Option<&[Finding]> {
79 let entry = self.data.entries.get(rel_path)?;
80 if entry.content_hash == content_hash {
81 Some(&entry.findings)
82 } else {
83 None
84 }
85 }
86
87 pub fn put(&mut self, rel_path: String, content_hash: u64, findings: Vec<Finding>) {
89 self.data.entries.insert(
90 rel_path,
91 CacheEntry {
92 content_hash,
93 findings,
94 },
95 );
96 self.dirty = true;
97 }
98
99 pub fn flush(&self) {
101 if !self.dirty {
102 return;
103 }
104 if let Some(dir) = self.path.parent() {
105 let _ = std::fs::create_dir_all(dir);
106 }
107 if let Ok(json) = serde_json::to_string(&self.data) {
108 let _ = std::fs::write(&self.path, json);
109 }
110 }
111
112 pub fn hash_content(content: &str) -> u64 {
114 use std::hash::{Hash, Hasher};
115 let mut h = std::collections::hash_map::DefaultHasher::new();
116 content.hash(&mut h);
117 h.finish()
118 }
119
120 pub fn env_hash(project_root: &Path, plugin_dirs: &[PathBuf]) -> u64 {
122 use std::hash::{Hash, Hasher};
123 let mut h = std::collections::hash_map::DefaultHasher::new();
124 hash_all_configs(project_root, &mut h);
126 for dir in plugin_dirs {
127 if let Ok(entries) = std::fs::read_dir(dir) {
128 for entry in entries.flatten() {
129 if let Ok(mtime) = entry.metadata().and_then(|m| m.modified()) {
130 mtime.hash(&mut h);
131 }
132 entry.file_name().hash(&mut h);
133 }
134 }
135 }
136 h.finish()
137 }
138}