use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileStats {
pub path: PathBuf,
pub language: String,
pub lines: LineStats,
pub size: u64,
pub complexity: Complexity,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct LineStats {
pub total: usize,
pub code: usize,
pub comment: usize,
pub blank: usize,
}
impl LineStats {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, other: &LineStats) {
self.total += other.total;
self.code += other.code;
self.comment += other.comment;
self.blank += other.blank;
}
}
impl std::ops::Add for LineStats {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
total: self.total + other.total,
code: self.code + other.code,
comment: self.comment + other.comment,
blank: self.blank + other.blank,
}
}
}
impl std::ops::AddAssign for LineStats {
fn add_assign(&mut self, other: Self) {
self.add(&other);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Complexity {
pub functions: usize,
pub cyclomatic: usize,
pub max_depth: usize,
pub avg_func_lines: f64,
}
impl Complexity {
pub fn add(&mut self, other: &Complexity) {
self.functions += other.functions;
self.cyclomatic += other.cyclomatic;
self.max_depth = self.max_depth.max(other.max_depth);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SizeDistribution {
pub tiny: usize,
pub small: usize,
pub medium: usize,
pub large: usize,
pub huge: usize,
}
impl SizeDistribution {
pub fn add(&mut self, size: u64) {
match size {
s if s < 1024 => self.tiny += 1,
s if s < 10 * 1024 => self.small += 1,
s if s < 100 * 1024 => self.medium += 1,
s if s < 1024 * 1024 => self.large += 1,
_ => self.huge += 1,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LanguageSummary {
pub files: usize,
pub lines: LineStats,
pub size: u64,
pub complexity: Complexity,
}
#[derive(Debug, Clone, Serialize)]
pub struct RepoStats {
pub name: String,
pub path: PathBuf,
pub primary_language: String,
pub files: Vec<FileStats>,
pub summary: RepoSummary,
pub by_language: IndexMap<String, LanguageSummary>,
pub git_info: Option<GitInfo>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct RepoSummary {
pub total_files: usize,
pub lines: LineStats,
pub total_size: u64,
pub complexity: Complexity,
pub size_distribution: SizeDistribution,
}
#[derive(Debug, Clone, Serialize)]
pub struct GitInfo {
pub branch: Option<String>,
pub commit: Option<String>,
pub author: Option<String>,
pub date: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Summary {
pub total_files: usize,
pub lines: LineStats,
pub total_size: u64,
pub by_language: IndexMap<String, LanguageSummary>,
pub size_distribution: SizeDistribution,
pub complexity: Complexity,
}
impl Summary {
pub fn from_file_stats(files: &[FileStats]) -> Self {
let mut summary = Summary::default();
let mut by_language: HashMap<String, LanguageSummary> = HashMap::new();
for file in files {
summary.total_files += 1;
summary.lines.add(&file.lines);
summary.total_size += file.size;
summary.size_distribution.add(file.size);
summary.complexity.add(&file.complexity);
let lang_summary = by_language.entry(file.language.clone()).or_default();
lang_summary.files += 1;
lang_summary.lines.add(&file.lines);
lang_summary.size += file.size;
lang_summary.complexity.add(&file.complexity);
}
let mut sorted: Vec<_> = by_language.into_iter().collect();
sorted.sort_by(|a, b| b.1.lines.code.cmp(&a.1.lines.code));
summary.by_language = sorted.into_iter().collect();
if summary.complexity.functions > 0 {
summary.complexity.avg_func_lines =
summary.lines.code as f64 / summary.complexity.functions as f64;
}
summary
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub files: Vec<FileStats>,
pub summary: Summary,
#[serde(with = "duration_serde")]
pub elapsed: Duration,
pub scanned_files: usize,
pub skipped_files: usize,
}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.as_secs_f64().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs = f64::deserialize(deserializer)?;
Ok(Duration::from_secs_f64(secs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_stats_default() {
let stats = LineStats::default();
assert_eq!(stats.total, 0);
assert_eq!(stats.code, 0);
assert_eq!(stats.comment, 0);
assert_eq!(stats.blank, 0);
}
#[test]
fn test_line_stats_add() {
let mut stats1 = LineStats {
total: 100,
code: 80,
comment: 10,
blank: 10,
};
let stats2 = LineStats {
total: 50,
code: 40,
comment: 5,
blank: 5,
};
stats1.add(&stats2);
assert_eq!(stats1.total, 150);
assert_eq!(stats1.code, 120);
assert_eq!(stats1.comment, 15);
assert_eq!(stats1.blank, 15);
}
#[test]
fn test_line_stats_add_trait() {
let stats1 = LineStats {
total: 100,
code: 80,
comment: 10,
blank: 10,
};
let stats2 = LineStats {
total: 50,
code: 40,
comment: 5,
blank: 5,
};
let result = stats1 + stats2;
assert_eq!(result.total, 150);
assert_eq!(result.code, 120);
}
#[test]
fn test_line_stats_add_assign() {
let mut stats1 = LineStats {
total: 100,
code: 80,
comment: 10,
blank: 10,
};
let stats2 = LineStats {
total: 50,
code: 40,
comment: 5,
blank: 5,
};
stats1 += stats2;
assert_eq!(stats1.total, 150);
assert_eq!(stats1.code, 120);
}
#[test]
fn test_complexity_add() {
let mut c1 = Complexity {
functions: 10,
cyclomatic: 20,
max_depth: 5,
avg_func_lines: 0.0,
};
let c2 = Complexity {
functions: 5,
cyclomatic: 10,
max_depth: 8,
avg_func_lines: 0.0,
};
c1.add(&c2);
assert_eq!(c1.functions, 15);
assert_eq!(c1.cyclomatic, 30);
assert_eq!(c1.max_depth, 8); }
#[test]
fn test_size_distribution() {
let mut dist = SizeDistribution::default();
dist.add(500); dist.add(1024); dist.add(5000); dist.add(15000); dist.add(500_000); dist.add(2_000_000);
assert_eq!(dist.tiny, 1);
assert_eq!(dist.small, 2);
assert_eq!(dist.medium, 1);
assert_eq!(dist.large, 1);
assert_eq!(dist.huge, 1);
}
#[test]
fn test_summary_from_file_stats() {
let files = vec![
FileStats {
path: PathBuf::from("src/main.rs"),
language: "Rust".to_string(),
lines: LineStats {
total: 100,
code: 80,
comment: 10,
blank: 10,
},
size: 2000,
complexity: Complexity {
functions: 5,
cyclomatic: 10,
max_depth: 3,
avg_func_lines: 16.0,
},
},
FileStats {
path: PathBuf::from("src/lib.rs"),
language: "Rust".to_string(),
lines: LineStats {
total: 50,
code: 40,
comment: 5,
blank: 5,
},
size: 1000,
complexity: Complexity {
functions: 3,
cyclomatic: 6,
max_depth: 2,
avg_func_lines: 13.3,
},
},
FileStats {
path: PathBuf::from("test.py"),
language: "Python".to_string(),
lines: LineStats {
total: 30,
code: 20,
comment: 5,
blank: 5,
},
size: 500,
complexity: Complexity {
functions: 2,
cyclomatic: 4,
max_depth: 2,
avg_func_lines: 10.0,
},
},
];
let summary = Summary::from_file_stats(&files);
assert_eq!(summary.total_files, 3);
assert_eq!(summary.lines.total, 180);
assert_eq!(summary.lines.code, 140);
assert_eq!(summary.total_size, 3500);
assert_eq!(summary.by_language.len(), 2);
assert_eq!(summary.complexity.functions, 10);
let first_lang = summary.by_language.keys().next().unwrap();
assert_eq!(first_lang, "Rust");
let rust_stats = summary.by_language.get("Rust").unwrap();
assert_eq!(rust_stats.files, 2);
assert_eq!(rust_stats.lines.code, 120);
}
#[test]
fn test_summary_empty() {
let summary = Summary::from_file_stats(&[]);
assert_eq!(summary.total_files, 0);
assert_eq!(summary.lines.total, 0);
assert!(summary.by_language.is_empty());
}
#[test]
fn test_file_stats_default() {
let stats = FileStats::default();
assert!(stats.path.as_os_str().is_empty());
assert!(stats.language.is_empty());
assert_eq!(stats.size, 0);
}
}