1use std::collections::{HashSet, VecDeque};
25
26use serde::{Deserialize, Serialize};
27
28use crate::callgraph::types::{CallGraph, FunctionRef};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DeadCodeResult {
33 pub dead_functions: Vec<DeadFunction>,
35 pub total_dead: usize,
37 pub entry_points: Vec<String>,
39 pub filtered_count: usize,
41 pub stats: DeadCodeStats,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DeadFunction {
48 pub file: String,
50 pub name: String,
52 pub qualified_name: Option<String>,
54 pub line: Option<usize>,
56 pub reason: DeadReason,
58 pub confidence: f64,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub enum DeadReason {
65 Unreachable,
67 NeverCalled,
69 CalledOnlyByDead,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct DeadCodeStats {
76 pub total_functions: usize,
78 pub entry_point_count: usize,
80 pub reachable_count: usize,
82 pub filtered_as_callback: usize,
84 pub filtered_as_handler: usize,
85 pub filtered_as_decorator: usize,
86 pub filtered_as_dynamic: usize,
87}
88
89#[derive(Debug, Clone)]
91pub struct DeadCodeConfig {
92 pub min_confidence: f64,
94 pub extra_entry_patterns: Vec<String>,
96 pub filter_patterns: Vec<String>,
98 pub language: Option<String>,
100 pub include_public_api_patterns: bool,
114}
115
116impl Default for DeadCodeConfig {
117 fn default() -> Self {
118 Self {
119 min_confidence: 0.7,
120 extra_entry_patterns: Vec::new(),
121 filter_patterns: Vec::new(),
122 language: None,
123 include_public_api_patterns: false, }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq)]
130pub enum EntryPointKind {
131 Main,
132 Test,
133 CliHandler,
134 ApiEndpoint,
135 FrameworkHook,
136 PythonDunder,
137}
138
139pub fn detect_entry_points_with_config(graph: &CallGraph, config: &DeadCodeConfig) -> Vec<FunctionRef> {
147 let all_funcs = graph.all_functions();
148 let called: HashSet<_> = graph.edges.iter().map(|e| &e.callee).collect();
149
150 all_funcs
151 .iter()
152 .filter(|f| !called.contains(f) || is_definitely_entry_point(&f.name))
153 .filter(|f| is_likely_entry_point(&f.name, Some(config)))
154 .cloned()
155 .collect()
156}
157
158pub fn classify_entry_point(name: &str) -> Option<EntryPointKind> {
160 if name == "main" || name == "Main" || name == "__main__" {
162 return Some(EntryPointKind::Main);
163 }
164
165 if name.starts_with("test_")
167 || name.starts_with("Test")
168 || name.ends_with("_test")
169 || name.ends_with("Test")
170 || name.ends_with("Tests")
171 || name.starts_with("spec_")
172 || name.ends_with("_spec")
173 || name.starts_with("it_")
174 || name.starts_with("should_")
175 || name == "setUp"
176 || name == "tearDown"
177 || name == "setUpClass"
178 || name == "tearDownClass"
179 || name == "beforeEach"
180 || name == "afterEach"
181 || name == "beforeAll"
182 || name == "afterAll"
183 {
184 return Some(EntryPointKind::Test);
185 }
186
187 if name.starts_with("pytest_") {
190 return Some(EntryPointKind::FrameworkHook);
191 }
192
193 if name.starts_with("conftest_") {
195 return Some(EntryPointKind::FrameworkHook);
196 }
197
198 if name.starts_with("cmd_")
200 || name.starts_with("handle_")
201 || name.starts_with("run_")
202 || name.starts_with("execute_")
203 || name.starts_with("do_")
204 || name.starts_with("action_")
205 || name.starts_with("command_")
206 {
207 return Some(EntryPointKind::CliHandler);
208 }
209
210 if name.starts_with("api_")
212 || name.starts_with("get_")
213 || name.starts_with("post_")
214 || name.starts_with("put_")
215 || name.starts_with("delete_")
216 || name.starts_with("patch_")
217 || name.starts_with("list_")
218 || name.starts_with("create_")
219 || name.starts_with("update_")
220 || name.starts_with("destroy_")
221 || name.starts_with("index_")
222 || name.starts_with("show_")
223 || name.starts_with("new_")
224 || name.starts_with("edit_")
225 {
226 return Some(EntryPointKind::ApiEndpoint);
227 }
228
229 if name == "setup"
231 || name == "teardown"
232 || name == "init"
233 || name == "cleanup"
234 || name == "configure"
235 || name == "register"
236 || name == "bootstrap"
237 || name == "mount"
238 || name == "unmount"
239 || name == "render"
240 || name == "componentDidMount"
241 || name == "componentWillUnmount"
242 || name == "ngOnInit"
243 || name == "ngOnDestroy"
244 || name == "created"
245 || name == "mounted"
246 || name == "destroyed"
247 {
248 return Some(EntryPointKind::FrameworkHook);
249 }
250
251 if name.starts_with("__") && name.ends_with("__") {
253 return Some(EntryPointKind::PythonDunder);
254 }
255
256 None
257}
258
259fn is_likely_entry_point(name: &str, config: Option<&DeadCodeConfig>) -> bool {
264 if classify_entry_point(name).is_some() || is_likely_callback(name) || is_likely_factory(name) {
266 return true;
267 }
268
269 if let Some(cfg) = config {
271 for pattern in &cfg.extra_entry_patterns {
272 if name.contains(pattern) {
273 return true;
274 }
275 }
276
277 if cfg.include_public_api_patterns && is_likely_public_api(name) {
280 return true;
281 }
282 }
283
284 false
285}
286
287fn is_definitely_entry_point(name: &str) -> bool {
289 name == "main"
290 || name == "Main"
291 || name == "__main__"
292 || name == "app"
293 || name == "start"
294 || name == "run"
295}
296
297fn is_likely_callback(name: &str) -> bool {
302 name.starts_with("on_")
304 || (name.starts_with("on")
307 && name.len() > 2
308 && name.chars().nth(2).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
309 || (name.starts_with("On")
312 && name.len() > 2
313 && name.chars().nth(2).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
314 || name.ends_with("_callback")
315 || name.ends_with("Callback")
316 || name.ends_with("_handler")
317 || name.ends_with("Handler")
318 || name.ends_with("_listener")
319 || name.ends_with("Listener")
320 || (name.starts_with("handle")
323 && name.len() > 6
324 && name.chars().nth(6).map(|c| c.is_ascii_uppercase()).unwrap_or(false))
325 || name.starts_with("handle_")
327 || name.contains("Callback")
328 || name.contains("Handler")
329}
330
331fn is_likely_factory(name: &str) -> bool {
333 name.starts_with("create_")
334 || name.starts_with("make_")
335 || name.starts_with("build_")
336 || name.starts_with("new_")
337 || name.ends_with("_factory")
338 || name.ends_with("Factory")
339 || name.starts_with("Create")
340 || name.starts_with("Make")
341 || name.starts_with("Build")
342 || name.starts_with("New")
343}
344
345fn is_likely_public_api(name: &str) -> bool {
350 if name.starts_with('_') {
351 return false;
352 }
353
354 let is_getter = name.starts_with("get_")
357 || (name.starts_with("get")
358 && name.len() > 3
359 && name.chars().nth(3).map(|c| c.is_uppercase()).unwrap_or(false));
360
361 let is_setter = name.starts_with("set_")
364 || (name.starts_with("set")
365 && name.len() > 3
366 && name.chars().nth(3).map(|c| c.is_uppercase()).unwrap_or(false));
367
368 let is_boolean_accessor = name.starts_with("is_")
370 || name.starts_with("has_")
371 || name.starts_with("can_")
372 || name.starts_with("should_");
373
374 let is_builder = name.starts_with("with_")
376 || name.starts_with("from_")
377 || name.starts_with("to_")
378 || name.starts_with("as_");
379
380 let is_public_by_case = name
382 .chars()
383 .next()
384 .map(|c| c.is_uppercase())
385 .unwrap_or(false);
386
387 is_getter || is_setter || is_boolean_accessor || is_builder || is_public_by_case
388}
389
390#[allow(dead_code)]
392pub fn analyze_dead_code(graph: &CallGraph) -> DeadCodeResult {
393 analyze_dead_code_with_config(graph, &DeadCodeConfig::default())
394}
395
396pub fn analyze_dead_code_with_config(graph: &CallGraph, config: &DeadCodeConfig) -> DeadCodeResult {
398 let entry_points = detect_entry_points_with_config(graph, config);
399 let all_funcs = graph.all_functions();
400
401 let mut stats = DeadCodeStats {
402 total_functions: all_funcs.len(),
403 entry_point_count: entry_points.len(),
404 ..Default::default()
405 };
406
407 let reachable = compute_reachability(graph, &entry_points);
409 stats.reachable_count = reachable.len();
410
411 let mut potentially_dead: Vec<_> = all_funcs
413 .difference(&reachable)
414 .filter(|f| !entry_points.contains(f))
415 .cloned()
416 .collect();
417
418 let mut dead_functions = Vec::new();
420 let mut filtered_count = 0;
421
422 for func in potentially_dead.drain(..) {
423 let (is_false_positive, filter_reason) = check_false_positive(&func, config);
424
425 if is_false_positive {
426 filtered_count += 1;
427 match filter_reason.as_str() {
428 "callback" => stats.filtered_as_callback += 1,
429 "handler" => stats.filtered_as_handler += 1,
430 "decorator" => stats.filtered_as_decorator += 1,
431 "dynamic" => stats.filtered_as_dynamic += 1,
432 _ => {}
433 }
434 continue;
435 }
436
437 let confidence = compute_confidence(&func, graph, &reachable);
438
439 if confidence >= config.min_confidence {
440 let reason = determine_dead_reason(&func, graph, &reachable);
441
442 dead_functions.push(DeadFunction {
443 file: func.file.clone(),
444 name: func.name.clone(),
445 qualified_name: func.qualified_name.clone(),
446 line: None, reason,
448 confidence,
449 });
450 }
451 }
452
453 DeadCodeResult {
454 total_dead: dead_functions.len(),
455 dead_functions,
456 entry_points: entry_points.iter().map(|f| f.name.clone()).collect(),
457 filtered_count,
458 stats,
459 }
460}
461
462fn compute_reachability(graph: &CallGraph, entry_points: &[FunctionRef]) -> HashSet<FunctionRef> {
471 use std::collections::HashMap;
472
473 let all_funcs: Vec<FunctionRef> = graph.all_functions().iter().cloned().collect();
475 if all_funcs.is_empty() {
476 return HashSet::new();
477 }
478
479 let func_to_idx: HashMap<&FunctionRef, usize> = all_funcs
480 .iter()
481 .enumerate()
482 .map(|(i, f)| (f, i))
483 .collect();
484
485 let mut visited = vec![false; all_funcs.len()];
487
488 let mut queue: VecDeque<usize> = entry_points
490 .iter()
491 .filter_map(|f| func_to_idx.get(f).copied())
492 .collect();
493
494 while let Some(idx) = queue.pop_front() {
496 if !visited[idx] {
497 visited[idx] = true;
498 let func = &all_funcs[idx];
499
500 if let Some(callees) = graph.callees.get(func) {
502 for callee in callees {
503 if let Some(&callee_idx) = func_to_idx.get(callee) {
504 if !visited[callee_idx] {
505 queue.push_back(callee_idx);
506 }
507 }
508 }
509 }
510 }
511 }
512
513 visited
515 .into_iter()
516 .enumerate()
517 .filter(|(_, v)| *v)
518 .map(|(i, _)| all_funcs[i].clone())
519 .collect()
520}
521
522fn check_false_positive(func: &FunctionRef, config: &DeadCodeConfig) -> (bool, String) {
526 let name = &func.name;
527
528 if is_likely_callback(name) {
532 return (true, "callback".to_string());
533 }
534
535 if name.ends_with("_event") || name.ends_with("Event") {
538 return (true, "handler".to_string());
539 }
540
541 if name.starts_with("route_")
543 || name.starts_with("endpoint_")
544 || name.starts_with("task_")
545 || name.starts_with("job_")
546 || name.starts_with("signal_")
547 || name.starts_with("hook_")
548 {
549 return (true, "decorator".to_string());
550 }
551
552 if name.starts_with("visit_")
556 || name.starts_with("Visit")
557 || name.starts_with("dispatch_")
558 || name.starts_with("Dispatch")
559 || name.contains("Strategy")
560 || name.contains("Visitor")
561 {
562 return (true, "dynamic".to_string());
563 }
564
565 if name.starts_with("impl_") || is_protocol_method(name) {
567 return (true, "dynamic".to_string());
568 }
569
570 for pattern in &config.filter_patterns {
572 if name.contains(pattern) {
573 return (true, "user_filter".to_string());
574 }
575 }
576
577 (false, String::new())
578}
579
580fn is_protocol_method(name: &str) -> bool {
582 matches!(
583 name,
584 "next"
585 | "iter"
586 | "len"
587 | "hash"
588 | "eq"
589 | "cmp"
590 | "clone"
591 | "drop"
592 | "deref"
593 | "index"
594 | "call"
595 | "enter"
596 | "exit"
597 | "read"
598 | "write"
599 | "close"
600 | "flush"
601 | "seek"
602 | "accept"
603 | "connect"
604 | "bind"
605 | "listen"
606 | "send"
607 | "recv"
608 )
609}
610
611fn is_in_common_module_path(file_path: &str) -> bool {
615 const COMMON_PATHS: &[&str] = &[
617 "/api/",
618 "/public/",
619 "/lib/",
620 "/handlers/",
621 "/routes/",
622 "/controllers/",
623 "/endpoints/",
624 "/views/",
625 "/services/",
626 "/commands/",
627 ];
628
629 for path in COMMON_PATHS {
630 if file_path.contains(path) {
631 return true;
632 }
633 }
634
635 false
636}
637
638fn compute_confidence(
658 func: &FunctionRef,
659 graph: &CallGraph,
660 reachable: &HashSet<FunctionRef>,
661) -> f64 {
662 let mut confidence: f64 = 0.5; let name = &func.name;
664
665 if !graph.callers.contains_key(func) {
669 confidence += 0.2;
670 }
671
672 if let Some(callees) = graph.callees.get(func) {
674 let dead_callees = callees.iter().filter(|c| !reachable.contains(*c)).count();
675 if dead_callees > 0 {
676 confidence += 0.1 * (dead_callees.min(3) as f64);
678 }
679 }
680
681 if name.starts_with('_') && !name.starts_with("__") {
684 confidence += 0.1;
685 }
686
687 if !is_in_common_module_path(&func.file) {
689 confidence += 0.1;
690 }
691
692 if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
696 confidence -= 0.15;
697 }
698
699 if name.len() <= 3 {
701 confidence -= 0.1;
702 }
703
704 if is_likely_factory(name) {
706 confidence -= 0.15;
707 }
708
709 if func.file.contains("/api/") || func.file.contains("/public/") {
711 confidence -= 0.2;
712 }
713
714 confidence.clamp(0.0, 1.0)
715}
716
717fn determine_dead_reason(
719 func: &FunctionRef,
720 graph: &CallGraph,
721 reachable: &HashSet<FunctionRef>,
722) -> DeadReason {
723 if !graph.callers.contains_key(func) {
725 return DeadReason::NeverCalled;
726 }
727
728 if let Some(callers) = graph.callers.get(func) {
730 let live_callers = callers.iter().filter(|c| reachable.contains(*c)).count();
731 if live_callers == 0 {
732 return DeadReason::CalledOnlyByDead;
733 }
734 }
735
736 DeadReason::Unreachable
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::callgraph::types::CallEdge;
743
744 fn create_test_graph() -> CallGraph {
745 let mut graph = CallGraph::default();
746
747 let main_ref = FunctionRef {
753 file: "main.py".to_string(),
754 name: "main".to_string(),
755 qualified_name: Some("main.main".to_string()),
756 };
757 let helper_ref = FunctionRef {
758 file: "main.py".to_string(),
759 name: "helper".to_string(),
760 qualified_name: Some("main.helper".to_string()),
761 };
762 let utility_ref = FunctionRef {
763 file: "utils.py".to_string(),
764 name: "utility".to_string(),
765 qualified_name: Some("utils.utility".to_string()),
766 };
767 let orphan_ref = FunctionRef {
768 file: "orphan.py".to_string(),
769 name: "orphan_func".to_string(),
770 qualified_name: Some("orphan.orphan_func".to_string()),
771 };
772 let test_ref = FunctionRef {
773 file: "test_main.py".to_string(),
774 name: "test_something".to_string(),
775 qualified_name: Some("test_main.test_something".to_string()),
776 };
777 let dead_island_ref = FunctionRef {
778 file: "dead.py".to_string(),
779 name: "dead_island".to_string(),
780 qualified_name: Some("dead.dead_island".to_string()),
781 };
782 let dead_helper_ref = FunctionRef {
783 file: "dead.py".to_string(),
784 name: "dead_helper".to_string(),
785 qualified_name: Some("dead.dead_helper".to_string()),
786 };
787
788 graph.edges.push(CallEdge {
789 caller: main_ref.clone(),
790 callee: helper_ref.clone(),
791 call_line: 5,
792 });
793 graph.edges.push(CallEdge {
794 caller: helper_ref.clone(),
795 callee: utility_ref.clone(),
796 call_line: 10,
797 });
798 graph.edges.push(CallEdge {
799 caller: test_ref.clone(),
800 callee: helper_ref.clone(),
801 call_line: 3,
802 });
803 graph.edges.push(CallEdge {
804 caller: dead_island_ref.clone(),
805 callee: dead_helper_ref.clone(),
806 call_line: 2,
807 });
808 graph.edges.push(CallEdge {
813 caller: orphan_ref.clone(),
814 callee: utility_ref.clone(),
815 call_line: 1,
816 });
817
818 graph.build_indexes();
819 graph
820 }
821
822 #[test]
823 fn test_detect_entry_points() {
824 let graph = create_test_graph();
825 let entry_points = detect_entry_points_with_config(&graph, &DeadCodeConfig::default());
826
827 let names: Vec<_> = entry_points.iter().map(|f| f.name.as_str()).collect();
829 assert!(names.contains(&"main"));
830 assert!(names.contains(&"test_something"));
831 }
832
833 #[test]
834 fn test_classify_entry_point() {
835 assert_eq!(classify_entry_point("main"), Some(EntryPointKind::Main));
836 assert_eq!(classify_entry_point("test_foo"), Some(EntryPointKind::Test));
837 assert_eq!(
838 classify_entry_point("cmd_deploy"),
839 Some(EntryPointKind::CliHandler)
840 );
841 assert_eq!(
842 classify_entry_point("api_users"),
843 Some(EntryPointKind::ApiEndpoint)
844 );
845 assert_eq!(
846 classify_entry_point("__init__"),
847 Some(EntryPointKind::PythonDunder)
848 );
849 assert_eq!(
850 classify_entry_point("setup"),
851 Some(EntryPointKind::FrameworkHook)
852 );
853 assert_eq!(classify_entry_point("random_name"), None);
854 }
855
856 #[test]
857 fn test_classify_entry_point_pytest_hooks() {
858 assert_eq!(
863 classify_entry_point("pytest_configure"),
864 Some(EntryPointKind::FrameworkHook)
865 );
866 assert_eq!(
867 classify_entry_point("pytest_collection"),
868 Some(EntryPointKind::FrameworkHook)
869 );
870 assert_eq!(
871 classify_entry_point("pytest_runtest_setup"),
872 Some(EntryPointKind::FrameworkHook)
873 );
874 assert_eq!(
875 classify_entry_point("pytest_runtest_teardown"),
876 Some(EntryPointKind::FrameworkHook)
877 );
878 assert_eq!(
879 classify_entry_point("pytest_sessionstart"),
880 Some(EntryPointKind::FrameworkHook)
881 );
882 assert_eq!(
883 classify_entry_point("pytest_sessionfinish"),
884 Some(EntryPointKind::FrameworkHook)
885 );
886 assert_eq!(
887 classify_entry_point("pytest_addoption"),
888 Some(EntryPointKind::FrameworkHook)
889 );
890 assert_eq!(
891 classify_entry_point("pytest_collection_modifyitems"),
892 Some(EntryPointKind::FrameworkHook)
893 );
894 assert_eq!(
895 classify_entry_point("pytest_generate_tests"),
896 Some(EntryPointKind::FrameworkHook)
897 );
898
899 assert_eq!(
901 classify_entry_point("conftest_setup"),
902 Some(EntryPointKind::FrameworkHook)
903 );
904 assert_eq!(
905 classify_entry_point("conftest_teardown"),
906 Some(EntryPointKind::FrameworkHook)
907 );
908
909 assert_eq!(classify_entry_point("test_pytest_works"), Some(EntryPointKind::Test));
911 assert_eq!(classify_entry_point("regular_function"), None);
912 }
913
914 #[test]
915 fn test_is_likely_callback() {
916 assert!(is_likely_callback("on_click"));
918 assert!(is_likely_callback("on_submit"));
919 assert!(is_likely_callback("onClick"));
920 assert!(is_likely_callback("onSubmit"));
921 assert!(is_likely_callback("OnClick"));
922 assert!(is_likely_callback("OnSubmit"));
923 assert!(is_likely_callback("button_callback"));
924 assert!(is_likely_callback("MyHandler"));
925 assert!(is_likely_callback("handleClick"));
926 assert!(is_likely_callback("handleSubmit"));
927 assert!(is_likely_callback("handle_click"));
928 assert!(is_likely_callback("event_listener"));
929 assert!(is_likely_callback("EventListener"));
930 assert!(is_likely_callback("MyCallback"));
931
932 assert!(!is_likely_callback("process_data"));
934 assert!(!is_likely_callback("calculate"));
935 }
936
937 #[test]
938 fn test_callback_detection_not_too_broad() {
939 assert!(!is_likely_callback("once"));
942 assert!(!is_likely_callback("online"));
943 assert!(!is_likely_callback("only"));
944 assert!(!is_likely_callback("ongoing"));
945 assert!(!is_likely_callback("onward"));
946 assert!(!is_likely_callback("onset"));
947
948 assert!(!is_likely_callback("Once"));
950 assert!(!is_likely_callback("Online"));
951 assert!(!is_likely_callback("Only"));
952 assert!(!is_likely_callback("Ongoing"));
953
954 assert!(!is_likely_callback("handler"));
957 assert!(!is_likely_callback("handling"));
958 assert!(!is_likely_callback("handled"));
959
960 assert!(!is_likely_callback("on"));
962 assert!(!is_likely_callback("On"));
963
964 assert!(is_likely_callback("onClick"));
966 assert!(is_likely_callback("on_click"));
967 assert!(is_likely_callback("OnClick"));
968 assert!(is_likely_callback("handleClick"));
969 assert!(is_likely_callback("handle_click"));
970 }
971
972 #[test]
973 fn test_is_likely_factory() {
974 assert!(is_likely_factory("create_user"));
975 assert!(is_likely_factory("make_config"));
976 assert!(is_likely_factory("build_query"));
977 assert!(is_likely_factory("UserFactory"));
978 assert!(!is_likely_factory("process_user"));
979 }
980
981 #[test]
982 fn test_is_likely_public_api() {
983 assert!(is_likely_public_api("get_user"));
987 assert!(is_likely_public_api("get_value"));
988 assert!(is_likely_public_api("get_config"));
989
990 assert!(is_likely_public_api("getUser"));
992 assert!(is_likely_public_api("getValue"));
993 assert!(is_likely_public_api("getConfig"));
994
995 assert!(is_likely_public_api("set_user"));
997 assert!(is_likely_public_api("set_value"));
998 assert!(is_likely_public_api("set_config"));
999
1000 assert!(is_likely_public_api("setUser"));
1002 assert!(is_likely_public_api("setValue"));
1003 assert!(is_likely_public_api("setConfig"));
1004
1005 assert!(is_likely_public_api("is_valid"));
1007 assert!(is_likely_public_api("has_data"));
1008 assert!(is_likely_public_api("can_proceed"));
1009 assert!(is_likely_public_api("should_retry"));
1010
1011 assert!(is_likely_public_api("with_timeout"));
1013 assert!(is_likely_public_api("from_bytes"));
1014 assert!(is_likely_public_api("to_string"));
1015 assert!(is_likely_public_api("as_ref"));
1016
1017 assert!(is_likely_public_api("UserManager"));
1019 assert!(is_likely_public_api("Config"));
1020 }
1021
1022 #[test]
1023 fn test_public_api_detection_not_too_broad() {
1024 assert!(!is_likely_public_api("gettext"));
1029 assert!(!is_likely_public_api("getter"));
1030 assert!(!is_likely_public_api("getaway"));
1031 assert!(!is_likely_public_api("getopt"));
1032 assert!(!is_likely_public_api("getenv")); assert!(!is_likely_public_api("settings"));
1036 assert!(!is_likely_public_api("settle"));
1037 assert!(!is_likely_public_api("setup"));
1038 assert!(!is_likely_public_api("setter"));
1039 assert!(!is_likely_public_api("setback"));
1040
1041 assert!(!is_likely_public_api("get"));
1043 assert!(!is_likely_public_api("set"));
1044
1045 assert!(!is_likely_public_api("_get_value"));
1047 assert!(!is_likely_public_api("_set_value"));
1048 assert!(!is_likely_public_api("_private"));
1049
1050 assert!(!is_likely_public_api("process_data"));
1052 assert!(!is_likely_public_api("calculate"));
1053 assert!(!is_likely_public_api("helper"));
1054 }
1055
1056 #[test]
1057 fn test_analyze_dead_code() {
1058 let graph = create_test_graph();
1059 let result = analyze_dead_code(&graph);
1060
1061 assert!(result.total_dead > 0);
1064
1065 assert!(result.stats.entry_point_count > 0);
1067 assert!(result.stats.reachable_count > 0);
1068 }
1069
1070 #[test]
1071 fn test_compute_reachability() {
1072 let graph = create_test_graph();
1073 let entry_points = detect_entry_points_with_config(&graph, &DeadCodeConfig::default());
1074 let reachable = compute_reachability(&graph, &entry_points);
1075
1076 assert!(reachable.iter().any(|f| f.name == "main"));
1079 assert!(reachable.iter().any(|f| f.name == "helper"));
1080 assert!(reachable.iter().any(|f| f.name == "utility"));
1081 assert!(reachable.iter().any(|f| f.name == "test_something"));
1082
1083 assert!(!reachable.iter().any(|f| f.name == "dead_island"));
1085 }
1086
1087 #[test]
1088 fn test_check_false_positive() {
1089 let config = DeadCodeConfig::default();
1090
1091 let callback_func = FunctionRef {
1092 file: "test.py".to_string(),
1093 name: "on_click".to_string(),
1094 qualified_name: None,
1095 };
1096 let (is_fp, reason) = check_false_positive(&callback_func, &config);
1097 assert!(is_fp);
1098 assert_eq!(reason, "callback");
1099
1100 let normal_func = FunctionRef {
1101 file: "test.py".to_string(),
1102 name: "process_data".to_string(),
1103 qualified_name: None,
1104 };
1105 let (is_fp, _) = check_false_positive(&normal_func, &config);
1106 assert!(!is_fp);
1107 }
1108
1109 #[test]
1110 fn test_check_false_positive_no_redundant_handler_detection() {
1111 let config = DeadCodeConfig::default();
1115
1116 let false_positive_cases = ["Ongoing", "Online", "Once", "Onward", "Onset"];
1119 for name in false_positive_cases {
1120 let func = FunctionRef {
1121 file: "test.py".to_string(),
1122 name: name.to_string(),
1123 qualified_name: None,
1124 };
1125 let (is_fp, reason) = check_false_positive(&func, &config);
1126 assert!(
1127 !is_fp,
1128 "{} should NOT be a false positive, but got reason: {}",
1129 name,
1130 reason
1131 );
1132 }
1133
1134 let valid_callbacks = ["OnClick", "OnSubmit", "OnChange", "on_click", "onClick"];
1136 for name in valid_callbacks {
1137 let func = FunctionRef {
1138 file: "test.py".to_string(),
1139 name: name.to_string(),
1140 qualified_name: None,
1141 };
1142 let (is_fp, reason) = check_false_positive(&func, &config);
1143 assert!(
1144 is_fp && reason == "callback",
1145 "{} should be detected as callback, but got: is_fp={}, reason={}",
1146 name,
1147 is_fp,
1148 reason
1149 );
1150 }
1151
1152 let event_handlers = ["user_event", "MouseEvent", "handle_event", "KeyboardEvent"];
1154 for name in event_handlers {
1155 let func = FunctionRef {
1156 file: "test.py".to_string(),
1157 name: name.to_string(),
1158 qualified_name: None,
1159 };
1160 let (is_fp, _) = check_false_positive(&func, &config);
1161 assert!(
1162 is_fp,
1163 "{} should be detected as false positive (event handler)",
1164 name
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn test_dead_reason_classification() {
1171 let mut graph = CallGraph::default();
1172
1173 let caller = FunctionRef {
1174 file: "a.py".to_string(),
1175 name: "caller".to_string(),
1176 qualified_name: None,
1177 };
1178 let callee = FunctionRef {
1179 file: "a.py".to_string(),
1180 name: "callee".to_string(),
1181 qualified_name: None,
1182 };
1183 let orphan = FunctionRef {
1184 file: "a.py".to_string(),
1185 name: "orphan".to_string(),
1186 qualified_name: None,
1187 };
1188
1189 graph.edges.push(CallEdge {
1190 caller: caller.clone(),
1191 callee: callee.clone(),
1192 call_line: 1,
1193 });
1194 graph.build_indexes();
1195
1196 let mut reachable = HashSet::new();
1197 reachable.insert(caller.clone());
1198
1199 let reason = determine_dead_reason(&orphan, &graph, &reachable);
1201 assert_eq!(reason, DeadReason::NeverCalled);
1202
1203 let empty_reachable = HashSet::new();
1206 let reason = determine_dead_reason(&callee, &graph, &empty_reachable);
1207 assert_eq!(reason, DeadReason::CalledOnlyByDead);
1208 }
1209
1210 #[test]
1211 fn test_config_min_confidence() {
1212 let graph = create_test_graph();
1213
1214 let config = DeadCodeConfig {
1216 min_confidence: 0.99,
1217 ..Default::default()
1218 };
1219 let result = analyze_dead_code_with_config(&graph, &config);
1220
1221 let config_low = DeadCodeConfig {
1223 min_confidence: 0.1,
1224 ..Default::default()
1225 };
1226 let result_low = analyze_dead_code_with_config(&graph, &config_low);
1227
1228 assert!(result_low.total_dead >= result.total_dead);
1230 }
1231
1232 #[test]
1233 fn test_user_defined_filter_patterns() {
1234 let graph = create_test_graph();
1235
1236 let config = DeadCodeConfig {
1238 filter_patterns: vec!["orphan".to_string()],
1239 ..Default::default()
1240 };
1241
1242 let result = analyze_dead_code_with_config(&graph, &config);
1243
1244 assert!(!result
1246 .dead_functions
1247 .iter()
1248 .any(|f| f.name.contains("orphan")));
1249 }
1250
1251 #[test]
1252 fn test_include_public_api_patterns_opt_in() {
1253 let default_config = DeadCodeConfig::default();
1257 assert!(!default_config.include_public_api_patterns); assert!(!is_likely_entry_point("UserManager", Some(&default_config)));
1262 assert!(!is_likely_entry_point("Config", Some(&default_config)));
1263 assert!(!is_likely_entry_point("DatabaseConnection", Some(&default_config)));
1264
1265 assert!(!is_likely_entry_point("getUser", Some(&default_config)));
1269 assert!(!is_likely_entry_point("setValue", Some(&default_config)));
1270 assert!(!is_likely_entry_point("getData", Some(&default_config)));
1271 assert!(!is_likely_entry_point("setConfig", Some(&default_config)));
1272
1273 assert!(!is_likely_entry_point("is_valid", Some(&default_config)));
1276 assert!(!is_likely_entry_point("has_data", Some(&default_config)));
1277 assert!(!is_likely_entry_point("can_proceed", Some(&default_config)));
1278
1279 assert!(!is_likely_entry_point("from_bytes", Some(&default_config)));
1282 assert!(!is_likely_entry_point("to_string", Some(&default_config)));
1283 assert!(!is_likely_entry_point("with_timeout", Some(&default_config)));
1284 assert!(!is_likely_entry_point("as_ref", Some(&default_config)));
1285
1286 let permissive_config = DeadCodeConfig {
1288 include_public_api_patterns: true,
1289 ..Default::default()
1290 };
1291
1292 assert!(is_likely_entry_point("UserManager", Some(&permissive_config)));
1293 assert!(is_likely_entry_point("getUser", Some(&permissive_config)));
1294 assert!(is_likely_entry_point("is_valid", Some(&permissive_config)));
1295 assert!(is_likely_entry_point("from_bytes", Some(&permissive_config)));
1296 assert!(is_likely_entry_point("to_string", Some(&permissive_config)));
1297
1298 assert!(is_likely_entry_point("main", Some(&default_config)));
1300 assert!(is_likely_entry_point("test_something", Some(&default_config)));
1301 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))); }
1305
1306 #[test]
1307 fn test_extra_entry_patterns() {
1308 let config = DeadCodeConfig {
1311 extra_entry_patterns: vec!["plugin_".to_string(), "hook_".to_string()],
1312 ..Default::default()
1313 };
1314
1315 assert!(is_likely_entry_point("plugin_load", Some(&config)));
1317 assert!(is_likely_entry_point("plugin_unload", Some(&config)));
1318 assert!(is_likely_entry_point("hook_before", Some(&config)));
1319 assert!(is_likely_entry_point("hook_after", Some(&config)));
1320
1321 assert!(!is_likely_entry_point("plugin_load", Some(&DeadCodeConfig::default())));
1323 assert!(!is_likely_entry_point("hook_before", Some(&DeadCodeConfig::default())));
1324
1325 assert!(!is_likely_entry_point("process_data", Some(&config)));
1327 assert!(!is_likely_entry_point("helper", Some(&config)));
1328 }
1329
1330 #[test]
1331 fn test_balanced_confidence_scoring() {
1332 let mut graph = CallGraph::default();
1338
1339 let short_func = FunctionRef {
1341 file: "utils.py".to_string(),
1342 name: "x".to_string(), qualified_name: None,
1344 };
1345
1346 let pascal_case_func = FunctionRef {
1347 file: "models.py".to_string(),
1348 name: "UserManager".to_string(), qualified_name: None,
1350 };
1351
1352 let private_func = FunctionRef {
1353 file: "internal.py".to_string(),
1354 name: "_helper".to_string(), qualified_name: None,
1356 };
1357
1358 let api_func = FunctionRef {
1359 file: "src/api/routes.py".to_string(),
1360 name: "process_request".to_string(),
1361 qualified_name: None,
1362 };
1363
1364 let dead_caller = FunctionRef {
1365 file: "dead.py".to_string(),
1366 name: "dead_caller".to_string(),
1367 qualified_name: None,
1368 };
1369
1370 let dead_callee1 = FunctionRef {
1371 file: "dead.py".to_string(),
1372 name: "dead_callee1".to_string(),
1373 qualified_name: None,
1374 };
1375
1376 let dead_callee2 = FunctionRef {
1377 file: "dead.py".to_string(),
1378 name: "dead_callee2".to_string(),
1379 qualified_name: None,
1380 };
1381
1382 let dead_callee3 = FunctionRef {
1383 file: "dead.py".to_string(),
1384 name: "dead_callee3".to_string(),
1385 qualified_name: None,
1386 };
1387
1388 let factory_func = FunctionRef {
1389 file: "factories.py".to_string(),
1390 name: "create_user".to_string(), qualified_name: None,
1392 };
1393
1394 graph.edges.push(CallEdge {
1396 caller: dead_caller.clone(),
1397 callee: dead_callee1.clone(),
1398 call_line: 1,
1399 });
1400 graph.edges.push(CallEdge {
1401 caller: dead_caller.clone(),
1402 callee: dead_callee2.clone(),
1403 call_line: 2,
1404 });
1405 graph.edges.push(CallEdge {
1406 caller: dead_caller.clone(),
1407 callee: dead_callee3.clone(),
1408 call_line: 3,
1409 });
1410
1411 graph.build_indexes();
1412
1413 let reachable = HashSet::new();
1415
1416 let short_conf = compute_confidence(&short_func, &graph, &reachable);
1419 assert!(
1420 short_conf < 0.8,
1421 "Short function 'x' should have confidence < 0.8, got {}",
1422 short_conf
1423 );
1424 assert!(
1425 short_conf >= 0.6 && short_conf <= 0.75,
1426 "Short function 'x' should have balanced confidence around 0.7, got {}",
1427 short_conf
1428 );
1429
1430 let pascal_conf = compute_confidence(&pascal_case_func, &graph, &reachable);
1433 assert!(
1434 pascal_conf < short_conf,
1435 "PascalCase function should have lower confidence than short function"
1436 );
1437
1438 let private_conf = compute_confidence(&private_func, &graph, &reachable);
1441 assert!(
1442 private_conf > short_conf,
1443 "Private function should have higher confidence than short function"
1444 );
1445
1446 let api_conf = compute_confidence(&api_func, &graph, &reachable);
1450 assert!(
1451 api_conf <= 0.55,
1452 "Function in /api/ path should have confidence around 0.5, got {}",
1453 api_conf
1454 );
1455
1456 let dead_caller_conf = compute_confidence(&dead_caller, &graph, &reachable);
1460 assert!(
1461 dead_caller_conf >= 0.8,
1462 "Function calling 3 dead functions should have confidence >= 0.8, got {}",
1463 dead_caller_conf
1464 );
1465
1466 let factory_conf = compute_confidence(&factory_func, &graph, &reachable);
1469 assert!(
1470 factory_conf < 0.7,
1471 "Factory function should have confidence < 0.7, got {}",
1472 factory_conf
1473 );
1474
1475 let neutral_func = FunctionRef {
1478 file: "src/lib/module.py".to_string(), name: "process".to_string(), qualified_name: None,
1481 };
1482 let neutral_conf = compute_confidence(&neutral_func, &graph, &reachable);
1483 assert!(
1485 neutral_conf >= 0.6 && neutral_conf <= 0.75,
1486 "Neutral function should have confidence around 0.7, got {}",
1487 neutral_conf
1488 );
1489 }
1490
1491 #[test]
1492 fn test_is_in_common_module_path() {
1493 assert!(is_in_common_module_path("/project/api/routes.py"));
1495 assert!(is_in_common_module_path("src/public/index.html"));
1496 assert!(is_in_common_module_path("/app/lib/utils.py"));
1497 assert!(is_in_common_module_path("src/handlers/user.py"));
1498 assert!(is_in_common_module_path("/project/routes/auth.ts"));
1499 assert!(is_in_common_module_path("app/controllers/main.rb"));
1500 assert!(is_in_common_module_path("src/endpoints/v1.py"));
1501 assert!(is_in_common_module_path("/app/views/home.py"));
1502 assert!(is_in_common_module_path("backend/services/auth.py"));
1503 assert!(is_in_common_module_path("cli/commands/deploy.py"));
1504
1505 assert!(!is_in_common_module_path("src/utils/helper.py"));
1507 assert!(!is_in_common_module_path("internal/processor.py"));
1508 assert!(!is_in_common_module_path("core/database.py"));
1509 assert!(!is_in_common_module_path("models/user.py"));
1510 assert!(!is_in_common_module_path("tests/test_main.py"));
1511 }
1512}