use crate::formatting::FormattingConfig;
use crate::priority;
use crate::priority::formatter_markdown::format_filter_metrics;
use anyhow::Result;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
pub fn output_markdown(
analysis: &priority::UnifiedAnalysis,
top: Option<usize>,
tail: Option<usize>,
verbosity: u8,
output_file: Option<PathBuf>,
formatting_config: FormattingConfig,
show_filter_stats: bool,
) -> Result<()> {
let filtered_analysis = apply_filters(analysis, top, tail);
let display_config = crate::config::get_display_config();
let limit = filtered_analysis
.items
.len()
.max(filtered_analysis.file_items.len());
let output = if display_config.tiered {
if show_filter_stats {
use priority::tiers::TierConfig;
use priority::UnifiedAnalysisQueries;
let tier_config = TierConfig::default();
let result =
filtered_analysis.get_top_mixed_priorities_with_metrics(limit, &tier_config);
let base_output =
priority::format_priorities_tiered_markdown(&filtered_analysis, limit, verbosity);
format!(
"{}\n\n{}",
base_output,
format_filter_metrics(&result.metrics)
)
} else {
priority::format_priorities_tiered_markdown(&filtered_analysis, limit, verbosity)
}
} else if show_filter_stats {
use priority::tiers::TierConfig;
use priority::UnifiedAnalysisQueries;
let tier_config = TierConfig::default();
let result = filtered_analysis.get_top_mixed_priorities_with_metrics(limit, &tier_config);
let base_output = priority::format_priorities_markdown(
&filtered_analysis,
limit,
verbosity,
formatting_config,
);
format!(
"{}\n\n{}",
base_output,
format_filter_metrics(&result.metrics)
)
} else {
priority::format_priorities_markdown(
&filtered_analysis,
limit,
verbosity,
formatting_config,
)
};
if let Some(path) = output_file {
if let Some(parent) = path.parent() {
crate::io::ensure_dir(parent)?;
}
let mut file = fs::File::create(path)?;
file.write_all(output.as_bytes())?;
} else {
println!("{output}");
}
Ok(())
}
fn apply_filters(
analysis: &priority::UnifiedAnalysis,
top: Option<usize>,
tail: Option<usize>,
) -> priority::UnifiedAnalysis {
let (top, tail) = match (top, tail) {
(None, None) => (Some(10), None),
(t, tl) => (t, tl),
};
let mut filtered = analysis.clone();
if let Some(n) = top {
filtered.items = filtered.items.iter().take(n).cloned().collect();
} else if let Some(n) = tail {
let total = filtered.items.len();
let skip = total.saturating_sub(n);
filtered.items = filtered.items.iter().skip(skip).cloned().collect();
}
if let Some(n) = top {
filtered.file_items = filtered.file_items.iter().take(n).cloned().collect();
} else if let Some(n) = tail {
let total = filtered.file_items.len();
let skip = total.saturating_sub(n);
filtered.file_items = filtered.file_items.iter().skip(skip).cloned().collect();
}
filtered
}
#[cfg(test)]
mod tests {
use super::*;
use crate::priority::{
call_graph::CallGraph, ActionableRecommendation, DebtType, FunctionRole, ImpactMetrics,
Location, UnifiedAnalysisUtils, UnifiedDebtItem, UnifiedScore,
};
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_item(name: &str, score: f64) -> UnifiedDebtItem {
UnifiedDebtItem {
location: Location {
file: PathBuf::from("test.rs"),
line: 10,
function: name.to_string(),
},
debt_type: DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
unified_score: UnifiedScore {
complexity_factor: 50.0,
coverage_factor: 80.0,
dependency_factor: 50.0,
role_multiplier: 2.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: FunctionRole::PureLogic,
recommendation: ActionableRecommendation {
primary_action: "Fix issue".to_string(),
rationale: "Test reason".to_string(),
implementation_steps: vec![],
related_items: vec![],
steps: None,
estimated_effort_hours: None,
},
expected_impact: ImpactMetrics {
complexity_reduction: 100.0,
risk_reduction: 10.0,
coverage_improvement: 100.0,
lines_reduction: 500,
},
transitive_coverage: None,
file_context: None,
upstream_dependencies: 10,
downstream_dependencies: 20,
upstream_callers: vec![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
nesting_depth: 5,
function_length: 200,
cyclomatic_complexity: 25,
cognitive_complexity: 40,
is_pure: Some(false),
purity_confidence: Some(0.8),
purity_level: None,
god_object_indicators: None,
tier: None,
function_context: None,
context_confidence: None,
contextual_recommendation: None,
pattern_analysis: 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,
}
}
fn create_test_analysis_with_items(count: usize) -> priority::UnifiedAnalysis {
let call_graph = CallGraph::new();
let mut analysis = priority::UnifiedAnalysis::new(call_graph);
for i in 0..count {
let mut item = create_test_item(&format!("func_{}", i), 100.0 - i as f64);
item.location.line = 10 + i;
analysis.add_item(item);
}
analysis.sort_by_priority();
analysis
}
#[test]
fn test_output_markdown_with_head_parameter() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(10);
let result = output_markdown(
&analysis,
Some(3),
None,
0,
Some(output_path.clone()),
FormattingConfig::default(),
false,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("**Total Debt Items:** 3"),
"Expected 3 items in summary"
);
assert!(
content.contains("3 items") || content.contains("Count: 3"),
"Expected to find reference to 3 items in content"
);
}
#[test]
fn test_output_markdown_with_tail_parameter() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(10);
let result = output_markdown(
&analysis,
None,
Some(3),
0,
Some(output_path.clone()),
FormattingConfig::default(),
false,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("**Total Debt Items:** 3"),
"Expected 3 items in summary"
);
assert!(
content.contains("3 items") || content.contains("Count: 3"),
"Expected to find reference to 3 items in content"
);
}
#[test]
fn test_output_markdown_default_limit() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(20);
let result = output_markdown(
&analysis,
None,
None,
0,
Some(output_path.clone()),
FormattingConfig::default(),
false,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("**Total Debt Items:** 10"),
"Expected 10 items (default limit)"
);
}
#[test]
fn test_output_markdown_head_larger_than_items() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(5);
let result = output_markdown(
&analysis,
Some(10),
None,
0,
Some(output_path.clone()),
FormattingConfig::default(),
false,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("**Total Debt Items:** 5"),
"Expected 5 items (all available)"
);
}
#[test]
fn test_output_markdown_tail_larger_than_items() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(5);
let result = output_markdown(
&analysis,
None,
Some(10),
0,
Some(output_path.clone()),
FormattingConfig::default(),
false,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("**Total Debt Items:** 5"),
"Expected 5 items (all available)"
);
}
#[test]
fn test_output_markdown_with_filter_stats() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.md");
let analysis = create_test_analysis_with_items(10);
let result = output_markdown(
&analysis,
Some(5),
None,
0,
Some(output_path.clone()),
FormattingConfig::default(),
true,
);
assert!(
result.is_ok(),
"Failed to write markdown: {:?}",
result.err()
);
let content = fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("## Filtering Summary"),
"Expected filter stats section in output"
);
assert!(
content.contains("Total items analyzed"),
"Expected total items count in filter stats"
);
assert!(
content.contains("Items included"),
"Expected included items count in filter stats"
);
}
#[test]
fn test_apply_filters_with_head() {
let analysis = create_test_analysis_with_items(10);
let filtered = apply_filters(&analysis, Some(3), None);
assert_eq!(filtered.items.len(), 3, "Expected 3 items with head=3");
assert_eq!(filtered.items[0].location.function, "func_0");
assert_eq!(filtered.items[2].location.function, "func_2");
}
#[test]
fn test_apply_filters_with_tail() {
let analysis = create_test_analysis_with_items(10);
let filtered = apply_filters(&analysis, None, Some(3));
assert_eq!(filtered.items.len(), 3, "Expected 3 items with tail=3");
assert_eq!(filtered.items[0].location.function, "func_7");
assert_eq!(filtered.items[2].location.function, "func_9");
}
#[test]
fn test_apply_filters_default_to_ten() {
let analysis = create_test_analysis_with_items(20);
let filtered = apply_filters(&analysis, None, None);
assert_eq!(filtered.items.len(), 10, "Expected 10 items (default)");
}
}