use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MetricLevel {
Good,
Warning,
High,
Critical,
}
impl MetricLevel {
pub fn color_code(&self) -> &'static str {
match self {
MetricLevel::Good => "\x1b[32m", MetricLevel::Warning => "\x1b[33m", MetricLevel::High => "\x1b[31m", MetricLevel::Critical => "\x1b[1;31m", }
}
pub fn emoji(&self) -> &'static str {
match self {
MetricLevel::Good => "🟢",
MetricLevel::Warning => "🟡",
MetricLevel::High => "🟠",
MetricLevel::Critical => "🔴",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComplexityMetrics {
pub cyclomatic: u32,
pub cognitive: u32,
pub loc: u32,
pub sloc: u32,
pub comment_lines: u32,
pub max_nesting: u32,
pub parameters: u32,
pub returns: u32,
pub halstead: Option<HalsteadMetrics>,
}
impl ComplexityMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn cyclomatic_level(&self) -> MetricLevel {
match self.cyclomatic {
0..=10 => MetricLevel::Good,
11..=20 => MetricLevel::Warning,
21..=50 => MetricLevel::High,
_ => MetricLevel::Critical,
}
}
pub fn cognitive_level(&self) -> MetricLevel {
match self.cognitive {
0..=15 => MetricLevel::Good,
16..=30 => MetricLevel::Warning,
31..=60 => MetricLevel::High,
_ => MetricLevel::Critical,
}
}
pub fn nesting_level(&self) -> MetricLevel {
match self.max_nesting {
0..=4 => MetricLevel::Good,
5..=6 => MetricLevel::Warning,
7..=8 => MetricLevel::High,
_ => MetricLevel::Critical,
}
}
pub fn overall_level(&self) -> MetricLevel {
let levels = [
self.cyclomatic_level(),
self.cognitive_level(),
self.nesting_level(),
];
levels
.into_iter()
.max_by_key(|l| match l {
MetricLevel::Good => 0,
MetricLevel::Warning => 1,
MetricLevel::High => 2,
MetricLevel::Critical => 3,
})
.unwrap_or(MetricLevel::Good)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HalsteadMetrics {
pub distinct_operators: u32,
pub distinct_operands: u32,
pub total_operators: u32,
pub total_operands: u32,
pub vocabulary: u32,
pub length: u32,
pub calculated_length: f64,
pub volume: f64,
pub difficulty: f64,
pub effort: f64,
pub time: f64,
pub bugs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionMetrics {
pub name: String,
pub start_line: u32,
pub end_line: u32,
pub metrics: ComplexityMetrics,
pub kind: String,
pub parent: Option<String>,
}
impl FunctionMetrics {
pub fn new(name: &str, start_line: u32, end_line: u32) -> Self {
Self {
name: name.to_string(),
start_line,
end_line,
metrics: ComplexityMetrics::new(),
kind: "function".to_string(),
parent: None,
}
}
pub fn lines(&self) -> u32 {
self.end_line.saturating_sub(self.start_line) + 1
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetrics {
pub path: PathBuf,
pub language: String,
pub metrics: ComplexityMetrics,
pub functions: Vec<FunctionMetrics>,
pub classes: u32,
pub imports: u32,
}
impl FileMetrics {
pub fn new(path: PathBuf, language: &str) -> Self {
Self {
path,
language: language.to_string(),
metrics: ComplexityMetrics::new(),
functions: Vec::new(),
classes: 0,
imports: 0,
}
}
pub fn most_complex_function(&self) -> Option<&FunctionMetrics> {
self.functions.iter().max_by_key(|f| f.metrics.cyclomatic)
}
pub fn average_cyclomatic(&self) -> f64 {
if self.functions.is_empty() {
return 0.0;
}
let sum: u32 = self.functions.iter().map(|f| f.metrics.cyclomatic).sum();
sum as f64 / self.functions.len() as f64
}
pub fn functions_above_threshold(&self, threshold: u32) -> Vec<&FunctionMetrics> {
self.functions
.iter()
.filter(|f| f.metrics.cyclomatic > threshold)
.collect()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SummaryStats {
pub total_files: usize,
pub total_functions: usize,
pub total_loc: u64,
pub total_sloc: u64,
pub avg_cyclomatic: f64,
pub avg_cognitive: f64,
pub max_cyclomatic: u32,
pub max_cognitive: u32,
pub high_complexity_files: usize,
pub high_complexity_functions: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complexity_metrics_default() {
let metrics = ComplexityMetrics::new();
assert_eq!(metrics.cyclomatic, 0);
assert_eq!(metrics.cognitive, 0);
assert_eq!(metrics.loc, 0);
}
#[test]
fn test_cyclomatic_level() {
let mut metrics = ComplexityMetrics::new();
metrics.cyclomatic = 5;
assert_eq!(metrics.cyclomatic_level(), MetricLevel::Good);
metrics.cyclomatic = 15;
assert_eq!(metrics.cyclomatic_level(), MetricLevel::Warning);
metrics.cyclomatic = 35;
assert_eq!(metrics.cyclomatic_level(), MetricLevel::High);
metrics.cyclomatic = 60;
assert_eq!(metrics.cyclomatic_level(), MetricLevel::Critical);
}
#[test]
fn test_function_metrics() {
let func = FunctionMetrics::new("test_func", 10, 20);
assert_eq!(func.lines(), 11);
}
#[test]
fn test_file_metrics() {
let file = FileMetrics::new(PathBuf::from("test.rs"), "rust");
assert!(file.functions.is_empty());
assert_eq!(file.average_cyclomatic(), 0.0);
}
}