1use crate::cache::shared_cache::SharedCache;
2use crate::priority::UnifiedAnalysis;
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9
10#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
12pub struct UnifiedAnalysisCacheKey {
13 pub source_hash: String,
15 pub project_path: PathBuf,
17 pub config_hash: String,
19 pub coverage_hash: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct UnifiedAnalysisCacheEntry {
26 pub analysis: UnifiedAnalysis,
28 pub timestamp: SystemTime,
30 pub source_files: Vec<PathBuf>,
32 pub config_summary: String,
34}
35
36pub struct UnifiedAnalysisCache {
38 shared_cache: SharedCache,
40 memory_cache: HashMap<UnifiedAnalysisCacheKey, UnifiedAnalysisCacheEntry>,
42 max_age: u64,
44}
45
46impl UnifiedAnalysisCache {
47 pub fn new(project_path: Option<&Path>) -> Result<Self> {
49 let shared_cache = SharedCache::new(project_path)?;
50 Ok(Self {
51 shared_cache,
52 memory_cache: HashMap::new(),
53 max_age: 3600, })
55 }
56
57 pub fn generate_key(
59 project_path: &Path,
60 source_files: &[PathBuf],
61 complexity_threshold: u32,
62 duplication_threshold: usize,
63 coverage_file: Option<&Path>,
64 semantic_off: bool,
65 parallel: bool,
66 ) -> Result<UnifiedAnalysisCacheKey> {
67 let source_hash = Self::hash_source_files(project_path, source_files);
68 let config_hash = Self::hash_config(
69 complexity_threshold,
70 duplication_threshold,
71 semantic_off,
72 parallel,
73 );
74 let coverage_hash = coverage_file.and_then(Self::hash_coverage_file);
75
76 Ok(UnifiedAnalysisCacheKey {
77 source_hash,
78 project_path: project_path.to_path_buf(),
79 config_hash,
80 coverage_hash,
81 })
82 }
83
84 fn hash_source_files(project_path: &Path, source_files: &[PathBuf]) -> String {
85 let mut hasher = Sha256::new();
86 hasher.update(project_path.to_string_lossy().as_bytes());
87
88 let mut sorted_files = source_files.to_vec();
89 sorted_files.sort();
90
91 for file in &sorted_files {
92 Self::hash_file_content(&mut hasher, file);
93 Self::hash_file_mtime(&mut hasher, file);
94 }
95
96 format!("{:x}", hasher.finalize())
97 }
98
99 fn hash_file_content(hasher: &mut Sha256, file: &Path) {
100 if let Ok(content) = std::fs::read_to_string(file) {
101 hasher.update(file.to_string_lossy().as_bytes());
102 hasher.update(content.as_bytes());
103 }
104 }
105
106 fn hash_file_mtime(hasher: &mut Sha256, file: &Path) {
107 let mtime_secs = std::fs::metadata(file)
108 .ok()
109 .and_then(|m| m.modified().ok())
110 .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
111 .map(|d| d.as_secs());
112
113 if let Some(secs) = mtime_secs {
114 hasher.update(secs.to_le_bytes());
115 }
116 }
117
118 fn hash_config(
119 complexity_threshold: u32,
120 duplication_threshold: usize,
121 semantic_off: bool,
122 parallel: bool,
123 ) -> String {
124 let mut hasher = Sha256::new();
125 hasher.update(complexity_threshold.to_le_bytes());
126 hasher.update(duplication_threshold.to_le_bytes());
127 hasher.update([semantic_off as u8]);
128 hasher.update([parallel as u8]);
129 format!("{:x}", hasher.finalize())
130 }
131
132 fn hash_coverage_file(coverage_path: &Path) -> Option<String> {
133 std::fs::read_to_string(coverage_path).ok().map(|content| {
134 let mut hasher = Sha256::new();
135 hasher.update(content.as_bytes());
136 format!("{:x}", hasher.finalize())
137 })
138 }
139
140 pub fn get(&mut self, key: &UnifiedAnalysisCacheKey) -> Option<UnifiedAnalysis> {
142 if let Some(entry) = self.memory_cache.get(key) {
144 if self.is_entry_valid(entry) {
145 log::info!("Unified analysis cache hit (memory)");
146 return Some(entry.analysis.clone());
147 } else {
148 self.memory_cache.remove(key);
150 }
151 }
152
153 let cache_key = self.generate_shared_cache_key(key);
155 if let Ok(data) = self.shared_cache.get(&cache_key, "unified_analysis") {
156 if let Ok(entry) = serde_json::from_slice::<UnifiedAnalysisCacheEntry>(&data) {
157 if self.is_entry_valid(&entry) {
158 log::info!("Unified analysis cache hit (shared)");
159 self.memory_cache.insert(key.clone(), entry.clone());
161 return Some(entry.analysis);
162 }
163 }
164 }
165
166 log::info!("Unified analysis cache miss");
167 None
168 }
169
170 pub fn put(
172 &mut self,
173 key: UnifiedAnalysisCacheKey,
174 analysis: UnifiedAnalysis,
175 source_files: Vec<PathBuf>,
176 ) -> Result<()> {
177 let entry = UnifiedAnalysisCacheEntry {
178 analysis: analysis.clone(),
179 timestamp: SystemTime::now(),
180 source_files,
181 config_summary: format!("{:?}", key), };
183
184 self.memory_cache.insert(key.clone(), entry.clone());
186
187 let cache_key = self.generate_shared_cache_key(&key);
189 let data = serde_json::to_vec(&entry)
190 .context("Failed to serialize unified analysis cache entry")?;
191
192 self.shared_cache
193 .put(&cache_key, "unified_analysis", &data)
194 .context("Failed to store unified analysis in shared cache")?;
195
196 log::info!("Unified analysis cached successfully");
197 Ok(())
198 }
199
200 pub fn clear(&mut self) -> Result<()> {
202 self.memory_cache.clear();
203 Ok(())
206 }
207
208 pub fn stats(&self) -> String {
210 format!(
211 "UnifiedAnalysisCache: {} entries in memory, max_age: {}s",
212 self.memory_cache.len(),
213 self.max_age
214 )
215 }
216
217 fn is_entry_valid(&self, entry: &UnifiedAnalysisCacheEntry) -> bool {
219 if let Ok(elapsed) = entry.timestamp.elapsed() {
220 elapsed.as_secs() <= self.max_age
221 } else {
222 false
223 }
224 }
225
226 fn generate_shared_cache_key(&self, key: &UnifiedAnalysisCacheKey) -> String {
228 format!(
229 "unified_analysis_{}_{}_{}",
230 key.source_hash,
231 key.config_hash,
232 key.coverage_hash.as_deref().unwrap_or("no_coverage")
233 )
234 }
235
236 pub fn set_max_age(&mut self, seconds: u64) {
238 self.max_age = seconds;
239 }
240
241 pub fn should_use_cache(file_count: usize, has_coverage: bool) -> bool {
243 file_count >= 20 || has_coverage
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use tempfile::TempDir;
252
253 #[test]
254 fn test_cache_key_generation() {
255 let temp_dir = TempDir::new().unwrap();
256 let project_path = temp_dir.path();
257
258 let file1 = project_path.join("test1.rs");
260 let file2 = project_path.join("test2.rs");
261 std::fs::write(&file1, "fn test1() {}").unwrap();
262 std::fs::write(&file2, "fn test2() {}").unwrap();
263
264 let files = vec![file1, file2];
265
266 let key = UnifiedAnalysisCache::generate_key(
267 project_path,
268 &files,
269 10, 50, None, false, true, )
275 .unwrap();
276
277 assert!(!key.source_hash.is_empty());
278 assert!(!key.config_hash.is_empty());
279 assert_eq!(key.project_path, project_path);
280 assert_eq!(key.coverage_hash, None);
281 }
282
283 #[test]
284 fn test_should_use_cache() {
285 assert!(!UnifiedAnalysisCache::should_use_cache(10, false));
286 assert!(UnifiedAnalysisCache::should_use_cache(25, false));
287 assert!(UnifiedAnalysisCache::should_use_cache(10, true));
288 assert!(UnifiedAnalysisCache::should_use_cache(100, true));
289 }
290
291 #[test]
292 fn test_cache_get_miss_on_empty_cache() {
293 let temp_dir = TempDir::new().unwrap();
294 let mut cache = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
295
296 let key = UnifiedAnalysisCacheKey {
297 project_path: temp_dir.path().to_path_buf(),
298 source_hash: "test_hash".to_string(),
299 config_hash: "config_hash".to_string(),
300 coverage_hash: None,
301 };
302
303 let result = cache.get(&key);
304 assert!(result.is_none());
305 }
306
307 #[test]
308 fn test_cache_put_and_get_memory_cache() {
309 use crate::priority::{CallGraph, UnifiedAnalysis};
310
311 let temp_dir = TempDir::new().unwrap();
312 let mut cache = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
313
314 let key = UnifiedAnalysisCacheKey {
315 project_path: temp_dir.path().to_path_buf(),
316 source_hash: "test_hash".to_string(),
317 config_hash: "config_hash".to_string(),
318 coverage_hash: None,
319 };
320
321 let analysis = UnifiedAnalysis::new(CallGraph::new());
322 let source_files = vec![temp_dir.path().join("test.rs")];
323
324 let put_result = cache.put(key.clone(), analysis.clone(), source_files);
326 assert!(put_result.is_ok());
327
328 let result = cache.get(&key);
330 assert!(result.is_some());
331 }
332
333 #[test]
334 fn test_cache_put_and_get_shared_cache() {
335 use crate::priority::{CallGraph, UnifiedAnalysis};
336
337 let temp_dir = TempDir::new().unwrap();
338
339 let mut cache1 = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
341
342 let key = UnifiedAnalysisCacheKey {
343 project_path: temp_dir.path().to_path_buf(),
344 source_hash: "test_hash_shared".to_string(),
345 config_hash: "config_hash_shared".to_string(),
346 coverage_hash: None,
347 };
348
349 let analysis = UnifiedAnalysis::new(CallGraph::new());
350 let source_files = vec![temp_dir.path().join("test.rs")];
351
352 let put_result = cache1.put(key.clone(), analysis.clone(), source_files);
353 assert!(put_result.is_ok());
354
355 let mut cache2 = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
357
358 let result = cache2.get(&key);
360 assert!(result.is_some());
361 }
362
363 #[test]
364 fn test_cache_clear() {
365 use crate::priority::{CallGraph, UnifiedAnalysis};
366
367 let temp_dir = TempDir::new().unwrap();
368 let mut cache = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
369
370 let key = UnifiedAnalysisCacheKey {
371 project_path: temp_dir.path().to_path_buf(),
372 source_hash: "test_hash_clear".to_string(),
373 config_hash: "config_hash_clear".to_string(),
374 coverage_hash: None,
375 };
376
377 let analysis = UnifiedAnalysis::new(CallGraph::new());
378 let source_files = vec![temp_dir.path().join("test.rs")];
379
380 let put_result = cache.put(key.clone(), analysis.clone(), source_files);
381 assert!(put_result.is_ok());
382
383 assert!(cache.get(&key).is_some());
385
386 let clear_result = cache.clear();
388 assert!(clear_result.is_ok());
389
390 }
393
394 #[test]
395 fn test_cache_put_updates_existing_entry() {
396 use crate::priority::{CallGraph, UnifiedAnalysis};
397
398 let temp_dir = TempDir::new().unwrap();
399 let mut cache = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
400
401 let key = UnifiedAnalysisCacheKey {
402 project_path: temp_dir.path().to_path_buf(),
403 source_hash: "test_hash_update".to_string(),
404 config_hash: "config_hash_update".to_string(),
405 coverage_hash: None,
406 };
407
408 let analysis1 = UnifiedAnalysis::new(CallGraph::new());
409 let source_files = vec![temp_dir.path().join("test.rs")];
410
411 let put_result1 = cache.put(key.clone(), analysis1.clone(), source_files.clone());
413 assert!(put_result1.is_ok());
414
415 let analysis2 = UnifiedAnalysis::new(CallGraph::new());
417
418 let put_result2 = cache.put(key.clone(), analysis2.clone(), source_files);
419 assert!(put_result2.is_ok());
420
421 let result = cache.get(&key);
423 assert!(result.is_some());
424 }
425
426 #[test]
427 fn test_cache_get_with_coverage_hash() {
428 use crate::priority::{CallGraph, UnifiedAnalysis};
429
430 let temp_dir = TempDir::new().unwrap();
431 let mut cache = UnifiedAnalysisCache::new(Some(temp_dir.path())).unwrap();
432
433 let key = UnifiedAnalysisCacheKey {
434 project_path: temp_dir.path().to_path_buf(),
435 source_hash: "test_hash_cov".to_string(),
436 config_hash: "config_hash_cov".to_string(),
437 coverage_hash: Some("coverage_123".to_string()),
438 };
439
440 let analysis = UnifiedAnalysis::new(CallGraph::new());
441 let source_files = vec![temp_dir.path().join("test.rs")];
442
443 let put_result = cache.put(key.clone(), analysis.clone(), source_files);
444 assert!(put_result.is_ok());
445
446 let result = cache.get(&key);
448 assert!(result.is_some());
449
450 let key_different_cov = UnifiedAnalysisCacheKey {
452 coverage_hash: Some("coverage_456".to_string()),
453 ..key
454 };
455 let result2 = cache.get(&key_different_cov);
456 assert!(result2.is_none());
457 }
458}