use std::collections::{HashSet, VecDeque};
use serde::{Deserialize, Serialize};
use crate::callgraph::types::{CallGraph, FunctionRef};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadCodeResult {
pub dead_functions: Vec<DeadFunction>,
pub total_dead: usize,
pub entry_points: Vec<String>,
pub filtered_count: usize,
pub stats: DeadCodeStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadFunction {
pub file: String,
pub name: String,
pub qualified_name: Option<String>,
pub line: Option<usize>,
pub reason: DeadReason,
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DeadReason {
Unreachable,
NeverCalled,
CalledOnlyByDead,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeadCodeStats {
pub total_functions: usize,
pub entry_point_count: usize,
pub reachable_count: usize,
pub filtered_as_callback: usize,
pub filtered_as_handler: usize,
pub filtered_as_decorator: usize,
pub filtered_as_dynamic: usize,
}
#[derive(Debug, Clone)]
pub struct DeadCodeConfig {
pub min_confidence: f64,
pub extra_entry_patterns: Vec<String>,
pub filter_patterns: Vec<String>,
pub language: Option<String>,
pub include_public_api_patterns: bool,
}
impl Default for DeadCodeConfig {
fn default() -> Self {
Self {
min_confidence: 0.7,
extra_entry_patterns: Vec::new(),
filter_patterns: Vec::new(),
language: None,
include_public_api_patterns: false, }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EntryPointKind {
Main,
Test,
CliHandler,
ApiEndpoint,
FrameworkHook,
PythonDunder,
}
pub fn detect_entry_points_with_config(graph: &CallGraph, config: &DeadCodeConfig) -> Vec<FunctionRef> {
let all_funcs = graph.all_functions();
let called: HashSet<_> = graph.edges.iter().map(|e| &e.callee).collect();
all_funcs
.iter()
.filter(|f| !called.contains(f) || is_definitely_entry_point(&f.name))
.filter(|f| is_likely_entry_point(&f.name, Some(config)))
.cloned()
.collect()
}
pub fn classify_entry_point(name: &str) -> Option<EntryPointKind> {
if name == "main" || name == "Main" || name == "__main__" {
return Some(EntryPointKind::Main);
}
if name.starts_with("test_")
|| name.starts_with("Test")
|| name.ends_with("_test")
|| name.ends_with("Test")
|| name.ends_with("Tests")
|| name.starts_with("spec_")
|| name.ends_with("_spec")
|| name.starts_with("it_")
|| name.starts_with("should_")
|| name == "setUp"
|| name == "tearDown"
|| name == "setUpClass"
|| name == "tearDownClass"
|| name == "beforeEach"
|| name == "afterEach"
|| name == "beforeAll"
|| name == "afterAll"
{
return Some(EntryPointKind::Test);
}
if name.starts_with("pytest_") {
return Some(EntryPointKind::FrameworkHook);
}
if name.starts_with("conftest_") {
return Some(EntryPointKind::FrameworkHook);
}
if name.starts_with("cmd_")
|| name.starts_with("handle_")
|| name.starts_with("run_")
|| name.starts_with("execute_")
|| name.starts_with("do_")
|| name.starts_with("action_")
|| name.starts_with("command_")
{
return Some(EntryPointKind::CliHandler);
}
if name.starts_with("api_")
|| name.starts_with("get_")
|| name.starts_with("post_")
|| name.starts_with("put_")
|| name.starts_with("delete_")
|| name.starts_with("patch_")
|| name.starts_with("list_")
|| name.starts_with("create_")
|| name.starts_with("update_")
|| name.starts_with("destroy_")
|| name.starts_with("index_")
|| name.starts_with("show_")
|| name.starts_with("new_")
|| name.starts_with("edit_")
{
return Some(EntryPointKind::ApiEndpoint);
}
if name == "setup"
|| name == "teardown"
|| name == "init"
|| name == "cleanup"
|| name == "configure"
|| name == "register"
|| name == "bootstrap"
|| name == "mount"
|| name == "unmount"
|| name == "render"
|| name == "componentDidMount"
|| name == "componentWillUnmount"
|| name == "ngOnInit"
|| name == "ngOnDestroy"
|| name == "created"
|| name == "mounted"
|| name == "destroyed"
{
return Some(EntryPointKind::FrameworkHook);
}
if name.starts_with("__") && name.ends_with("__") {
return Some(EntryPointKind::PythonDunder);
}
None
}
fn is_likely_entry_point(name: &str, config: Option<&DeadCodeConfig>) -> bool {
if classify_entry_point(name).is_some() || is_likely_callback(name) || is_likely_factory(name) {
return true;
}
if let Some(cfg) = config {
for pattern in &cfg.extra_entry_patterns {
if name.contains(pattern) {
return true;
}
}
if cfg.include_public_api_patterns && is_likely_public_api(name) {
return true;
}
}
false
}
fn is_definitely_entry_point(name: &str) -> bool {
name == "main"
|| name == "Main"
|| name == "__main__"
|| name == "app"
|| name == "start"
|| name == "run"
}
fn is_likely_callback(name: &str) -> bool {
name.starts_with("on_")
|| (name.starts_with("on")
&& name.len() > 2
&& name.chars().nth(2).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
|| (name.starts_with("On")
&& name.len() > 2
&& name.chars().nth(2).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
|| name.ends_with("_callback")
|| name.ends_with("Callback")
|| name.ends_with("_handler")
|| name.ends_with("Handler")
|| name.ends_with("_listener")
|| name.ends_with("Listener")
|| (name.starts_with("handle")
&& name.len() > 6
&& name.chars().nth(6).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
|| name.starts_with("handle_")
|| name.contains("Callback")
|| name.contains("Handler")
}
fn is_likely_factory(name: &str) -> bool {
name.starts_with("create_")
|| name.starts_with("make_")
|| name.starts_with("build_")
|| name.starts_with("new_")
|| name.ends_with("_factory")
|| name.ends_with("Factory")
|| name.starts_with("Create")
|| name.starts_with("Make")
|| name.starts_with("Build")
|| name.starts_with("New")
}
fn is_likely_public_api(name: &str) -> bool {
if name.starts_with('_') {
return false;
}
let is_getter = name.starts_with("get_")
|| (name.starts_with("get")
&& name.len() > 3
&& name.chars().nth(3).map(|c| c.is_uppercase()).unwrap_or(false));
let is_setter = name.starts_with("set_")
|| (name.starts_with("set")
&& name.len() > 3
&& name.chars().nth(3).map(|c| c.is_uppercase()).unwrap_or(false));
let is_boolean_accessor = name.starts_with("is_")
|| name.starts_with("has_")
|| name.starts_with("can_")
|| name.starts_with("should_");
let is_builder = name.starts_with("with_")
|| name.starts_with("from_")
|| name.starts_with("to_")
|| name.starts_with("as_");
let is_public_by_case = name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false);
is_getter || is_setter || is_boolean_accessor || is_builder || is_public_by_case
}
#[allow(dead_code)]
pub fn analyze_dead_code(graph: &CallGraph) -> DeadCodeResult {
analyze_dead_code_with_config(graph, &DeadCodeConfig::default())
}
pub fn analyze_dead_code_with_config(graph: &CallGraph, config: &DeadCodeConfig) -> DeadCodeResult {
let entry_points = detect_entry_points_with_config(graph, config);
let all_funcs = graph.all_functions();
let mut stats = DeadCodeStats {
total_functions: all_funcs.len(),
entry_point_count: entry_points.len(),
..Default::default()
};
let reachable = compute_reachability(graph, &entry_points);
stats.reachable_count = reachable.len();
let mut potentially_dead: Vec<_> = all_funcs
.difference(&reachable)
.filter(|f| !entry_points.contains(f))
.cloned()
.collect();
let mut dead_functions = Vec::new();
let mut filtered_count = 0;
for func in potentially_dead.drain(..) {
let (is_false_positive, filter_reason) = check_false_positive(&func, config);
if is_false_positive {
filtered_count += 1;
match filter_reason.as_str() {
"callback" => stats.filtered_as_callback += 1,
"handler" => stats.filtered_as_handler += 1,
"decorator" => stats.filtered_as_decorator += 1,
"dynamic" => stats.filtered_as_dynamic += 1,
_ => {}
}
continue;
}
let confidence = compute_confidence(&func, graph, &reachable);
if confidence >= config.min_confidence {
let reason = determine_dead_reason(&func, graph, &reachable);
dead_functions.push(DeadFunction {
file: func.file.clone(),
name: func.name.clone(),
qualified_name: func.qualified_name.clone(),
line: None, reason,
confidence,
});
}
}
DeadCodeResult {
total_dead: dead_functions.len(),
dead_functions,
entry_points: entry_points.iter().map(|f| f.name.clone()).collect(),
filtered_count,
stats,
}
}
fn compute_reachability(graph: &CallGraph, entry_points: &[FunctionRef]) -> HashSet<FunctionRef> {
use std::collections::HashMap;
let all_funcs: Vec<FunctionRef> = graph.all_functions().iter().cloned().collect();
if all_funcs.is_empty() {
return HashSet::new();
}
let func_to_idx: HashMap<&FunctionRef, usize> = all_funcs
.iter()
.enumerate()
.map(|(i, f)| (f, i))
.collect();
let mut visited = vec![false; all_funcs.len()];
let mut queue: VecDeque<usize> = entry_points
.iter()
.filter_map(|f| func_to_idx.get(f).copied())
.collect();
while let Some(idx) = queue.pop_front() {
if !visited[idx] {
visited[idx] = true;
let func = &all_funcs[idx];
if let Some(callees) = graph.callees.get(func) {
for callee in callees {
if let Some(&callee_idx) = func_to_idx.get(callee) {
if !visited[callee_idx] {
queue.push_back(callee_idx);
}
}
}
}
}
}
visited
.into_iter()
.enumerate()
.filter(|(_, v)| *v)
.map(|(i, _)| all_funcs[i].clone())
.collect()
}
fn check_false_positive(func: &FunctionRef, config: &DeadCodeConfig) -> (bool, String) {
let name = &func.name;
if is_likely_callback(name) {
return (true, "callback".to_string());
}
if name.ends_with("_event") || name.ends_with("Event") {
return (true, "handler".to_string());
}
if name.starts_with("route_")
|| name.starts_with("endpoint_")
|| name.starts_with("task_")
|| name.starts_with("job_")
|| name.starts_with("signal_")
|| name.starts_with("hook_")
{
return (true, "decorator".to_string());
}
if name.starts_with("visit_")
|| name.starts_with("Visit")
|| name.starts_with("dispatch_")
|| name.starts_with("Dispatch")
|| name.contains("Strategy")
|| name.contains("Visitor")
{
return (true, "dynamic".to_string());
}
if name.starts_with("impl_") || is_protocol_method(name) {
return (true, "dynamic".to_string());
}
for pattern in &config.filter_patterns {
if name.contains(pattern) {
return (true, "user_filter".to_string());
}
}
(false, String::new())
}
fn is_protocol_method(name: &str) -> bool {
matches!(
name,
"next"
| "iter"
| "len"
| "hash"
| "eq"
| "cmp"
| "clone"
| "drop"
| "deref"
| "index"
| "call"
| "enter"
| "exit"
| "read"
| "write"
| "close"
| "flush"
| "seek"
| "accept"
| "connect"
| "bind"
| "listen"
| "send"
| "recv"
)
}
fn is_in_common_module_path(file_path: &str) -> bool {
const COMMON_PATHS: &[&str] = &[
"/api/",
"/public/",
"/lib/",
"/handlers/",
"/routes/",
"/controllers/",
"/endpoints/",
"/views/",
"/services/",
"/commands/",
];
for path in COMMON_PATHS {
if file_path.contains(path) {
return true;
}
}
false
}
fn compute_confidence(
func: &FunctionRef,
graph: &CallGraph,
reachable: &HashSet<FunctionRef>,
) -> f64 {
let mut confidence: f64 = 0.5; let name = &func.name;
if !graph.callers.contains_key(func) {
confidence += 0.2;
}
if let Some(callees) = graph.callees.get(func) {
let dead_callees = callees.iter().filter(|c| !reachable.contains(*c)).count();
if dead_callees > 0 {
confidence += 0.1 * (dead_callees.min(3) as f64);
}
}
if name.starts_with('_') && !name.starts_with("__") {
confidence += 0.1;
}
if !is_in_common_module_path(&func.file) {
confidence += 0.1;
}
if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
confidence -= 0.15;
}
if name.len() <= 3 {
confidence -= 0.1;
}
if is_likely_factory(name) {
confidence -= 0.15;
}
if func.file.contains("/api/") || func.file.contains("/public/") {
confidence -= 0.2;
}
confidence.clamp(0.0, 1.0)
}
fn determine_dead_reason(
func: &FunctionRef,
graph: &CallGraph,
reachable: &HashSet<FunctionRef>,
) -> DeadReason {
if !graph.callers.contains_key(func) {
return DeadReason::NeverCalled;
}
if let Some(callers) = graph.callers.get(func) {
let live_callers = callers.iter().filter(|c| reachable.contains(*c)).count();
if live_callers == 0 {
return DeadReason::CalledOnlyByDead;
}
}
DeadReason::Unreachable
}
#[cfg(test)]
mod tests {
use super::*;
use crate::callgraph::types::CallEdge;
fn create_test_graph() -> CallGraph {
let mut graph = CallGraph::default();
let main_ref = FunctionRef {
file: "main.py".to_string(),
name: "main".to_string(),
qualified_name: Some("main.main".to_string()),
};
let helper_ref = FunctionRef {
file: "main.py".to_string(),
name: "helper".to_string(),
qualified_name: Some("main.helper".to_string()),
};
let utility_ref = FunctionRef {
file: "utils.py".to_string(),
name: "utility".to_string(),
qualified_name: Some("utils.utility".to_string()),
};
let orphan_ref = FunctionRef {
file: "orphan.py".to_string(),
name: "orphan_func".to_string(),
qualified_name: Some("orphan.orphan_func".to_string()),
};
let test_ref = FunctionRef {
file: "test_main.py".to_string(),
name: "test_something".to_string(),
qualified_name: Some("test_main.test_something".to_string()),
};
let dead_island_ref = FunctionRef {
file: "dead.py".to_string(),
name: "dead_island".to_string(),
qualified_name: Some("dead.dead_island".to_string()),
};
let dead_helper_ref = FunctionRef {
file: "dead.py".to_string(),
name: "dead_helper".to_string(),
qualified_name: Some("dead.dead_helper".to_string()),
};
graph.edges.push(CallEdge {
caller: main_ref.clone(),
callee: helper_ref.clone(),
call_line: 5,
});
graph.edges.push(CallEdge {
caller: helper_ref.clone(),
callee: utility_ref.clone(),
call_line: 10,
});
graph.edges.push(CallEdge {
caller: test_ref.clone(),
callee: helper_ref.clone(),
call_line: 3,
});
graph.edges.push(CallEdge {
caller: dead_island_ref.clone(),
callee: dead_helper_ref.clone(),
call_line: 2,
});
graph.edges.push(CallEdge {
caller: orphan_ref.clone(),
callee: utility_ref.clone(),
call_line: 1,
});
graph.build_indexes();
graph
}
#[test]
fn test_detect_entry_points() {
let graph = create_test_graph();
let entry_points = detect_entry_points_with_config(&graph, &DeadCodeConfig::default());
let names: Vec<_> = entry_points.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"main"));
assert!(names.contains(&"test_something"));
}
#[test]
fn test_classify_entry_point() {
assert_eq!(classify_entry_point("main"), Some(EntryPointKind::Main));
assert_eq!(classify_entry_point("test_foo"), Some(EntryPointKind::Test));
assert_eq!(
classify_entry_point("cmd_deploy"),
Some(EntryPointKind::CliHandler)
);
assert_eq!(
classify_entry_point("api_users"),
Some(EntryPointKind::ApiEndpoint)
);
assert_eq!(
classify_entry_point("__init__"),
Some(EntryPointKind::PythonDunder)
);
assert_eq!(
classify_entry_point("setup"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(classify_entry_point("random_name"), None);
}
#[test]
fn test_classify_entry_point_pytest_hooks() {
assert_eq!(
classify_entry_point("pytest_configure"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_collection"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_runtest_setup"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_runtest_teardown"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_sessionstart"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_sessionfinish"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_addoption"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_collection_modifyitems"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("pytest_generate_tests"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("conftest_setup"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(
classify_entry_point("conftest_teardown"),
Some(EntryPointKind::FrameworkHook)
);
assert_eq!(classify_entry_point("test_pytest_works"), Some(EntryPointKind::Test));
assert_eq!(classify_entry_point("regular_function"), None);
}
#[test]
fn test_is_likely_callback() {
assert!(is_likely_callback("on_click"));
assert!(is_likely_callback("on_submit"));
assert!(is_likely_callback("onClick"));
assert!(is_likely_callback("onSubmit"));
assert!(is_likely_callback("OnClick"));
assert!(is_likely_callback("OnSubmit"));
assert!(is_likely_callback("button_callback"));
assert!(is_likely_callback("MyHandler"));
assert!(is_likely_callback("handleClick"));
assert!(is_likely_callback("handleSubmit"));
assert!(is_likely_callback("handle_click"));
assert!(is_likely_callback("event_listener"));
assert!(is_likely_callback("EventListener"));
assert!(is_likely_callback("MyCallback"));
assert!(!is_likely_callback("process_data"));
assert!(!is_likely_callback("calculate"));
}
#[test]
fn test_callback_detection_not_too_broad() {
assert!(!is_likely_callback("once"));
assert!(!is_likely_callback("online"));
assert!(!is_likely_callback("only"));
assert!(!is_likely_callback("ongoing"));
assert!(!is_likely_callback("onward"));
assert!(!is_likely_callback("onset"));
assert!(!is_likely_callback("Once"));
assert!(!is_likely_callback("Online"));
assert!(!is_likely_callback("Only"));
assert!(!is_likely_callback("Ongoing"));
assert!(!is_likely_callback("handler"));
assert!(!is_likely_callback("handling"));
assert!(!is_likely_callback("handled"));
assert!(!is_likely_callback("on"));
assert!(!is_likely_callback("On"));
assert!(is_likely_callback("onClick"));
assert!(is_likely_callback("on_click"));
assert!(is_likely_callback("OnClick"));
assert!(is_likely_callback("handleClick"));
assert!(is_likely_callback("handle_click"));
}
#[test]
fn test_is_likely_factory() {
assert!(is_likely_factory("create_user"));
assert!(is_likely_factory("make_config"));
assert!(is_likely_factory("build_query"));
assert!(is_likely_factory("UserFactory"));
assert!(!is_likely_factory("process_user"));
}
#[test]
fn test_is_likely_public_api() {
assert!(is_likely_public_api("get_user"));
assert!(is_likely_public_api("get_value"));
assert!(is_likely_public_api("get_config"));
assert!(is_likely_public_api("getUser"));
assert!(is_likely_public_api("getValue"));
assert!(is_likely_public_api("getConfig"));
assert!(is_likely_public_api("set_user"));
assert!(is_likely_public_api("set_value"));
assert!(is_likely_public_api("set_config"));
assert!(is_likely_public_api("setUser"));
assert!(is_likely_public_api("setValue"));
assert!(is_likely_public_api("setConfig"));
assert!(is_likely_public_api("is_valid"));
assert!(is_likely_public_api("has_data"));
assert!(is_likely_public_api("can_proceed"));
assert!(is_likely_public_api("should_retry"));
assert!(is_likely_public_api("with_timeout"));
assert!(is_likely_public_api("from_bytes"));
assert!(is_likely_public_api("to_string"));
assert!(is_likely_public_api("as_ref"));
assert!(is_likely_public_api("UserManager"));
assert!(is_likely_public_api("Config"));
}
#[test]
fn test_public_api_detection_not_too_broad() {
assert!(!is_likely_public_api("gettext"));
assert!(!is_likely_public_api("getter"));
assert!(!is_likely_public_api("getaway"));
assert!(!is_likely_public_api("getopt"));
assert!(!is_likely_public_api("getenv"));
assert!(!is_likely_public_api("settings"));
assert!(!is_likely_public_api("settle"));
assert!(!is_likely_public_api("setup"));
assert!(!is_likely_public_api("setter"));
assert!(!is_likely_public_api("setback"));
assert!(!is_likely_public_api("get"));
assert!(!is_likely_public_api("set"));
assert!(!is_likely_public_api("_get_value"));
assert!(!is_likely_public_api("_set_value"));
assert!(!is_likely_public_api("_private"));
assert!(!is_likely_public_api("process_data"));
assert!(!is_likely_public_api("calculate"));
assert!(!is_likely_public_api("helper"));
}
#[test]
fn test_analyze_dead_code() {
let graph = create_test_graph();
let result = analyze_dead_code(&graph);
assert!(result.total_dead > 0);
assert!(result.stats.entry_point_count > 0);
assert!(result.stats.reachable_count > 0);
}
#[test]
fn test_compute_reachability() {
let graph = create_test_graph();
let entry_points = detect_entry_points_with_config(&graph, &DeadCodeConfig::default());
let reachable = compute_reachability(&graph, &entry_points);
assert!(reachable.iter().any(|f| f.name == "main"));
assert!(reachable.iter().any(|f| f.name == "helper"));
assert!(reachable.iter().any(|f| f.name == "utility"));
assert!(reachable.iter().any(|f| f.name == "test_something"));
assert!(!reachable.iter().any(|f| f.name == "dead_island"));
}
#[test]
fn test_check_false_positive() {
let config = DeadCodeConfig::default();
let callback_func = FunctionRef {
file: "test.py".to_string(),
name: "on_click".to_string(),
qualified_name: None,
};
let (is_fp, reason) = check_false_positive(&callback_func, &config);
assert!(is_fp);
assert_eq!(reason, "callback");
let normal_func = FunctionRef {
file: "test.py".to_string(),
name: "process_data".to_string(),
qualified_name: None,
};
let (is_fp, _) = check_false_positive(&normal_func, &config);
assert!(!is_fp);
}
#[test]
fn test_check_false_positive_no_redundant_handler_detection() {
let config = DeadCodeConfig::default();
let false_positive_cases = ["Ongoing", "Online", "Once", "Onward", "Onset"];
for name in false_positive_cases {
let func = FunctionRef {
file: "test.py".to_string(),
name: name.to_string(),
qualified_name: None,
};
let (is_fp, reason) = check_false_positive(&func, &config);
assert!(
!is_fp,
"{} should NOT be a false positive, but got reason: {}",
name,
reason
);
}
let valid_callbacks = ["OnClick", "OnSubmit", "OnChange", "on_click", "onClick"];
for name in valid_callbacks {
let func = FunctionRef {
file: "test.py".to_string(),
name: name.to_string(),
qualified_name: None,
};
let (is_fp, reason) = check_false_positive(&func, &config);
assert!(
is_fp && reason == "callback",
"{} should be detected as callback, but got: is_fp={}, reason={}",
name,
is_fp,
reason
);
}
let event_handlers = ["user_event", "MouseEvent", "handle_event", "KeyboardEvent"];
for name in event_handlers {
let func = FunctionRef {
file: "test.py".to_string(),
name: name.to_string(),
qualified_name: None,
};
let (is_fp, _) = check_false_positive(&func, &config);
assert!(
is_fp,
"{} should be detected as false positive (event handler)",
name
);
}
}
#[test]
fn test_dead_reason_classification() {
let mut graph = CallGraph::default();
let caller = FunctionRef {
file: "a.py".to_string(),
name: "caller".to_string(),
qualified_name: None,
};
let callee = FunctionRef {
file: "a.py".to_string(),
name: "callee".to_string(),
qualified_name: None,
};
let orphan = FunctionRef {
file: "a.py".to_string(),
name: "orphan".to_string(),
qualified_name: None,
};
graph.edges.push(CallEdge {
caller: caller.clone(),
callee: callee.clone(),
call_line: 1,
});
graph.build_indexes();
let mut reachable = HashSet::new();
reachable.insert(caller.clone());
let reason = determine_dead_reason(&orphan, &graph, &reachable);
assert_eq!(reason, DeadReason::NeverCalled);
let empty_reachable = HashSet::new();
let reason = determine_dead_reason(&callee, &graph, &empty_reachable);
assert_eq!(reason, DeadReason::CalledOnlyByDead);
}
#[test]
fn test_config_min_confidence() {
let graph = create_test_graph();
let config = DeadCodeConfig {
min_confidence: 0.99,
..Default::default()
};
let result = analyze_dead_code_with_config(&graph, &config);
let config_low = DeadCodeConfig {
min_confidence: 0.1,
..Default::default()
};
let result_low = analyze_dead_code_with_config(&graph, &config_low);
assert!(result_low.total_dead >= result.total_dead);
}
#[test]
fn test_user_defined_filter_patterns() {
let graph = create_test_graph();
let config = DeadCodeConfig {
filter_patterns: vec!["orphan".to_string()],
..Default::default()
};
let result = analyze_dead_code_with_config(&graph, &config);
assert!(!result
.dead_functions
.iter()
.any(|f| f.name.contains("orphan")));
}
#[test]
fn test_include_public_api_patterns_opt_in() {
let default_config = DeadCodeConfig::default();
assert!(!default_config.include_public_api_patterns);
assert!(!is_likely_entry_point("UserManager", Some(&default_config)));
assert!(!is_likely_entry_point("Config", Some(&default_config)));
assert!(!is_likely_entry_point("DatabaseConnection", Some(&default_config)));
assert!(!is_likely_entry_point("getUser", Some(&default_config)));
assert!(!is_likely_entry_point("setValue", Some(&default_config)));
assert!(!is_likely_entry_point("getData", Some(&default_config)));
assert!(!is_likely_entry_point("setConfig", Some(&default_config)));
assert!(!is_likely_entry_point("is_valid", Some(&default_config)));
assert!(!is_likely_entry_point("has_data", Some(&default_config)));
assert!(!is_likely_entry_point("can_proceed", Some(&default_config)));
assert!(!is_likely_entry_point("from_bytes", Some(&default_config)));
assert!(!is_likely_entry_point("to_string", Some(&default_config)));
assert!(!is_likely_entry_point("with_timeout", Some(&default_config)));
assert!(!is_likely_entry_point("as_ref", Some(&default_config)));
let permissive_config = DeadCodeConfig {
include_public_api_patterns: true,
..Default::default()
};
assert!(is_likely_entry_point("UserManager", Some(&permissive_config)));
assert!(is_likely_entry_point("getUser", Some(&permissive_config)));
assert!(is_likely_entry_point("is_valid", Some(&permissive_config)));
assert!(is_likely_entry_point("from_bytes", Some(&permissive_config)));
assert!(is_likely_entry_point("to_string", Some(&permissive_config)));
assert!(is_likely_entry_point("main", Some(&default_config)));
assert!(is_likely_entry_point("test_something", Some(&default_config)));
assert!(is_likely_entry_point("onClick", Some(&default_config))); assert!(is_likely_entry_point("create_user", Some(&default_config))); assert!(is_likely_entry_point("get_user", Some(&default_config))); }
#[test]
fn test_extra_entry_patterns() {
let config = DeadCodeConfig {
extra_entry_patterns: vec!["plugin_".to_string(), "hook_".to_string()],
..Default::default()
};
assert!(is_likely_entry_point("plugin_load", Some(&config)));
assert!(is_likely_entry_point("plugin_unload", Some(&config)));
assert!(is_likely_entry_point("hook_before", Some(&config)));
assert!(is_likely_entry_point("hook_after", Some(&config)));
assert!(!is_likely_entry_point("plugin_load", Some(&DeadCodeConfig::default())));
assert!(!is_likely_entry_point("hook_before", Some(&DeadCodeConfig::default())));
assert!(!is_likely_entry_point("process_data", Some(&config)));
assert!(!is_likely_entry_point("helper", Some(&config)));
}
#[test]
fn test_balanced_confidence_scoring() {
let mut graph = CallGraph::default();
let short_func = FunctionRef {
file: "utils.py".to_string(),
name: "x".to_string(), qualified_name: None,
};
let pascal_case_func = FunctionRef {
file: "models.py".to_string(),
name: "UserManager".to_string(), qualified_name: None,
};
let private_func = FunctionRef {
file: "internal.py".to_string(),
name: "_helper".to_string(), qualified_name: None,
};
let api_func = FunctionRef {
file: "src/api/routes.py".to_string(),
name: "process_request".to_string(),
qualified_name: None,
};
let dead_caller = FunctionRef {
file: "dead.py".to_string(),
name: "dead_caller".to_string(),
qualified_name: None,
};
let dead_callee1 = FunctionRef {
file: "dead.py".to_string(),
name: "dead_callee1".to_string(),
qualified_name: None,
};
let dead_callee2 = FunctionRef {
file: "dead.py".to_string(),
name: "dead_callee2".to_string(),
qualified_name: None,
};
let dead_callee3 = FunctionRef {
file: "dead.py".to_string(),
name: "dead_callee3".to_string(),
qualified_name: None,
};
let factory_func = FunctionRef {
file: "factories.py".to_string(),
name: "create_user".to_string(), qualified_name: None,
};
graph.edges.push(CallEdge {
caller: dead_caller.clone(),
callee: dead_callee1.clone(),
call_line: 1,
});
graph.edges.push(CallEdge {
caller: dead_caller.clone(),
callee: dead_callee2.clone(),
call_line: 2,
});
graph.edges.push(CallEdge {
caller: dead_caller.clone(),
callee: dead_callee3.clone(),
call_line: 3,
});
graph.build_indexes();
let reachable = HashSet::new();
let short_conf = compute_confidence(&short_func, &graph, &reachable);
assert!(
short_conf < 0.8,
"Short function 'x' should have confidence < 0.8, got {}",
short_conf
);
assert!(
short_conf >= 0.6 && short_conf <= 0.75,
"Short function 'x' should have balanced confidence around 0.7, got {}",
short_conf
);
let pascal_conf = compute_confidence(&pascal_case_func, &graph, &reachable);
assert!(
pascal_conf < short_conf,
"PascalCase function should have lower confidence than short function"
);
let private_conf = compute_confidence(&private_func, &graph, &reachable);
assert!(
private_conf > short_conf,
"Private function should have higher confidence than short function"
);
let api_conf = compute_confidence(&api_func, &graph, &reachable);
assert!(
api_conf <= 0.55,
"Function in /api/ path should have confidence around 0.5, got {}",
api_conf
);
let dead_caller_conf = compute_confidence(&dead_caller, &graph, &reachable);
assert!(
dead_caller_conf >= 0.8,
"Function calling 3 dead functions should have confidence >= 0.8, got {}",
dead_caller_conf
);
let factory_conf = compute_confidence(&factory_func, &graph, &reachable);
assert!(
factory_conf < 0.7,
"Factory function should have confidence < 0.7, got {}",
factory_conf
);
let neutral_func = FunctionRef {
file: "src/lib/module.py".to_string(), name: "process".to_string(), qualified_name: None,
};
let neutral_conf = compute_confidence(&neutral_func, &graph, &reachable);
assert!(
neutral_conf >= 0.6 && neutral_conf <= 0.75,
"Neutral function should have confidence around 0.7, got {}",
neutral_conf
);
}
#[test]
fn test_is_in_common_module_path() {
assert!(is_in_common_module_path("/project/api/routes.py"));
assert!(is_in_common_module_path("src/public/index.html"));
assert!(is_in_common_module_path("/app/lib/utils.py"));
assert!(is_in_common_module_path("src/handlers/user.py"));
assert!(is_in_common_module_path("/project/routes/auth.ts"));
assert!(is_in_common_module_path("app/controllers/main.rb"));
assert!(is_in_common_module_path("src/endpoints/v1.py"));
assert!(is_in_common_module_path("/app/views/home.py"));
assert!(is_in_common_module_path("backend/services/auth.py"));
assert!(is_in_common_module_path("cli/commands/deploy.py"));
assert!(!is_in_common_module_path("src/utils/helper.py"));
assert!(!is_in_common_module_path("internal/processor.py"));
assert!(!is_in_common_module_path("core/database.py"));
assert!(!is_in_common_module_path("models/user.py"));
assert!(!is_in_common_module_path("tests/test_main.py"));
}
}