#![cfg_attr(coverage_nightly, coverage(off))]
use crate::models::dead_code::{DeadCodeResult, DeadCodeType};
use anyhow::Result;
use std::path::PathBuf;
pub trait DeadCodeFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String>;
}
pub struct SummaryFormatter;
impl DeadCodeFormatter for SummaryFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String> {
let mut output = String::new();
self.write_header(&mut output);
self.write_overview(&mut output, result);
self.write_type_breakdown(&mut output, result);
self.write_top_files(&mut output, result);
Ok(output)
}
}
impl SummaryFormatter {
fn write_header(&self, output: &mut String) {
output.push_str("# Dead Code Analysis Summary\n\n");
}
fn write_overview(&self, output: &mut String, result: &DeadCodeResult) {
output.push_str(&format!("📊 **Files analyzed**: {}\n", result.total_files));
output.push_str(&format!(
"☠️ **Files with dead code**: {}\n",
result.summary.files_with_dead_code
));
output.push_str(&format!(
"📏 **Total dead lines**: {}\n",
result.summary.total_dead_lines
));
output.push_str(&format!(
"📈 **Dead code percentage**: {:.2}%\n\n",
result.summary.dead_percentage
));
}
fn write_type_breakdown(&self, output: &mut String, result: &DeadCodeResult) {
if result.summary.dead_functions > 0
|| result.summary.dead_classes > 0
|| result.summary.dead_modules > 0
|| result.summary.unreachable_blocks > 0
{
output.push_str("## Dead Code by Type\n\n");
if result.summary.dead_functions > 0 {
output.push_str(&format!(
"- **Dead functions**: {}\n",
result.summary.dead_functions
));
}
if result.summary.dead_classes > 0 {
output.push_str(&format!(
"- **Dead classes**: {}\n",
result.summary.dead_classes
));
}
if result.summary.dead_modules > 0 {
output.push_str(&format!(
"- **Dead variables**: {}\n",
result.summary.dead_modules
));
}
if result.summary.unreachable_blocks > 0 {
output.push_str(&format!(
"- **Unreachable blocks**: {}\n",
result.summary.unreachable_blocks
));
}
}
}
fn write_top_files(&self, output: &mut String, result: &DeadCodeResult) {
if !result.files.is_empty() {
output.push_str("\n## Top Files with Dead Code\n\n");
for (i, file) in result.files.iter().take(10).enumerate() {
output.push_str(&format!(
"{}. `{}` - {:.1}% dead ({} lines)\n",
i + 1,
file.path,
file.dead_percentage,
file.dead_lines
));
self.write_file_details(output, file);
}
}
}
fn write_file_details(
&self,
output: &mut String,
file: &crate::models::dead_code::FileDeadCodeMetrics,
) {
if file.dead_functions > 0 || file.dead_classes > 0 {
output.push_str(" ");
if file.dead_functions > 0 {
output.push_str(&format!("Functions: {} ", file.dead_functions));
}
if file.dead_classes > 0 {
output.push_str(&format!("Classes: {} ", file.dead_classes));
}
output.push('\n');
}
}
}
pub struct JsonFormatter;
impl DeadCodeFormatter for JsonFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String> {
Ok(serde_json::to_string_pretty(result)?)
}
}
pub struct MarkdownFormatter;
impl DeadCodeFormatter for MarkdownFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String> {
let mut output = String::new();
self.write_header(&mut output);
self.write_summary_table(&mut output, result);
self.write_detailed_files(&mut output, result);
Ok(output)
}
}
impl MarkdownFormatter {
fn write_header(&self, output: &mut String) {
output.push_str("# Dead Code Analysis Report\n\n");
}
fn write_summary_table(&self, output: &mut String, result: &DeadCodeResult) {
output.push_str("## Summary\n\n");
output.push_str("| Metric | Value |\n");
output.push_str("|--------|-------|\n");
let metrics = vec![
("Files Analyzed", result.total_files.to_string()),
(
"Files with Dead Code",
result.summary.files_with_dead_code.to_string(),
),
(
"Total Dead Lines",
result.summary.total_dead_lines.to_string(),
),
(
"Dead Code Percentage",
format!("{:.2}%", result.summary.dead_percentage),
),
("Dead Functions", result.summary.dead_functions.to_string()),
("Dead Classes", result.summary.dead_classes.to_string()),
("Dead Variables", result.summary.dead_modules.to_string()),
(
"Unreachable Blocks",
result.summary.unreachable_blocks.to_string(),
),
];
for (metric, value) in metrics {
output.push_str(&format!("| {metric} | {value} |\n"));
}
}
fn write_detailed_files(&self, output: &mut String, result: &DeadCodeResult) {
if !result.files.is_empty() {
output.push_str("\n## Files with Dead Code\n\n");
output.push_str("| File | Dead % | Dead Lines | Functions | Classes | Confidence |\n");
output.push_str("|------|--------|------------|-----------|---------|------------|\n");
for file in &result.files {
output.push_str(&format!(
"| {} | {:.1}% | {} | {} | {} | {:?} |\n",
file.path,
file.dead_percentage,
file.dead_lines,
file.dead_functions,
file.dead_classes,
file.confidence
));
}
}
}
}
pub struct CsvFormatter;
impl DeadCodeFormatter for CsvFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String> {
let mut output = String::new();
self.write_header(&mut output);
self.write_rows(&mut output, result);
Ok(output)
}
}
impl CsvFormatter {
fn write_header(&self, output: &mut String) {
output.push_str("file_path,dead_percentage,dead_lines,total_lines,");
output.push_str("dead_functions,dead_classes,dead_modules,");
output.push_str("unreachable_blocks,confidence,score\n");
}
fn write_rows(&self, output: &mut String, result: &DeadCodeResult) {
for file in &result.files {
output.push_str(&format!(
"{},{:.2},{},{},{},{},{},{},{:?},{:.2}\n",
file.path,
file.dead_percentage,
file.dead_lines,
file.total_lines,
file.dead_functions,
file.dead_classes,
file.dead_modules,
file.unreachable_blocks,
file.confidence,
file.dead_score
));
}
}
}
pub struct GccFormatter;
impl DeadCodeFormatter for GccFormatter {
fn format(&self, result: &DeadCodeResult) -> Result<String> {
let mut output = String::new();
for file in &result.files {
for item in &file.items {
self.write_item(&mut output, &file.path, item);
}
}
Ok(output)
}
}
impl GccFormatter {
fn write_item(
&self,
output: &mut String,
file_path: &str,
item: &crate::models::dead_code::DeadCodeItem,
) {
let level = self.get_level(&item.item_type);
let type_str = match &item.item_type {
DeadCodeType::Function => "function",
DeadCodeType::Class => "class",
DeadCodeType::Variable => "variable",
DeadCodeType::UnreachableCode => "unreachable",
};
output.push_str(&format!(
"{}:{}:0: {}: {} '{}' - {}\n",
file_path, item.line, level, type_str, item.name, item.reason
));
}
fn get_level(&self, item_type: &DeadCodeType) -> &'static str {
match item_type {
DeadCodeType::Function => "warning",
DeadCodeType::Class => "warning",
DeadCodeType::Variable => "note",
DeadCodeType::UnreachableCode => "warning",
}
}
}
pub struct DeadCodeFormatterFactory;
impl DeadCodeFormatterFactory {
#[must_use]
pub fn create(format: crate::cli::DeadCodeOutputFormat) -> Box<dyn DeadCodeFormatter> {
match format {
crate::cli::DeadCodeOutputFormat::Summary => Box::new(SummaryFormatter),
crate::cli::DeadCodeOutputFormat::Json => Box::new(JsonFormatter),
crate::cli::DeadCodeOutputFormat::Markdown => Box::new(MarkdownFormatter),
crate::cli::DeadCodeOutputFormat::Sarif => Box::new(GccFormatter), }
}
}
pub fn format_and_output_dead_code(
format: crate::cli::DeadCodeOutputFormat,
result: &DeadCodeResult,
output_path: Option<PathBuf>,
) -> Result<()> {
let formatter = DeadCodeFormatterFactory::create(format);
let formatted = formatter.format(result)?;
if let Some(path) = output_path {
std::fs::write(path, formatted)?;
} else {
print!("{formatted}");
}
Ok(())
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::models::dead_code::{
ConfidenceLevel, DeadCodeItem, DeadCodeSummary, FileDeadCodeMetrics,
};
fn create_test_result() -> DeadCodeResult {
DeadCodeResult {
summary: DeadCodeSummary {
total_files_analyzed: 100,
files_with_dead_code: 10,
total_dead_lines: 500,
dead_percentage: 5.0,
dead_functions: 15,
dead_classes: 3,
dead_modules: 7,
unreachable_blocks: 2,
},
files: vec![FileDeadCodeMetrics {
path: "src/main.rs".to_string(),
dead_lines: 50,
total_lines: 500,
dead_percentage: 10.0,
dead_functions: 3,
dead_classes: 1,
dead_modules: 2,
unreachable_blocks: 0,
dead_score: 15.5,
confidence: ConfidenceLevel::High,
items: vec![DeadCodeItem {
item_type: DeadCodeType::Function,
name: "unused_func".to_string(),
line: 42,
reason: "Not reachable from any entry point".to_string(),
}],
}],
total_files: 100,
analyzed_files: 100,
}
}
#[test]
fn test_summary_formatter() {
let formatter = SummaryFormatter;
let result = create_test_result();
let output = formatter.format(&result).unwrap();
assert!(output.contains("Dead Code Analysis Summary"));
assert!(output.contains("**Files analyzed**: 100"));
assert!(output.contains("**Dead functions**: 15"));
}
#[test]
fn test_json_formatter() {
let formatter = JsonFormatter;
let result = create_test_result();
let output = formatter.format(&result).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["total_files"], 100);
}
#[test]
fn test_csv_formatter() {
let formatter = CsvFormatter;
let result = create_test_result();
let output = formatter.format(&result).unwrap();
assert!(output.contains("file_path,dead_percentage"));
assert!(output.contains("src/main.rs,10.00"));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}