context_creator/core/
semantic_cache.rs

1//! Semantic analysis caching to avoid redundant parsing
2//!
3//! This module provides a thread-safe cache for semantic analysis results,
4//! keyed by file path and content hash to ensure cache invalidation on changes.
5
6use crate::core::semantic::analyzer::AnalysisResult;
7use dashmap::DashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11/// Cache key combining file path and content hash
12#[derive(Debug, Clone, Hash, PartialEq, Eq)]
13struct CacheKey {
14    path: PathBuf,
15    content_hash: u64,
16}
17
18/// Thread-safe semantic analysis cache
19pub struct SemanticCache {
20    cache: DashMap<CacheKey, Arc<AnalysisResult>>,
21}
22
23impl SemanticCache {
24    /// Create a new empty cache
25    pub fn new() -> Self {
26        SemanticCache {
27            cache: DashMap::new(),
28        }
29    }
30
31    /// Get cached analysis result or None if not cached
32    pub fn get(&self, path: &Path, content_hash: u64) -> Option<Arc<AnalysisResult>> {
33        let key = CacheKey {
34            path: path.to_path_buf(),
35            content_hash,
36        };
37        self.cache.get(&key).map(|entry| entry.clone())
38    }
39
40    /// Store analysis result in cache
41    pub fn insert(&self, path: &Path, content_hash: u64, result: AnalysisResult) {
42        let key = CacheKey {
43            path: path.to_path_buf(),
44            content_hash,
45        };
46        self.cache.insert(key, Arc::new(result));
47    }
48
49    /// Get cache statistics
50    pub fn stats(&self) -> CacheStats {
51        CacheStats {
52            entries: self.cache.len(),
53        }
54    }
55
56    /// Clear all cache entries
57    pub fn clear(&self) {
58        self.cache.clear();
59    }
60}
61
62impl Default for SemanticCache {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68/// Cache statistics
69#[derive(Debug, Clone)]
70pub struct CacheStats {
71    pub entries: usize,
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::core::semantic::analyzer::Import;
78
79    #[test]
80    fn test_cache_hit_returns_same_result() {
81        let cache = SemanticCache::new();
82        let path = PathBuf::from("/test/file.rs");
83        let content_hash = 12345u64;
84
85        let result = AnalysisResult {
86            imports: vec![Import {
87                module: "std::collections".to_string(),
88                items: vec!["HashMap".to_string()],
89                is_relative: false,
90                line: 1,
91            }],
92            function_calls: vec![],
93            type_references: vec![],
94            exported_functions: vec![],
95            errors: vec![],
96        };
97
98        // Store in cache
99        cache.insert(&path, content_hash, result);
100
101        // Retrieve from cache
102        let cached = cache.get(&path, content_hash).unwrap();
103        assert_eq!(cached.imports.len(), 1);
104        assert_eq!(cached.imports[0].module, "std::collections");
105    }
106
107    #[test]
108    fn test_cache_miss_on_different_hash() {
109        let cache = SemanticCache::new();
110        let path = PathBuf::from("/test/file.rs");
111
112        let result = AnalysisResult::default();
113        cache.insert(&path, 12345, result);
114
115        // Different hash should miss
116        assert!(cache.get(&path, 67890).is_none());
117    }
118
119    #[test]
120    fn test_cache_miss_on_different_path() {
121        let cache = SemanticCache::new();
122        let path1 = PathBuf::from("/test/file1.rs");
123        let path2 = PathBuf::from("/test/file2.rs");
124
125        let result = AnalysisResult::default();
126        cache.insert(&path1, 12345, result);
127
128        // Different path should miss
129        assert!(cache.get(&path2, 12345).is_none());
130    }
131
132    #[test]
133    fn test_cache_stats() {
134        let cache = SemanticCache::new();
135        assert_eq!(cache.stats().entries, 0);
136
137        cache.insert(&PathBuf::from("/test1.rs"), 111, AnalysisResult::default());
138        cache.insert(&PathBuf::from("/test2.rs"), 222, AnalysisResult::default());
139
140        assert_eq!(cache.stats().entries, 2);
141    }
142
143    #[test]
144    fn test_cache_clear() {
145        let cache = SemanticCache::new();
146
147        cache.insert(&PathBuf::from("/test.rs"), 123, AnalysisResult::default());
148        assert_eq!(cache.stats().entries, 1);
149
150        cache.clear();
151        assert_eq!(cache.stats().entries, 0);
152    }
153}