use anyhow::Result;
use dashmap::DashMap;
use rayon::prelude::*;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::tdg::{
config::TdgConfig,
language::{Language, LanguageRegistry},
scorers::ScorerSet,
Comparison, MetricCategory, PenaltyTracker, ProjectScore, TdgScore,
};
pub struct TdgAnalyzer {
config: TdgConfig,
registry: Arc<LanguageRegistry>,
scorers: ScorerSet,
cache: DashMap<PathBuf, CachedScore>,
}
impl TdgAnalyzer {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Result<Self> {
Self::with_config(TdgConfig::default())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn with_config(config: TdgConfig) -> Result<Self> {
Ok(Self {
config,
registry: Arc::new(LanguageRegistry::new()?),
scorers: ScorerSet::new(),
cache: DashMap::new(),
})
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn from_config_file(path: &Path) -> Result<Self> {
let config = TdgConfig::from_file(path)?;
Self::with_config(config)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn analyze_file(&self, path: &Path) -> Result<TdgScore> {
let file_hash = self.hash_file(path)?;
if let Some(cached) = self.cache.get(path) {
if cached.hash == file_hash {
return Ok(cached.score.clone());
}
}
let language = Language::from_extension(path);
let adapter = self.registry.get_adapter(language)?;
let source = fs::read_to_string(path)?;
let tree = adapter.parse(&source)?;
let mut tracker = PenaltyTracker::new();
let mut score = TdgScore {
language,
confidence: adapter.confidence(),
file_path: Some(path.to_path_buf()),
..Default::default()
};
for scorer in self.scorers.iter() {
let metric_score = scorer.score(&tree, &source, language, &self.config, &mut tracker)?;
score.set_metric(scorer.category(), metric_score);
}
score.penalties_applied = tracker.get_attributions();
score.calculate_total();
self.cache.insert(path.to_path_buf(), CachedScore {
score: score.clone(),
hash: file_hash,
});
Ok(score)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn analyze_str(&self, source: &str, language: Language) -> Result<TdgScore> {
let adapter = self.registry.get_adapter(language)?;
let tree = adapter.parse(source)?;
let mut tracker = PenaltyTracker::new();
let mut score = TdgScore {
language,
confidence: adapter.confidence(),
file_path: None,
..Default::default()
};
for scorer in self.scorers.iter() {
let metric_score = scorer.score(&tree, source, language, &self.config, &mut tracker)?;
score.set_metric(scorer.category(), metric_score);
}
score.penalties_applied = tracker.get_attributions();
score.calculate_total();
Ok(score)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn analyze_project(&self, dir: &Path) -> Result<ProjectScore> {
let files = self.discover_files(dir)?;
let results: Result<Vec<_>> = files
.par_iter()
.map(|file| self.analyze_file(file))
.collect();
let scores = results?;
Ok(ProjectScore::aggregate(scores))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn compare(&self, path1: &Path, path2: &Path) -> Result<Comparison> {
let score1 = if path1.is_dir() {
self.analyze_project(path1)?.average()
} else {
self.analyze_file(path1)?
};
let score2 = if path2.is_dir() {
self.analyze_project(path2)?.average()
} else {
self.analyze_file(path2)?
};
Ok(Comparison::new(score1, score2))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn clear_cache(&self) {
self.cache.clear();
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache.len(), self.cache.capacity())
}
fn discover_files(&self, dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
self.discover_files_recursive(dir, &mut files)?;
Ok(files)
}
fn discover_files_recursive(&self, dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if !self.should_skip_directory(&path) {
self.discover_files_recursive(&path, files)?;
}
} else if self.should_analyze_file(&path) {
files.push(path);
}
}
Ok(())
}
fn should_skip_directory(&self, path: &Path) -> bool {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
matches!(
name,
"node_modules" | "target" | "build" | "dist" | ".git" |
"__pycache__" | ".pytest_cache" | "venv" | ".venv" |
"vendor" | ".idea" | ".vscode"
)
} else {
false
}
}
fn should_analyze_file(&self, path: &Path) -> bool {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
matches!(
ext,
"rs" | "py" | "js" | "ts" | "jsx" | "tsx" | "go" |
"java" | "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "cu" | "cuh" |
"rb" | "swift" | "kt" | "kts"
)
} else {
false
}
}
fn hash_file(&self, path: &Path) -> Result<String> {
let metadata = fs::metadata(path)?;
let modified = metadata.modified()?.duration_since(std::time::UNIX_EPOCH)?.as_secs();
let size = metadata.len();
let mut hasher = Sha256::new();
hasher.update(path.to_string_lossy().as_bytes());
hasher.update(&modified.to_le_bytes());
hasher.update(&size.to_le_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
}
#[derive(Clone, Debug)]
struct CachedScore {
score: TdgScore,
hash: String,
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
use std::io::Write;
#[test]
fn test_analyze_str() -> Result<()> {
let analyzer = TdgAnalyzer::new()?;
let source = r#"
fn simple_function(x: i32) -> i32 {
if x > 0 {
x * 2
} else {
0
}
}
"#;
let score = analyzer.analyze_str(source, Language::Rust)?;
assert_eq!(score.language, Language::Rust);
assert!(score.total > 0.0);
assert!(score.total <= 100.0);
assert!(score.confidence > 0.0);
Ok(())
}
#[test]
fn test_analyze_file() -> Result<()> {
let analyzer = TdgAnalyzer::new()?;
let mut temp_file = NamedTempFile::with_suffix(".rs")?;
writeln!(
temp_file,
r#"
/// A well-documented function
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn documented_function() -> i32 {{
42
}}
"#
)?;
let score = analyzer.analyze_file(temp_file.path())?;
assert_eq!(score.language, Language::Rust);
assert!(score.total > 0.0);
assert_eq!(score.file_path, Some(temp_file.path().to_path_buf()));
Ok(())
}
#[test]
fn test_caching() -> Result<()> {
let analyzer = TdgAnalyzer::new()?;
let mut temp_file = NamedTempFile::with_suffix(".rs")?;
writeln!(temp_file, "fn test() {{ }}")?;
let score1 = analyzer.analyze_file(temp_file.path())?;
let score2 = analyzer.analyze_file(temp_file.path())?;
assert_eq!(score1.total, score2.total);
let (cache_size, _) = analyzer.cache_stats();
assert_eq!(cache_size, 1);
Ok(())
}
#[test]
fn test_comparison() -> Result<()> {
let analyzer = TdgAnalyzer::new()?;
let source1 = "fn simple() { }";
let source2 = r#"
/// Well documented function with examples
///
/// # Example
/// ```
/// complex();
/// ```
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn complex() {
// Implementation
}
"#;
let score1 = analyzer.analyze_str(source1, Language::Rust)?;
let score2 = analyzer.analyze_str(source2, Language::Rust)?;
let comparison = Comparison::new(score1, score2);
assert!(comparison.delta != 0.0);
assert!(!comparison.improvements.is_empty() || !comparison.regressions.is_empty());
Ok(())
}
#[test]
fn test_file_discovery() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let rust_file = temp_dir.path().join("test.rs");
let python_file = temp_dir.path().join("test.py");
let ignored_file = temp_dir.path().join("README.md");
fs::write(&rust_file, "fn main() {}")?;
fs::write(&python_file, "def main(): pass")?;
fs::write(&ignored_file, "# README")?;
let analyzer = TdgAnalyzer::new()?;
let files = analyzer.discover_files(temp_dir.path())?;
assert_eq!(files.len(), 2);
assert!(files.contains(&rust_file));
assert!(files.contains(&python_file));
assert!(!files.contains(&ignored_file));
Ok(())
}
#[test]
fn test_penalty_tracking() -> Result<()> {
let analyzer = TdgAnalyzer::new()?;
let source = r#"
fn complex_function() {
if true {
if true {
if true {
if true {
if true {
if true {
// Very deeply nested
}
}
}
}
}
}
}
"#;
let score = analyzer.analyze_str(source, Language::Rust)?;
assert!(!score.penalties_applied.is_empty());
assert!(score.structural_complexity < 25.0);
Ok(())
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}