use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphStore;
use crate::models::{Finding, Severity};
use anyhow::Result;
use std::collections::HashSet;
use std::path::PathBuf;
use tracing::{debug, info};
use uuid::Uuid;
static ENTRY_POINTS: &[&str] = &[
"main",
"__main__",
"__init__",
"setUp",
"tearDown",
"run",
"detect",
"name",
"description",
"new",
"default",
"from",
"into",
"try_from",
"try_into",
"clone",
"fmt",
"eq",
"cmp",
"hash",
"drop",
"deref",
"serialize",
"deserialize",
];
static FRAMEWORK_AUTO_LOAD_PATTERNS: &[&str] = &[
"/page.tsx", "/page.ts", "/page.jsx", "/page.js",
"/layout.tsx", "/layout.ts", "/layout.jsx", "/layout.js",
"/loading.tsx", "/loading.ts",
"/error.tsx", "/error.ts",
"/not-found.tsx", "/not-found.ts",
"/template.tsx", "/template.ts",
"/route.tsx", "/route.ts",
"/pages/", "pages/",
"/routes/", "routes/",
"/plugins/", "plugins/",
"/routes.", "routes.",
"/app/", "app/",
];
static MAGIC_METHODS: &[&str] = &[
"__str__",
"__repr__",
"__enter__",
"__exit__",
"__call__",
"__len__",
"__iter__",
"__next__",
"__getitem__",
"__setitem__",
"__delitem__",
"__eq__",
"__ne__",
"__lt__",
"__le__",
"__gt__",
"__ge__",
"__hash__",
"__bool__",
"__add__",
"__sub__",
"__mul__",
"__truediv__",
"__floordiv__",
"__mod__",
"__pow__",
"__post_init__",
"__init_subclass__",
"__set_name__",
];
#[derive(Debug, Clone)]
pub struct DeadCodeThresholds {
pub base_confidence: f64,
pub max_results: usize,
}
impl Default for DeadCodeThresholds {
fn default() -> Self {
Self {
base_confidence: 0.70,
max_results: 100,
}
}
}
pub struct DeadCodeDetector {
config: DetectorConfig,
thresholds: DeadCodeThresholds,
entry_points: HashSet<String>,
magic_methods: HashSet<String>,
}
impl DeadCodeDetector {
pub fn new() -> Self {
Self::with_thresholds(DeadCodeThresholds::default())
}
pub fn with_thresholds(thresholds: DeadCodeThresholds) -> Self {
let entry_points: HashSet<String> = ENTRY_POINTS.iter().map(|s| s.to_string()).collect();
let magic_methods: HashSet<String> = MAGIC_METHODS.iter().map(|s| s.to_string()).collect();
Self {
config: DetectorConfig::new(),
thresholds,
entry_points,
magic_methods,
}
}
pub fn with_config(config: DetectorConfig) -> Self {
let thresholds = DeadCodeThresholds {
base_confidence: config.get_option_or("base_confidence", 0.70),
max_results: config.get_option_or("max_results", 100),
};
Self::with_thresholds(thresholds)
}
fn is_entry_point(&self, name: &str) -> bool {
self.entry_points.contains(name) || name.starts_with("test_")
}
fn is_framework_auto_load(&self, file_path: &str) -> bool {
FRAMEWORK_AUTO_LOAD_PATTERNS.iter().any(|pattern| file_path.contains(pattern))
}
fn is_framework_export(&self, name: &str, file_path: &str) -> bool {
if self.is_framework_auto_load(file_path) && name == "default" {
return true;
}
if name.ends_with("Screen") || name.ends_with("Page") {
return true;
}
if matches!(name, "loader" | "action" | "meta" | "links" | "headers" |
"generateStaticParams" | "generateMetadata" | "revalidate" |
"GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS") {
return true;
}
if file_path.contains("/routes/") || file_path.starts_with("routes/") {
return true;
}
false
}
fn is_magic_method(&self, name: &str) -> bool {
self.magic_methods.contains(name)
}
fn should_filter(&self, name: &str, is_method: bool, has_decorators: bool) -> bool {
if self.is_magic_method(name) {
return true;
}
if self.is_entry_point(name) {
return true;
}
if is_method && !name.starts_with('_') {
return true;
}
if has_decorators {
return true;
}
let filter_patterns = [
"handle",
"on_",
"callback",
"load_data",
"loader",
"_loader",
"load_",
"create_",
"build_",
"make_",
"_parse_",
"_process_",
"load_config",
"generate_",
"validate_",
"setup_",
"initialize_",
"to_dict",
"to_json",
"from_dict",
"from_json",
"serialize",
"deserialize",
"_side_effect",
"_effect",
"_extract_",
"_find_",
"_calculate_",
"_get_",
"_set_",
"_check_",
"with_", "into_",
"as_",
"is_",
"has_",
"can_",
"should_",
"try_",
"parse_",
"render_",
"run_",
"execute_",
"process_",
"extract_",
];
let name_lower = name.to_lowercase();
for pattern in filter_patterns {
if name_lower.contains(pattern) {
return true;
}
}
false
}
fn calculate_function_severity(&self, complexity: usize) -> Severity {
if complexity >= 20 {
Severity::High
} else if complexity >= 10 {
Severity::Medium
} else {
Severity::Low
}
}
fn calculate_class_severity(&self, method_count: usize, complexity: usize) -> Severity {
if method_count >= 10 || complexity >= 50 {
Severity::High
} else if method_count >= 5 || complexity >= 20 {
Severity::Medium
} else {
Severity::Low
}
}
fn create_function_finding(
&self,
_qualified_name: String,
name: String,
file_path: String,
line_start: Option<u32>,
complexity: usize,
) -> Finding {
let severity = self.calculate_function_severity(complexity);
let confidence = self.thresholds.base_confidence;
Finding {
id: Uuid::new_v4().to_string(),
detector: "DeadCodeDetector".to_string(),
severity,
title: format!("Unused function: {}", name),
description: format!(
"Function '{}' is never called in the codebase. \
It has complexity {}.\n\n\
**Confidence:** {:.0}% (graph analysis only)\n\
**Recommendation:** Review before removing",
name,
complexity,
confidence * 100.0
),
affected_files: vec![PathBuf::from(&file_path)],
line_start,
line_end: None,
suggested_fix: Some(format!(
"**REVIEW REQUIRED** (confidence: {:.0}%)\n\
1. Remove the function from {}\n\
2. Check for dynamic calls (getattr, eval) that might use it\n\
3. Verify it's not an API endpoint or callback",
confidence * 100.0,
file_path.split('/').last().unwrap_or(&file_path)
)),
estimated_effort: Some("Small (30-60 minutes)".to_string()),
category: Some("dead_code".to_string()),
cwe_id: Some("CWE-561".to_string()), why_it_matters: Some(
"Dead code increases maintenance burden, confuses developers, \
and can hide bugs. Removing unused code improves readability \
and reduces the codebase size."
.to_string(),
),
}
}
fn create_class_finding(
&self,
_qualified_name: String,
name: String,
file_path: String,
method_count: usize,
complexity: usize,
) -> Finding {
let severity = self.calculate_class_severity(method_count, complexity);
let confidence = self.thresholds.base_confidence;
let effort = if method_count >= 10 {
"Medium (2-4 hours)"
} else if method_count >= 5 {
"Small (1-2 hours)"
} else {
"Small (30 minutes)"
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "DeadCodeDetector".to_string(),
severity,
title: format!("Unused class: {}", name),
description: format!(
"Class '{}' is never instantiated or inherited from. \
It has {} methods and complexity {}.\n\n\
**Confidence:** {:.0}% (graph analysis only)\n\
**Recommendation:** Review before removing",
name,
method_count,
complexity,
confidence * 100.0
),
affected_files: vec![PathBuf::from(&file_path)],
line_start: None,
line_end: None,
suggested_fix: Some(format!(
"**REVIEW REQUIRED** (confidence: {:.0}%)\n\
1. Remove the class and its {} methods\n\
2. Check for dynamic instantiation (factory patterns, reflection)\n\
3. Verify it's not used in configuration or plugins",
confidence * 100.0,
method_count
)),
estimated_effort: Some(effort.to_string()),
category: Some("dead_code".to_string()),
cwe_id: Some("CWE-561".to_string()),
why_it_matters: Some(
"Unused classes bloat the codebase and increase cognitive load. \
They may also cause confusion about the system's actual behavior."
.to_string(),
),
}
}
fn find_dead_functions(&self, graph: &GraphStore) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
let functions = graph.get_functions();
let mut functions: Vec<_> = functions.into_iter().collect();
functions.sort_by(|a, b| {
b.complexity().unwrap_or(0).cmp(&a.complexity().unwrap_or(0))
});
for func in functions {
let name = &func.name;
let file_path = &func.file_path;
if name.starts_with("test_") || self.is_entry_point(name) {
continue;
}
if self.is_framework_export(name, file_path) {
continue;
}
let callers = graph.get_callers(&func.qualified_name);
if !callers.is_empty() {
continue; }
let is_method = func.get_bool("is_method").unwrap_or(false);
let has_decorators = func.get_bool("has_decorators").unwrap_or(false);
let is_exported = func.get_bool("is_exported").unwrap_or(false);
if is_exported && self.is_framework_auto_load(file_path) {
continue;
}
if self.should_filter(name, is_method, has_decorators) {
continue;
}
let complexity = func.complexity().unwrap_or(1) as usize;
let line_start = Some(func.line_start);
findings.push(self.create_function_finding(
func.qualified_name.clone(),
name.clone(),
func.file_path.clone(),
line_start,
complexity,
));
if findings.len() >= self.thresholds.max_results {
break;
}
}
Ok(findings)
}
fn find_dead_classes(&self, graph: &GraphStore) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
let classes = graph.get_classes();
let mut classes: Vec<_> = classes.into_iter().collect();
classes.sort_by(|a, b| {
b.complexity().unwrap_or(0).cmp(&a.complexity().unwrap_or(0))
});
for class in classes {
let name = &class.name;
let file_path = &class.file_path;
if name.ends_with("Error")
|| name.ends_with("Exception")
|| name.ends_with("Mixin")
|| name.contains("Mixin")
|| name.starts_with("Test")
|| name.ends_with("Test")
|| name == "ABC"
|| name == "Enum"
|| name == "Exception"
|| name == "BaseException"
{
continue;
}
if name.ends_with("Screen") || name.ends_with("Page") ||
name.ends_with("Layout") || name.ends_with("Component") ||
name.ends_with("Provider") || name.ends_with("Context") {
continue;
}
let is_exported = class.get_bool("is_exported").unwrap_or(false);
if is_exported && self.is_framework_auto_load(file_path) {
continue;
}
let callers = graph.get_callers(&class.qualified_name);
if !callers.is_empty() {
continue;
}
let children = graph.get_child_classes(&class.qualified_name);
if !children.is_empty() {
continue;
}
let class_file = class.file_path.to_lowercase();
let imports = graph.get_imports();
let file_is_imported = imports.iter().any(|(_, target)| {
let target_lower = target.to_lowercase();
class_file.ends_with(&target_lower) ||
target_lower.ends_with(&class_file.replace("/tmp/", "").replace("/home/", "")) ||
class_file.split('/').last() == target_lower.split('/').last()
});
if file_is_imported {
continue;
}
let is_public = !name.starts_with('_') &&
name.chars().next().map_or(false, |c| c.is_uppercase());
let is_test_file = class_file.contains("/test") || class_file.contains("_test.");
if is_public && !is_test_file {
continue;
}
let has_decorators = class.get_bool("has_decorators").unwrap_or(false);
if has_decorators {
continue;
}
let complexity = class.complexity().unwrap_or(1) as usize;
let method_count = class.get_i64("methodCount").unwrap_or(0) as usize;
findings.push(self.create_class_finding(
class.qualified_name.clone(),
name.clone(),
class.file_path.clone(),
method_count,
complexity,
));
if findings.len() >= 50 {
break;
}
}
Ok(findings)
}
}
impl Default for DeadCodeDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for DeadCodeDetector {
fn name(&self) -> &'static str {
"DeadCodeDetector"
}
fn description(&self) -> &'static str {
"Detects unused functions and classes"
}
fn category(&self) -> &'static str {
"dead_code"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn detect(&self, graph: &GraphStore) -> Result<Vec<Finding>> {
debug!("Starting dead code detection");
let mut findings = Vec::new();
let function_findings = self.find_dead_functions(graph)?;
findings.extend(function_findings);
let class_findings = self.find_dead_classes(graph)?;
findings.extend(class_findings);
findings.sort_by(|a, b| b.severity.cmp(&a.severity));
info!("DeadCodeDetector found {} dead code issues", findings.len());
Ok(findings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_points() {
let detector = DeadCodeDetector::new();
assert!(detector.is_entry_point("main"));
assert!(detector.is_entry_point("__init__"));
assert!(detector.is_entry_point("test_something"));
assert!(!detector.is_entry_point("my_function"));
}
#[test]
fn test_magic_methods() {
let detector = DeadCodeDetector::new();
assert!(detector.is_magic_method("__str__"));
assert!(detector.is_magic_method("__repr__"));
assert!(!detector.is_magic_method("my_method"));
}
#[test]
fn test_should_filter() {
let detector = DeadCodeDetector::new();
assert!(detector.should_filter("__str__", false, false));
assert!(detector.should_filter("main", false, false));
assert!(detector.should_filter("test_foo", false, false));
assert!(detector.should_filter("public_method", true, false));
assert!(!detector.should_filter("_private_method", true, false));
assert!(detector.should_filter("any_func", false, true));
assert!(detector.should_filter("load_config", false, false));
assert!(detector.should_filter("to_dict", false, false));
}
#[test]
fn test_severity() {
let detector = DeadCodeDetector::new();
assert_eq!(detector.calculate_function_severity(5), Severity::Low);
assert_eq!(detector.calculate_function_severity(10), Severity::Medium);
assert_eq!(detector.calculate_function_severity(25), Severity::High);
assert_eq!(detector.calculate_class_severity(3, 10), Severity::Low);
assert_eq!(detector.calculate_class_severity(5, 10), Severity::Medium);
assert_eq!(detector.calculate_class_severity(10, 10), Severity::High);
}
}