1use crate::rules::Issue;
2use ahash::AHashMap;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FileMetadata {
10 pub path: PathBuf,
11 pub size: u64,
12 pub modified: u64,
13 pub hash: u64,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CachedAnalysis {
18 pub metadata: FileMetadata,
19 pub issues: Vec<Issue>,
20 pub ast_hash: Option<u64>,
21}
22
23#[derive(Debug, Default)]
24pub struct AnalysisCache {
25 cache: AHashMap<PathBuf, CachedAnalysis>,
26 cache_file: PathBuf,
27 dirty: bool,
28}
29
30impl AnalysisCache {
31 pub fn new(cache_dir: impl AsRef<Path>) -> Self {
32 let cache_file = cache_dir.as_ref().join("cargo-fl-cache.bin");
33 let mut cache = Self {
34 cache: AHashMap::new(),
35 cache_file,
36 dirty: false,
37 };
38
39 if let Err(e) = cache.load() {
40 eprintln!("Warning: Failed to load cache: {}", e);
41 }
42
43 cache
44 }
45
46 pub fn get_metadata(path: &Path) -> Result<FileMetadata, std::io::Error> {
47 let metadata = fs::metadata(path)?;
48 let size = metadata.len();
49 let modified = metadata
50 .modified()?
51 .duration_since(UNIX_EPOCH)
52 .unwrap_or_default()
53 .as_secs();
54
55 let mut hasher = std::collections::hash_map::DefaultHasher::new();
57 use std::hash::{Hash, Hasher};
58 path.hash(&mut hasher);
59 size.hash(&mut hasher);
60 modified.hash(&mut hasher);
61 let hash = hasher.finish();
62
63 Ok(FileMetadata {
64 path: path.to_path_buf(),
65 size,
66 modified,
67 hash,
68 })
69 }
70
71 pub fn is_file_changed(&self, path: &Path) -> Result<bool, std::io::Error> {
72 let current_metadata = Self::get_metadata(path)?;
73
74 if let Some(cached) = self.cache.get(path) {
75 Ok(cached.metadata.hash != current_metadata.hash)
76 } else {
77 Ok(true) }
79 }
80
81 pub fn get_cached_analysis(&self, path: &Path) -> Option<&CachedAnalysis> {
82 self.cache.get(path)
83 }
84
85 pub fn store_analysis(&mut self, path: PathBuf, issues: Vec<Issue>, ast_hash: Option<u64>) -> Result<(), std::io::Error> {
86 let metadata = Self::get_metadata(&path)?;
87
88 let cached = CachedAnalysis {
89 metadata,
90 issues,
91 ast_hash,
92 };
93
94 self.cache.insert(path, cached);
95 self.dirty = true;
96
97 Ok(())
98 }
99
100 pub fn remove_file(&mut self, path: &Path) {
101 if self.cache.remove(path).is_some() {
102 self.dirty = true;
103 }
104 }
105
106 pub fn cleanup_stale_entries(&mut self) {
107 let mut stale_paths = Vec::new();
108
109 for (path, cached) in &self.cache {
110 if !path.exists() {
111 stale_paths.push(path.clone());
112 } else if let Ok(current_meta) = Self::get_metadata(path) {
113 if current_meta.hash != cached.metadata.hash {
114 stale_paths.push(path.clone());
115 }
116 }
117 }
118
119 for path in stale_paths {
120 self.cache.remove(&path);
121 self.dirty = true;
122 }
123 }
124
125 pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
126 if !self.dirty {
127 return Ok(());
128 }
129
130 if let Some(parent) = self.cache_file.parent() {
131 fs::create_dir_all(parent)?;
132 }
133
134 let serialized = bincode::serialize(&self.cache)?;
135 fs::write(&self.cache_file, serialized)?;
136 self.dirty = false;
137
138 Ok(())
139 }
140
141 pub fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
142 if !self.cache_file.exists() {
143 return Ok(());
144 }
145
146 let data = fs::read(&self.cache_file)?;
147 self.cache = bincode::deserialize(&data)?;
148 self.dirty = false;
149
150 Ok(())
151 }
152
153 pub fn cache_stats(&self) -> CacheStats {
154 let total_files = self.cache.len();
155 let total_issues = self.cache.values().map(|c| c.issues.len()).sum();
156 let cache_size_bytes = bincode::serialized_size(&self.cache).unwrap_or(0);
157
158 CacheStats {
159 total_files,
160 total_issues,
161 cache_size_bytes,
162 }
163 }
164}
165
166#[derive(Debug)]
167pub struct CacheStats {
168 pub total_files: usize,
169 pub total_issues: usize,
170 pub cache_size_bytes: u64,
171}
172
173impl Drop for AnalysisCache {
174 fn drop(&mut self) {
175 if let Err(e) = self.save() {
176 eprintln!("Warning: Failed to save cache on drop: {}", e);
177 }
178 }
179}