#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::DeadCodeOutputFormat;
use anyhow::Result;
use std::path::{Path, PathBuf};
#[allow(clippy::too_many_arguments)]
struct DeadCodeAnalysisFilters {
include_unreachable: bool,
include_tests: bool,
min_dead_lines: usize,
top_files: Option<usize>,
include: Vec<String>,
exclude: Vec<String>,
max_depth: usize,
}
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_analyze_dead_code(
path: PathBuf,
format: DeadCodeOutputFormat,
top_files: Option<usize>,
include_unreachable: bool,
min_dead_lines: usize,
include_tests: bool,
output: Option<PathBuf>,
fail_on_violation: bool,
max_percentage: f64,
timeout: u64,
include: Vec<String>,
exclude: Vec<String>,
max_depth: usize,
) -> Result<()> {
eprintln!("☠️ Analyzing dead code in project...");
eprintln!("⏰ Analysis timeout set to {timeout} seconds");
if !include.is_empty() || !exclude.is_empty() {
eprintln!("🔍 Applying file filters...");
if !include.is_empty() {
eprintln!(" Include patterns: {include:?}");
}
if !exclude.is_empty() {
eprintln!(" Exclude patterns: {exclude:?}");
}
}
let timeout_duration = tokio::time::Duration::from_secs(timeout);
let result = tokio::time::timeout(timeout_duration, async {
run_dead_code_analysis_with_filters(
&path,
DeadCodeAnalysisFilters {
include_unreachable,
include_tests,
min_dead_lines,
top_files,
include,
exclude,
max_depth,
},
)
.await
})
.await
.map_err(|_| anyhow::anyhow!("Dead code analysis timed out after {timeout} seconds"))??;
eprintln!(
"📊 Analysis complete: {} files analyzed, {} with dead code",
result.summary.total_files_analyzed, result.summary.files_with_dead_code
);
let formatted_output = format_dead_code_result(&result, &format)?;
write_dead_code_output(formatted_output, output).await?;
if fail_on_violation {
let dead_code_percentage = result.summary.dead_percentage;
if dead_code_percentage > max_percentage as f32 {
eprintln!(
"\n❌ Dead code violations found: {dead_code_percentage:.1}% exceeds threshold of {max_percentage:.1}%"
);
std::process::exit(1);
}
}
Ok(())
}
include!("dead_code_handlers_analysis.rs");
include!("dead_code_handlers_output.rs");
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod output_tests {
use super::*;
use crate::models::dead_code::{
ConfidenceLevel, DeadCodeItem, DeadCodeResult, DeadCodeSummary, DeadCodeType,
FileDeadCodeMetrics,
};
fn item(ty: DeadCodeType, line: u32, name: &str, reason: &str) -> DeadCodeItem {
DeadCodeItem {
item_type: ty,
name: name.to_string(),
line,
reason: reason.to_string(),
}
}
fn file(path: &str, conf: ConfidenceLevel, items: Vec<DeadCodeItem>) -> FileDeadCodeMetrics {
FileDeadCodeMetrics {
path: path.to_string(),
dead_lines: 10,
total_lines: 100,
dead_percentage: 10.0,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 0.0,
confidence: conf,
items,
}
}
fn empty_summary() -> DeadCodeSummary {
DeadCodeSummary {
total_files_analyzed: 5,
files_with_dead_code: 0,
total_dead_lines: 0,
dead_percentage: 0.0,
dead_functions: 0,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
}
}
fn full_summary() -> DeadCodeSummary {
DeadCodeSummary {
total_files_analyzed: 5,
files_with_dead_code: 2,
total_dead_lines: 20,
dead_percentage: 12.0,
dead_functions: 3,
dead_classes: 2,
dead_modules: 1,
unreachable_blocks: 4,
}
}
fn populated_result() -> DeadCodeResult {
DeadCodeResult {
summary: full_summary(),
files: vec![
file(
"src/a.rs",
ConfidenceLevel::High,
vec![
item(DeadCodeType::Function, 10, "f", "no callers"),
item(DeadCodeType::Class, 20, "C", "unused"),
],
),
file(
"src/b.rs",
ConfidenceLevel::Medium,
vec![item(DeadCodeType::Variable, 5, "x", "never read")],
),
file(
"src/c.rs",
ConfidenceLevel::Low,
vec![item(
DeadCodeType::UnreachableCode,
99,
"block",
"after panic",
)],
),
],
total_files: 5,
analyzed_files: 5,
files_with_dead_code_found: 2,
files_truncated: false,
}
}
fn empty_result() -> DeadCodeResult {
DeadCodeResult {
summary: empty_summary(),
files: vec![],
total_files: 5,
analyzed_files: 5,
files_with_dead_code_found: 2,
files_truncated: false,
}
}
#[test]
fn test_format_dispatcher_json_arm() {
let r = format_dead_code_result(&empty_result(), &DeadCodeOutputFormat::Json).unwrap();
assert!(r.contains("summary") || r.contains("files"));
}
#[test]
fn test_format_dispatcher_sarif_arm() {
let r = format_dead_code_result(&populated_result(), &DeadCodeOutputFormat::Sarif).unwrap();
assert!(r.contains("\"version\": \"2.1.0\""));
assert!(r.contains("dead-code"));
}
#[test]
fn test_format_dispatcher_summary_arm() {
let r =
format_dead_code_result(&populated_result(), &DeadCodeOutputFormat::Summary).unwrap();
assert!(!r.is_empty());
}
#[test]
fn test_format_dispatcher_markdown_arm() {
let r =
format_dead_code_result(&populated_result(), &DeadCodeOutputFormat::Markdown).unwrap();
assert!(r.contains("# Dead Code Analysis Report"));
}
#[test]
fn test_sarif_levels_for_each_confidence() {
let r = format_dead_code_as_sarif(&populated_result()).unwrap();
assert!(r.contains("\"error\""));
assert!(r.contains("\"warning\""));
assert!(r.contains("\"note\""));
}
#[test]
fn test_sarif_message_for_each_dead_code_type() {
let r = format_dead_code_as_sarif(&populated_result()).unwrap();
assert!(r.contains("Dead function"));
assert!(r.contains("Dead class"));
assert!(r.contains("Dead variable"));
assert!(r.contains("Unreachable code"));
}
#[test]
fn test_sarif_empty_files_yields_empty_results_array() {
let r = format_dead_code_as_sarif(&empty_result()).unwrap();
assert!(r.contains("\"results\": []"));
}
#[test]
fn test_summary_with_dead_functions_emits_breakdown_section() {
let r = format_dead_code_as_summary(&populated_result()).unwrap();
assert!(r.contains("Dead Code by Type"));
assert!(r.contains("Top Files"));
}
#[test]
fn test_summary_without_dead_functions_still_breaks_down_by_type() {
let mut res = populated_result();
res.summary.dead_functions = 0;
let r = format_dead_code_as_summary(&res).unwrap();
assert!(r.contains("Dead Code by Type"));
assert!(r.contains("Other (fields, constants, statics):"));
assert!(r.contains("Top Files"));
}
#[test]
fn test_summary_empty_files_skips_top_files_section() {
let r = format_dead_code_as_summary(&empty_result()).unwrap();
assert!(!r.contains("Top Files"));
}
#[test]
fn test_markdown_with_full_data_emits_all_sections() {
let r = format_dead_code_as_markdown(&populated_result()).unwrap();
assert!(r.contains("# Dead Code Analysis Report"));
assert!(r.contains("## Summary"));
assert!(r.contains("## Dead Code Breakdown"));
assert!(r.contains("## File Details"));
assert!(r.contains("## Recommendations"));
}
#[test]
fn test_markdown_empty_skips_breakdown_and_files() {
let r = format_dead_code_as_markdown(&empty_result()).unwrap();
assert!(r.contains("## Summary"));
assert!(r.contains("## Recommendations"));
assert!(!r.contains("## Dead Code Breakdown"));
assert!(!r.contains("## File Details"));
}
#[test]
fn test_markdown_file_details_section_takes_first_20_files() {
let mut res = populated_result();
for i in 0..30 {
res.files.push(file(
&format!("src/extra-{i}.rs"),
ConfidenceLevel::High,
vec![],
));
}
let r = format_dead_code_as_markdown(&res).unwrap();
assert!(r.contains("src/a.rs"));
assert!(!r.contains("src/extra-29.rs"));
}
#[test]
fn test_summary_top_files_section_takes_first_10_files() {
let mut res = populated_result();
for i in 0..15 {
res.files.push(file(
&format!("src/extra-{i}.rs"),
ConfidenceLevel::High,
vec![],
));
}
let r = format_dead_code_as_summary(&res).unwrap();
assert!(!r.contains("src/extra-14.rs"));
}
#[test]
fn test_recommendations_section_is_static_text() {
let r = format_dead_code_recommendations_section();
assert!(r.contains("## Recommendations"));
assert!(r.contains("High Confidence Dead Code"));
assert!(r.contains("Test Coverage"));
}
use crate::services::cargo_dead_code_analyzer::{
DeadCodeKind, DeadItem, FileDeadCode as CargoFileDeadCode,
};
fn cargo_item(name: &str, kind: DeadCodeKind, line: usize) -> DeadItem {
DeadItem {
name: name.to_string(),
kind,
line,
column: 1,
message: format!("`{name}` is never used"),
}
}
#[test]
fn test_per_file_total_lines_is_the_measured_count() {
let files = vec![
CargoFileDeadCode {
file_path: std::path::PathBuf::from("src/big.rs"),
dead_items: vec![
cargo_item("f", DeadCodeKind::Function, 10),
cargo_item("S", DeadCodeKind::Struct, 20),
],
file_dead_percentage: 8.0 / 370.0 * 100.0,
total_lines: Some(370),
},
CargoFileDeadCode {
file_path: std::path::PathBuf::from("src/small.rs"),
dead_items: vec![cargo_item("g", DeadCodeKind::Function, 3)],
file_dead_percentage: 5.0 / 20.0 * 100.0,
total_lines: Some(20),
},
];
let metrics = convert_cargo_files_to_metrics(files, 0);
assert_eq!(metrics.len(), 2);
let by_path = |name: &str| {
metrics
.iter()
.find(|m| m.path.ends_with(name))
.unwrap_or_else(|| panic!("{name} missing"))
};
let big = by_path("big.rs");
let small = by_path("small.rs");
assert_eq!(big.total_lines, 370, "the constant 100 is the bug");
assert_eq!(small.total_lines, 20);
assert_ne!(
big.total_lines, small.total_lines,
"two files of different length cannot share one line count"
);
assert_eq!(big.dead_lines, 8);
assert_eq!(small.dead_lines, 5);
let expected = 8.0 / 370.0 * 100.0;
assert!(
(big.dead_percentage - expected).abs() < 0.01,
"dead_percentage {} does not match dead_lines/total_lines {expected}",
big.dead_percentage
);
assert_eq!(big.items.len(), 2);
}
#[test]
fn test_summary_agrees_with_the_list_it_heads() {
let listed = vec![
file("src/a.rs", ConfidenceLevel::High, vec![]),
file("src/b.rs", ConfidenceLevel::High, vec![]),
];
let mut summary = DeadCodeSummary {
total_files_analyzed: 4257,
files_with_dead_code: 26, total_dead_lines: 94, dead_percentage: 9.9,
dead_functions: 11,
dead_classes: 5,
dead_modules: 2,
unreachable_blocks: 0,
};
resummarize_from_listed_files(&mut summary, &listed, 10_000);
assert_eq!(summary.files_with_dead_code, listed.len());
assert_eq!(
summary.total_dead_lines,
listed.iter().map(|f| f.dead_lines).sum::<usize>()
);
assert_eq!(
summary.dead_functions,
listed.iter().map(|f| f.dead_functions).sum::<usize>()
);
assert!((summary.dead_percentage - 0.2).abs() < 1e-4);
assert!(summary.dead_percentage <= 100.0);
}
#[test]
fn test_empty_list_summarises_as_empty() {
let mut summary = DeadCodeSummary {
total_files_analyzed: 10,
files_with_dead_code: 1,
total_dead_lines: 40,
dead_percentage: 4.0,
dead_functions: 2,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
};
resummarize_from_listed_files(&mut summary, &[], 1000);
assert_eq!(summary.files_with_dead_code, 0);
assert_eq!(summary.total_dead_lines, 0);
assert_eq!(summary.dead_percentage, 0.0);
}
}