use crate::complexity::entropy_core::{EntropyConfig, EntropyScore, UniversalEntropyCalculator};
use crate::complexity::EntropyAnalysis;
use crate::core::FunctionMetrics;
use crate::priority::{TransitiveCoverage, UnifiedDebtItem};
use crate::risk::context::ContextualRisk;
use crate::risk::lcov::LcovData;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
pub const FUNCTION_COMPLEXITY_THRESHOLD: u32 = 15;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComplexityDistribution {
Concentrated,
Mixed,
Distributed,
}
impl ComplexityDistribution {
pub fn display_name(&self) -> &'static str {
match self {
Self::Concentrated => "Concentrated",
Self::Mixed => "Mixed",
Self::Distributed => "Distributed",
}
}
pub fn classification_explanation(&self) -> &'static str {
match self {
Self::Concentrated => "Contains god function(s) - refactoring recommended",
Self::Mixed => "Some complexity concentration - review recommended",
Self::Distributed => "Well-Structured File - complexity evenly distributed",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionMetrics {
pub function_count: usize,
pub max_complexity: u32,
pub avg_complexity: f64,
pub median_complexity: u32,
pub exceeding_threshold: usize,
pub distribution: ComplexityDistribution,
pub production_loc: usize,
pub test_loc: usize,
}
#[derive(Debug, Clone)]
pub struct GodObjectAggregatedMetrics {
pub total_cyclomatic: u32,
pub total_cognitive: u32,
pub max_nesting_depth: u32,
pub weighted_coverage: Option<TransitiveCoverage>,
pub unique_upstream_callers: Vec<String>,
pub unique_downstream_callees: Vec<String>,
pub upstream_dependencies: usize,
pub downstream_dependencies: usize,
pub aggregated_contextual_risk: Option<ContextualRisk>,
pub total_error_swallowing_count: u32,
pub error_swallowing_patterns: Vec<String>,
pub aggregated_entropy: Option<EntropyAnalysis>,
pub distribution_metrics: Option<DistributionMetrics>,
}
#[inline]
pub fn extract_member_functions<'a>(
items: impl Iterator<Item = &'a UnifiedDebtItem>,
file_path: &Path,
) -> Vec<&'a UnifiedDebtItem> {
items
.filter(|item| item.location.file == file_path)
.collect()
}
pub fn aggregate_complexity_metrics(members: &[&UnifiedDebtItem]) -> (u32, u32, u32) {
let total_cyclomatic = members.iter().map(|m| m.cyclomatic_complexity).sum();
let total_cognitive = members.iter().map(|m| m.cognitive_complexity).sum();
let max_nesting = members.iter().map(|m| m.nesting_depth).max().unwrap_or(0);
(total_cyclomatic, total_cognitive, max_nesting)
}
pub fn aggregate_coverage_metrics(members: &[&UnifiedDebtItem]) -> Option<TransitiveCoverage> {
let coverages: Vec<_> = members
.iter()
.filter_map(|m| {
m.transitive_coverage
.as_ref()
.map(|c| (c, m.function_length))
})
.collect();
if coverages.is_empty() {
return None;
}
let total_length: usize = coverages.iter().map(|(_, len)| len).sum();
if total_length == 0 {
return None;
}
let weighted_direct = coverages
.iter()
.map(|(cov, len)| cov.direct * (*len as f64))
.sum::<f64>()
/ total_length as f64;
let weighted_transitive = coverages
.iter()
.map(|(cov, len)| cov.transitive * (*len as f64))
.sum::<f64>()
/ total_length as f64;
let uncovered_lines: Vec<usize> = coverages
.iter()
.flat_map(|(cov, _)| &cov.uncovered_lines)
.copied()
.collect::<HashSet<_>>()
.into_iter()
.collect();
let propagated_from = coverages
.iter()
.flat_map(|(cov, _)| &cov.propagated_from)
.cloned()
.collect::<HashSet<_>>()
.into_iter()
.collect();
Some(TransitiveCoverage {
direct: weighted_direct,
transitive: weighted_transitive,
propagated_from,
uncovered_lines,
})
}
pub fn calculate_median(values: &[u32]) -> u32 {
if values.is_empty() {
return 0;
}
let mut sorted: Vec<u32> = values.to_vec();
sorted.sort_unstable();
let mid = sorted.len() / 2;
if sorted.len() % 2 == 0 {
(sorted[mid - 1] + sorted[mid]) / 2
} else {
sorted[mid]
}
}
pub fn classify_distribution(max_complexity: u32, total_complexity: u32) -> ComplexityDistribution {
if total_complexity == 0 {
return ComplexityDistribution::Distributed;
}
let ratio = max_complexity as f64 / total_complexity as f64;
if ratio > 0.5 {
ComplexityDistribution::Concentrated
} else if ratio > 0.2 {
ComplexityDistribution::Mixed
} else {
ComplexityDistribution::Distributed
}
}
pub fn aggregate_distribution_metrics(members: &[&UnifiedDebtItem]) -> DistributionMetrics {
let complexities: Vec<u32> = members.iter().map(|m| m.cyclomatic_complexity).collect();
let total: u32 = complexities.iter().sum();
let max = complexities.iter().max().copied().unwrap_or(0);
let count = complexities.len();
let avg = if count > 0 {
total as f64 / count as f64
} else {
0.0
};
let median = calculate_median(&complexities);
let exceeding = complexities
.iter()
.filter(|&&c| c > FUNCTION_COMPLEXITY_THRESHOLD)
.count();
let distribution = classify_distribution(max, total);
let production_loc: usize = members.iter().map(|m| m.function_length).sum();
DistributionMetrics {
function_count: count,
max_complexity: max,
avg_complexity: avg,
median_complexity: median,
exceeding_threshold: exceeding,
distribution,
production_loc,
test_loc: 0, }
}
pub fn aggregate_distribution_metrics_from_raw(
functions: &[FunctionMetrics],
) -> DistributionMetrics {
let production_functions: Vec<_> = functions.iter().filter(|f| !f.is_test).collect();
let test_functions: Vec<_> = functions.iter().filter(|f| f.is_test).collect();
let complexities: Vec<u32> = production_functions.iter().map(|f| f.cyclomatic).collect();
let total: u32 = complexities.iter().sum();
let max = complexities.iter().max().copied().unwrap_or(0);
let count = complexities.len();
let avg = if count > 0 {
total as f64 / count as f64
} else {
0.0
};
let median = calculate_median(&complexities);
let exceeding = complexities
.iter()
.filter(|&&c| c > FUNCTION_COMPLEXITY_THRESHOLD)
.count();
let distribution = classify_distribution(max, total);
let production_loc: usize = production_functions.iter().map(|f| f.length).sum();
let test_loc: usize = test_functions.iter().map(|f| f.length).sum();
DistributionMetrics {
function_count: count,
max_complexity: max,
avg_complexity: avg,
median_complexity: median,
exceeding_threshold: exceeding,
distribution,
production_loc,
test_loc,
}
}
pub fn aggregate_dependency_metrics(
members: &[&UnifiedDebtItem],
) -> (Vec<String>, Vec<String>, usize, usize) {
let mut unique_callers: HashSet<String> = HashSet::new();
let mut unique_callees: HashSet<String> = HashSet::new();
for item in members {
unique_callers.extend(item.upstream_callers.iter().cloned());
unique_callees.extend(item.downstream_callees.iter().cloned());
}
let upstream_count = unique_callers.len();
let downstream_count = unique_callees.len();
(
unique_callers.into_iter().collect(),
unique_callees.into_iter().collect(),
upstream_count,
downstream_count,
)
}
pub fn aggregate_contextual_risk(members: &[&UnifiedDebtItem]) -> Option<ContextualRisk> {
let risks: Vec<_> = members
.iter()
.filter_map(|m| m.contextual_risk.as_ref())
.collect();
if risks.is_empty() {
return None;
}
let avg_base_risk = risks.iter().map(|r| r.base_risk).sum::<f64>() / risks.len() as f64;
let avg_contextual_risk =
risks.iter().map(|r| r.contextual_risk).sum::<f64>() / risks.len() as f64;
let all_contexts: Vec<_> = risks.iter().flat_map(|r| &r.contexts).cloned().collect();
let explanation = format!(
"Aggregated from {} functions (avg base: {:.1}, avg contextual: {:.1})",
risks.len(),
avg_base_risk,
avg_contextual_risk
);
Some(ContextualRisk {
base_risk: avg_base_risk,
contextual_risk: avg_contextual_risk,
contexts: all_contexts,
explanation,
})
}
pub fn aggregate_error_swallowing(functions: &[FunctionMetrics]) -> (u32, Vec<String>) {
let total_count = functions
.iter()
.filter_map(|f| f.error_swallowing_count)
.sum();
let mut unique_patterns: HashSet<String> = HashSet::new();
for func in functions {
if let Some(ref patterns) = func.error_swallowing_patterns {
unique_patterns.extend(patterns.iter().cloned());
}
}
(total_count, unique_patterns.into_iter().collect())
}
pub fn aggregate_dependency_metrics_from_raw(
functions: &[FunctionMetrics],
) -> (Vec<String>, Vec<String>, usize, usize) {
let mut unique_callers: HashSet<String> = HashSet::new();
let mut unique_callees: HashSet<String> = HashSet::new();
for func in functions {
if let Some(ref callers) = func.upstream_callers {
unique_callers.extend(callers.iter().cloned());
}
if let Some(ref callees) = func.downstream_callees {
unique_callees.extend(callees.iter().cloned());
}
}
let upstream_count = unique_callers.len();
let downstream_count = unique_callees.len();
(
unique_callers.into_iter().collect(),
unique_callees.into_iter().collect(),
upstream_count,
downstream_count,
)
}
fn compute_weighted_average<I>(iter: I) -> Option<f64>
where
I: Iterator<Item = (f64, usize)>,
{
let (sum, total_weight) = iter.fold((0.0, 0usize), |(sum, total), (value, weight)| {
(sum + value * (weight as f64), total + weight)
});
(total_weight > 0).then(|| sum / total_weight as f64)
}
fn sum_complexity(entropy_data: &[(&EntropyAnalysis, usize)]) -> (u32, u32) {
entropy_data.iter().fold((0, 0), |(orig, adj), (e, _)| {
(orig + e.original_complexity, adj + e.adjusted_complexity)
})
}
fn aggregate_reasoning(entropy_data: &[(&EntropyAnalysis, usize)]) -> Vec<String> {
let mut reasoning: Vec<String> = entropy_data
.iter()
.flat_map(|(e, _)| e.reasoning.iter().cloned())
.collect();
reasoning.dedup();
reasoning.truncate(5);
reasoning
}
pub fn aggregate_entropy_metrics(members: &[&UnifiedDebtItem]) -> Option<EntropyAnalysis> {
let entropy_data: Vec<_> = members
.iter()
.filter_map(|m| m.entropy_analysis.as_ref().map(|e| (e, m.function_length)))
.collect();
if entropy_data.is_empty() {
return None;
}
let weighted_entropy =
compute_weighted_average(entropy_data.iter().map(|(e, len)| (e.entropy_score, *len)))?;
let weighted_repetition = compute_weighted_average(
entropy_data
.iter()
.map(|(e, len)| (e.pattern_repetition, *len)),
)?;
let weighted_branch_similarity = compute_weighted_average(
entropy_data
.iter()
.map(|(e, len)| (e.branch_similarity, *len)),
)?;
let weighted_dampening = compute_weighted_average(
entropy_data
.iter()
.map(|(e, len)| (e.dampening_factor, *len)),
)?;
let (total_original, total_adjusted) = sum_complexity(&entropy_data);
let reasoning = aggregate_reasoning(&entropy_data);
Some(EntropyAnalysis {
entropy_score: weighted_entropy,
pattern_repetition: weighted_repetition,
branch_similarity: weighted_branch_similarity,
dampening_factor: weighted_dampening,
dampening_was_applied: weighted_dampening < 1.0,
original_complexity: total_original,
adjusted_complexity: total_adjusted,
reasoning,
})
}
pub fn aggregate_god_object_metrics(members: &[&UnifiedDebtItem]) -> GodObjectAggregatedMetrics {
let (total_cyc, total_cog, max_nest) = aggregate_complexity_metrics(members);
let weighted_cov = aggregate_coverage_metrics(members);
let (callers, callees, up_count, down_count) = aggregate_dependency_metrics(members);
let contextual_risk = aggregate_contextual_risk(members);
let entropy = aggregate_entropy_metrics(members);
let distribution = aggregate_distribution_metrics(members);
GodObjectAggregatedMetrics {
total_cyclomatic: total_cyc,
total_cognitive: total_cog,
max_nesting_depth: max_nest,
weighted_coverage: weighted_cov,
unique_upstream_callers: callers,
unique_downstream_callees: callees,
upstream_dependencies: up_count,
downstream_dependencies: down_count,
aggregated_contextual_risk: contextual_risk,
total_error_swallowing_count: 0,
error_swallowing_patterns: Vec::new(),
aggregated_entropy: entropy,
distribution_metrics: Some(distribution),
}
}
fn extract_entropy_data(functions: &[FunctionMetrics]) -> Vec<(&EntropyScore, usize, u32)> {
functions
.iter()
.filter_map(|f| f.entropy_score.as_ref().map(|e| (e, f.length, f.cognitive)))
.collect()
}
fn weighted_average<F>(data: &[(&EntropyScore, usize, u32)], total_length: usize, f: F) -> f64
where
F: Fn(&EntropyScore) -> f64,
{
data.iter()
.map(|(e, len, _)| f(e) * (*len as f64))
.sum::<f64>()
/ total_length as f64
}
fn sum_cognitive(data: &[(&EntropyScore, usize, u32)]) -> u32 {
data.iter().map(|(_, _, cog)| cog).sum()
}
fn total_length(data: &[(&EntropyScore, usize, u32)]) -> usize {
data.iter().map(|(_, len, _)| *len).sum()
}
pub fn aggregate_entropy_from_raw(functions: &[FunctionMetrics]) -> Option<EntropyAnalysis> {
let data = extract_entropy_data(functions);
let len = total_length(&data);
if data.is_empty() || len == 0 {
return None;
}
let entropy = weighted_average(&data, len, |e| e.token_entropy);
let repetition = weighted_average(&data, len, |e| e.pattern_repetition);
let branch_similarity = weighted_average(&data, len, |e| e.branch_similarity);
let total_cognitive = sum_cognitive(&data);
let calculator = UniversalEntropyCalculator::new(EntropyConfig::default());
let dampening_factor = calculator.calculate_dampening_factor(entropy, repetition);
let adjusted_complexity = (total_cognitive as f64 * dampening_factor) as u32;
Some(EntropyAnalysis {
entropy_score: entropy,
pattern_repetition: repetition,
branch_similarity,
dampening_factor,
dampening_was_applied: dampening_factor < 1.0,
original_complexity: total_cognitive,
adjusted_complexity,
reasoning: vec![format!("Aggregated from {} functions", functions.len())],
})
}
pub fn aggregate_coverage_from_raw_metrics(
functions: &[FunctionMetrics],
coverage: &LcovData,
) -> Option<TransitiveCoverage> {
if functions.is_empty() {
return None;
}
let mut coverage_data: Vec<(f64, usize, Vec<usize>)> = Vec::with_capacity(functions.len());
for func in functions {
let end_line = func.line + func.length.saturating_sub(1);
let direct_coverage = coverage
.get_function_coverage_with_bounds(&func.file, &func.name, func.line, end_line)
.unwrap_or(0.0);
let uncovered = coverage
.get_function_uncovered_lines(&func.file, &func.name, func.line)
.unwrap_or_default();
coverage_data.push((direct_coverage, func.length, uncovered));
}
let total_length: usize = coverage_data.iter().map(|(_, len, _)| len).sum();
if total_length == 0 {
return None;
}
let weighted_direct = coverage_data
.iter()
.map(|(cov, len, _)| cov * (*len as f64))
.sum::<f64>()
/ total_length as f64;
let all_uncovered: Vec<usize> = coverage_data
.iter()
.flat_map(|(_, _, uncovered)| uncovered.iter().copied())
.collect::<HashSet<_>>()
.into_iter()
.collect();
Some(TransitiveCoverage {
direct: weighted_direct,
transitive: weighted_direct, propagated_from: vec![],
uncovered_lines: all_uncovered,
})
}
pub fn aggregate_from_raw_metrics(functions: &[FunctionMetrics]) -> GodObjectAggregatedMetrics {
let total_cyclomatic = functions.iter().map(|f| f.cyclomatic).sum();
let total_cognitive = functions.iter().map(|f| f.cognitive).sum();
let max_nesting = functions.iter().map(|f| f.nesting).max().unwrap_or(0);
let (total_error_swallowing, error_patterns) = aggregate_error_swallowing(functions);
let aggregated_entropy = aggregate_entropy_from_raw(functions);
let (
unique_upstream_callers,
unique_downstream_callees,
upstream_dependencies,
downstream_dependencies,
) = aggregate_dependency_metrics_from_raw(functions);
let distribution_metrics = aggregate_distribution_metrics_from_raw(functions);
GodObjectAggregatedMetrics {
total_cyclomatic,
total_cognitive,
max_nesting_depth: max_nesting,
weighted_coverage: None,
unique_upstream_callers,
unique_downstream_callees,
upstream_dependencies,
downstream_dependencies,
aggregated_contextual_risk: None,
total_error_swallowing_count: total_error_swallowing,
error_swallowing_patterns: error_patterns,
aggregated_entropy,
distribution_metrics: Some(distribution_metrics),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::complexity::EntropyAnalysis;
use crate::priority::{
ActionableRecommendation, DebtType, FunctionRole, ImpactMetrics, Location, UnifiedScore,
};
use std::path::PathBuf;
fn create_test_item(
file: &str,
cyc: u32,
cog: u32,
nest: u32,
length: usize,
) -> UnifiedDebtItem {
UnifiedDebtItem {
location: Location {
file: PathBuf::from(file),
function: "test_fn".to_string(),
line: 1,
},
debt_type: DebtType::Complexity {
cyclomatic: cyc,
cognitive: cog,
},
unified_score: UnifiedScore {
final_score: 50.0,
complexity_factor: 5.0,
coverage_factor: 0.0,
dependency_factor: 0.0,
role_multiplier: 1.0,
base_score: None,
exponential_factor: None,
risk_boost: None,
pre_adjustment_score: None,
adjustment_applied: None,
purity_factor: None,
refactorability_factor: None,
pattern_factor: None,
debt_adjustment: None,
pre_normalization_score: None,
structural_multiplier: Some(1.0),
has_coverage_data: false,
contextual_risk_multiplier: None,
pre_contextual_score: None,
debt_type_multiplier: None,
},
function_role: FunctionRole::Unknown,
recommendation: ActionableRecommendation {
primary_action: "Refactor".to_string(),
rationale: "Test".to_string(),
implementation_steps: Vec::new(),
related_items: Vec::new(),
steps: None,
estimated_effort_hours: None,
},
expected_impact: ImpactMetrics {
coverage_improvement: 0.0,
lines_reduction: 0,
complexity_reduction: 0.0,
risk_reduction: 0.0,
},
transitive_coverage: None,
upstream_dependencies: 0,
downstream_dependencies: 0,
upstream_callers: Vec::new(),
downstream_callees: Vec::new(),
upstream_production_callers: Vec::new(),
upstream_test_callers: Vec::new(),
production_blast_radius: 0,
nesting_depth: nest,
function_length: length,
cyclomatic_complexity: cyc,
cognitive_complexity: cog,
is_pure: None,
purity_confidence: None,
purity_level: None,
god_object_indicators: None,
tier: None,
function_context: None,
context_confidence: None,
contextual_recommendation: None,
pattern_analysis: None,
file_context: None,
context_multiplier: None,
context_type: None,
language_specific: None,
detected_pattern: None,
contextual_risk: None,
file_line_count: None,
responsibility_category: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
context_suggestion: None,
}
}
#[test]
fn test_complexity_aggregation_sums_and_maxes() {
let members = vec![
create_test_item("file.rs", 5, 10, 2, 50),
create_test_item("file.rs", 10, 15, 5, 100),
create_test_item("file.rs", 15, 20, 3, 75),
];
let member_refs: Vec<_> = members.iter().collect();
let (total_cyc, total_cog, max_nest) = aggregate_complexity_metrics(&member_refs);
assert_eq!(total_cyc, 30); assert_eq!(total_cog, 45); assert_eq!(max_nest, 5); }
#[test]
fn test_coverage_weighted_average() {
let mut members = vec![
create_test_item("file.rs", 0, 0, 0, 10),
create_test_item("file.rs", 0, 0, 0, 50),
create_test_item("file.rs", 0, 0, 0, 40),
];
members[0].transitive_coverage = Some(TransitiveCoverage {
direct: 0.8,
transitive: 0.9,
propagated_from: vec![],
uncovered_lines: vec![],
});
members[1].transitive_coverage = Some(TransitiveCoverage {
direct: 0.2,
transitive: 0.3,
propagated_from: vec![],
uncovered_lines: vec![],
});
members[2].transitive_coverage = Some(TransitiveCoverage {
direct: 0.5,
transitive: 0.6,
propagated_from: vec![],
uncovered_lines: vec![],
});
let member_refs: Vec<_> = members.iter().collect();
let cov = aggregate_coverage_metrics(&member_refs).unwrap();
assert!((cov.direct - 0.38).abs() < 0.01);
assert!((cov.transitive - 0.48).abs() < 0.01); }
#[test]
fn test_dependencies_deduplicate() {
let mut members = vec![
create_test_item("file.rs", 0, 0, 0, 10),
create_test_item("file.rs", 0, 0, 0, 20),
];
members[0].upstream_callers = vec!["main".to_string(), "init".to_string()];
members[0].downstream_callees = vec!["log".to_string()];
members[1].upstream_callers = vec!["main".to_string(), "process".to_string()]; members[1].downstream_callees = vec!["log".to_string(), "db".to_string()];
let member_refs: Vec<_> = members.iter().collect();
let (callers, _callees, up_count, down_count) = aggregate_dependency_metrics(&member_refs);
assert_eq!(up_count, 3); assert_eq!(down_count, 2); assert!(callers.contains(&"main".to_string()));
assert!(callers.contains(&"init".to_string()));
assert!(callers.contains(&"process".to_string()));
}
#[test]
fn test_extract_member_functions() {
let items = vec![
create_test_item("file1.rs", 5, 10, 2, 50),
create_test_item("file2.rs", 10, 15, 3, 100),
create_test_item("file1.rs", 15, 20, 4, 75),
];
let members = extract_member_functions(items.iter(), Path::new("file1.rs"));
assert_eq!(members.len(), 2);
assert!(members
.iter()
.all(|m| m.location.file == Path::new("file1.rs")));
}
#[test]
fn test_aggregate_god_object_metrics_composition() {
let members = vec![
create_test_item("file.rs", 5, 10, 2, 50),
create_test_item("file.rs", 10, 15, 5, 100),
];
let member_refs: Vec<_> = members.iter().collect();
let metrics = aggregate_god_object_metrics(&member_refs);
assert_eq!(metrics.total_cyclomatic, 15);
assert_eq!(metrics.total_cognitive, 25);
assert_eq!(metrics.max_nesting_depth, 5);
assert!(metrics.weighted_coverage.is_none()); }
#[test]
fn test_aggregate_error_swallowing() {
let functions = vec![
FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 5,
cognitive: 5,
nesting: 1,
length: 20,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: Some(2),
error_swallowing_patterns: Some(vec![
"if let Ok(...) without else branch".to_string()
]),
entropy_analysis: None,
},
FunctionMetrics {
name: "func2".to_string(),
file: PathBuf::from("test.rs"),
line: 25,
cyclomatic: 3,
cognitive: 3,
nesting: 1,
length: 15,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: Some(3),
error_swallowing_patterns: Some(vec![
"if let Ok(...) without else branch".to_string(),
"let _ = discarding Result".to_string(),
]),
entropy_analysis: None,
},
FunctionMetrics {
name: "func3".to_string(),
file: PathBuf::from("test.rs"),
line: 50,
cyclomatic: 2,
cognitive: 2,
nesting: 1,
length: 10,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None, error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let (total, patterns) = aggregate_error_swallowing(&functions);
assert_eq!(total, 5); assert_eq!(patterns.len(), 2); assert!(patterns.contains(&"if let Ok(...) without else branch".to_string()));
assert!(patterns.contains(&"let _ = discarding Result".to_string()));
}
#[test]
fn test_aggregate_entropy_metrics_weighted_average() {
let mut item1 = create_test_item("file.rs", 10, 20, 2, 100); item1.entropy_analysis = Some(EntropyAnalysis {
entropy_score: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.3,
original_complexity: 20,
adjusted_complexity: 16,
dampening_factor: 0.8,
dampening_was_applied: true,
reasoning: vec![],
});
let mut item2 = create_test_item("file.rs", 15, 30, 3, 200); item2.entropy_analysis = Some(EntropyAnalysis {
entropy_score: 0.5,
pattern_repetition: 0.3,
branch_similarity: 0.2,
original_complexity: 30,
adjusted_complexity: 27,
dampening_factor: 0.9,
dampening_was_applied: true,
reasoning: vec![],
});
let members = vec![&item1, &item2];
let result = aggregate_entropy_metrics(&members).expect("should have entropy");
assert!((result.entropy_score - 0.467).abs() < 0.01);
assert!((result.pattern_repetition - 0.4).abs() < 0.01);
assert!((result.dampening_factor - 0.867).abs() < 0.01);
assert_eq!(result.original_complexity, 50);
assert_eq!(result.adjusted_complexity, 43);
}
#[test]
fn test_aggregate_entropy_metrics_empty() {
let item1 = create_test_item("file.rs", 10, 20, 2, 100);
let item2 = create_test_item("file.rs", 15, 30, 3, 200);
let members = vec![&item1, &item2];
let result = aggregate_entropy_metrics(&members);
assert!(result.is_none());
}
#[test]
fn test_aggregate_entropy_metrics_partial() {
let mut item1 = create_test_item("file.rs", 10, 20, 2, 100);
item1.entropy_analysis = Some(EntropyAnalysis {
entropy_score: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.3,
original_complexity: 20,
adjusted_complexity: 16,
dampening_factor: 0.8,
dampening_was_applied: true,
reasoning: vec![],
});
let item2 = create_test_item("file.rs", 15, 30, 3, 200);
let members = vec![&item1, &item2];
let result = aggregate_entropy_metrics(&members).expect("should have entropy from item1");
assert!((result.entropy_score - 0.4).abs() < 0.001);
assert_eq!(result.original_complexity, 20);
}
#[test]
fn test_aggregate_god_object_metrics_includes_entropy() {
let mut item1 = create_test_item("file.rs", 10, 20, 2, 100);
item1.entropy_analysis = Some(EntropyAnalysis {
entropy_score: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.3,
original_complexity: 20,
adjusted_complexity: 16,
dampening_factor: 0.8,
dampening_was_applied: true,
reasoning: vec![],
});
let members = vec![&item1];
let metrics = aggregate_god_object_metrics(&members);
assert!(metrics.aggregated_entropy.is_some());
let entropy = metrics.aggregated_entropy.unwrap();
assert!((entropy.entropy_score - 0.4).abs() < 0.001);
}
#[test]
fn test_aggregate_from_raw_metrics_includes_error_swallowing() {
let functions = vec![FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 10,
cognitive: 15,
nesting: 3,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: Some(4),
error_swallowing_patterns: Some(vec!["match with ignored Err variant".to_string()]),
entropy_analysis: None,
}];
let metrics = aggregate_from_raw_metrics(&functions);
assert_eq!(metrics.total_cyclomatic, 10);
assert_eq!(metrics.total_cognitive, 15);
assert_eq!(metrics.total_error_swallowing_count, 4);
assert_eq!(metrics.error_swallowing_patterns.len(), 1);
assert!(metrics
.error_swallowing_patterns
.contains(&"match with ignored Err variant".to_string()));
}
#[test]
fn test_aggregate_entropy_from_raw() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let functions = vec![
FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 10,
cognitive: 20,
nesting: 2,
length: 100,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: Some(RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
}),
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "func2".to_string(),
file: PathBuf::from("test.rs"),
line: 50,
cyclomatic: 5,
cognitive: 10,
nesting: 1,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: Some(RawEntropyScore {
token_entropy: 0.5,
pattern_repetition: 0.3,
branch_similarity: 0.0,
effective_complexity: 3.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
}),
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let result = aggregate_entropy_from_raw(&functions).expect("should have entropy");
assert!((result.entropy_score - 0.433).abs() < 0.01);
assert!((result.pattern_repetition - 0.5).abs() < 0.01);
assert_eq!(result.original_complexity, 30);
}
#[test]
fn test_aggregate_from_raw_metrics_includes_entropy() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let functions = vec![FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 10,
cognitive: 20,
nesting: 2,
length: 100,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: Some(RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
}),
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}];
let metrics = aggregate_from_raw_metrics(&functions);
assert!(metrics.aggregated_entropy.is_some());
let entropy = metrics.aggregated_entropy.unwrap();
assert!((entropy.entropy_score - 0.4).abs() < 0.001);
assert_eq!(entropy.original_complexity, 20);
}
#[test]
fn test_extract_entropy_data_filters_correctly() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let functions = vec![
FunctionMetrics {
name: "with_entropy".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 5,
cognitive: 10,
nesting: 1,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: Some(RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
}),
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "without_entropy".to_string(),
file: PathBuf::from("test.rs"),
line: 50,
cyclomatic: 3,
cognitive: 5,
nesting: 1,
length: 25,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None, is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let data = extract_entropy_data(&functions);
assert_eq!(data.len(), 1); assert_eq!(data[0].1, 50); assert_eq!(data[0].2, 10); }
#[test]
fn test_weighted_average_calculation() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let e1 = RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
};
let e2 = RawEntropyScore {
token_entropy: 0.6,
pattern_repetition: 0.2,
branch_similarity: 0.0,
effective_complexity: 3.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
};
let data: Vec<(&RawEntropyScore, usize, u32)> = vec![(&e1, 100, 20), (&e2, 50, 10)];
let avg = weighted_average(&data, 150, |e| e.token_entropy);
assert!((avg - 0.467).abs() < 0.01);
let rep = weighted_average(&data, 150, |e| e.pattern_repetition);
assert!((rep - 0.467).abs() < 0.01);
}
#[test]
fn test_sum_cognitive() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let e = RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
};
let data: Vec<(&RawEntropyScore, usize, u32)> =
vec![(&e, 100, 20), (&e, 50, 15), (&e, 25, 5)];
let total = sum_cognitive(&data);
assert_eq!(total, 40); }
#[test]
fn test_total_length() {
use crate::complexity::entropy_core::EntropyScore as RawEntropyScore;
let e = RawEntropyScore {
token_entropy: 0.4,
pattern_repetition: 0.6,
branch_similarity: 0.0,
effective_complexity: 5.0,
unique_variables: 0,
max_nesting: 0,
dampening_applied: 0.0,
};
let data: Vec<(&RawEntropyScore, usize, u32)> = vec![(&e, 100, 20), (&e, 50, 15)];
let total = total_length(&data);
assert_eq!(total, 150); }
#[test]
fn test_calculate_dampening_factor_direct() {
use crate::complexity::entropy_core::{EntropyConfig, UniversalEntropyCalculator};
let calculator = UniversalEntropyCalculator::new(EntropyConfig::default());
let dampening = calculator.calculate_dampening_factor(0.4, 0.6);
assert!((0.5..=1.0).contains(&dampening));
let low_dampening = calculator.calculate_dampening_factor(0.2, 0.8);
assert!(low_dampening >= 0.5);
let high_dampening = calculator.calculate_dampening_factor(0.8, 0.2);
assert!(high_dampening <= 1.0);
}
#[test]
fn test_aggregate_dependency_metrics_from_raw() {
let functions = vec![
FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 5,
cognitive: 10,
nesting: 1,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: Some(vec!["caller1".to_string(), "caller2".to_string()]),
downstream_callees: Some(vec!["callee1".to_string()]),
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "func2".to_string(),
file: PathBuf::from("test.rs"),
line: 10,
cyclomatic: 3,
cognitive: 5,
nesting: 1,
length: 30,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: Some(vec!["caller2".to_string(), "caller3".to_string()]), downstream_callees: Some(vec!["callee2".to_string(), "callee3".to_string()]),
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let (callers, callees, upstream_count, downstream_count) =
aggregate_dependency_metrics_from_raw(&functions);
assert_eq!(upstream_count, 3);
assert_eq!(downstream_count, 3);
assert!(callers.contains(&"caller1".to_string()));
assert!(callers.contains(&"caller2".to_string()));
assert!(callers.contains(&"caller3".to_string()));
assert!(callees.contains(&"callee1".to_string()));
assert!(callees.contains(&"callee2".to_string()));
assert!(callees.contains(&"callee3".to_string()));
}
#[test]
fn test_aggregate_from_raw_metrics_includes_dependencies() {
let functions = vec![FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 5,
cognitive: 10,
nesting: 1,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: Some(vec!["caller1".to_string(), "caller2".to_string()]),
downstream_callees: Some(vec!["callee1".to_string()]),
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}];
let metrics = aggregate_from_raw_metrics(&functions);
assert_eq!(metrics.upstream_dependencies, 2);
assert_eq!(metrics.downstream_dependencies, 1);
assert_eq!(metrics.unique_upstream_callers.len(), 2);
assert_eq!(metrics.unique_downstream_callees.len(), 1);
}
#[test]
fn test_calculate_median_odd_count() {
assert_eq!(calculate_median(&[1, 2, 3, 4, 5]), 3);
assert_eq!(calculate_median(&[5, 1, 3]), 3); assert_eq!(calculate_median(&[100]), 100);
}
#[test]
fn test_calculate_median_even_count() {
assert_eq!(calculate_median(&[1, 2, 3, 4]), 2); assert_eq!(calculate_median(&[1, 3, 5, 7]), 4); }
#[test]
fn test_calculate_median_empty() {
assert_eq!(calculate_median(&[]), 0);
}
#[test]
fn test_classify_distribution_concentrated() {
assert_eq!(
classify_distribution(60, 100),
ComplexityDistribution::Concentrated
);
assert_eq!(
classify_distribution(51, 100),
ComplexityDistribution::Concentrated
);
}
#[test]
fn test_classify_distribution_mixed() {
assert_eq!(
classify_distribution(30, 100),
ComplexityDistribution::Mixed
);
assert_eq!(
classify_distribution(50, 100),
ComplexityDistribution::Mixed
);
assert_eq!(
classify_distribution(21, 100),
ComplexityDistribution::Mixed
);
}
#[test]
fn test_classify_distribution_distributed() {
assert_eq!(
classify_distribution(10, 100),
ComplexityDistribution::Distributed
);
assert_eq!(
classify_distribution(20, 100),
ComplexityDistribution::Distributed
);
assert_eq!(
classify_distribution(0, 0),
ComplexityDistribution::Distributed
);
}
#[test]
fn test_aggregate_distribution_metrics() {
let members = vec![
create_test_item("file.rs", 5, 10, 2, 50), create_test_item("file.rs", 8, 15, 3, 100), create_test_item("file.rs", 12, 20, 4, 75), create_test_item("file.rs", 6, 12, 2, 60), create_test_item("file.rs", 7, 14, 3, 80), ];
let member_refs: Vec<_> = members.iter().collect();
let dist = aggregate_distribution_metrics(&member_refs);
assert_eq!(dist.function_count, 5);
assert_eq!(dist.max_complexity, 12);
assert!((dist.avg_complexity - 7.6).abs() < 0.01); assert_eq!(dist.median_complexity, 7); assert_eq!(dist.exceeding_threshold, 0); assert_eq!(dist.distribution, ComplexityDistribution::Mixed);
assert_eq!(dist.production_loc, 365); }
#[test]
fn test_aggregate_distribution_metrics_distributed_file() {
let mut members = Vec::new();
for i in 0..30 {
members.push(create_test_item("file.rs", 3 + (i % 5), 10, 2, 20));
}
let member_refs: Vec<_> = members.iter().collect();
let dist = aggregate_distribution_metrics(&member_refs);
assert_eq!(dist.function_count, 30);
assert_eq!(dist.max_complexity, 7); assert_eq!(dist.exceeding_threshold, 0);
assert_eq!(dist.distribution, ComplexityDistribution::Distributed);
}
#[test]
fn test_aggregate_distribution_metrics_concentrated_file() {
let members = vec![
create_test_item("file.rs", 60, 100, 5, 500), create_test_item("file.rs", 5, 10, 2, 30), create_test_item("file.rs", 5, 10, 2, 30), ];
let member_refs: Vec<_> = members.iter().collect();
let dist = aggregate_distribution_metrics(&member_refs);
assert_eq!(dist.function_count, 3);
assert_eq!(dist.max_complexity, 60);
assert_eq!(dist.exceeding_threshold, 1); assert_eq!(dist.distribution, ComplexityDistribution::Concentrated);
}
#[test]
fn test_aggregate_distribution_metrics_from_raw() {
let functions = vec![
FunctionMetrics {
name: "prod_func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 10,
cognitive: 15,
nesting: 2,
length: 100,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "prod_func2".to_string(),
file: PathBuf::from("test.rs"),
line: 50,
cyclomatic: 8,
cognitive: 12,
nesting: 1,
length: 50,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "test_something".to_string(),
file: PathBuf::from("test.rs"),
line: 100,
cyclomatic: 5,
cognitive: 8,
nesting: 1,
length: 200, is_test: true,
visibility: None,
is_trait_method: false,
in_test_module: true,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let dist = aggregate_distribution_metrics_from_raw(&functions);
assert_eq!(dist.function_count, 2);
assert_eq!(dist.max_complexity, 10);
assert!((dist.avg_complexity - 9.0).abs() < 0.01); assert_eq!(dist.production_loc, 150);
assert_eq!(dist.test_loc, 200);
assert_eq!(dist.distribution, ComplexityDistribution::Concentrated);
}
#[test]
fn test_distributed_file_gets_correct_classification() {
let mut functions = Vec::new();
for i in 0..30 {
functions.push(FunctionMetrics {
name: format!("func_{}", i),
file: PathBuf::from("overflow.rs"),
line: i * 20 + 1,
cyclomatic: 5 + (i as u32 % 4), cognitive: 4 + (i as u32 % 3),
nesting: 2,
length: 20, is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
});
}
let dist = aggregate_distribution_metrics_from_raw(&functions);
assert_eq!(dist.function_count, 30);
assert_eq!(dist.max_complexity, 8); assert_eq!(dist.distribution, ComplexityDistribution::Distributed);
assert_eq!(dist.production_loc, 600); }
#[test]
fn test_complexity_distribution_display_names() {
assert_eq!(
ComplexityDistribution::Concentrated.display_name(),
"Concentrated"
);
assert_eq!(ComplexityDistribution::Mixed.display_name(), "Mixed");
assert_eq!(
ComplexityDistribution::Distributed.display_name(),
"Distributed"
);
}
#[test]
fn test_complexity_distribution_explanations() {
assert!(ComplexityDistribution::Concentrated
.classification_explanation()
.contains("god function"));
assert!(ComplexityDistribution::Mixed
.classification_explanation()
.contains("review"));
assert!(ComplexityDistribution::Distributed
.classification_explanation()
.contains("Well-Structured"));
}
#[test]
fn test_aggregate_god_object_metrics_includes_distribution() {
let members = vec![
create_test_item("file.rs", 10, 20, 2, 100),
create_test_item("file.rs", 8, 15, 3, 80),
];
let member_refs: Vec<_> = members.iter().collect();
let metrics = aggregate_god_object_metrics(&member_refs);
assert!(metrics.distribution_metrics.is_some());
let dist = metrics.distribution_metrics.unwrap();
assert_eq!(dist.function_count, 2);
assert_eq!(dist.max_complexity, 10);
}
#[test]
fn test_aggregate_from_raw_metrics_includes_distribution() {
let functions = vec![FunctionMetrics {
name: "func1".to_string(),
file: PathBuf::from("test.rs"),
line: 1,
cyclomatic: 10,
cognitive: 15,
nesting: 2,
length: 100,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}];
let metrics = aggregate_from_raw_metrics(&functions);
assert!(metrics.distribution_metrics.is_some());
let dist = metrics.distribution_metrics.unwrap();
assert_eq!(dist.function_count, 1);
assert_eq!(dist.max_complexity, 10);
assert_eq!(dist.production_loc, 100);
}
}