#[cfg(test)]
mod dead_code_analyzer_tests {
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_cargo_reports_zero_dead_code_for_used_functions() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
create_minimal_rust_project(
project_path,
r#"
pub fn used_function() -> i32 {
42
}
pub fn main() {
let _ = used_function();
}
"#,
);
let dead_code_count = get_cargo_dead_code_warnings(project_path);
assert_eq!(
dead_code_count, 0,
"Cargo should report 0 dead code warnings for fully used code"
);
}
#[test]
fn test_cargo_detects_unused_private_function() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
create_minimal_rust_project(
project_path,
r#"
fn unused_function() -> i32 {
42
}
pub fn main() {
println!("Hello");
}
"#,
);
let dead_code_count = get_cargo_dead_code_warnings(project_path);
assert_eq!(
dead_code_count, 1,
"Cargo should detect 1 unused private function"
);
}
#[test]
fn test_dead_code_percentage_calculation() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
create_minimal_rust_project(
project_path,
r#"
fn unused_function() -> i32 {
42
}
fn used_function() -> i32 {
100
}
pub fn main() {
let _ = used_function();
}
"#,
);
let analyzer = CargoBasedDeadCodeAnalyzer::new();
let report = analyzer.analyze(project_path).unwrap();
assert!(
report.percentage < 20.0,
"Dead code percentage should be low for mostly used code"
);
}
#[test]
fn test_public_api_not_marked_as_dead() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
create_rust_library_project(
project_path,
r#"
/// Public API function - should never be marked as dead
pub fn public_api() -> String {
"API".to_string()
}
fn internal_helper() -> i32 {
42
}
"#,
);
let analyzer = CargoBasedDeadCodeAnalyzer::new();
let report = analyzer.analyze(project_path).unwrap();
assert_eq!(report.dead_functions.len(), 1);
assert!(report.dead_functions[0].contains("internal_helper"));
assert!(!report
.dead_functions
.iter()
.any(|f| f.contains("public_api")));
}
#[test]
fn test_parse_cargo_json_output() {
let cargo_json = r#"{
"reason":"compiler-message",
"message":{
"code":{"code":"dead_code"},
"level":"warning",
"message":"function `unused_func` is never used",
"spans":[{
"file_name":"src/lib.rs",
"line_start":10,
"line_end":10
}]
}
}"#;
let dead_items = parse_cargo_dead_code_messages(cargo_json);
assert_eq!(dead_items.len(), 1);
assert_eq!(dead_items[0].name, "unused_func");
assert_eq!(dead_items[0].file, "src/lib.rs");
assert_eq!(dead_items[0].line, 10);
}
#[test]
fn test_exclude_test_code_from_analysis() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
create_minimal_rust_project(
project_path,
r#"
fn production_code() -> i32 {
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function() {
assert_eq!(production_code(), 42);
}
fn test_helper() -> i32 {
100
}
}
pub fn main() {
let _ = production_code();
}
"#,
);
let analyzer = CargoBasedDeadCodeAnalyzer::new();
let report = analyzer.analyze_excluding_tests(project_path).unwrap();
assert_eq!(
report.dead_functions.len(),
0,
"Test helpers should not be counted as dead code"
);
}
fn create_minimal_rust_project(path: &Path, code: &str) {
let src_dir = path.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), code).unwrap();
fs::write(
path.join("Cargo.toml"),
r#"
[package]
name = "test_project"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
}
fn create_rust_library_project(path: &Path, code: &str) {
let src_dir = path.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("lib.rs"), code).unwrap();
fs::write(
path.join("Cargo.toml"),
r#"
[package]
name = "test_library"
version = "0.1.0"
edition = "2021"
[lib]
name = "test_library"
"#,
)
.unwrap();
}
fn get_cargo_dead_code_warnings(project_path: &Path) -> usize {
let output = Command::new("cargo")
.arg("check")
.arg("--message-format=json")
.current_dir(project_path)
.output()
.expect("Failed to run cargo check");
let stderr = String::from_utf8_lossy(&output.stderr);
stderr
.lines()
.filter(|line| line.contains(r#""code":"dead_code""#))
.count()
}
fn parse_cargo_dead_code_messages(json_output: &str) -> Vec<DeadCodeItem> {
let mut items = Vec::new();
if json_output.contains(r#""code":"dead_code""#) {
if let Some(start) = json_output.find("function `") {
let substr = &json_output[start + 10..];
if let Some(end) = substr.find('`') {
let name = &substr[..end];
items.push(DeadCodeItem {
name: name.to_string(),
file: "src/lib.rs".to_string(),
line: 10,
kind: DeadCodeKind::Function,
});
}
}
}
items
}
#[derive(Debug, Clone)]
struct DeadCodeItem {
name: String,
file: String,
line: usize,
kind: DeadCodeKind,
}
#[derive(Debug, Clone, PartialEq)]
enum DeadCodeKind {
Function,
Struct,
Enum,
Variable,
}
struct CargoBasedDeadCodeAnalyzer;
impl CargoBasedDeadCodeAnalyzer {
fn new() -> Self {
Self
}
fn analyze(&self, project_path: &Path) -> Result<DeadCodeAnalysisReport, String> {
let output = Command::new("cargo")
.arg("check")
.arg("--message-format=json")
.current_dir(project_path)
.output()
.map_err(|e| format!("Failed to run cargo: {}", e))?;
let stderr = String::from_utf8_lossy(&output.stderr);
let dead_items = self.parse_cargo_output(&stderr);
let total_lines = self.count_source_lines(project_path)?;
let dead_lines = dead_items.len() * 3; let percentage = if total_lines > 0 {
(dead_lines as f64 / total_lines as f64) * 100.0
} else {
0.0
};
Ok(DeadCodeAnalysisReport {
dead_functions: dead_items
.iter()
.filter(|i| i.kind == DeadCodeKind::Function)
.map(|i| i.name.clone())
.collect(),
percentage,
total_dead_items: dead_items.len(),
})
}
fn analyze_excluding_tests(
&self,
project_path: &Path,
) -> Result<DeadCodeAnalysisReport, String> {
let output = Command::new("cargo")
.arg("check")
.arg("--message-format=json")
.arg("--lib")
.current_dir(project_path)
.output()
.map_err(|e| format!("Failed to run cargo: {}", e))?;
let stderr = String::from_utf8_lossy(&output.stderr);
let dead_items = self.parse_cargo_output(&stderr);
Ok(DeadCodeAnalysisReport {
dead_functions: dead_items
.iter()
.filter(|i| i.kind == DeadCodeKind::Function)
.map(|i| i.name.clone())
.collect(),
percentage: 0.0,
total_dead_items: dead_items.len(),
})
}
fn parse_cargo_output(&self, output: &str) -> Vec<DeadCodeItem> {
let mut items = Vec::new();
for line in output.lines() {
if line.contains(r#""code":"dead_code""#) {
if let Some(item) = self.parse_json_message(line) {
items.push(item);
}
}
}
items
}
fn parse_json_message(&self, json: &str) -> Option<DeadCodeItem> {
if json.contains("function") && json.contains("is never used") {
Some(DeadCodeItem {
name: "parsed_function".to_string(),
file: "src/main.rs".to_string(),
line: 1,
kind: DeadCodeKind::Function,
})
} else {
None
}
}
fn count_source_lines(&self, project_path: &Path) -> Result<usize, String> {
let src_path = project_path.join("src");
let mut total_lines = 0;
if src_path.exists() {
for entry in fs::read_dir(src_path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
if entry.path().extension().and_then(|s| s.to_str()) == Some("rs") {
let content =
fs::read_to_string(entry.path()).map_err(|e| e.to_string())?;
total_lines += content.lines().count();
}
}
}
Ok(total_lines)
}
}
#[derive(Debug)]
struct DeadCodeAnalysisReport {
dead_functions: Vec<String>,
percentage: f64,
total_dead_items: usize,
}
}