1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use crate::model::FileExtraction;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct Fingerprint([u8; 32]);
15
16impl Fingerprint {
17 pub fn of(bytes: &[u8]) -> Self {
19 Self(*blake3::hash(bytes).as_bytes())
20 }
21
22 pub fn as_bytes(&self) -> &[u8; 32] {
24 &self.0
25 }
26}
27
28pub fn fingerprint(bytes: &[u8]) -> Fingerprint {
30 Fingerprint::of(bytes)
31}
32
33#[derive(Debug, Default)]
38pub struct ExtractionCache {
39 pub(crate) extractions: HashMap<PathBuf, Arc<FileExtraction>>,
40 pub(crate) fingerprints: HashMap<PathBuf, Fingerprint>,
41}
42
43fn max_raw(ids: impl Iterator<Item = u32>) -> u32 {
47 ids.max().unwrap_or(0)
48}
49
50impl ExtractionCache {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn get(&self, path: &Path) -> Option<&Arc<FileExtraction>> {
58 self.extractions.get(path)
59 }
60
61 pub fn fingerprint_of(&self, path: &Path) -> Option<Fingerprint> {
63 self.fingerprints.get(path).copied()
64 }
65
66 pub fn max_symbol_id(&self) -> u32 {
68 max_raw(
69 self.extractions
70 .values()
71 .flat_map(|ext| ext.symbols.iter().map(|s| s.id.to_raw())),
72 )
73 }
74
75 #[cfg(feature = "dataflow")]
77 pub fn max_data_node_id(&self) -> u32 {
78 max_raw(
79 self.extractions
80 .values()
81 .flat_map(|ext| ext.data_nodes.iter().map(|d| d.id.to_raw())),
82 )
83 }
84
85 pub fn update(&mut self, path: PathBuf, fp: Fingerprint, extraction: Arc<FileExtraction>) {
87 self.fingerprints.insert(path.clone(), fp);
88 self.extractions.insert(path, extraction);
89 }
90
91 pub fn remove(&mut self, path: &Path) {
93 self.fingerprints.remove(path);
94 self.extractions.remove(path);
95 }
96
97 pub fn len(&self) -> usize {
99 self.extractions.len()
100 }
101
102 pub fn is_empty(&self) -> bool {
104 self.extractions.is_empty()
105 }
106
107 pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
109 self.extractions.keys()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn blake3_fingerprint_is_deterministic() {
119 let content = b"fn main() { println!(\"hello\"); }";
120 let fp1 = fingerprint(content);
121 let fp2 = fingerprint(content);
122 assert_eq!(fp1, fp2);
123 assert_eq!(
124 blake3::hash(content).as_bytes(),
125 fp1.as_bytes(),
126 "Fingerprint must match BLAKE3 hash"
127 );
128 }
129
130 #[test]
131 fn cache_update_and_remove() {
132 let mut cache = ExtractionCache::new();
133 let path = PathBuf::from("foo.py");
134 let bytes = b"def foo(): pass\n";
135 let fp = fingerprint(bytes);
136 let mut base = FileExtraction::empty(path.clone(), crate::language::LangId::Python);
137 base.ast_node_count = 5;
138 let extraction = Arc::new(base);
139
140 cache.update(path.clone(), fp, Arc::clone(&extraction));
141 assert_eq!(cache.len(), 1);
142 assert_eq!(cache.fingerprint_of(&path), Some(fp));
143
144 let cached = cache.get(&path).unwrap();
145 assert!(Arc::ptr_eq(&extraction, cached));
146
147 cache.remove(&path);
148 assert!(cache.is_empty());
149 assert!(cache.fingerprint_of(&path).is_none());
150 }
151
152 #[test]
153 fn max_symbol_id_scans_cached_files() {
154 let mut cache = ExtractionCache::new();
155 assert_eq!(cache.max_symbol_id(), 0);
156 let path = PathBuf::from("a.py");
157 let mut file = FileExtraction::empty(path.clone(), crate::language::LangId::Python);
158 file.symbols.push(crate::model::Symbol {
159 id: crate::model::SymbolId::new(7).unwrap(),
160 name: "seven".to_string(),
161 kind: crate::model::SymbolKind::Function,
162 language: crate::language::LangId::Python,
163 file_path: path.clone(),
164 source_range: crate::model::SourceRange {
165 byte_start: 0,
166 byte_end: 1,
167 start: crate::model::LineColumn { line: 0, column: 0 },
168 end: crate::model::LineColumn { line: 0, column: 1 },
169 },
170 name_range: None,
171 visibility: None,
172 signature: None,
173 docstring: None,
174 is_async: false,
175 });
176 cache.update(path, fingerprint(b"x"), Arc::new(file));
177 assert_eq!(cache.max_symbol_id(), 7);
178 }
179}