use super::types::{
ComplexityLevel, FunctionComplexityDetail, FunctionInfo, QualityMetrics, StructureInfo,
};
use crate::core::types::{CodeStats, FileStats};
pub struct QualityCalculator;
impl QualityCalculator {
pub fn new() -> Self {
Self
}
pub fn calculate_quality_metrics(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
_structures: &[StructureInfo],
) -> QualityMetrics {
let code_health_score = self.calculate_code_health_score(functions, file_stats);
let maintainability_index = self.calculate_maintainability_index(functions, file_stats);
let documentation_coverage = self.calculate_documentation_coverage(file_stats);
let avg_complexity = self.calculate_average_complexity(functions);
let function_size_health = self.calculate_function_size_health(functions, file_stats);
let nesting_depth_health = self.calculate_nesting_depth_health(functions, file_stats);
let code_duplication_ratio = self.estimate_code_duplication(file_stats);
let technical_debt_ratio = self.calculate_technical_debt_ratio(functions, file_stats);
QualityMetrics {
code_health_score,
maintainability_index,
documentation_coverage,
avg_complexity,
function_size_health,
nesting_depth_health,
code_duplication_ratio,
technical_debt_ratio,
}
}
fn calculate_code_health_score(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
) -> f64 {
let maintainability = self.calculate_maintainability_index(functions, file_stats);
let documentation = self.calculate_documentation_coverage(file_stats);
let complexity = 100.0 - (self.calculate_average_complexity(functions) * 10.0).min(100.0); let function_size = self.calculate_function_size_health(functions, file_stats);
let nesting_depth = self.calculate_nesting_depth_health(functions, file_stats);
(maintainability * 0.3
+ documentation * 0.2
+ complexity * 0.25
+ function_size * 0.15
+ nesting_depth * 0.1)
.clamp(0.0, 100.0)
}
fn calculate_maintainability_index(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
) -> f64 {
if functions.is_empty() {
let mut score = 85.0;
let file_length_penalty = if file_stats.total_lines > 500 {
if file_stats.total_lines > 2000 {
((file_stats.total_lines - 500) as f64 / 100.0).min(40.0)
} else if file_stats.total_lines > 1000 {
((file_stats.total_lines - 500) as f64 / 150.0).min(25.0)
} else {
((file_stats.total_lines - 500) as f64 / 200.0).min(15.0)
}
} else {
0.0
};
score -= file_length_penalty;
let doc_ratio = (file_stats.comment_lines + file_stats.doc_lines) as f64
/ file_stats.code_lines.max(1) as f64;
if doc_ratio > 0.2 {
score += 10.0;
} else if doc_ratio < 0.05 {
score -= 15.0;
}
if file_stats.code_lines < 10 {
score -= 20.0;
}
return score.clamp(0.0, 100.0);
}
let mut total_score = 0.0;
for func in functions {
let length_score = (50.0 - func.line_count as f64).max(0.0);
let cyclomatic_score = (30.0 - func.cyclomatic_complexity as f64 * 2.0).max(0.0);
let cognitive_score = (30.0 - func.cognitive_complexity as f64 * 2.0).max(0.0);
let param_score = (20.0 - func.parameter_count as f64 * 3.0).max(0.0);
total_score += length_score + cyclomatic_score + cognitive_score + param_score;
}
let base_score = (total_score / functions.len() as f64).clamp(0.0, 100.0);
let file_length_penalty = if file_stats.total_lines > 500 {
if file_stats.total_lines > 2000 {
((file_stats.total_lines - 500) as f64 / 100.0).min(40.0)
} else if file_stats.total_lines > 1000 {
((file_stats.total_lines - 500) as f64 / 150.0).min(25.0)
} else {
((file_stats.total_lines - 500) as f64 / 200.0).min(15.0)
}
} else {
0.0
};
(base_score - file_length_penalty).max(0.0)
}
fn estimate_code_duplication(&self, file_stats: &FileStats) -> f64 {
let mut duplication_score: f64 = 0.0;
if file_stats.total_lines > 2000 {
duplication_score += 12.0; } else if file_stats.total_lines > 1000 {
duplication_score += 8.0;
} else if file_stats.total_lines > 500 {
duplication_score += 5.0;
} else {
duplication_score += 2.0; }
let code_density = file_stats.code_lines as f64 / file_stats.total_lines.max(1) as f64;
if code_density > 0.8 {
duplication_score += 3.0; }
let comment_ratio = (file_stats.comment_lines + file_stats.doc_lines) as f64
/ file_stats.code_lines.max(1) as f64;
if comment_ratio > 0.2 {
duplication_score -= 2.0;
} else if comment_ratio < 0.05 {
duplication_score += 2.0;
}
duplication_score.clamp(0.0, 25.0) }
pub fn calculate_project_quality_metrics(
&self,
functions: &[FunctionInfo],
code_stats: &CodeStats,
_structures: &[StructureInfo],
) -> QualityMetrics {
let project_file_stats = FileStats {
total_lines: code_stats.total_lines,
code_lines: code_stats.total_code_lines,
comment_lines: code_stats.total_comment_lines,
doc_lines: code_stats.total_doc_lines,
blank_lines: code_stats.total_blank_lines,
file_size: code_stats.total_size,
};
let code_health_score = self.calculate_code_health_score(functions, &project_file_stats);
let maintainability_index =
self.calculate_maintainability_index(functions, &project_file_stats);
let documentation_coverage = self.calculate_documentation_coverage(&project_file_stats);
let avg_complexity = self.calculate_average_complexity(functions);
let function_size_health =
self.calculate_function_size_health(functions, &project_file_stats);
let nesting_depth_health =
self.calculate_nesting_depth_health(functions, &project_file_stats);
let code_duplication_ratio = self.estimate_project_code_duplication(code_stats);
let technical_debt_ratio =
self.calculate_technical_debt_ratio(functions, &project_file_stats);
QualityMetrics {
code_health_score,
maintainability_index,
documentation_coverage,
avg_complexity,
function_size_health,
nesting_depth_health,
code_duplication_ratio,
technical_debt_ratio,
}
}
fn estimate_project_code_duplication(&self, code_stats: &CodeStats) -> f64 {
let total_lines = code_stats.total_lines;
let ratio = if total_lines > 10000 {
0.20 } else if total_lines > 5000 {
0.15 } else if total_lines > 1000 {
0.10 } else {
0.05 };
ratio * 100.0 }
pub fn classify_complexity_level(&self, complexity: usize) -> ComplexityLevel {
match complexity {
1..=5 => ComplexityLevel::VeryLow,
6..=10 => ComplexityLevel::Low,
11..=20 => ComplexityLevel::Medium,
21..=50 => ComplexityLevel::High,
_ => ComplexityLevel::VeryHigh,
}
}
pub fn identify_maintainability_concerns(&self, func: &FunctionInfo) -> Vec<String> {
let mut concerns = Vec::new();
if func.line_count > 50 {
concerns.push("Function is too long (>50 lines)".to_string());
}
if func.cyclomatic_complexity > 10 {
concerns.push("High cyclomatic complexity".to_string());
}
if func.cognitive_complexity > 15 {
concerns.push("High cognitive complexity".to_string());
}
if func.parameter_count > 5 {
concerns.push("Too many parameters".to_string());
}
if func.nesting_depth > 4 {
concerns.push("Deep nesting detected".to_string());
}
if func.has_recursion {
concerns.push("Contains recursion".to_string());
}
if func.return_path_count > 5 {
concerns.push("Multiple return paths".to_string());
}
concerns
}
pub fn create_function_complexity_details(
&self,
functions: &[FunctionInfo],
file_path: &str,
) -> Vec<FunctionComplexityDetail> {
functions
.iter()
.map(|func| {
let complexity_level = self.classify_complexity_level(func.cyclomatic_complexity);
let maintainability_concerns = self.identify_maintainability_concerns(func);
FunctionComplexityDetail {
name: func.name.clone(),
file_path: file_path.to_string(),
start_line: func.start_line,
end_line: func.end_line,
line_count: func.line_count,
cyclomatic_complexity: func.cyclomatic_complexity,
cognitive_complexity: func.cognitive_complexity,
parameter_count: func.parameter_count,
return_path_count: func.return_path_count,
nesting_depth: func.nesting_depth,
is_method: func.is_method,
parent_class: func.parent_class.clone(),
local_variable_count: 0, has_recursion: func.has_recursion,
has_exception_handling: func.has_exception_handling,
complexity_level,
maintainability_concerns,
}
})
.collect()
}
fn calculate_documentation_coverage(&self, file_stats: &FileStats) -> f64 {
if file_stats.code_lines == 0 {
return 0.0;
}
let documentation_lines = file_stats.comment_lines + file_stats.doc_lines;
let coverage = (documentation_lines as f64 / file_stats.code_lines as f64) * 100.0;
(coverage * 5.0).min(100.0)
}
fn calculate_average_complexity(&self, functions: &[FunctionInfo]) -> f64 {
if functions.is_empty() {
return 0.0;
}
let total_complexity: usize = functions.iter().map(|f| f.cyclomatic_complexity).sum();
total_complexity as f64 / functions.len() as f64
}
fn calculate_function_size_health(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
) -> f64 {
if functions.is_empty() {
let mut score = 75.0;
let avg_lines_per_unit =
file_stats.code_lines as f64 / (file_stats.code_lines / 20).max(1) as f64;
if avg_lines_per_unit > 50.0 {
score -= (avg_lines_per_unit - 50.0) * 0.5;
} else if avg_lines_per_unit < 5.0 {
score -= (5.0 - avg_lines_per_unit) * 2.0;
}
if file_stats.total_lines > 500 {
score -= ((file_stats.total_lines - 500) as f64 / 100.0).min(25.0);
}
return score.clamp(0.0, 100.0);
}
let mut score = 100.0;
let avg_length =
functions.iter().map(|f| f.line_count).sum::<usize>() as f64 / functions.len() as f64;
if avg_length > 20.0 {
score -= (avg_length - 20.0) * 2.0;
}
for func in functions {
if func.line_count > 100 {
score -= 10.0;
} else if func.line_count > 50 {
score -= 5.0;
}
}
score.clamp(0.0, 100.0)
}
fn calculate_nesting_depth_health(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
) -> f64 {
if functions.is_empty() {
let mut score: f64 = 80.0;
let brace_density =
file_stats.code_lines as f64 / (file_stats.total_lines.max(1) as f64);
if brace_density > 0.8 {
score -= 20.0; } else if brace_density > 0.6 {
score -= 10.0; }
if file_stats.total_lines > 1000 {
score -= 15.0;
}
return score.clamp(0.0, 100.0);
}
let mut score = 100.0;
let avg_nesting = functions.iter().map(|f| f.nesting_depth).sum::<usize>() as f64
/ functions.len() as f64;
if avg_nesting > 3.0 {
score -= (avg_nesting - 3.0) * 15.0;
}
for func in functions {
if func.nesting_depth > 8 {
score -= 15.0;
} else if func.nesting_depth > 5 {
score -= 10.0;
}
}
score.clamp(0.0, 100.0)
}
fn calculate_technical_debt_ratio(
&self,
functions: &[FunctionInfo],
file_stats: &FileStats,
) -> f64 {
if functions.is_empty() {
return 0.0;
}
let mut debt_score = 0.0;
for func in functions {
if func.cyclomatic_complexity > 20 {
debt_score += 20.0;
} else if func.cyclomatic_complexity > 10 {
debt_score += 10.0;
} else if func.cyclomatic_complexity > 5 {
debt_score += 5.0;
}
}
for func in functions {
if func.line_count > 100 {
debt_score += 15.0;
} else if func.line_count > 50 {
debt_score += 10.0;
}
}
let doc_coverage = self.calculate_documentation_coverage(file_stats);
if doc_coverage < 20.0 {
debt_score += 30.0 - doc_coverage;
}
for func in functions {
if func.nesting_depth > 5 {
debt_score += (func.nesting_depth - 5) as f64 * 5.0;
}
}
let max_possible_debt = functions.len() as f64 * 50.0; (debt_score / max_possible_debt.max(1.0) * 100.0).min(100.0)
}
}
impl Default for QualityCalculator {
fn default() -> Self {
Self::new()
}
}