Skip to main content

cargo_fl/
cache.rs

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        // Simple hash based on path, size, and modification time
56        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) // File not in cache, consider it changed
78        }
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}