use crate::analysis::ContextDetector;
use crate::core::{DebtItem, FunctionMetrics, Language};
use crate::data_flow::DataFlowGraph;
use crate::debt::suppression::{parse_suppression_comments, SuppressionContext};
use crate::priority::call_graph::{CallGraph, FunctionId};
use crate::priority::debt_aggregator::{DebtAggregator, FunctionId as AggregatorFunctionId};
use crate::priority::scoring::{debt_item, ContextRecommendationEngine};
use crate::priority::UnifiedDebtItem;
use crate::risk::lcov::LcovData;
use crate::risk::RiskAnalyzer;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ScoringWeights {
pub cyclomatic: f64,
pub cognitive: f64,
pub coupling: f64,
pub coverage: f64,
}
impl Default for ScoringWeights {
fn default() -> Self {
Self {
cyclomatic: 1.0,
cognitive: 1.5,
coupling: 0.5,
coverage: 2.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PriorityConfig {
pub threshold: f64,
}
impl Default for PriorityConfig {
fn default() -> Self {
Self { threshold: 10.0 }
}
}
pub type FileLineCountCache = HashMap<PathBuf, usize>;
pub fn build_file_line_count_cache(metrics: &[FunctionMetrics]) -> FileLineCountCache {
use crate::metrics::LocCounter;
let mut unique_files: Vec<&PathBuf> = metrics
.iter()
.map(|m| &m.file)
.collect::<HashSet<_>>()
.into_iter()
.collect();
unique_files.sort();
unique_files
.par_iter()
.filter_map(|path| {
let loc_counter = LocCounter::default();
loc_counter
.count_file(path)
.ok()
.map(|count| ((*path).clone(), count.physical_lines))
})
.collect()
}
pub type SuppressionContextCache = HashMap<PathBuf, SuppressionContext>;
pub fn build_suppression_context_cache(metrics: &[FunctionMetrics]) -> SuppressionContextCache {
let mut unique_files: Vec<&PathBuf> = metrics
.iter()
.map(|m| &m.file)
.collect::<HashSet<_>>()
.into_iter()
.collect();
unique_files.sort();
unique_files
.par_iter()
.filter_map(|path| {
let content = std::fs::read_to_string(path).ok()?;
let language = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| match ext {
"rs" => Language::Rust,
"py" | "pyw" => Language::Python,
_ => Language::Rust, })
.unwrap_or(Language::Rust);
let context = parse_suppression_comments(&content, language, path);
if context.function_allows.is_empty() {
None
} else {
Some(((*path).clone(), context))
}
})
.collect()
}
fn is_item_suppressed(item: &UnifiedDebtItem, suppression_cache: &SuppressionContextCache) -> bool {
if let Some(context) = suppression_cache.get(&item.location.file) {
context.is_function_allowed(item.location.line, &item.debt_type)
} else {
false
}
}
pub fn create_function_mappings(
metrics: &[FunctionMetrics],
) -> Vec<(AggregatorFunctionId, usize, usize)> {
metrics
.iter()
.map(|m| {
let func_id = AggregatorFunctionId::new(m.file.clone(), m.name.clone(), m.line);
(func_id, m.line, m.line + m.length)
})
.collect()
}
pub fn setup_debt_aggregator(
metrics: &[FunctionMetrics],
debt_items: Option<&[DebtItem]>,
) -> DebtAggregator {
let mut debt_aggregator = DebtAggregator::new();
if let Some(items) = debt_items {
let function_mappings = create_function_mappings(metrics);
debt_aggregator.aggregate_debt(items.to_vec(), &function_mappings);
}
debt_aggregator
}
pub fn metrics_to_purity_map(
metrics: &[FunctionMetrics],
) -> std::collections::HashMap<String, bool> {
metrics
.iter()
.map(|m| (m.name.clone(), m.is_pure.unwrap_or(false)))
.collect()
}
#[allow(clippy::too_many_arguments)]
pub fn create_debt_items_from_metric(
metric: &FunctionMetrics,
call_graph: &CallGraph,
coverage_data: Option<&LcovData>,
framework_exclusions: &HashSet<FunctionId>,
function_pointer_used_functions: Option<&HashSet<FunctionId>>,
debt_aggregator: &DebtAggregator,
data_flow: Option<&DataFlowGraph>,
risk_analyzer: Option<&RiskAnalyzer>,
project_path: &Path,
file_line_counts: &FileLineCountCache,
context_detector: &ContextDetector,
recommendation_engine: &ContextRecommendationEngine,
) -> Vec<UnifiedDebtItem> {
debt_item::create_unified_debt_item_with_aggregator_and_data_flow(
metric,
call_graph,
coverage_data,
framework_exclusions,
function_pointer_used_functions,
debt_aggregator,
data_flow,
risk_analyzer,
project_path,
file_line_counts,
context_detector,
recommendation_engine,
)
}
#[allow(clippy::too_many_arguments)]
pub fn process_metrics_to_debt_items(
metrics: &[FunctionMetrics],
call_graph: &CallGraph,
test_only_functions: &HashSet<FunctionId>,
coverage_data: Option<&LcovData>,
framework_exclusions: &HashSet<FunctionId>,
function_pointer_used_functions: Option<&HashSet<FunctionId>>,
debt_aggregator: &DebtAggregator,
data_flow: Option<&DataFlowGraph>,
risk_analyzer: Option<&RiskAnalyzer>,
project_path: &Path,
file_line_counts: &FileLineCountCache,
) -> Vec<UnifiedDebtItem> {
use super::call_graph::should_process_metric;
let context_detector = ContextDetector::global();
let recommendation_engine = ContextRecommendationEngine::global();
let suppression_cache = build_suppression_context_cache(metrics);
let items: Vec<UnifiedDebtItem> = metrics
.par_iter() .filter(|metric| should_process_metric(metric, call_graph, test_only_functions))
.flat_map(|metric| {
create_debt_items_from_metric(
metric,
call_graph,
coverage_data,
framework_exclusions,
function_pointer_used_functions,
debt_aggregator,
data_flow,
risk_analyzer,
project_path,
file_line_counts,
context_detector,
recommendation_engine,
)
})
.collect();
items
.into_iter()
.filter(|item| !is_item_suppressed(item, &suppression_cache))
.collect()
}
pub fn calculate_total_complexity(metrics: &[FunctionMetrics]) -> u32 {
metrics.iter().map(|m| m.cyclomatic + m.cognitive).sum()
}
pub fn calculate_average_complexity(metrics: &[FunctionMetrics]) -> f64 {
if metrics.is_empty() {
return 0.0;
}
calculate_total_complexity(metrics) as f64 / metrics.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_metric(name: &str, cyclomatic: u32, cognitive: u32) -> FunctionMetrics {
FunctionMetrics {
name: name.to_string(),
file: PathBuf::from("test.rs"),
line: 1,
length: 10,
cyclomatic,
cognitive,
nesting: 0,
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,
}
}
fn create_metric_for_mappings(name: &str, line: usize, length: usize) -> FunctionMetrics {
let mut metric = create_test_metric(name, 1, 0);
metric.line = line;
metric.length = length;
metric
}
fn create_metric_with_purity(name: &str, is_pure: Option<bool>) -> FunctionMetrics {
let mut metric = create_test_metric(name, 1, 0);
metric.is_pure = is_pure;
metric
}
#[test]
fn test_calculate_total_complexity() {
let metrics = vec![
create_test_metric("a", 5, 3),
create_test_metric("b", 10, 7),
];
let total = calculate_total_complexity(&metrics);
assert_eq!(total, 25); }
#[test]
fn test_calculate_average_complexity() {
let metrics = vec![
create_test_metric("a", 5, 3),
create_test_metric("b", 10, 7),
];
let avg = calculate_average_complexity(&metrics);
assert!((avg - 12.5).abs() < 0.001); }
#[test]
fn test_calculate_average_complexity_empty() {
let metrics: Vec<FunctionMetrics> = vec![];
let avg = calculate_average_complexity(&metrics);
assert_eq!(avg, 0.0);
}
#[test]
fn test_create_function_mappings() {
let metrics = vec![
create_metric_for_mappings("foo", 10, 20),
create_metric_for_mappings("bar", 50, 30),
];
let mappings = create_function_mappings(&metrics);
assert_eq!(mappings.len(), 2);
assert_eq!(mappings[0].1, 10); assert_eq!(mappings[0].2, 30); assert_eq!(mappings[1].1, 50);
assert_eq!(mappings[1].2, 80); }
#[test]
fn test_metrics_to_purity_map() {
let metrics = vec![
create_metric_with_purity("pure_fn", Some(true)),
create_metric_with_purity("impure_fn", Some(false)),
create_metric_with_purity("unknown_fn", None),
];
let map = metrics_to_purity_map(&metrics);
assert_eq!(map.get("pure_fn"), Some(&true));
assert_eq!(map.get("impure_fn"), Some(&false));
assert_eq!(map.get("unknown_fn"), Some(&false)); }
#[test]
fn test_is_item_suppressed_with_allow_annotation() {
use crate::priority::DebtType;
let file_path = PathBuf::from("test.rs");
let content = r#"// debtmap:ignore[testing] -- I/O orchestration function
fn run() {}"#;
let context = parse_suppression_comments(content, Language::Rust, &file_path);
let mut cache = SuppressionContextCache::new();
cache.insert(file_path.clone(), context);
let item = create_test_unified_item(
file_path,
"run",
2,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 5,
cognitive: 10,
},
);
assert!(
is_item_suppressed(&item, &cache),
"Item with debtmap:ignore[testing] should be suppressed"
);
}
#[test]
fn test_is_item_suppressed_without_annotation() {
use crate::priority::DebtType;
let cache = SuppressionContextCache::new();
let file_path = PathBuf::from("test.rs");
let item = create_test_unified_item(
file_path,
"run",
10,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 5,
cognitive: 10,
},
);
assert!(
!is_item_suppressed(&item, &cache),
"Item without annotation should not be suppressed"
);
}
fn create_test_unified_item(
file: PathBuf,
function: &str,
line: usize,
debt_type: crate::priority::DebtType,
) -> crate::priority::UnifiedDebtItem {
use crate::priority::{
semantic_classifier::FunctionRole, ActionableRecommendation, ImpactMetrics, Location,
UnifiedScore,
};
crate::priority::UnifiedDebtItem {
location: Location {
file,
function: function.to_string(),
line,
},
debt_type,
unified_score: UnifiedScore {
complexity_factor: 5.0,
coverage_factor: 5.0,
dependency_factor: 5.0,
role_multiplier: 1.0,
final_score: 50.0,
base_score: Some(50.0),
exponential_factor: Some(1.0),
risk_boost: Some(1.0),
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::PureLogic,
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: 10,
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,
}
}
}