use recoco::utils::fingerprint::{Fingerprint, Fingerprinter};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thread_utilities::RapidSet;
#[derive(Debug, Clone)]
pub struct AnalysisDefFingerprint {
pub source_files: RapidSet<PathBuf>,
pub fingerprint: Fingerprint,
pub last_analyzed: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DependencyEdge {
pub from: PathBuf,
pub to: PathBuf,
pub dep_type: DependencyType,
pub symbol: Option<SymbolDependency>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DependencyType {
Import,
Export,
Macro,
Type,
Trait,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DependencyStrength {
Strong,
Weak,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolDependency {
pub from_symbol: String,
pub to_symbol: String,
pub kind: SymbolKind,
pub strength: DependencyStrength,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SymbolKind {
Function,
Class,
Interface,
TypeAlias,
Constant,
Enum,
Module,
Macro,
}
impl AnalysisDefFingerprint {
pub fn new(content: &[u8]) -> Self {
let mut fingerprinter = Fingerprinter::default();
fingerprinter.write_raw_bytes(content);
Self {
source_files: thread_utilities::get_set(),
fingerprint: fingerprinter.into_fingerprint(),
last_analyzed: None,
}
}
pub fn with_sources(content: &[u8], source_files: RapidSet<PathBuf>) -> Self {
let mut fingerprinter = Fingerprinter::default();
fingerprinter.write_raw_bytes(content);
Self {
source_files,
fingerprint: fingerprinter.into_fingerprint(),
last_analyzed: None,
}
}
pub fn update_fingerprint(&self, content: &[u8]) -> Self {
let mut fingerprinter = Fingerprinter::default();
fingerprinter.write_raw_bytes(content);
Self {
source_files: self.source_files.clone(),
fingerprint: fingerprinter.into_fingerprint(),
last_analyzed: None,
}
}
pub fn content_matches(&self, content: &[u8]) -> bool {
let mut fingerprinter = Fingerprinter::default();
fingerprinter.write_raw_bytes(content);
let other = fingerprinter.into_fingerprint();
self.fingerprint.as_slice() == other.as_slice()
}
pub fn add_source_file(&mut self, path: PathBuf) {
self.source_files.insert(path);
}
pub fn remove_source_file(&mut self, path: &Path) -> bool {
self.source_files.remove(path)
}
pub fn set_last_analyzed(&mut self, timestamp: i64) {
self.last_analyzed = Some(timestamp);
}
pub fn source_file_count(&self) -> usize {
self.source_files.len()
}
pub fn fingerprint(&self) -> &Fingerprint {
&self.fingerprint
}
}
impl DependencyEdge {
pub fn new(from: PathBuf, to: PathBuf, dep_type: DependencyType) -> Self {
Self {
from,
to,
dep_type,
symbol: None,
}
}
pub fn with_symbol(
from: PathBuf,
to: PathBuf,
dep_type: DependencyType,
symbol: SymbolDependency,
) -> Self {
Self {
from,
to,
dep_type,
symbol: Some(symbol),
}
}
pub fn effective_strength(&self) -> DependencyStrength {
if let Some(ref sym) = self.symbol {
return sym.strength;
}
match self.dep_type {
DependencyType::Import | DependencyType::Trait | DependencyType::Macro => {
DependencyStrength::Strong
}
DependencyType::Export | DependencyType::Type => DependencyStrength::Weak,
}
}
}
impl std::fmt::Display for DependencyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Import => write!(f, "import"),
Self::Export => write!(f, "export"),
Self::Macro => write!(f, "macro"),
Self::Type => write!(f, "type"),
Self::Trait => write!(f, "trait"),
}
}
}
impl std::fmt::Display for DependencyStrength {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Strong => write!(f, "strong"),
Self::Weak => write!(f, "weak"),
}
}
}
impl std::fmt::Display for SymbolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Function => write!(f, "function"),
Self::Class => write!(f, "class"),
Self::Interface => write!(f, "interface"),
Self::TypeAlias => write!(f, "type_alias"),
Self::Constant => write!(f, "constant"),
Self::Enum => write!(f, "enum"),
Self::Module => write!(f, "module"),
Self::Macro => write!(f, "macro"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fingerprint_new_creates_valid_fingerprint() {
let content = b"fn main() { println!(\"hello\"); }";
let fp = AnalysisDefFingerprint::new(content);
assert_eq!(fp.fingerprint.as_slice().len(), 16);
assert!(fp.source_files.is_empty());
assert!(fp.last_analyzed.is_none());
}
#[test]
fn test_fingerprint_content_matches_same_content() {
let content = b"use std::collections::HashMap;";
let fp = AnalysisDefFingerprint::new(content);
assert!(fp.content_matches(content));
}
#[test]
fn test_fingerprint_content_does_not_match_different_content() {
let fp = AnalysisDefFingerprint::new(b"original content");
assert!(!fp.content_matches(b"modified content"));
}
#[test]
fn test_fingerprint_deterministic() {
let content = b"deterministic test content";
let fp1 = AnalysisDefFingerprint::new(content);
let fp2 = AnalysisDefFingerprint::new(content);
assert_eq!(fp1.fingerprint.as_slice(), fp2.fingerprint.as_slice());
}
#[test]
fn test_fingerprint_different_content_different_hash() {
let fp1 = AnalysisDefFingerprint::new(b"content A");
let fp2 = AnalysisDefFingerprint::new(b"content B");
assert_ne!(fp1.fingerprint.as_slice(), fp2.fingerprint.as_slice());
}
#[test]
fn test_fingerprint_empty_content() {
let fp = AnalysisDefFingerprint::new(b"");
assert_eq!(fp.fingerprint.as_slice().len(), 16);
assert!(fp.content_matches(b""));
assert!(!fp.content_matches(b"non-empty"));
}
#[test]
fn test_fingerprint_with_sources() {
let sources: RapidSet<PathBuf> = [
PathBuf::from("src/utils.rs"),
PathBuf::from("src/config.rs"),
]
.into_iter()
.collect();
let fp = AnalysisDefFingerprint::with_sources(b"content", sources.clone());
assert_eq!(fp.source_files, sources);
assert!(fp.content_matches(b"content"));
}
#[test]
fn test_fingerprint_update_changes_hash() {
let fp = AnalysisDefFingerprint::new(b"old content");
let updated = fp.update_fingerprint(b"new content");
assert_ne!(
fp.fingerprint.as_slice(),
updated.fingerprint.as_slice(),
"Updated fingerprint should differ from original"
);
assert!(updated.content_matches(b"new content"));
assert!(!updated.content_matches(b"old content"));
}
#[test]
fn test_fingerprint_update_preserves_source_files() {
let sources: RapidSet<PathBuf> = [PathBuf::from("dep.rs")].into_iter().collect();
let fp = AnalysisDefFingerprint::with_sources(b"old", sources.clone());
let updated = fp.update_fingerprint(b"new");
assert_eq!(updated.source_files, sources);
}
#[test]
fn test_fingerprint_update_resets_timestamp() {
let mut fp = AnalysisDefFingerprint::new(b"content");
fp.set_last_analyzed(1000000);
let updated = fp.update_fingerprint(b"new content");
assert!(
updated.last_analyzed.is_none(),
"Updated fingerprint should reset timestamp"
);
}
#[test]
fn test_fingerprint_add_source_file() {
let mut fp = AnalysisDefFingerprint::new(b"content");
assert_eq!(fp.source_file_count(), 0);
fp.add_source_file(PathBuf::from("a.rs"));
assert_eq!(fp.source_file_count(), 1);
fp.add_source_file(PathBuf::from("b.rs"));
assert_eq!(fp.source_file_count(), 2);
fp.add_source_file(PathBuf::from("a.rs"));
assert_eq!(fp.source_file_count(), 2);
}
#[test]
fn test_fingerprint_remove_source_file() {
let mut fp = AnalysisDefFingerprint::with_sources(
b"content",
[PathBuf::from("a.rs"), PathBuf::from("b.rs")]
.into_iter()
.collect::<RapidSet<PathBuf>>(),
);
assert!(fp.remove_source_file(Path::new("a.rs")));
assert_eq!(fp.source_file_count(), 1);
assert!(!fp.remove_source_file(Path::new("c.rs")));
assert_eq!(fp.source_file_count(), 1);
}
#[test]
fn test_fingerprint_set_last_analyzed() {
let mut fp = AnalysisDefFingerprint::new(b"content");
assert!(fp.last_analyzed.is_none());
fp.set_last_analyzed(1_706_400_000_000_000); assert_eq!(fp.last_analyzed, Some(1_706_400_000_000_000));
}
#[test]
fn test_fingerprint_accessor() {
let fp = AnalysisDefFingerprint::new(b"test");
let fingerprint_ref = fp.fingerprint();
assert_eq!(fingerprint_ref.as_slice().len(), 16);
}
#[test]
fn test_dependency_edge_new() {
let edge = DependencyEdge::new(
PathBuf::from("src/main.rs"),
PathBuf::from("src/utils.rs"),
DependencyType::Import,
);
assert_eq!(edge.from, PathBuf::from("src/main.rs"));
assert_eq!(edge.to, PathBuf::from("src/utils.rs"));
assert_eq!(edge.dep_type, DependencyType::Import);
assert!(edge.symbol.is_none());
}
#[test]
fn test_dependency_edge_with_symbol() {
let symbol = SymbolDependency {
from_symbol: "main".to_string(),
to_symbol: "parse_config".to_string(),
kind: SymbolKind::Function,
strength: DependencyStrength::Strong,
};
let edge = DependencyEdge::with_symbol(
PathBuf::from("main.rs"),
PathBuf::from("config.rs"),
DependencyType::Import,
symbol.clone(),
);
assert!(edge.symbol.is_some());
assert_eq!(edge.symbol.unwrap().to_symbol, "parse_config");
}
#[test]
fn test_dependency_edge_effective_strength_import() {
let edge = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
}
#[test]
fn test_dependency_edge_effective_strength_export() {
let edge = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Export,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
}
#[test]
fn test_dependency_edge_effective_strength_trait() {
let edge = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Trait,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
}
#[test]
fn test_dependency_edge_effective_strength_macro() {
let edge = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Macro,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
}
#[test]
fn test_dependency_edge_effective_strength_type() {
let edge = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Type,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
}
#[test]
fn test_dependency_edge_symbol_overrides_strength() {
let symbol = SymbolDependency {
from_symbol: "a".to_string(),
to_symbol: "b".to_string(),
kind: SymbolKind::Function,
strength: DependencyStrength::Weak,
};
let edge = DependencyEdge::with_symbol(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
symbol,
);
assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
}
#[test]
fn test_dependency_edge_equality() {
let edge1 = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
);
let edge2 = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
);
assert_eq!(edge1, edge2);
}
#[test]
fn test_dependency_edge_inequality_different_type() {
let edge1 = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
);
let edge2 = DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Export,
);
assert_ne!(edge1, edge2);
}
#[test]
fn test_dependency_edge_serialization_roundtrip() {
let edge = DependencyEdge::new(
PathBuf::from("src/main.rs"),
PathBuf::from("src/lib.rs"),
DependencyType::Import,
);
let json = serde_json::to_string(&edge).expect("serialize");
let deserialized: DependencyEdge = serde_json::from_str(&json).expect("deserialize");
assert_eq!(edge, deserialized);
}
#[test]
fn test_dependency_edge_with_symbol_serialization_roundtrip() {
let symbol = SymbolDependency {
from_symbol: "handler".to_string(),
to_symbol: "Router".to_string(),
kind: SymbolKind::Class,
strength: DependencyStrength::Strong,
};
let edge = DependencyEdge::with_symbol(
PathBuf::from("api.rs"),
PathBuf::from("router.rs"),
DependencyType::Import,
symbol,
);
let json = serde_json::to_string(&edge).expect("serialize");
let deserialized: DependencyEdge = serde_json::from_str(&json).expect("deserialize");
assert_eq!(edge, deserialized);
}
#[test]
fn test_dependency_type_display() {
assert_eq!(format!("{}", DependencyType::Import), "import");
assert_eq!(format!("{}", DependencyType::Export), "export");
assert_eq!(format!("{}", DependencyType::Macro), "macro");
assert_eq!(format!("{}", DependencyType::Type), "type");
assert_eq!(format!("{}", DependencyType::Trait), "trait");
}
#[test]
fn test_dependency_strength_display() {
assert_eq!(format!("{}", DependencyStrength::Strong), "strong");
assert_eq!(format!("{}", DependencyStrength::Weak), "weak");
}
#[test]
fn test_symbol_kind_display() {
assert_eq!(format!("{}", SymbolKind::Function), "function");
assert_eq!(format!("{}", SymbolKind::Class), "class");
assert_eq!(format!("{}", SymbolKind::Interface), "interface");
assert_eq!(format!("{}", SymbolKind::TypeAlias), "type_alias");
assert_eq!(format!("{}", SymbolKind::Constant), "constant");
assert_eq!(format!("{}", SymbolKind::Enum), "enum");
assert_eq!(format!("{}", SymbolKind::Module), "module");
assert_eq!(format!("{}", SymbolKind::Macro), "macro");
}
#[test]
fn test_symbol_dependency_creation() {
let dep = SymbolDependency {
from_symbol: "parse".to_string(),
to_symbol: "Config".to_string(),
kind: SymbolKind::Class,
strength: DependencyStrength::Strong,
};
assert_eq!(dep.from_symbol, "parse");
assert_eq!(dep.to_symbol, "Config");
assert_eq!(dep.kind, SymbolKind::Class);
assert_eq!(dep.strength, DependencyStrength::Strong);
}
#[test]
fn test_symbol_dependency_serialization_roundtrip() {
let dep = SymbolDependency {
from_symbol: "main".to_string(),
to_symbol: "run_server".to_string(),
kind: SymbolKind::Function,
strength: DependencyStrength::Strong,
};
let json = serde_json::to_string(&dep).expect("serialize");
let deserialized: SymbolDependency = serde_json::from_str(&json).expect("deserialize");
assert_eq!(dep, deserialized);
}
#[test]
fn test_fingerprint_large_content() {
let large_content: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
let fp = AnalysisDefFingerprint::new(&large_content);
assert!(fp.content_matches(&large_content));
let mut modified = large_content.clone();
modified[500_000] = modified[500_000].wrapping_add(1);
assert!(!fp.content_matches(&modified));
}
#[test]
fn test_fingerprint_binary_content() {
let binary = vec![0u8, 1, 255, 128, 0, 0, 64, 32];
let fp = AnalysisDefFingerprint::new(&binary);
assert!(fp.content_matches(&binary));
}
}