use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use crate::error::{Result, BrrrError};
use super::cohesion::{analyze_cohesion, CohesionLevel};
use super::complexity::{
analyze_complexity, analyze_cognitive_complexity, analyze_halstead, analyze_maintainability,
CognitiveRiskLevel, HalsteadMetrics, MaintainabilityRiskLevel, RiskLevel,
};
use super::coupling::CouplingLevel;
use super::function_size::analyze_function_size;
use super::loc::{analyze_loc, LOCMetrics};
use super::nesting::{analyze_nesting, NestingDepthLevel};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricThresholds {
pub cyclomatic_warning: u32,
pub cyclomatic_critical: u32,
pub cognitive_warning: u32,
pub cognitive_critical: u32,
pub maintainability_warning: f64,
pub maintainability_critical: f64,
pub loc_warning: u32,
pub loc_critical: u32,
pub nesting_warning: u32,
pub nesting_critical: u32,
pub params_warning: u32,
pub params_critical: u32,
pub coupling_distance_warning: f64,
pub coupling_distance_critical: f64,
pub lcom_warning: u32,
pub lcom_critical: u32,
pub halstead_volume_warning: f64,
pub halstead_volume_critical: f64,
pub halstead_bugs_warning: f64,
pub halstead_bugs_critical: f64,
}
impl Default for MetricThresholds {
fn default() -> Self {
Self {
cyclomatic_warning: 10,
cyclomatic_critical: 20,
cognitive_warning: 15,
cognitive_critical: 30,
maintainability_warning: 40.0,
maintainability_critical: 20.0,
loc_warning: 50,
loc_critical: 100,
nesting_warning: 4,
nesting_critical: 6,
params_warning: 5,
params_critical: 8,
coupling_distance_warning: 0.5,
coupling_distance_critical: 0.7,
lcom_warning: 2,
lcom_critical: 4,
halstead_volume_warning: 1000.0,
halstead_volume_critical: 3000.0,
halstead_bugs_warning: 0.5,
halstead_bugs_critical: 2.0,
}
}
}
impl MetricThresholds {
#[must_use]
pub fn strict() -> Self {
Self {
cyclomatic_warning: 5,
cyclomatic_critical: 10,
cognitive_warning: 8,
cognitive_critical: 15,
maintainability_warning: 60.0,
maintainability_critical: 40.0,
loc_warning: 30,
loc_critical: 50,
nesting_warning: 3,
nesting_critical: 4,
params_warning: 3,
params_critical: 5,
coupling_distance_warning: 0.3,
coupling_distance_critical: 0.5,
lcom_warning: 1,
lcom_critical: 2,
halstead_volume_warning: 500.0,
halstead_volume_critical: 1500.0,
halstead_bugs_warning: 0.2,
halstead_bugs_critical: 1.0,
}
}
#[must_use]
pub fn relaxed() -> Self {
Self {
cyclomatic_warning: 15,
cyclomatic_critical: 30,
cognitive_warning: 25,
cognitive_critical: 50,
maintainability_warning: 30.0,
maintainability_critical: 15.0,
loc_warning: 80,
loc_critical: 150,
nesting_warning: 5,
nesting_critical: 8,
params_warning: 7,
params_critical: 10,
coupling_distance_warning: 0.6,
coupling_distance_critical: 0.8,
lcom_warning: 3,
lcom_critical: 6,
halstead_volume_warning: 2000.0,
halstead_volume_critical: 5000.0,
halstead_bugs_warning: 1.0,
halstead_bugs_critical: 3.0,
}
}
pub fn from_toml(content: &str) -> std::result::Result<Self, String> {
let value: toml::Value = content.parse()
.map_err(|e| format!("Invalid TOML: {}", e))?;
let mut thresholds = Self::default();
if let Some(metrics) = value.get("metrics") {
if let Some(t) = metrics.get("thresholds") {
if let Some(v) = t.get("cyclomatic_warning").and_then(|v| v.as_integer()) {
thresholds.cyclomatic_warning = v as u32;
}
if let Some(v) = t.get("cyclomatic_critical").and_then(|v| v.as_integer()) {
thresholds.cyclomatic_critical = v as u32;
}
if let Some(v) = t.get("cognitive_warning").and_then(|v| v.as_integer()) {
thresholds.cognitive_warning = v as u32;
}
if let Some(v) = t.get("cognitive_critical").and_then(|v| v.as_integer()) {
thresholds.cognitive_critical = v as u32;
}
if let Some(v) = t.get("maintainability_warning").and_then(|v| v.as_float()) {
thresholds.maintainability_warning = v;
}
if let Some(v) = t.get("maintainability_critical").and_then(|v| v.as_float()) {
thresholds.maintainability_critical = v;
}
if let Some(v) = t.get("loc_warning").and_then(|v| v.as_integer()) {
thresholds.loc_warning = v as u32;
}
if let Some(v) = t.get("loc_critical").and_then(|v| v.as_integer()) {
thresholds.loc_critical = v as u32;
}
if let Some(v) = t.get("nesting_warning").and_then(|v| v.as_integer()) {
thresholds.nesting_warning = v as u32;
}
if let Some(v) = t.get("nesting_critical").and_then(|v| v.as_integer()) {
thresholds.nesting_critical = v as u32;
}
if let Some(v) = t.get("params_warning").and_then(|v| v.as_integer()) {
thresholds.params_warning = v as u32;
}
if let Some(v) = t.get("params_critical").and_then(|v| v.as_integer()) {
thresholds.params_critical = v as u32;
}
if let Some(v) = t.get("coupling_distance_warning").and_then(|v| v.as_float()) {
thresholds.coupling_distance_warning = v;
}
if let Some(v) = t.get("coupling_distance_critical").and_then(|v| v.as_float()) {
thresholds.coupling_distance_critical = v;
}
if let Some(v) = t.get("lcom_warning").and_then(|v| v.as_integer()) {
thresholds.lcom_warning = v as u32;
}
if let Some(v) = t.get("lcom_critical").and_then(|v| v.as_integer()) {
thresholds.lcom_critical = v as u32;
}
if let Some(v) = t.get("halstead_volume_warning").and_then(|v| v.as_float()) {
thresholds.halstead_volume_warning = v;
}
if let Some(v) = t.get("halstead_volume_critical").and_then(|v| v.as_float()) {
thresholds.halstead_volume_critical = v;
}
if let Some(v) = t.get("halstead_bugs_warning").and_then(|v| v.as_float()) {
thresholds.halstead_bugs_warning = v;
}
if let Some(v) = t.get("halstead_bugs_critical").and_then(|v| v.as_float()) {
thresholds.halstead_bugs_critical = v;
}
}
}
Ok(thresholds)
}
pub fn load_from_project<P: AsRef<Path>>(project_root: P) -> Self {
let config_path = project_root.as_ref().join(".brrr").join("config.toml");
if config_path.exists() {
match fs::read_to_string(&config_path) {
Ok(content) => {
match Self::from_toml(&content) {
Ok(thresholds) => {
info!("Loaded metric thresholds from {}", config_path.display());
return thresholds;
}
Err(e) => {
warn!("Failed to parse {}: {}", config_path.display(), e);
}
}
}
Err(e) => {
warn!("Failed to read {}: {}", config_path.display(), e);
}
}
}
Self::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub thresholds: MetricThresholds,
pub include_halstead_tokens: bool,
pub include_cognitive_breakdown: bool,
pub include_cohesion_components: bool,
pub analyze_coupling: bool,
pub coupling_level: CouplingLevel,
pub max_files: usize,
pub show_progress: bool,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
thresholds: MetricThresholds::default(),
include_halstead_tokens: false,
include_cognitive_breakdown: false,
include_cohesion_components: false,
analyze_coupling: true,
coupling_level: CouplingLevel::File,
max_files: 0,
show_progress: false,
}
}
}
impl MetricsConfig {
#[must_use]
pub fn minimal() -> Self {
Self {
analyze_coupling: false,
max_files: 100,
..Default::default()
}
}
#[must_use]
pub fn comprehensive() -> Self {
Self {
include_halstead_tokens: true,
include_cognitive_breakdown: true,
include_cohesion_components: true,
analyze_coupling: true,
coupling_level: CouplingLevel::File,
..Default::default()
}
}
#[must_use]
pub fn with_thresholds(mut self, thresholds: MetricThresholds) -> Self {
self.thresholds = thresholds;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum IssueSeverity {
Info,
Warning,
Critical,
}
impl IssueSeverity {
#[must_use]
pub const fn color_code(&self) -> &'static str {
match self {
Self::Info => "\x1b[36m", Self::Warning => "\x1b[33m", Self::Critical => "\x1b[31m", }
}
}
impl std::fmt::Display for IssueSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Warning => write!(f, "warning"),
Self::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IssueCategory {
CyclomaticComplexity,
CognitiveComplexity,
Maintainability,
FunctionLength,
ParameterCount,
NestingDepth,
Coupling,
Cohesion,
HalsteadComplexity,
EstimatedBugs,
}
impl std::fmt::Display for IssueCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CyclomaticComplexity => write!(f, "cyclomatic_complexity"),
Self::CognitiveComplexity => write!(f, "cognitive_complexity"),
Self::Maintainability => write!(f, "maintainability"),
Self::FunctionLength => write!(f, "function_length"),
Self::ParameterCount => write!(f, "parameter_count"),
Self::NestingDepth => write!(f, "nesting_depth"),
Self::Coupling => write!(f, "coupling"),
Self::Cohesion => write!(f, "cohesion"),
Self::HalsteadComplexity => write!(f, "halstead_complexity"),
Self::EstimatedBugs => write!(f, "estimated_bugs"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricIssue {
pub severity: IssueSeverity,
pub category: IssueCategory,
pub message: String,
pub file: PathBuf,
pub line: Option<usize>,
pub unit_name: String,
pub value: f64,
pub threshold: f64,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMetrics {
pub total_files: u32,
pub total_functions: u32,
pub total_classes: u32,
pub total_loc: LOCMetrics,
pub avg_cyclomatic: f64,
pub avg_cognitive: f64,
pub avg_maintainability: f64,
pub avg_nesting: f64,
pub avg_function_size: f64,
pub total_estimated_bugs: f64,
pub total_estimated_hours: f64,
pub files_with_critical_issues: u32,
pub complex_functions: u32,
pub low_cohesion_classes: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetrics {
pub path: PathBuf,
pub language: Option<String>,
pub loc: LOCMetrics,
pub function_count: u32,
pub class_count: u32,
pub avg_cyclomatic: f64,
pub max_cyclomatic: u32,
pub avg_cognitive: f64,
pub max_cognitive: u32,
pub avg_maintainability: f64,
pub min_maintainability: f64,
pub issue_count: u32,
pub critical_issue_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionMetrics {
pub name: String,
pub file: PathBuf,
pub line: usize,
pub end_line: usize,
pub cyclomatic: u32,
pub cyclomatic_risk: RiskLevel,
pub cognitive: u32,
pub cognitive_risk: CognitiveRiskLevel,
pub halstead: Option<HalsteadMetrics>,
pub maintainability: f64,
pub maintainability_risk: MaintainabilityRiskLevel,
pub loc: u32,
pub statements: u32,
pub nesting: u32,
pub nesting_risk: NestingDepthLevel,
pub params: u32,
pub variables: u32,
pub returns: u32,
pub size_issues: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassMetrics {
pub name: String,
pub file: PathBuf,
pub line: usize,
pub end_line: usize,
pub method_count: u32,
pub attribute_count: u32,
pub lcom3: u32,
pub lcom4: u32,
pub cohesion_level: CohesionLevel,
pub is_low_cohesion: bool,
pub avg_method_complexity: f64,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsReport {
pub path: PathBuf,
pub language: Option<String>,
pub analysis_duration_ms: u64,
pub config: MetricsConfig,
pub project_summary: ProjectMetrics,
pub file_metrics: Vec<FileMetrics>,
pub function_metrics: Vec<FunctionMetrics>,
pub class_metrics: Vec<ClassMetrics>,
pub issues: Vec<MetricIssue>,
pub issue_stats: IssueStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IssueStats {
pub total: u32,
pub info: u32,
pub warnings: u32,
pub critical: u32,
pub by_category: HashMap<String, u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGate {
pub fail_on_critical: bool,
pub max_critical_issues: u32,
pub max_warning_issues: u32,
pub min_maintainability: Option<f64>,
pub max_avg_cyclomatic: Option<f64>,
}
impl Default for QualityGate {
fn default() -> Self {
Self {
fail_on_critical: true,
max_critical_issues: 0,
max_warning_issues: 0,
min_maintainability: None,
max_avg_cyclomatic: None,
}
}
}
impl QualityGate {
#[must_use]
pub fn permissive() -> Self {
Self {
fail_on_critical: true,
max_critical_issues: 0,
max_warning_issues: 0,
min_maintainability: None,
max_avg_cyclomatic: None,
}
}
#[must_use]
pub fn strict() -> Self {
Self {
fail_on_critical: true,
max_critical_issues: 0,
max_warning_issues: 10,
min_maintainability: Some(50.0),
max_avg_cyclomatic: Some(10.0),
}
}
#[must_use]
pub fn check(&self, report: &MetricsReport) -> QualityGateResult {
let mut reasons = Vec::new();
let critical_count = report.issue_stats.critical;
if self.fail_on_critical && critical_count > 0 {
reasons.push(format!("{} critical issues found", critical_count));
}
if self.max_critical_issues > 0 && critical_count > self.max_critical_issues {
reasons.push(format!(
"Critical issues ({}) exceed maximum ({})",
critical_count, self.max_critical_issues
));
}
let warning_count = report.issue_stats.warnings;
if self.max_warning_issues > 0 && warning_count > self.max_warning_issues {
reasons.push(format!(
"Warning issues ({}) exceed maximum ({})",
warning_count, self.max_warning_issues
));
}
if let Some(min_mi) = self.min_maintainability {
if report.project_summary.avg_maintainability < min_mi {
reasons.push(format!(
"Average maintainability ({:.1}) below minimum ({:.1})",
report.project_summary.avg_maintainability, min_mi
));
}
}
if let Some(max_cc) = self.max_avg_cyclomatic {
if report.project_summary.avg_cyclomatic > max_cc {
reasons.push(format!(
"Average cyclomatic complexity ({:.1}) exceeds maximum ({:.1})",
report.project_summary.avg_cyclomatic, max_cc
));
}
}
QualityGateResult {
passed: reasons.is_empty(),
failed: !reasons.is_empty(),
critical_count,
warning_count,
reasons,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateResult {
pub passed: bool,
pub failed: bool,
pub critical_count: u32,
pub warning_count: u32,
pub reasons: Vec<String>,
}
pub fn analyze_all_metrics<P: AsRef<Path>>(
path: P,
lang: Option<&str>,
config: &MetricsConfig,
) -> Result<MetricsReport> {
let path = path.as_ref();
let start = Instant::now();
if !path.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {}", path.display())
)));
}
info!("Starting unified metrics analysis for: {}", path.display());
let lang_str = lang.map(ToString::to_string);
let (complexity_result, other_results) = rayon::join(
|| analyze_complexity(path, lang_str.as_deref(), None),
|| {
let (cognitive, rest1) = rayon::join(
|| analyze_cognitive_complexity(path, lang_str.as_deref(), None),
|| {
let (halstead, rest2) = rayon::join(
|| analyze_halstead(path, lang_str.as_deref(), config.include_halstead_tokens),
|| {
let (maint, rest3) = rayon::join(
|| analyze_maintainability(path, lang_str.as_deref(), None, false),
|| {
let (loc, rest4) = rayon::join(
|| analyze_loc(path, lang_str.as_deref(), None),
|| {
rayon::join(
|| analyze_nesting(path, lang_str.as_deref(), None),
|| analyze_function_size(path, lang_str.as_deref(), None),
)
},
);
(loc, rest4)
},
);
(maint, rest3)
},
);
(halstead, rest2)
},
);
(cognitive, rest1)
},
);
let (cognitive_result, (halstead_result, (maintainability_result, (loc_result, (nesting_result, function_size_result))))) =
other_results;
let complexity_analysis = complexity_result.ok();
let cognitive_analysis = cognitive_result.ok();
let halstead_analysis = halstead_result.ok();
let maintainability_analysis = maintainability_result.ok();
let loc_analysis = loc_result.ok();
let nesting_analysis = nesting_result.ok();
let function_size_analysis = function_size_result.ok();
let cohesion_analysis = if config.analyze_coupling {
analyze_cohesion(path, lang_str.as_deref(), None).ok()
} else {
None
};
let mut function_metrics = build_function_metrics(
&complexity_analysis,
&cognitive_analysis,
&halstead_analysis,
&maintainability_analysis,
&loc_analysis,
&nesting_analysis,
&function_size_analysis,
);
let mut issues = detect_issues(
&function_metrics,
&cohesion_analysis,
&config.thresholds,
);
issues.sort_by(|a, b| b.severity.cmp(&a.severity));
let class_metrics = build_class_metrics(&cohesion_analysis);
let file_metrics = build_file_metrics(
&complexity_analysis,
&cognitive_analysis,
&maintainability_analysis,
&loc_analysis,
&issues,
);
let project_summary = build_project_summary(
&file_metrics,
&function_metrics,
&class_metrics,
&loc_analysis,
&halstead_analysis,
&issues,
);
let issue_stats = build_issue_stats(&issues);
let duration = start.elapsed();
Ok(MetricsReport {
path: path.to_path_buf(),
language: lang.map(ToString::to_string)
.or_else(|| complexity_analysis.as_ref().and_then(|a| a.language.clone())),
analysis_duration_ms: duration.as_millis() as u64,
config: config.clone(),
project_summary,
file_metrics,
function_metrics,
class_metrics,
issues,
issue_stats,
})
}
fn build_function_metrics(
complexity: &Option<super::complexity::ComplexityAnalysis>,
cognitive: &Option<super::complexity::CognitiveAnalysis>,
halstead: &Option<super::complexity::HalsteadAnalysis>,
maintainability: &Option<super::complexity::MaintainabilityAnalysis>,
_loc: &Option<super::loc::LOCAnalysis>,
nesting: &Option<super::nesting::NestingAnalysis>,
function_size: &Option<super::function_size::FunctionSizeAnalysis>,
) -> Vec<FunctionMetrics> {
let Some(cc) = complexity else {
return Vec::new();
};
let mut result = Vec::with_capacity(cc.functions.len());
for func in &cc.functions {
let key = (&func.file, &func.function_name, func.line);
let (cognitive_val, cognitive_risk) = cognitive
.as_ref()
.and_then(|ca| {
ca.functions.iter().find(|f| {
&f.file == key.0 && &f.function_name == key.1 && f.line == key.2
})
})
.map(|f| (f.complexity, f.risk_level))
.unwrap_or((0, CognitiveRiskLevel::Low));
let halstead_metrics = halstead.as_ref().and_then(|ha| {
ha.functions.iter().find(|f| {
&f.file == key.0 && &f.function_name == key.1 && f.line == key.2
})
}).map(|f| f.metrics.clone());
let (mi_val, mi_risk) = maintainability
.as_ref()
.and_then(|ma| {
ma.functions.iter().find(|f| {
&f.file == key.0 && &f.function_name == key.1 && f.line == key.2
})
})
.map(|f| (f.index.score, f.index.risk_level))
.unwrap_or((100.0, MaintainabilityRiskLevel::Low));
let (nesting_val, nesting_risk) = nesting
.as_ref()
.and_then(|na| {
na.functions.iter().find(|f| {
&f.file == key.0 && &f.function_name == key.1 && f.line == key.2
})
})
.map(|f| (f.max_depth, f.risk_level))
.unwrap_or((0, NestingDepthLevel::Good));
let (loc, stmts, params, vars, returns, end_line, size_issues) = function_size
.as_ref()
.and_then(|fa| {
fa.functions.iter().find(|f| {
&f.file == key.0 && &f.name == key.1 && f.line == key.2
})
})
.map(|f| {
let issues: Vec<String> = f.issues.iter()
.map(|i| format!("{:?}", i))
.collect();
(f.sloc, f.statements, f.parameters, f.local_variables, f.return_statements, f.end_line, issues)
})
.unwrap_or((0, 0, 0, 0, 0, func.line, Vec::new()));
result.push(FunctionMetrics {
name: func.function_name.clone(),
file: func.file.clone(),
line: func.line,
end_line,
cyclomatic: func.complexity,
cyclomatic_risk: func.risk_level,
cognitive: cognitive_val,
cognitive_risk,
halstead: halstead_metrics,
maintainability: mi_val,
maintainability_risk: mi_risk,
loc,
statements: stmts,
nesting: nesting_val,
nesting_risk,
params,
variables: vars,
returns,
size_issues,
});
}
result
}
fn detect_issues(
functions: &[FunctionMetrics],
cohesion: &Option<super::cohesion::CohesionAnalysis>,
thresholds: &MetricThresholds,
) -> Vec<MetricIssue> {
let mut issues = Vec::new();
for func in functions {
if func.cyclomatic >= thresholds.cyclomatic_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::CyclomaticComplexity,
message: format!(
"Function '{}' has critical cyclomatic complexity ({})",
func.name, func.cyclomatic
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.cyclomatic),
threshold: f64::from(thresholds.cyclomatic_critical),
suggestion: Some("Consider extracting smaller helper functions".to_string()),
});
} else if func.cyclomatic >= thresholds.cyclomatic_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::CyclomaticComplexity,
message: format!(
"Function '{}' has high cyclomatic complexity ({})",
func.name, func.cyclomatic
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.cyclomatic),
threshold: f64::from(thresholds.cyclomatic_warning),
suggestion: Some("Review for potential simplification".to_string()),
});
}
if func.cognitive >= thresholds.cognitive_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::CognitiveComplexity,
message: format!(
"Function '{}' has critical cognitive complexity ({})",
func.name, func.cognitive
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.cognitive),
threshold: f64::from(thresholds.cognitive_critical),
suggestion: Some("Reduce nesting depth and extract helper functions".to_string()),
});
} else if func.cognitive >= thresholds.cognitive_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::CognitiveComplexity,
message: format!(
"Function '{}' has high cognitive complexity ({})",
func.name, func.cognitive
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.cognitive),
threshold: f64::from(thresholds.cognitive_warning),
suggestion: Some("Consider using early returns and extracting logic".to_string()),
});
}
if func.maintainability <= thresholds.maintainability_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Maintainability,
message: format!(
"Function '{}' has critical maintainability index ({:.1})",
func.name, func.maintainability
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: func.maintainability,
threshold: thresholds.maintainability_critical,
suggestion: Some("Major refactoring recommended - reduce complexity and size".to_string()),
});
} else if func.maintainability <= thresholds.maintainability_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Maintainability,
message: format!(
"Function '{}' has low maintainability index ({:.1})",
func.name, func.maintainability
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: func.maintainability,
threshold: thresholds.maintainability_warning,
suggestion: Some("Consider reducing complexity and improving documentation".to_string()),
});
}
if func.loc >= thresholds.loc_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::FunctionLength,
message: format!(
"Function '{}' is critically long ({} lines)",
func.name, func.loc
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.loc),
threshold: f64::from(thresholds.loc_critical),
suggestion: Some("Split into smaller, focused functions".to_string()),
});
} else if func.loc >= thresholds.loc_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::FunctionLength,
message: format!(
"Function '{}' is too long ({} lines)",
func.name, func.loc
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.loc),
threshold: f64::from(thresholds.loc_warning),
suggestion: Some("Consider extracting logical sections into helper functions".to_string()),
});
}
if func.nesting >= thresholds.nesting_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::NestingDepth,
message: format!(
"Function '{}' has critical nesting depth ({})",
func.name, func.nesting
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.nesting),
threshold: f64::from(thresholds.nesting_critical),
suggestion: Some("Use early returns, extract methods, or flatten conditionals".to_string()),
});
} else if func.nesting >= thresholds.nesting_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::NestingDepth,
message: format!(
"Function '{}' has deep nesting ({})",
func.name, func.nesting
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.nesting),
threshold: f64::from(thresholds.nesting_warning),
suggestion: Some("Consider using guard clauses and extracting nested logic".to_string()),
});
}
if func.params >= thresholds.params_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::ParameterCount,
message: format!(
"Function '{}' has too many parameters ({})",
func.name, func.params
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.params),
threshold: f64::from(thresholds.params_critical),
suggestion: Some("Use a parameter object or builder pattern".to_string()),
});
} else if func.params >= thresholds.params_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::ParameterCount,
message: format!(
"Function '{}' has many parameters ({})",
func.name, func.params
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: f64::from(func.params),
threshold: f64::from(thresholds.params_warning),
suggestion: Some("Consider grouping related parameters".to_string()),
});
}
if let Some(ref halstead) = func.halstead {
if halstead.bugs >= thresholds.halstead_bugs_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::EstimatedBugs,
message: format!(
"Function '{}' has high estimated bug count ({:.2})",
func.name, halstead.bugs
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: halstead.bugs,
threshold: thresholds.halstead_bugs_critical,
suggestion: Some("Simplify logic and reduce vocabulary".to_string()),
});
} else if halstead.bugs >= thresholds.halstead_bugs_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::EstimatedBugs,
message: format!(
"Function '{}' may have bugs (estimated: {:.2})",
func.name, halstead.bugs
),
file: func.file.clone(),
line: Some(func.line),
unit_name: func.name.clone(),
value: halstead.bugs,
threshold: thresholds.halstead_bugs_warning,
suggestion: Some("Review carefully and consider simplification".to_string()),
});
}
}
}
if let Some(ref ca) = cohesion {
for class in &ca.classes {
if class.lcom3 >= thresholds.lcom_critical {
issues.push(MetricIssue {
severity: IssueSeverity::Critical,
category: IssueCategory::Cohesion,
message: format!(
"Class '{}' has critically low cohesion (LCOM3={})",
class.class_name, class.lcom3
),
file: class.file.clone(),
line: Some(class.line),
unit_name: class.class_name.clone(),
value: f64::from(class.lcom3),
threshold: f64::from(thresholds.lcom_critical),
suggestion: Some(format!(
"Consider splitting into {} separate classes",
class.lcom3
)),
});
} else if class.lcom3 >= thresholds.lcom_warning {
issues.push(MetricIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Cohesion,
message: format!(
"Class '{}' has low cohesion (LCOM3={})",
class.class_name, class.lcom3
),
file: class.file.clone(),
line: Some(class.line),
unit_name: class.class_name.clone(),
value: f64::from(class.lcom3),
threshold: f64::from(thresholds.lcom_warning),
suggestion: Some("Review class responsibilities for Single Responsibility Principle".to_string()),
});
}
}
}
issues
}
fn build_class_metrics(
cohesion: &Option<super::cohesion::CohesionAnalysis>,
) -> Vec<ClassMetrics> {
let Some(ca) = cohesion else {
return Vec::new();
};
ca.classes
.iter()
.map(|c| ClassMetrics {
name: c.class_name.clone(),
file: c.file.clone(),
line: c.line,
end_line: c.end_line,
method_count: c.methods,
attribute_count: c.attributes,
lcom3: c.lcom3,
lcom4: c.lcom4,
cohesion_level: c.cohesion_level,
is_low_cohesion: c.is_low_cohesion,
avg_method_complexity: 0.0, suggestion: c.suggestion.clone(),
})
.collect()
}
fn build_file_metrics(
complexity: &Option<super::complexity::ComplexityAnalysis>,
cognitive: &Option<super::complexity::CognitiveAnalysis>,
maintainability: &Option<super::complexity::MaintainabilityAnalysis>,
loc: &Option<super::loc::LOCAnalysis>,
issues: &[MetricIssue],
) -> Vec<FileMetrics> {
let mut file_data: HashMap<PathBuf, FileMetrics> = HashMap::new();
if let Some(la) = loc {
for file_loc in &la.files {
let issue_count = issues.iter()
.filter(|i| i.file == file_loc.file)
.count() as u32;
let critical_count = issues.iter()
.filter(|i| i.file == file_loc.file && i.severity == IssueSeverity::Critical)
.count() as u32;
file_data.insert(file_loc.file.clone(), FileMetrics {
path: file_loc.file.clone(),
language: file_loc.language.clone(),
loc: file_loc.metrics.clone(),
function_count: file_loc.functions.len() as u32,
class_count: 0, avg_cyclomatic: 0.0,
max_cyclomatic: 0,
avg_cognitive: 0.0,
max_cognitive: 0,
avg_maintainability: 0.0,
min_maintainability: 100.0,
issue_count,
critical_issue_count: critical_count,
});
}
}
if let Some(ca) = complexity {
for func in &ca.functions {
if let Some(fm) = file_data.get_mut(&func.file) {
fm.max_cyclomatic = fm.max_cyclomatic.max(func.complexity);
}
}
let mut file_cc: HashMap<PathBuf, (u32, u32)> = HashMap::new();
for func in &ca.functions {
let entry = file_cc.entry(func.file.clone()).or_insert((0, 0));
entry.0 += func.complexity;
entry.1 += 1;
}
for (file, (sum, count)) in file_cc {
if let Some(fm) = file_data.get_mut(&file) {
if count > 0 {
fm.avg_cyclomatic = f64::from(sum) / f64::from(count);
}
}
}
}
if let Some(ca) = cognitive {
for func in &ca.functions {
if let Some(fm) = file_data.get_mut(&func.file) {
fm.max_cognitive = fm.max_cognitive.max(func.complexity);
}
}
let mut file_cog: HashMap<PathBuf, (u32, u32)> = HashMap::new();
for func in &ca.functions {
let entry = file_cog.entry(func.file.clone()).or_insert((0, 0));
entry.0 += func.complexity;
entry.1 += 1;
}
for (file, (sum, count)) in file_cog {
if let Some(fm) = file_data.get_mut(&file) {
if count > 0 {
fm.avg_cognitive = f64::from(sum) / f64::from(count);
}
}
}
}
if let Some(ma) = maintainability {
for func in &ma.functions {
if let Some(fm) = file_data.get_mut(&func.file) {
fm.min_maintainability = fm.min_maintainability.min(func.index.score);
}
}
let mut file_mi: HashMap<PathBuf, (f64, u32)> = HashMap::new();
for func in &ma.functions {
let entry = file_mi.entry(func.file.clone()).or_insert((0.0, 0));
entry.0 += func.index.score;
entry.1 += 1;
}
for (file, (sum, count)) in file_mi {
if let Some(fm) = file_data.get_mut(&file) {
if count > 0 {
fm.avg_maintainability = sum / f64::from(count);
}
}
}
}
file_data.into_values().collect()
}
fn build_project_summary(
files: &[FileMetrics],
functions: &[FunctionMetrics],
classes: &[ClassMetrics],
loc: &Option<super::loc::LOCAnalysis>,
halstead: &Option<super::complexity::HalsteadAnalysis>,
issues: &[MetricIssue],
) -> ProjectMetrics {
let total_files = files.len() as u32;
let total_functions = functions.len() as u32;
let total_classes = classes.len() as u32;
let total_loc = loc.as_ref()
.map(|la| la.stats.clone())
.map(|stats| LOCMetrics {
physical: stats.total_physical,
source: stats.total_sloc,
logical: stats.total_logical,
comment: stats.total_comment,
blank: stats.total_blank,
code_to_comment_ratio: stats.code_to_comment_ratio,
})
.unwrap_or_default();
let avg_cyclomatic = if functions.is_empty() {
0.0
} else {
functions.iter().map(|f| f64::from(f.cyclomatic)).sum::<f64>() / functions.len() as f64
};
let avg_cognitive = if functions.is_empty() {
0.0
} else {
functions.iter().map(|f| f64::from(f.cognitive)).sum::<f64>() / functions.len() as f64
};
let avg_maintainability = if functions.is_empty() {
100.0
} else {
functions.iter().map(|f| f.maintainability).sum::<f64>() / functions.len() as f64
};
let avg_nesting = if functions.is_empty() {
0.0
} else {
functions.iter().map(|f| f64::from(f.nesting)).sum::<f64>() / functions.len() as f64
};
let avg_function_size = if functions.is_empty() {
0.0
} else {
functions.iter().map(|f| f64::from(f.loc)).sum::<f64>() / functions.len() as f64
};
let (total_estimated_bugs, total_estimated_hours) = halstead.as_ref()
.map(|ha| (ha.stats.total_bugs, ha.stats.total_time_seconds / 3600.0))
.unwrap_or((0.0, 0.0));
let files_with_critical = files.iter()
.filter(|f| f.critical_issue_count > 0)
.count() as u32;
let complex_functions = functions.iter()
.filter(|f| f.cyclomatic_risk == RiskLevel::High || f.cyclomatic_risk == RiskLevel::Critical)
.count() as u32;
let low_cohesion_classes = classes.iter()
.filter(|c| c.is_low_cohesion)
.count() as u32;
ProjectMetrics {
total_files,
total_functions,
total_classes,
total_loc,
avg_cyclomatic,
avg_cognitive,
avg_maintainability,
avg_nesting,
avg_function_size,
total_estimated_bugs,
total_estimated_hours,
files_with_critical_issues: files_with_critical,
complex_functions,
low_cohesion_classes,
}
}
fn build_issue_stats(issues: &[MetricIssue]) -> IssueStats {
let mut stats = IssueStats {
total: issues.len() as u32,
..Default::default()
};
for issue in issues {
match issue.severity {
IssueSeverity::Info => stats.info += 1,
IssueSeverity::Warning => stats.warnings += 1,
IssueSeverity::Critical => stats.critical += 1,
}
*stats.by_category
.entry(issue.category.to_string())
.or_insert(0) += 1;
}
stats
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionSortBy {
Cyclomatic,
Cognitive,
Maintainability,
Loc,
Nesting,
Params,
Location,
}
impl std::str::FromStr for FunctionSortBy {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"cyclomatic" | "cc" | "complexity" => Ok(Self::Cyclomatic),
"cognitive" | "cog" => Ok(Self::Cognitive),
"maintainability" | "mi" => Ok(Self::Maintainability),
"loc" | "lines" | "sloc" => Ok(Self::Loc),
"nesting" | "depth" => Ok(Self::Nesting),
"params" | "parameters" => Ok(Self::Params),
"location" | "file" => Ok(Self::Location),
_ => Err(format!("Unknown sort field: {}", s)),
}
}
}
pub fn sort_functions(functions: &mut [FunctionMetrics], sort_by: FunctionSortBy) {
match sort_by {
FunctionSortBy::Cyclomatic => {
functions.sort_by(|a, b| b.cyclomatic.cmp(&a.cyclomatic));
}
FunctionSortBy::Cognitive => {
functions.sort_by(|a, b| b.cognitive.cmp(&a.cognitive));
}
FunctionSortBy::Maintainability => {
functions.sort_by(|a, b| {
a.maintainability.partial_cmp(&b.maintainability)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
FunctionSortBy::Loc => {
functions.sort_by(|a, b| b.loc.cmp(&a.loc));
}
FunctionSortBy::Nesting => {
functions.sort_by(|a, b| b.nesting.cmp(&a.nesting));
}
FunctionSortBy::Params => {
functions.sort_by(|a, b| b.params.cmp(&a.params));
}
FunctionSortBy::Location => {
functions.sort_by(|a, b| {
a.file.cmp(&b.file).then_with(|| a.line.cmp(&b.line))
});
}
}
}
pub fn format_functions_csv(functions: &[FunctionMetrics]) -> String {
let mut output = String::with_capacity(functions.len() * 256);
output.push_str("name,file,line,end_line,cyclomatic,cyclomatic_risk,cognitive,");
output.push_str("cognitive_risk,maintainability,maintainability_risk,loc,statements,");
output.push_str("nesting,nesting_risk,params,variables,returns,");
output.push_str("halstead_volume,halstead_difficulty,halstead_bugs\n");
for func in functions {
let (volume, difficulty, bugs) = func.halstead.as_ref()
.map(|h| (h.volume, h.difficulty, h.bugs))
.unwrap_or((0.0, 0.0, 0.0));
let name = escape_csv_field(&func.name);
let file = escape_csv_field(&func.file.display().to_string());
output.push_str(&format!(
"{},{},{},{},{},{},{},{},{:.2},{},{},{},{},{},{},{},{},{:.2},{:.2},{:.4}\n",
name,
file,
func.line,
func.end_line,
func.cyclomatic,
func.cyclomatic_risk,
func.cognitive,
func.cognitive_risk,
func.maintainability,
func.maintainability_risk,
func.loc,
func.statements,
func.nesting,
func.nesting_risk,
func.params,
func.variables,
func.returns,
volume,
difficulty,
bugs,
));
}
output
}
pub fn format_issues_csv(issues: &[MetricIssue]) -> String {
let mut output = String::with_capacity(issues.len() * 128);
output.push_str("severity,category,file,line,unit_name,value,threshold,message,suggestion\n");
for issue in issues {
let file = escape_csv_field(&issue.file.display().to_string());
let unit = escape_csv_field(&issue.unit_name);
let msg = escape_csv_field(&issue.message);
let suggestion = issue.suggestion.as_ref()
.map(|s| escape_csv_field(s))
.unwrap_or_default();
output.push_str(&format!(
"{},{},{},{},{},{:.2},{:.2},{},{}\n",
issue.severity,
issue.category,
file,
issue.line.map(|l| l.to_string()).unwrap_or_default(),
unit,
issue.value,
issue.threshold,
msg,
suggestion,
));
}
output
}
pub fn format_classes_csv(classes: &[ClassMetrics]) -> String {
let mut output = String::with_capacity(classes.len() * 128);
output.push_str("name,file,line,end_line,method_count,attribute_count,lcom3,lcom4,");
output.push_str("cohesion_level,is_low_cohesion,avg_method_complexity\n");
for class in classes {
let name = escape_csv_field(&class.name);
let file = escape_csv_field(&class.file.display().to_string());
output.push_str(&format!(
"{},{},{},{},{},{},{},{},{},{},{:.2}\n",
name,
file,
class.line,
class.end_line,
class.method_count,
class.attribute_count,
class.lcom3,
class.lcom4,
class.cohesion_level,
class.is_low_cohesion,
class.avg_method_complexity,
));
}
output
}
pub fn format_files_csv(files: &[FileMetrics]) -> String {
let mut output = String::with_capacity(files.len() * 128);
output.push_str("path,language,physical_loc,source_loc,comment_loc,blank_loc,");
output.push_str("function_count,class_count,avg_cyclomatic,max_cyclomatic,");
output.push_str("avg_cognitive,max_cognitive,avg_maintainability,min_maintainability,");
output.push_str("issue_count,critical_issue_count\n");
for file in files {
let path = escape_csv_field(&file.path.display().to_string());
let lang = file.language.as_deref().unwrap_or("");
output.push_str(&format!(
"{},{},{},{},{},{},{},{},{:.2},{},{:.2},{},{:.2},{:.2},{},{}\n",
path,
lang,
file.loc.physical,
file.loc.source,
file.loc.comment,
file.loc.blank,
file.function_count,
file.class_count,
file.avg_cyclomatic,
file.max_cyclomatic,
file.avg_cognitive,
file.max_cognitive,
file.avg_maintainability,
file.min_maintainability,
file.issue_count,
file.critical_issue_count,
));
}
output
}
fn escape_csv_field(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
pub type ProgressCallback = Box<dyn Fn(usize, usize, &str) + Send + Sync>;
pub fn analyze_all_metrics_with_progress<P: AsRef<Path>>(
path: P,
lang: Option<&str>,
config: &MetricsConfig,
progress: Option<ProgressCallback>,
) -> Result<MetricsReport> {
let _ = progress; analyze_all_metrics(path, lang, config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_thresholds_default() {
let thresholds = MetricThresholds::default();
assert_eq!(thresholds.cyclomatic_warning, 10);
assert_eq!(thresholds.cyclomatic_critical, 20);
assert_eq!(thresholds.cognitive_warning, 15);
assert_eq!(thresholds.cognitive_critical, 30);
}
#[test]
fn test_metric_thresholds_strict() {
let thresholds = MetricThresholds::strict();
assert_eq!(thresholds.cyclomatic_warning, 5);
assert_eq!(thresholds.cyclomatic_critical, 10);
}
#[test]
fn test_quality_gate_check() {
let gate = QualityGate::default();
let report = MetricsReport {
path: PathBuf::from("."),
language: Some("rust".to_string()),
analysis_duration_ms: 100,
config: MetricsConfig::default(),
project_summary: ProjectMetrics {
total_files: 1,
total_functions: 1,
total_classes: 0,
total_loc: LOCMetrics::default(),
avg_cyclomatic: 5.0,
avg_cognitive: 3.0,
avg_maintainability: 70.0,
avg_nesting: 2.0,
avg_function_size: 20.0,
total_estimated_bugs: 0.1,
total_estimated_hours: 1.0,
files_with_critical_issues: 0,
complex_functions: 0,
low_cohesion_classes: 0,
},
file_metrics: vec![],
function_metrics: vec![],
class_metrics: vec![],
issues: vec![],
issue_stats: IssueStats::default(),
};
let result = gate.check(&report);
assert!(result.passed);
assert!(!result.failed);
}
#[test]
fn test_issue_severity_ordering() {
assert!(IssueSeverity::Critical > IssueSeverity::Warning);
assert!(IssueSeverity::Warning > IssueSeverity::Info);
}
#[test]
fn test_function_sort_by_parse() {
assert_eq!(
"cyclomatic".parse::<FunctionSortBy>().unwrap(),
FunctionSortBy::Cyclomatic
);
assert_eq!(
"cc".parse::<FunctionSortBy>().unwrap(),
FunctionSortBy::Cyclomatic
);
assert_eq!(
"mi".parse::<FunctionSortBy>().unwrap(),
FunctionSortBy::Maintainability
);
}
#[test]
fn test_metric_thresholds_from_toml() {
let toml_content = r#"
[metrics.thresholds]
cyclomatic_warning = 8
cyclomatic_critical = 15
cognitive_warning = 12
maintainability_warning = 50.0
"#;
let thresholds = MetricThresholds::from_toml(toml_content).unwrap();
assert_eq!(thresholds.cyclomatic_warning, 8);
assert_eq!(thresholds.cyclomatic_critical, 15);
assert_eq!(thresholds.cognitive_warning, 12);
assert_eq!(thresholds.cognitive_critical, 30);
assert!((thresholds.maintainability_warning - 50.0).abs() < 0.01);
}
#[test]
fn test_metric_thresholds_from_toml_invalid() {
let invalid_toml = "this is not valid [toml";
let result = MetricThresholds::from_toml(invalid_toml);
assert!(result.is_err());
}
#[test]
fn test_metric_thresholds_from_toml_empty() {
let empty_toml = "";
let thresholds = MetricThresholds::from_toml(empty_toml).unwrap();
assert_eq!(thresholds.cyclomatic_warning, 10);
assert_eq!(thresholds.cyclomatic_critical, 20);
}
#[test]
fn test_escape_csv_field() {
assert_eq!(escape_csv_field("simple"), "simple");
assert_eq!(escape_csv_field("with,comma"), "\"with,comma\"");
assert_eq!(escape_csv_field("with\"quote"), "\"with\"\"quote\"");
assert_eq!(escape_csv_field("with\nnewline"), "\"with\nnewline\"");
}
#[test]
fn test_format_functions_csv_header() {
let functions: Vec<FunctionMetrics> = vec![];
let csv = format_functions_csv(&functions);
assert!(csv.starts_with("name,file,line,end_line,"));
assert!(csv.contains("cyclomatic,"));
assert!(csv.contains("halstead_bugs"));
}
#[test]
fn test_format_issues_csv_header() {
let issues: Vec<MetricIssue> = vec![];
let csv = format_issues_csv(&issues);
assert!(csv.starts_with("severity,category,file,"));
assert!(csv.contains("suggestion"));
}
}