lint 0.1.3

A versatile linting tool with CLI, MCP, and library interfaces
Documentation
use crate::output::LintMessage;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
    pub mtime: u64,
    pub size: u64,
    pub content_hash: Option<String>,
    pub config_hash: Option<String>,
    pub messages: Vec<LintMessage>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cache {
    entries: HashMap<PathBuf, CacheEntry>,
}

impl Cache {
    pub fn new() -> Self {
        Self {
            entries: HashMap::new(),
        }
    }

    pub fn load(path: &Path) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(path)
            .map_err(|e| anyhow::anyhow!("Failed to read cache file: {}", e))?;
        let entries: HashMap<PathBuf, CacheEntry> = serde_json::from_str(&content)
            .map_err(|e| anyhow::anyhow!("Failed to parse cache file: {}", e))?;
        Ok(Self { entries })
    }

    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
        let content = serde_json::to_string_pretty(&self.entries)
            .map_err(|e| anyhow::anyhow!("Failed to serialize cache: {}", e))?;
        std::fs::write(path, content)
            .map_err(|e| anyhow::anyhow!("Failed to write cache file: {}", e))?;
        Ok(())
    }

    pub fn get(
        &self,
        path: &Path,
        mtime: u64,
        size: u64,
        config_hash: Option<&str>,
    ) -> Option<&Vec<LintMessage>> {
        self.entries.get(path).and_then(|entry| {
            let config_match = config_hash.is_none() || entry.config_hash.as_deref() == config_hash;
            if entry.mtime == mtime && entry.size == size && config_match {
                Some(&entry.messages)
            } else {
                None
            }
        })
    }

    pub fn get_by_hash(
        &self,
        path: &Path,
        hash: &str,
        config_hash: Option<&str>,
    ) -> Option<&Vec<LintMessage>> {
        self.entries.get(path).and_then(|entry| {
            let config_match = config_hash.is_none() || entry.config_hash.as_deref() == config_hash;
            if entry.content_hash.as_deref() == Some(hash) && config_match {
                Some(&entry.messages)
            } else {
                None
            }
        })
    }

    pub fn insert(
        &mut self,
        path: PathBuf,
        mtime: u64,
        size: u64,
        config_hash: Option<String>,
        messages: Vec<LintMessage>,
    ) {
        self.entries.insert(
            path,
            CacheEntry {
                mtime,
                size,
                content_hash: None,
                config_hash,
                messages,
            },
        );
    }

    pub fn insert_with_hash(
        &mut self,
        path: PathBuf,
        hash: String,
        config_hash: Option<String>,
        messages: Vec<LintMessage>,
    ) {
        self.entries.insert(
            path,
            CacheEntry {
                mtime: 0,
                size: 0,
                content_hash: Some(hash),
                config_hash,
                messages,
            },
        );
    }
}

impl Default for Cache {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::output::{LintMessage, Severity};

    #[test]
    fn test_cache_hit() {
        let mut cache = Cache::new();
        let path = PathBuf::from("test.rs");
        let messages = vec![LintMessage::new(
            1,
            1,
            Severity::Warning,
            "test".to_string(),
            "rule".to_string(),
            None,
        )];
        cache.insert(path.clone(), 123, 456, None, messages.clone());

        let result = cache.get(&path, 123, 456, None);
        assert!(result.is_some());
        assert_eq!(result.unwrap().len(), 1);
    }

    #[test]
    fn test_cache_miss_mtime_changed() {
        let mut cache = Cache::new();
        let path = PathBuf::from("test.rs");
        cache.insert(path.clone(), 123, 456, None, vec![]);

        assert!(cache.get(&path, 124, 456, None).is_none());
    }

    #[test]
    fn test_cache_miss_size_changed() {
        let mut cache = Cache::new();
        let path = PathBuf::from("test.rs");
        cache.insert(path.clone(), 123, 456, None, vec![]);

        assert!(cache.get(&path, 123, 457, None).is_none());
    }

    #[test]
    fn test_cache_miss_config_hash_changed() {
        let mut cache = Cache::new();
        let path = PathBuf::from("test.rs");
        cache.insert(path.clone(), 123, 456, Some("abc".to_string()), vec![]);

        assert!(cache.get(&path, 123, 456, Some("xyz")).is_none());
        assert!(cache.get(&path, 123, 456, Some("abc")).is_some());
    }

    #[test]
    fn test_cache_save_and_load() -> anyhow::Result<()> {
        let temp_file = tempfile::NamedTempFile::new()?;
        let path = PathBuf::from("test.rs");
        let messages = vec![LintMessage::new(
            1,
            1,
            Severity::Warning,
            "test".to_string(),
            "rule".to_string(),
            None,
        )];

        let mut cache = Cache::new();
        cache.insert(path.clone(), 123, 456, None, messages);
        cache.save(temp_file.path())?;

        let loaded = Cache::load(temp_file.path())?;
        assert!(loaded.get(&path, 123, 456, None).is_some());
        assert!(loaded.get(&path, 124, 456, None).is_none());

        Ok(())
    }
}