use crate::utils::types::{LintIssue, Severity};
use crate::Language;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
pub content_hash: u64,
#[serde(with = "system_time_serde")]
pub mtime: SystemTime,
pub issues: Vec<CachedIssue>,
#[serde(with = "system_time_serde")]
pub cached_at: SystemTime,
}
impl CacheEntry {
pub fn new(content_hash: u64, mtime: SystemTime, issues: Vec<CachedIssue>) -> Self {
Self {
content_hash,
mtime,
issues,
cached_at: SystemTime::now(),
}
}
pub fn to_lint_issues(&self, file_path: &Path, language: Option<Language>) -> Vec<LintIssue> {
self.issues
.iter()
.map(|ci| ci.to_lint_issue(file_path.to_path_buf(), language))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedIssue {
pub line: usize,
pub column: Option<usize>,
pub message: String,
pub severity: Severity,
pub code: Option<String>,
pub source: Option<String>,
pub suggestion: Option<String>,
}
impl CachedIssue {
pub fn from_lint_issue(issue: &LintIssue) -> Self {
Self {
line: issue.line,
column: issue.column,
message: issue.message.clone(),
severity: issue.severity,
code: issue.code.clone(),
source: issue.source.clone(),
suggestion: issue.suggestion.clone(),
}
}
pub fn to_lint_issue(&self, file_path: PathBuf, language: Option<Language>) -> LintIssue {
let mut issue = LintIssue::new(file_path, self.line, self.message.clone(), self.severity);
if let Some(col) = self.column {
issue = issue.with_column(col);
}
if let Some(ref code) = self.code {
issue = issue.with_code(code.clone());
}
if let Some(ref source) = self.source {
issue = issue.with_source(source.clone());
}
if let Some(ref suggestion) = self.suggestion {
issue = issue.with_suggestion(suggestion.clone());
}
if let Some(lang) = language {
issue = issue.with_language(lang);
}
issue
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub cache_hits: usize,
pub cache_misses: usize,
pub invalidated: usize,
}
impl CacheStats {
pub fn new() -> Self {
Self::default()
}
pub fn total(&self) -> usize {
self.cache_hits + self.cache_misses
}
pub fn hit_rate(&self) -> f64 {
if self.total() == 0 {
0.0
} else {
(self.cache_hits as f64 / self.total() as f64) * 100.0
}
}
}
mod system_time_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
(duration.as_secs(), duration.subsec_nanos()).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
where
D: Deserializer<'de>,
{
let (secs, nanos): (u64, u32) = Deserialize::deserialize(deserializer)?;
Ok(UNIX_EPOCH + Duration::new(secs, nanos))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cached_issue_roundtrip() {
let original = LintIssue::new(
PathBuf::from("test.rs"),
10,
"test message".to_string(),
Severity::Warning,
)
.with_column(5)
.with_code("W001".to_string())
.with_source("clippy".to_string());
let cached = CachedIssue::from_lint_issue(&original);
let restored = cached.to_lint_issue(PathBuf::from("test.rs"), Some(Language::Rust));
assert_eq!(restored.line, original.line);
assert_eq!(restored.column, original.column);
assert_eq!(restored.message, original.message);
assert_eq!(restored.severity, original.severity);
assert_eq!(restored.code, original.code);
assert_eq!(restored.source, original.source);
}
#[test]
fn test_cache_stats() {
let mut stats = CacheStats::new();
stats.cache_hits = 8;
stats.cache_misses = 2;
assert_eq!(stats.total(), 10);
assert!((stats.hit_rate() - 80.0).abs() < 0.01);
}
#[test]
fn test_cache_stats_empty() {
let stats = CacheStats::new();
assert_eq!(stats.total(), 0);
assert!((stats.hit_rate() - 0.0).abs() < 0.01);
}
}