use crate::priority::classification::Severity;
use crate::priority::{TransitiveCoverage, UnifiedDebtItem};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
use super::sort::SortCriteria;
#[derive(Debug, Clone)]
pub struct LocationGroup<'a> {
pub location: &'a crate::priority::Location,
pub items: Vec<&'a UnifiedDebtItem>,
pub combined_score: f64,
pub max_severity: String,
}
#[derive(Debug)]
pub struct AggregatedMetrics<'a> {
pub cognitive_complexity: u32,
pub nesting_depth: u32,
pub function_length: usize,
pub coverage: Option<&'a TransitiveCoverage>,
}
pub fn group_by_location<'a>(
items: impl Iterator<Item = &'a UnifiedDebtItem>,
sort_by: SortCriteria,
) -> Vec<LocationGroup<'a>> {
let mut groups: HashMap<(&PathBuf, &str, usize), Vec<&UnifiedDebtItem>> = HashMap::new();
for item in items {
let key = (
&item.location.file,
item.location.function.as_str(),
item.location.line,
);
groups.entry(key).or_default().push(item);
}
let mut result: Vec<LocationGroup> = groups
.into_values()
.map(|items| {
let combined_score = items
.iter()
.map(|i| i.unified_score.final_score)
.sum::<f64>();
let max_severity = items
.iter()
.map(|i| {
Severity::from_score_100(i.unified_score.final_score)
.as_str()
.to_lowercase()
})
.max_by(|a, b| severity_rank(a).cmp(&severity_rank(b)))
.unwrap_or_else(|| "low".to_string());
LocationGroup {
location: &items[0].location,
items,
combined_score,
max_severity,
}
})
.collect();
sort_groups(&mut result, sort_by);
result
}
fn sort_groups(groups: &mut [LocationGroup], criteria: SortCriteria) {
groups.sort_by(|a, b| {
let primary = match criteria {
SortCriteria::Score => {
b.combined_score
.partial_cmp(&a.combined_score)
.unwrap_or(Ordering::Equal)
}
SortCriteria::Coverage => {
let cov_a = aggregate_metrics(a).coverage.map(|c| c.direct);
let cov_b = aggregate_metrics(b).coverage.map(|c| c.direct);
match (cov_a, cov_b) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater,
(Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
}
}
SortCriteria::Complexity => {
let comp_a = aggregate_metrics(a).cognitive_complexity;
let comp_b = aggregate_metrics(b).cognitive_complexity;
comp_b.cmp(&comp_a)
}
SortCriteria::FilePath => {
a.location.file.cmp(&b.location.file)
}
SortCriteria::FunctionName => {
a.location.function.cmp(&b.location.function)
}
};
match primary {
Ordering::Equal => match a.location.file.cmp(&b.location.file) {
Ordering::Equal => a.location.line.cmp(&b.location.line),
other => other,
},
other => other,
}
});
}
pub fn aggregate_metrics<'a>(group: &LocationGroup<'a>) -> AggregatedMetrics<'a> {
let max_cog = group
.items
.iter()
.map(|i| i.cognitive_complexity)
.max()
.unwrap_or(0);
let max_nest = group
.items
.iter()
.map(|i| i.nesting_depth)
.max()
.unwrap_or(0);
let max_len = group
.items
.iter()
.map(|i| i.function_length)
.max()
.unwrap_or(0);
let coverage = group.items[0].transitive_coverage.as_ref();
AggregatedMetrics {
cognitive_complexity: max_cog,
nesting_depth: max_nest,
function_length: max_len,
coverage,
}
}
fn severity_rank(severity: &str) -> u8 {
match severity {
"critical" => 4,
"high" => 3,
"medium" => 2,
"low" => 1,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::priority::{
ActionableRecommendation, DebtType, ImpactMetrics, Location, UnifiedScore,
};
use std::path::PathBuf;
fn create_test_item(file: &str, function: &str, line: usize, score: f64) -> UnifiedDebtItem {
UnifiedDebtItem {
location: Location {
file: PathBuf::from(file),
function: function.to_string(),
line,
},
debt_type: DebtType::ComplexityHotspot {
cyclomatic: 5,
cognitive: 10,
},
unified_score: UnifiedScore {
complexity_factor: 5.0,
coverage_factor: 5.0,
dependency_factor: 5.0,
role_multiplier: 1.0,
final_score: score.max(0.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: crate::priority::semantic_classifier::FunctionRole::Unknown,
recommendation: ActionableRecommendation {
primary_action: "Test".to_string(),
rationale: "Test".to_string(),
implementation_steps: vec![],
related_items: vec![],
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![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
nesting_depth: 1,
function_length: 10,
cyclomatic_complexity: 5,
cognitive_complexity: 10,
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_group_by_location_single_item() {
let items = vec![create_test_item("file.rs", "func", 10, 50.0)];
let groups = group_by_location(items.iter(), SortCriteria::Score);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].items.len(), 1);
assert_eq!(groups[0].combined_score, 50.0);
}
#[test]
fn test_group_by_location_multiple_types() {
let items = vec![
create_test_item("file.rs", "func", 10, 75.0),
create_test_item("file.rs", "func", 10, 60.0),
create_test_item("file.rs", "func", 10, 45.0),
];
let groups = group_by_location(items.iter(), SortCriteria::Score);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].items.len(), 3);
assert_eq!(groups[0].combined_score, 180.0);
}
#[test]
fn test_combined_score_calculation() {
let items = vec![
create_test_item("file.rs", "func", 10, 75.0),
create_test_item("file.rs", "func", 10, 60.0),
create_test_item("file.rs", "func", 10, 45.0),
];
let groups = group_by_location(items.iter(), SortCriteria::Score);
assert_eq!(groups[0].combined_score, 180.0);
}
#[test]
fn test_separate_locations() {
let items = vec![
create_test_item("file.rs", "func1", 10, 50.0),
create_test_item("file.rs", "func2", 20, 50.0),
create_test_item("other.rs", "func1", 10, 50.0),
];
let groups = group_by_location(items.iter(), SortCriteria::Score);
assert_eq!(groups.len(), 3);
for group in &groups {
assert_eq!(group.items.len(), 1);
}
}
#[test]
fn test_max_severity() {
let items = vec![
create_test_item("file.rs", "func", 10, 75.0), create_test_item("file.rs", "func", 10, 120.0), create_test_item("file.rs", "func", 10, 45.0), ];
let groups = group_by_location(items.iter(), SortCriteria::Score);
assert_eq!(groups[0].max_severity, "critical");
}
}