1use std::collections::{HashMap, HashSet};
19use std::path::{Path, PathBuf};
20
21use crate::types::{DeadCodeReport, FunctionRef, ProjectCallGraph};
22use crate::TldrResult;
23
24#[allow(unused_imports)]
26use super::refcount::is_rescued_by_refcount;
27
28pub fn dead_code_analysis(
38 call_graph: &ProjectCallGraph,
39 all_functions: &[FunctionRef],
40 entry_points: Option<&[String]>,
41) -> TldrResult<DeadCodeReport> {
42 let mut called_functions: HashSet<FunctionRef> = HashSet::new();
44
45 for edge in call_graph.edges() {
46 called_functions.insert(FunctionRef::new(
47 edge.dst_file.clone(),
48 edge.dst_func.clone(),
49 ));
50 }
51
52 let mut callers: HashSet<FunctionRef> = HashSet::new();
54 for edge in call_graph.edges() {
55 callers.insert(FunctionRef::new(
56 edge.src_file.clone(),
57 edge.src_func.clone(),
58 ));
59 }
60
61 let mut dead_functions: Vec<FunctionRef> = Vec::new();
63 let mut possibly_dead: Vec<FunctionRef> = Vec::new();
64 let mut by_file: HashMap<PathBuf, Vec<String>> = HashMap::new();
65
66 for func_ref in all_functions {
67 if called_functions.contains(func_ref) {
69 continue;
70 }
71
72 if is_entry_point_name(&func_ref.name, entry_points) {
74 continue;
75 }
76
77 let bare_name = if func_ref.name.contains('.') {
80 func_ref.name.rsplit('.').next().unwrap_or(&func_ref.name)
81 } else if func_ref.name.contains(':') {
82 func_ref.name.rsplit(':').next().unwrap_or(&func_ref.name)
83 } else {
84 &func_ref.name
85 };
86
87 static PHP_MAGIC: &[&str] = &[
90 "__construct",
91 "__destruct",
92 "__call",
93 "__callStatic",
94 "__get",
95 "__set",
96 "__isset",
97 "__unset",
98 "__sleep",
99 "__wakeup",
100 "__serialize",
101 "__unserialize",
102 "__toString",
103 "__invoke",
104 "__set_state",
105 "__clone",
106 "__debugInfo",
107 ];
108 if PHP_MAGIC.contains(&bare_name) {
109 continue;
110 }
111
112 if bare_name.starts_with("__") && bare_name.ends_with("__") {
113 continue;
114 }
115
116 if func_ref.is_trait_method {
118 continue;
119 }
120
121 if func_ref.is_test {
123 continue;
124 }
125
126 if func_ref.has_decorator {
128 continue;
129 }
130
131 if func_ref.is_public {
134 possibly_dead.push(func_ref.clone());
135 } else {
136 dead_functions.push(func_ref.clone());
137 by_file
138 .entry(func_ref.file.clone())
139 .or_default()
140 .push(func_ref.name.clone());
141 }
142 }
143
144 let total_dead = dead_functions.len();
145 let total_possibly_dead = possibly_dead.len();
146 let total_functions = all_functions.len();
147 let dead_percentage = if total_functions > 0 {
148 (total_dead as f64 / total_functions as f64) * 100.0
149 } else {
150 0.0
151 };
152
153 Ok(DeadCodeReport {
154 dead_functions,
155 possibly_dead,
156 by_file,
157 total_dead,
158 total_possibly_dead,
159 total_functions,
160 dead_percentage,
161 })
162}
163
164pub fn dead_code_analysis_refcount(
183 all_functions: &[FunctionRef],
184 ref_counts: &HashMap<String, usize>,
185 entry_points: Option<&[String]>,
186) -> TldrResult<DeadCodeReport> {
187 let mut dead_functions: Vec<FunctionRef> = Vec::new();
188 let mut possibly_dead: Vec<FunctionRef> = Vec::new();
189 let mut by_file: HashMap<PathBuf, Vec<String>> = HashMap::new();
190
191 for func_ref in all_functions {
192 if is_entry_point_name(&func_ref.name, entry_points) {
194 continue;
195 }
196
197 let bare_name = if func_ref.name.contains('.') {
200 func_ref.name.rsplit('.').next().unwrap_or(&func_ref.name)
201 } else if func_ref.name.contains(':') {
202 func_ref.name.rsplit(':').next().unwrap_or(&func_ref.name)
203 } else {
204 &func_ref.name
205 };
206
207 static PHP_MAGIC: &[&str] = &[
210 "__construct",
211 "__destruct",
212 "__call",
213 "__callStatic",
214 "__get",
215 "__set",
216 "__isset",
217 "__unset",
218 "__sleep",
219 "__wakeup",
220 "__serialize",
221 "__unserialize",
222 "__toString",
223 "__invoke",
224 "__set_state",
225 "__clone",
226 "__debugInfo",
227 ];
228 if PHP_MAGIC.contains(&bare_name) {
229 continue;
230 }
231
232 if bare_name.starts_with("__") && bare_name.ends_with("__") {
233 continue;
234 }
235
236 if func_ref.is_trait_method {
238 continue;
239 }
240
241 if func_ref.is_test {
243 continue;
244 }
245
246 if func_ref.has_decorator {
248 continue;
249 }
250
251 if is_rescued_by_refcount(&func_ref.name, ref_counts) {
253 continue;
254 }
255
256 let mut enriched = func_ref.clone();
259 let lookup_name = bare_name;
261 enriched.ref_count = ref_counts.get(lookup_name).copied().unwrap_or(0) as u32;
262
263 if func_ref.is_public {
264 possibly_dead.push(enriched);
265 } else {
266 by_file
267 .entry(func_ref.file.clone())
268 .or_default()
269 .push(func_ref.name.clone());
270 dead_functions.push(enriched);
271 }
272 }
273
274 let total_dead = dead_functions.len();
275 let total_possibly_dead = possibly_dead.len();
276 let total_functions = all_functions.len();
277 let dead_percentage = if total_functions > 0 {
278 (total_dead as f64 / total_functions as f64) * 100.0
279 } else {
280 0.0
281 };
282
283 Ok(DeadCodeReport {
284 dead_functions,
285 possibly_dead,
286 by_file,
287 total_dead,
288 total_possibly_dead,
289 total_functions,
290 dead_percentage,
291 })
292}
293
294fn is_entry_point_name(name: &str, custom_patterns: Option<&[String]>) -> bool {
296 let standard_patterns = [
298 "main",
300 "__main__",
301 "cli",
302 "app",
303 "run",
304 "start",
305 "setup",
307 "teardown",
308 "setUp",
309 "tearDown",
310 "create_app",
312 "make_app",
313 "ServeHTTP",
315 "Handler",
316 "handler",
317 "OnLoad",
319 "OnInit",
320 "OnExit",
321 "onCreate",
323 "onStart",
324 "onStop",
325 "onResume",
326 "onPause",
327 "onDestroy",
328 "onBind",
329 "onClick",
330 "onCreateView",
331 "doGet",
333 "doPost",
334 "doPut",
335 "doDelete",
336 "init",
337 "destroy",
338 "service",
339 "load",
341 "configure",
342 "request",
343 "response",
344 "error",
345 "invoke",
346 "call",
347 "execute",
348 "register",
350 "onRequestError",
351 ];
352
353 if standard_patterns.contains(&name) {
354 return true;
355 }
356
357 let bare_name = if name.contains('.') {
359 name.rsplit('.').next().unwrap_or(name)
360 } else if name.contains(':') {
361 name.rsplit(':').next().unwrap_or(name)
362 } else {
363 name
364 };
365 if bare_name != name && standard_patterns.contains(&bare_name) {
366 return true;
367 }
368
369 if name.starts_with("test_") || name.starts_with("pytest_") {
371 return true;
372 }
373
374 if bare_name != name && (bare_name.starts_with("test_") || bare_name.starts_with("pytest_")) {
376 return true;
377 }
378
379 if name.starts_with("Test") || name.starts_with("Benchmark") || name.starts_with("Example") {
381 return true;
382 }
383
384 if bare_name.starts_with("test") {
386 return true;
387 }
388
389 if bare_name.starts_with("handle") || bare_name.starts_with("Handle") {
391 return true;
392 }
393 if bare_name.starts_with("on_")
394 || bare_name.starts_with("before_")
395 || bare_name.starts_with("after_")
396 {
397 return true;
398 }
399
400 if let Some(patterns) = custom_patterns {
402 for pattern in patterns {
403 if name == pattern {
404 return true;
405 }
406 if pattern.ends_with('*') {
408 let prefix = pattern.trim_end_matches('*');
409 if name.starts_with(prefix) {
410 return true;
411 }
412 }
413 if pattern.starts_with('*') {
414 let suffix = pattern.trim_start_matches('*');
415 if name.ends_with(suffix) {
416 return true;
417 }
418 }
419 }
420 }
421
422 false
423}
424
425fn build_signature(name: &str, params: &[String], return_type: Option<&str>) -> String {
431 let params_str = params.join(", ");
432 match return_type {
433 Some(rt) if !rt.is_empty() => format!("{}({}) -> {}", name, params_str, rt),
434 _ => format!("{}({})", name, params_str),
435 }
436}
437
438pub fn collect_all_functions(
445 module_infos: &[(PathBuf, crate::types::ModuleInfo)],
446) -> Vec<FunctionRef> {
447 let mut functions = Vec::new();
448
449 for (file_path, info) in module_infos {
450 let language = info.language;
451 let is_test_file = is_test_file_path(file_path);
452 let is_framework_entry =
453 is_framework_entry_file(file_path, language) || has_framework_directive(file_path);
454
455 for func in &info.functions {
457 let is_public =
458 infer_visibility_from_name(&func.name, language, !func.decorators.is_empty(), &func.decorators);
459 let has_decorator =
460 !func.decorators.is_empty() || (is_framework_entry && is_public);
461 let is_test = is_test_file
462 || is_test_function_name(&func.name)
463 || has_test_decorator(&func.decorators);
464 let signature = build_signature(&func.name, &func.params, func.return_type.as_deref());
465
466 functions.push(FunctionRef {
467 file: file_path.clone(),
468 name: func.name.clone(),
469 line: func.line_number,
470 signature,
471 ref_count: 0,
472 is_public,
473 is_test,
474 is_trait_method: false,
475 has_decorator,
476 decorator_names: func.decorators.clone(),
477 });
478 }
479
480 for class in &info.classes {
482 let is_trait = is_trait_or_interface(class, language);
483
484 for method in &class.methods {
485 let full_name = format!("{}.{}", class.name, method.name);
486 let is_public = infer_visibility_from_name(
487 &method.name,
488 language,
489 !method.decorators.is_empty(),
490 &method.decorators,
491 );
492 let has_decorator =
493 !method.decorators.is_empty() || (is_framework_entry && is_public);
494 let is_test = is_test_file
495 || is_test_function_name(&method.name)
496 || has_test_decorator(&method.decorators);
497 let signature =
498 build_signature(&method.name, &method.params, method.return_type.as_deref());
499
500 functions.push(FunctionRef {
501 file: file_path.clone(),
502 name: full_name,
503 line: method.line_number,
504 signature,
505 ref_count: 0,
506 is_public,
507 is_test,
508 is_trait_method: is_trait,
509 has_decorator,
510 decorator_names: method.decorators.clone(),
511 });
512 }
513 }
514 }
515
516 functions
517}
518
519fn is_test_file_path(path: &Path) -> bool {
521 let path_str = path.to_string_lossy();
522 let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
523
524 file_name.starts_with("test_")
526 || file_name.ends_with("_test")
527 || file_name.ends_with("_tests")
528 || file_name.ends_with("_spec")
529 || file_name.starts_with("Test")
530 || file_name.ends_with("Test")
531 || file_name.ends_with("Tests")
532 || file_name.ends_with("Spec")
533 || path_str.contains("/test/")
534 || path_str.contains("/tests/")
535 || path_str.contains("/spec/")
536 || path_str.contains("/__tests__/")
537}
538
539fn is_test_function_name(name: &str) -> bool {
541 let bare = name.rsplit('.').next().unwrap_or(name);
542 bare.starts_with("test_")
543 || bare.starts_with("Test")
544 || bare.starts_with("Benchmark")
545 || bare.starts_with("Example")
546}
547
548fn has_test_decorator(decorators: &[String]) -> bool {
550 decorators.iter().any(|d| {
551 let lower = d.to_lowercase();
552 lower == "test" || lower == "pytest.mark.parametrize" || lower.starts_with("test")
553 })
554}
555
556fn infer_visibility_from_name(
561 name: &str,
562 language: crate::types::Language,
563 _has_decorator: bool,
564 _decorators: &[String],
565) -> bool {
566 use crate::types::Language;
567
568 let bare_name = name.rsplit('.').next().unwrap_or(name);
569
570 match language {
571 Language::Python => !bare_name.starts_with('_'),
573
574 Language::Go => bare_name
576 .chars()
577 .next()
578 .map(|c| c.is_uppercase())
579 .unwrap_or(false),
580
581 Language::Rust => !bare_name.starts_with('_'),
586
587 Language::TypeScript | Language::JavaScript => !bare_name.starts_with('_'),
590
591 Language::Java | Language::Kotlin | Language::CSharp | Language::Scala => {
595 !bare_name.starts_with('_')
596 }
597
598 Language::C | Language::Cpp => true,
601
602 Language::Ruby => !bare_name.starts_with('_'),
605
606 Language::Php => !bare_name.starts_with('_'),
609
610 Language::Elixir => !bare_name.starts_with('_'),
612
613 Language::Lua | Language::Luau => {
616 if name.starts_with("_M:") || name.starts_with("_M.") {
618 return true;
619 }
620 let lua_bare = if let Some(pos) = bare_name.find(':') {
622 &bare_name[pos + 1..]
623 } else {
624 bare_name
625 };
626 !lua_bare.starts_with('_')
627 }
628
629 Language::Ocaml => !bare_name.starts_with('_'),
632
633 Language::Swift => !bare_name.starts_with('_'),
635 }
636}
637
638fn is_trait_or_interface(
640 class: &crate::types::ClassInfo,
641 language: crate::types::Language,
642) -> bool {
643 use crate::types::Language;
644
645 let name = &class.name;
646
647 let has_abstract_base = class
649 .bases
650 .iter()
651 .any(|b| b == "ABC" || b == "ABCMeta" || b == "Protocol" || b == "Interface");
652
653 if has_abstract_base {
654 return true;
655 }
656
657 let has_type_decorator = class.decorators.iter().any(|d| {
665 d == "abstract" || d == "interface" || d == "protocol" || d == "trait" || d == "module"
666 });
667
668 if has_type_decorator {
669 return true;
670 }
671
672 match language {
673 Language::Rust => false,
678
679 Language::Go => {
683 if name.ends_with("Interface") {
685 return true;
686 }
687 if name.len() >= 3
691 && name.ends_with("er")
692 && name
693 .chars()
694 .next()
695 .map(|c| c.is_uppercase())
696 .unwrap_or(false)
697 {
698 return true;
699 }
700 false
701 }
702
703 Language::Java | Language::Kotlin => {
705 name.starts_with('I')
707 && name.len() > 1
708 && name
709 .chars()
710 .nth(1)
711 .map(|c| c.is_uppercase())
712 .unwrap_or(false)
713 }
714
715 Language::CSharp => {
717 name.starts_with('I')
718 && name.len() > 1
719 && name
720 .chars()
721 .nth(1)
722 .map(|c| c.is_uppercase())
723 .unwrap_or(false)
724 }
725
726 Language::Swift => {
729 name.ends_with("Protocol")
730 || name.ends_with("Delegate")
731 || name.ends_with("DataSource")
732 || name.ends_with("able")
733 || name.ends_with("ible")
734 }
735
736 Language::Scala => {
739 name.starts_with('I')
741 && name.len() > 1
742 && name
743 .chars()
744 .nth(1)
745 .map(|c| c.is_uppercase())
746 .unwrap_or(false)
747 }
748
749 Language::Php => {
753 name.starts_with('I')
754 && name.len() > 1
755 && name
756 .chars()
757 .nth(1)
758 .map(|c| c.is_uppercase())
759 .unwrap_or(false)
760 }
761
762 Language::Ruby => {
765 name.ends_with("able") || name.ends_with("ible") || name.contains("Mixin")
766 }
767
768 _ => false,
769 }
770}
771
772fn is_framework_entry_file(path: &Path, language: crate::types::Language) -> bool {
778 use crate::types::Language;
779
780 let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
781 let path_str = path.to_string_lossy();
782
783 match language {
784 Language::TypeScript | Language::JavaScript => {
785 matches!(
787 file_name,
788 "page.tsx"
789 | "page.ts"
790 | "page.jsx"
791 | "page.js"
792 | "layout.tsx"
793 | "layout.ts"
794 | "layout.jsx"
795 | "layout.js"
796 | "route.tsx"
797 | "route.ts"
798 | "route.jsx"
799 | "route.js"
800 | "loading.tsx"
801 | "loading.ts"
802 | "loading.jsx"
803 | "loading.js"
804 | "error.tsx"
805 | "error.ts"
806 | "error.jsx"
807 | "error.js"
808 | "not-found.tsx"
809 | "not-found.ts"
810 | "not-found.jsx"
811 | "not-found.js"
812 | "template.tsx"
813 | "template.ts"
814 | "template.jsx"
815 | "template.js"
816 | "default.tsx"
817 | "default.ts"
818 | "default.jsx"
819 | "default.js"
820 | "middleware.ts"
821 | "middleware.js"
822 | "manifest.ts"
823 | "manifest.js"
824 | "opengraph-image.tsx"
825 | "opengraph-image.ts"
826 | "sitemap.ts"
827 | "sitemap.js"
828 | "robots.ts"
829 | "robots.js"
830 )
831 || matches!(
833 file_name,
834 "+page.svelte"
835 | "+layout.svelte"
836 | "+error.svelte"
837 | "+page.ts"
838 | "+page.js"
839 | "+page.server.ts"
840 | "+page.server.js"
841 | "+layout.ts"
842 | "+layout.js"
843 | "+layout.server.ts"
844 | "+layout.server.js"
845 | "+server.ts"
846 | "+server.js"
847 )
848 || (path_str.contains("/pages/") && file_name.ends_with(".vue"))
850 || (path_str.contains("/layouts/") && file_name.ends_with(".vue"))
851 || (path_str.contains("/middleware/")
852 && (file_name.ends_with(".ts") || file_name.ends_with(".js")))
853 || path_str.contains("/routes/")
855 || (path_str.contains("/pages/") && file_name.ends_with(".astro"))
857 }
858 Language::Python => {
859 file_name == "views.py"
861 || file_name == "admin.py"
862 || file_name == "urls.py"
863 || file_name == "models.py"
864 || file_name == "forms.py"
865 || file_name == "serializers.py"
866 || file_name == "signals.py"
867 || file_name == "apps.py"
868 || file_name == "middleware.py"
869 || file_name == "context_processors.py"
870 || file_name == "wsgi.py"
872 || file_name == "asgi.py"
873 || file_name == "conftest.py"
874 || file_name == "tasks.py"
876 }
877 Language::Ruby => {
878 (path_str.contains("/controllers/") && file_name.ends_with("_controller.rb"))
880 || (path_str.contains("/models/") && file_name.ends_with(".rb"))
881 || (path_str.contains("/helpers/") && file_name.ends_with("_helper.rb"))
882 || (path_str.contains("/mailers/") && file_name.ends_with("_mailer.rb"))
883 || (path_str.contains("/jobs/") && file_name.ends_with("_job.rb"))
884 || (path_str.contains("/channels/") && file_name.ends_with("_channel.rb"))
885 || file_name == "application.rb"
886 || file_name == "routes.rb"
887 || file_name == "schema.rb"
888 }
889 Language::Java | Language::Kotlin => {
890 file_name.ends_with("Controller.java")
892 || file_name.ends_with("Controller.kt")
893 || file_name.ends_with("Service.java")
894 || file_name.ends_with("Service.kt")
895 || file_name.ends_with("Repository.java")
896 || file_name.ends_with("Repository.kt")
897 || file_name.ends_with("Configuration.java")
898 || file_name.ends_with("Configuration.kt")
899 || file_name.ends_with("Application.java")
900 || file_name.ends_with("Application.kt")
901 || file_name.ends_with("Activity.java")
903 || file_name.ends_with("Activity.kt")
904 || file_name.ends_with("Fragment.java")
905 || file_name.ends_with("Fragment.kt")
906 || file_name.ends_with("ViewModel.java")
907 || file_name.ends_with("ViewModel.kt")
908 }
909 Language::CSharp => {
910 file_name.ends_with("Controller.cs")
912 || file_name.ends_with("Hub.cs")
913 || file_name.ends_with("Middleware.cs")
914 || (path_str.contains("/Pages/") && file_name.ends_with(".cshtml.cs"))
915 || file_name == "Program.cs"
916 || file_name == "Startup.cs"
917 }
918 Language::Go => {
919 file_name == "main.go"
921 || file_name.ends_with("_handler.go")
922 || file_name.ends_with("_handlers.go")
923 }
924 Language::Php => {
925 (path_str.contains("/Controllers/") && file_name.ends_with(".php"))
927 || (path_str.contains("/Middleware/") && file_name.ends_with(".php"))
928 || (path_str.contains("/Models/") && file_name.ends_with(".php"))
929 || (path_str.contains("/Providers/") && file_name.ends_with(".php"))
930 || file_name == "routes.php"
931 || file_name == "web.php"
932 || file_name == "api.php"
933 }
934 Language::Elixir => {
935 (path_str.contains("/controllers/") && file_name.ends_with("_controller.ex"))
937 || (path_str.contains("/live/") && file_name.ends_with("_live.ex"))
938 || (path_str.contains("/channels/") && file_name.ends_with("_channel.ex"))
939 || file_name == "router.ex"
940 || file_name == "endpoint.ex"
941 }
942 Language::Swift => {
943 file_name.ends_with("View.swift")
945 || file_name.ends_with("ViewController.swift")
946 || file_name.ends_with("App.swift")
947 || file_name.ends_with("Delegate.swift")
948 }
949 Language::Scala => {
950 (path_str.contains("/controllers/") && file_name.ends_with(".scala"))
952 || file_name == "routes"
953 }
954 _ => false,
955 }
956}
957
958fn has_framework_directive(path: &Path) -> bool {
963 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
965 if !matches!(ext, "ts" | "tsx" | "js" | "jsx" | "mjs") {
966 return false;
967 }
968
969 if let Ok(content) = std::fs::read_to_string(path) {
971 for line in content.lines().take(5) {
972 let trimmed = line.trim();
973 if trimmed == r#""use server""#
974 || trimmed == r#"'use server'"#
975 || trimmed == r#""use server";"#
976 || trimmed == r#"'use server';"#
977 || trimmed == r#""use client""#
978 || trimmed == r#"'use client'"#
979 || trimmed == r#""use client";"#
980 || trimmed == r#"'use client';"#
981 {
982 return true;
983 }
984 if !trimmed.is_empty()
986 && !trimmed.starts_with("//")
987 && !trimmed.starts_with("/*")
988 && !trimmed.starts_with('*')
989 {
990 if !trimmed.starts_with('"') && !trimmed.starts_with('\'') {
993 break;
994 }
995 }
996 }
997 }
998 false
999}
1000
1001#[cfg(test)]
1002mod tests {
1003 use super::*;
1004 use crate::types::CallEdge;
1005
1006 fn create_test_graph() -> ProjectCallGraph {
1007 let mut graph = ProjectCallGraph::new();
1008
1009 graph.add_edge(CallEdge {
1011 src_file: "main.py".into(),
1012 src_func: "main".to_string(),
1013 dst_file: "main.py".into(),
1014 dst_func: "process".to_string(),
1015 });
1016 graph.add_edge(CallEdge {
1017 src_file: "main.py".into(),
1018 src_func: "process".to_string(),
1019 dst_file: "utils.py".into(),
1020 dst_func: "helper".to_string(),
1021 });
1022
1023 graph
1024 }
1025
1026 #[test]
1027 fn test_dead_finds_uncalled() {
1028 let graph = create_test_graph();
1029 let functions = vec![
1030 FunctionRef::new("main.py".into(), "main"),
1031 FunctionRef::new("main.py".into(), "process"),
1032 FunctionRef::new("utils.py".into(), "helper"),
1033 FunctionRef::new("utils.py".into(), "unused"),
1034 ];
1035
1036 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1037
1038 assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
1040 assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
1042 assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
1044 assert!(!result.dead_functions.iter().any(|f| f.name == "helper"));
1046 }
1047
1048 #[test]
1049 fn test_dead_excludes_entry_points() {
1050 let graph = ProjectCallGraph::new(); let functions = vec![
1052 FunctionRef::new("main.py".into(), "main"),
1053 FunctionRef::new("test.py".into(), "test_something"),
1054 FunctionRef::new("setup.py".into(), "setup"),
1055 FunctionRef::new("utils.py".into(), "__init__"),
1056 ];
1057
1058 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1059
1060 assert!(result.dead_functions.is_empty());
1062 }
1063
1064 #[test]
1065 fn test_dead_custom_entry_points() {
1066 let graph = ProjectCallGraph::new();
1067 let functions = vec![
1068 FunctionRef::new("handler.py".into(), "handle_request"),
1069 FunctionRef::new("handler.py".into(), "process_event"),
1070 ];
1071
1072 let custom = vec!["handle_*".to_string()];
1073 let result = dead_code_analysis(&graph, &functions, Some(&custom)).unwrap();
1074
1075 assert!(!result
1077 .dead_functions
1078 .iter()
1079 .any(|f| f.name == "handle_request"));
1080 assert!(result
1082 .dead_functions
1083 .iter()
1084 .any(|f| f.name == "process_event"));
1085 }
1086
1087 #[test]
1088 fn test_dead_percentage() {
1089 let graph = ProjectCallGraph::new();
1090 let functions = vec![
1091 FunctionRef::new("a.py".into(), "dead1"),
1092 FunctionRef::new("a.py".into(), "dead2"),
1093 FunctionRef::new("a.py".into(), "main"), FunctionRef::new("a.py".into(), "test_x"), ];
1096
1097 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1098
1099 assert_eq!(result.total_dead, 2);
1100 assert_eq!(result.total_functions, 4);
1101 assert!((result.dead_percentage - 50.0).abs() < 0.01);
1102 }
1103
1104 #[test]
1105 fn test_is_entry_point_name() {
1106 assert!(is_entry_point_name("main", None));
1107 assert!(is_entry_point_name("test_something", None));
1108 assert!(is_entry_point_name("setup", None));
1109 assert!(!is_entry_point_name("helper", None));
1110
1111 let custom = vec!["handler_*".to_string()];
1112 assert!(is_entry_point_name("handler_request", Some(&custom)));
1113 assert!(!is_entry_point_name("process_request", Some(&custom)));
1114 }
1115
1116 #[test]
1117 fn test_entry_point_go_patterns() {
1118 assert!(is_entry_point_name("ServeHTTP", None));
1120 assert!(is_entry_point_name("Handler", None));
1121 assert!(is_entry_point_name("TestUserLogin", None));
1123 assert!(is_entry_point_name("BenchmarkSort", None));
1124 assert!(is_entry_point_name("ExampleParse", None));
1125 }
1126
1127 #[test]
1128 fn test_entry_point_android_lifecycle() {
1129 assert!(is_entry_point_name("onCreate", None));
1130 assert!(is_entry_point_name("onStart", None));
1131 assert!(is_entry_point_name("onDestroy", None));
1132 assert!(is_entry_point_name("onClick", None));
1133 assert!(is_entry_point_name("onBind", None));
1134 }
1135
1136 #[test]
1137 fn test_entry_point_plugin_hooks() {
1138 assert!(is_entry_point_name("load", None));
1139 assert!(is_entry_point_name("configure", None));
1140 assert!(is_entry_point_name("request", None));
1141 assert!(is_entry_point_name("invoke", None));
1142 assert!(is_entry_point_name("execute", None));
1143 }
1144
1145 #[test]
1146 fn test_entry_point_handler_prefix() {
1147 assert!(is_entry_point_name("handleRequest", None));
1148 assert!(is_entry_point_name("handle_event", None));
1149 assert!(is_entry_point_name("HandleConnection", None));
1150 }
1151
1152 #[test]
1153 fn test_entry_point_hook_prefix() {
1154 assert!(is_entry_point_name("on_message", None));
1155 assert!(is_entry_point_name("before_request", None));
1156 assert!(is_entry_point_name("after_response", None));
1157 }
1158
1159 #[test]
1160 fn test_entry_point_class_method_format() {
1161 assert!(is_entry_point_name("MyServlet.doGet", None));
1163 assert!(is_entry_point_name("Activity.onCreate", None));
1164 assert!(is_entry_point_name("Server.handleRequest", None));
1165 assert!(is_entry_point_name("TestSuite.test_login", None));
1166 assert!(!is_entry_point_name("Utils.compute", None));
1168 }
1169
1170 #[test]
1171 fn test_entry_point_java_servlet() {
1172 assert!(is_entry_point_name("doGet", None));
1173 assert!(is_entry_point_name("doPost", None));
1174 assert!(is_entry_point_name("init", None));
1175 assert!(is_entry_point_name("destroy", None));
1176 assert!(is_entry_point_name("service", None));
1177 }
1178
1179 fn enriched_func(
1185 name: &str,
1186 is_public: bool,
1187 is_trait_method: bool,
1188 has_decorator: bool,
1189 decorator_names: Vec<&str>,
1190 ) -> FunctionRef {
1191 FunctionRef {
1192 file: PathBuf::from("test.rs"),
1193 name: name.to_string(),
1194 line: 0,
1195 signature: String::new(),
1196 ref_count: 0,
1197 is_public,
1198 is_test: false,
1199 is_trait_method,
1200 has_decorator,
1201 decorator_names: decorator_names.into_iter().map(|s| s.to_string()).collect(),
1202 }
1203 }
1204
1205 #[test]
1206 fn test_public_uncalled_is_possibly_dead_not_dead() {
1207 let graph = ProjectCallGraph::new();
1210 let functions = vec![enriched_func("pub_helper", true, false, false, vec![])];
1211
1212 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1213
1214 assert!(
1216 !result.dead_functions.iter().any(|f| f.name == "pub_helper"),
1217 "Public uncalled function should not be in dead_functions"
1218 );
1219 assert!(
1221 result.possibly_dead.iter().any(|f| f.name == "pub_helper"),
1222 "Public uncalled function should be in possibly_dead"
1223 );
1224 }
1225
1226 #[test]
1227 fn test_private_uncalled_is_dead() {
1228 let graph = ProjectCallGraph::new();
1230 let functions = vec![enriched_func(
1231 "_private_helper",
1232 false,
1233 false,
1234 false,
1235 vec![],
1236 )];
1237
1238 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1239
1240 assert!(
1241 result
1242 .dead_functions
1243 .iter()
1244 .any(|f| f.name == "_private_helper"),
1245 "Private uncalled function should be in dead_functions"
1246 );
1247 assert!(
1248 !result
1249 .possibly_dead
1250 .iter()
1251 .any(|f| f.name == "_private_helper"),
1252 "Private uncalled function should not be in possibly_dead"
1253 );
1254 }
1255
1256 #[test]
1257 fn test_trait_method_not_dead() {
1258 let graph = ProjectCallGraph::new();
1260 let functions = vec![
1261 enriched_func("serialize", false, true, false, vec![]),
1262 enriched_func("deserialize", true, true, false, vec![]),
1263 ];
1264
1265 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1266
1267 assert!(
1268 result.dead_functions.is_empty(),
1269 "Trait methods should never be in dead_functions"
1270 );
1271 assert!(
1272 result.possibly_dead.is_empty(),
1273 "Trait methods should never be in possibly_dead"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_decorated_function_not_dead() {
1279 let graph = ProjectCallGraph::new();
1281 let functions = vec![
1282 enriched_func("index", false, false, true, vec!["route"]),
1283 enriched_func(
1284 "admin_panel",
1285 true,
1286 false,
1287 true,
1288 vec!["route", "login_required"],
1289 ),
1290 ];
1291
1292 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1293
1294 assert!(
1295 result.dead_functions.is_empty(),
1296 "Decorated functions should not be in dead_functions"
1297 );
1298 assert!(
1299 result.possibly_dead.is_empty(),
1300 "Decorated functions should not be in possibly_dead"
1301 );
1302 }
1303
1304 #[test]
1305 fn test_test_function_not_dead() {
1306 let graph = ProjectCallGraph::new();
1308 let functions = vec![FunctionRef {
1309 file: PathBuf::from("test.rs"),
1310 name: "unusual_test_name".to_string(),
1311 line: 0,
1312 signature: String::new(),
1313 ref_count: 0,
1314 is_public: false,
1315 is_test: true,
1316 is_trait_method: false,
1317 has_decorator: false,
1318 decorator_names: vec![],
1319 }];
1320
1321 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1322
1323 assert!(
1324 result.dead_functions.is_empty(),
1325 "Test functions should not be dead"
1326 );
1327 }
1328
1329 #[test]
1330 fn test_mixed_enrichment_filtering() {
1331 let graph = ProjectCallGraph::new();
1333 let functions = vec![
1334 enriched_func("_internal_cache", false, false, false, vec![]),
1336 enriched_func("public_api_method", true, false, false, vec![]),
1338 enriched_func("Serialize.serialize", false, true, false, vec![]),
1340 enriched_func("handle_index", false, false, true, vec!["get"]),
1342 enriched_func("_orphan", false, false, false, vec![]),
1344 ];
1345
1346 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1347
1348 assert_eq!(
1350 result.total_dead,
1351 2,
1352 "Should have exactly 2 definitely-dead functions, got: {:?}",
1353 result
1354 .dead_functions
1355 .iter()
1356 .map(|f| &f.name)
1357 .collect::<Vec<_>>()
1358 );
1359 assert!(result
1360 .dead_functions
1361 .iter()
1362 .any(|f| f.name == "_internal_cache"));
1363 assert!(result.dead_functions.iter().any(|f| f.name == "_orphan"));
1364
1365 assert_eq!(
1367 result.total_possibly_dead, 1,
1368 "Should have exactly 1 possibly-dead function"
1369 );
1370 assert!(result
1371 .possibly_dead
1372 .iter()
1373 .any(|f| f.name == "public_api_method"));
1374
1375 assert!(
1378 (result.dead_percentage - 40.0).abs() < 0.01,
1379 "Dead percentage should be 40%, got {}",
1380 result.dead_percentage
1381 );
1382 }
1383
1384 #[test]
1385 fn test_unenriched_functionref_backwards_compat() {
1386 let func = FunctionRef::new("test.py".into(), "some_func");
1389 assert!(!func.is_public);
1390 assert!(!func.is_test);
1391 assert!(!func.is_trait_method);
1392 assert!(!func.has_decorator);
1393 assert!(func.decorator_names.is_empty());
1394 }
1395
1396 #[test]
1397 fn test_dead_code_report_has_possibly_dead_field() {
1398 let graph = ProjectCallGraph::new();
1399 let functions = vec![
1400 enriched_func("pub_func", true, false, false, vec![]),
1401 enriched_func("_priv_func", false, false, false, vec![]),
1402 ];
1403
1404 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1405
1406 assert_eq!(result.total_possibly_dead, 1);
1408 assert_eq!(result.total_dead, 1);
1409 assert_eq!(result.total_functions, 2);
1410 }
1411
1412 #[test]
1413 fn test_called_public_function_not_in_any_dead_list() {
1414 let mut graph = ProjectCallGraph::new();
1416 graph.add_edge(CallEdge {
1417 src_file: "main.rs".into(),
1418 src_func: "main".to_string(),
1419 dst_file: "test.rs".into(), dst_func: "pub_helper".to_string(),
1421 });
1422
1423 let functions = vec![enriched_func("pub_helper", true, false, false, vec![])];
1424
1425 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1426 assert!(result.dead_functions.is_empty());
1427 assert!(result.possibly_dead.is_empty());
1428 }
1429
1430 #[test]
1431 fn test_old_tests_still_pass_with_new_fields() {
1432 let graph = create_test_graph();
1436 let functions = vec![
1437 FunctionRef::new("main.py".into(), "main"),
1438 FunctionRef::new("main.py".into(), "process"),
1439 FunctionRef::new("utils.py".into(), "helper"),
1440 FunctionRef::new("utils.py".into(), "unused"),
1441 ];
1442
1443 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1444
1445 assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
1447 assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
1449 assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
1451 assert!(!result.dead_functions.iter().any(|f| f.name == "helper"));
1453 }
1454
1455 #[test]
1466 fn test_refcount_no_cg_rescues() {
1467 let mut ref_counts = HashMap::new();
1468 ref_counts.insert("process_data".to_string(), 3);
1469
1470 let functions = vec![enriched_func("process_data", false, false, false, vec![])];
1471
1472 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1473
1474 assert!(
1475 !result
1476 .dead_functions
1477 .iter()
1478 .any(|f| f.name == "process_data"),
1479 "Function with ref_count=3 should NOT be in dead_functions"
1480 );
1481 assert!(
1482 !result
1483 .possibly_dead
1484 .iter()
1485 .any(|f| f.name == "process_data"),
1486 "Function with ref_count=3 should NOT be in possibly_dead"
1487 );
1488 }
1489
1490 #[test]
1493 fn test_refcount_no_cg_confirms_dead() {
1494 let mut ref_counts = HashMap::new();
1495 ref_counts.insert("_unused_helper".to_string(), 1);
1496
1497 let functions = vec![enriched_func("_unused_helper", false, false, false, vec![])];
1498
1499 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1500
1501 assert!(
1502 result
1503 .dead_functions
1504 .iter()
1505 .any(|f| f.name == "_unused_helper"),
1506 "_unused_helper with ref_count=1 should be in dead_functions"
1507 );
1508 }
1509
1510 #[test]
1514 fn test_refcount_exclusions_apply() {
1515 let mut ref_counts = HashMap::new();
1516 ref_counts.insert("main".to_string(), 1);
1517 ref_counts.insert("__init__".to_string(), 1);
1518 ref_counts.insert("test_something".to_string(), 1);
1519
1520 let functions = vec![
1521 enriched_func("main", false, false, false, vec![]),
1523 enriched_func("__init__", false, false, false, vec![]),
1525 enriched_func("test_something", false, false, false, vec![]),
1527 ];
1528
1529 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1530
1531 assert!(
1532 result.dead_functions.is_empty(),
1533 "Entry points, dunders, and test functions should NOT be in dead_functions, got: {:?}",
1534 result
1535 .dead_functions
1536 .iter()
1537 .map(|f| &f.name)
1538 .collect::<Vec<_>>()
1539 );
1540 assert!(
1541 result.possibly_dead.is_empty(),
1542 "Entry points, dunders, and test functions should NOT be in possibly_dead, got: {:?}",
1543 result
1544 .possibly_dead
1545 .iter()
1546 .map(|f| &f.name)
1547 .collect::<Vec<_>>()
1548 );
1549 }
1550
1551 #[test]
1554 fn test_refcount_short_name_low_count_stays_dead() {
1555 let mut ref_counts = HashMap::new();
1556 ref_counts.insert("fn".to_string(), 3);
1558
1559 let functions = vec![enriched_func("fn", false, false, false, vec![])];
1560
1561 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1562
1563 assert!(
1564 result.dead_functions.iter().any(|f| f.name == "fn"),
1565 "Short name 'fn' (2 chars) with count=3 should be in dead_functions (needs >= 5)"
1566 );
1567 }
1568
1569 #[test]
1571 fn test_refcount_short_name_high_count_rescued() {
1572 let mut ref_counts = HashMap::new();
1573 ref_counts.insert("cn".to_string(), 50);
1574
1575 let functions = vec![enriched_func("cn", false, false, false, vec![])];
1576
1577 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1578
1579 assert!(
1580 !result.dead_functions.iter().any(|f| f.name == "cn"),
1581 "Short name 'cn' (2 chars) with count=50 should NOT be dead (rescued at >= 5)"
1582 );
1583 assert!(
1584 !result.possibly_dead.iter().any(|f| f.name == "cn"),
1585 "Short name 'cn' (2 chars) with count=50 should NOT be possibly_dead"
1586 );
1587 }
1588
1589 #[test]
1592 fn test_refcount_public_vs_private() {
1593 let mut ref_counts = HashMap::new();
1594 ref_counts.insert("public_func".to_string(), 1);
1595 ref_counts.insert("_private_func".to_string(), 1);
1596
1597 let functions = vec![
1598 enriched_func("public_func", true, false, false, vec![]),
1600 enriched_func("_private_func", false, false, false, vec![]),
1602 ];
1603
1604 let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1605
1606 assert!(
1608 result.possibly_dead.iter().any(|f| f.name == "public_func"),
1609 "Public function with ref_count=1 should be in possibly_dead"
1610 );
1611 assert!(
1612 !result
1613 .dead_functions
1614 .iter()
1615 .any(|f| f.name == "public_func"),
1616 "Public function should NOT be in dead_functions"
1617 );
1618
1619 assert!(
1621 result
1622 .dead_functions
1623 .iter()
1624 .any(|f| f.name == "_private_func"),
1625 "Private function with ref_count=1 should be in dead_functions"
1626 );
1627 assert!(
1628 !result
1629 .possibly_dead
1630 .iter()
1631 .any(|f| f.name == "_private_func"),
1632 "Private function should NOT be in possibly_dead"
1633 );
1634 }
1635
1636 #[test]
1640 fn test_backward_compat_cg() {
1641 let graph = create_test_graph();
1642 let functions = vec![
1643 FunctionRef::new("main.py".into(), "main"),
1644 FunctionRef::new("main.py".into(), "process"),
1645 FunctionRef::new("utils.py".into(), "helper"),
1646 FunctionRef::new("utils.py".into(), "_orphaned"),
1647 enriched_func("public_orphan", true, false, false, vec![]),
1648 ];
1649
1650 let result = dead_code_analysis(&graph, &functions, None).unwrap();
1651
1652 assert!(
1654 !result.dead_functions.iter().any(|f| f.name == "main"),
1655 "main should not be dead (entry point)"
1656 );
1657 assert!(
1659 !result.dead_functions.iter().any(|f| f.name == "process"),
1660 "process should not be dead (called)"
1661 );
1662 assert!(
1664 !result.dead_functions.iter().any(|f| f.name == "helper"),
1665 "helper should not be dead (called)"
1666 );
1667 assert!(
1669 result.dead_functions.iter().any(|f| f.name == "_orphaned"),
1670 "_orphaned should be in dead_functions (private, uncalled)"
1671 );
1672 assert!(
1674 result
1675 .possibly_dead
1676 .iter()
1677 .any(|f| f.name == "public_orphan"),
1678 "public_orphan should be in possibly_dead (public, uncalled)"
1679 );
1680
1681 assert_eq!(
1683 result.total_dead, 1,
1684 "Should have 1 definitely dead function"
1685 );
1686 assert_eq!(
1687 result.total_possibly_dead, 1,
1688 "Should have 1 possibly dead function"
1689 );
1690 assert_eq!(result.total_functions, 5, "Should have 5 total functions");
1691 assert!(
1693 (result.dead_percentage - 20.0).abs() < 0.01,
1694 "Dead percentage should be 20%, got {}",
1695 result.dead_percentage
1696 );
1697 }
1698
1699 #[test]
1704 fn test_functionref_has_line_field() {
1705 let func = FunctionRef::new("test.py".into(), "my_func");
1707 assert_eq!(func.line, 0, "Default line should be 0");
1709
1710 let func_with_line = FunctionRef { line: 42, ..func };
1712 assert_eq!(func_with_line.line, 42);
1713 }
1714
1715 #[test]
1716 fn test_functionref_has_signature_field() {
1717 let func = FunctionRef::new("test.py".into(), "my_func");
1719 assert!(
1721 func.signature.is_empty(),
1722 "Default signature should be empty"
1723 );
1724
1725 let func_with_sig = FunctionRef {
1727 signature: "def my_func(x, y)".to_string(),
1728 ..func
1729 };
1730 assert_eq!(func_with_sig.signature, "def my_func(x, y)");
1731 }
1732
1733 #[test]
1734 fn test_functionref_line_serializes_in_json() {
1735 let func = FunctionRef {
1737 file: PathBuf::from("test.py"),
1738 name: "my_func".to_string(),
1739 line: 42,
1740 signature: String::new(),
1741 ref_count: 0,
1742 is_public: false,
1743 is_test: false,
1744 is_trait_method: false,
1745 has_decorator: false,
1746 decorator_names: vec![],
1747 };
1748
1749 let json = serde_json::to_string(&func).unwrap();
1750 assert!(
1751 json.contains("\"line\":42"),
1752 "JSON should contain line field, got: {}",
1753 json
1754 );
1755 }
1756
1757 #[test]
1758 fn test_functionref_signature_serializes_in_json() {
1759 let func = FunctionRef {
1761 file: PathBuf::from("test.py"),
1762 name: "my_func".to_string(),
1763 line: 10,
1764 signature: "def my_func(x: int, y: int) -> int".to_string(),
1765 ref_count: 0,
1766 is_public: false,
1767 is_test: false,
1768 is_trait_method: false,
1769 has_decorator: false,
1770 decorator_names: vec![],
1771 };
1772
1773 let json = serde_json::to_string(&func).unwrap();
1774 assert!(
1775 json.contains("\"signature\""),
1776 "JSON should contain signature field, got: {}",
1777 json
1778 );
1779 assert!(
1780 json.contains("my_func(x: int"),
1781 "JSON should contain signature content"
1782 );
1783 }
1784
1785 #[test]
1786 fn test_collect_all_functions_carries_line_number() {
1787 use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
1789
1790 let module_infos = vec![(
1791 PathBuf::from("test.py"),
1792 ModuleInfo {
1793 file_path: PathBuf::from("test.py"),
1794 language: Language::Python,
1795 docstring: None,
1796 imports: vec![],
1797 functions: vec![FunctionInfo {
1798 name: "my_func".to_string(),
1799 params: vec!["x".to_string(), "y".to_string()],
1800 return_type: Some("int".to_string()),
1801 docstring: None,
1802 is_method: false,
1803 is_async: false,
1804 decorators: vec![],
1805 line_number: 42,
1806 }],
1807 classes: vec![],
1808 constants: vec![],
1809 call_graph: IntraFileCallGraph::default(),
1810 },
1811 )];
1812
1813 let functions = collect_all_functions(&module_infos);
1814 assert_eq!(functions.len(), 1);
1815 assert_eq!(
1816 functions[0].line, 42,
1817 "line should be populated from FunctionInfo.line_number"
1818 );
1819 }
1820
1821 #[test]
1822 fn test_collect_all_functions_builds_signature() {
1823 use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
1825
1826 let module_infos = vec![(
1827 PathBuf::from("test.py"),
1828 ModuleInfo {
1829 file_path: PathBuf::from("test.py"),
1830 language: Language::Python,
1831 docstring: None,
1832 imports: vec![],
1833 functions: vec![FunctionInfo {
1834 name: "calculate".to_string(),
1835 params: vec!["x".to_string(), "y".to_string()],
1836 return_type: Some("int".to_string()),
1837 docstring: None,
1838 is_method: false,
1839 is_async: false,
1840 decorators: vec![],
1841 line_number: 10,
1842 }],
1843 classes: vec![],
1844 constants: vec![],
1845 call_graph: IntraFileCallGraph::default(),
1846 },
1847 )];
1848
1849 let functions = collect_all_functions(&module_infos);
1850 assert_eq!(functions.len(), 1);
1851 assert!(
1853 !functions[0].signature.is_empty(),
1854 "Signature should be populated, got empty"
1855 );
1856 assert!(
1857 functions[0].signature.contains("calculate"),
1858 "Signature should contain function name, got: {}",
1859 functions[0].signature
1860 );
1861 assert!(
1862 functions[0].signature.contains("x"),
1863 "Signature should contain parameter names, got: {}",
1864 functions[0].signature
1865 );
1866 }
1867
1868 #[test]
1869 fn test_functionref_new_defaults_line_and_signature() {
1870 let func = FunctionRef::new("test.py".into(), "func");
1872 assert_eq!(func.line, 0);
1873 assert_eq!(func.signature, "");
1874 }
1875
1876 fn make_class(name: &str, bases: Vec<&str>, decorators: Vec<&str>) -> crate::types::ClassInfo {
1882 crate::types::ClassInfo {
1883 name: name.to_string(),
1884 bases: bases.into_iter().map(|s| s.to_string()).collect(),
1885 docstring: None,
1886 methods: vec![],
1887 fields: vec![],
1888 decorators: decorators.into_iter().map(|s| s.to_string()).collect(),
1889 line_number: 1,
1890 }
1891 }
1892
1893 #[test]
1894 fn test_is_trait_or_interface_rust_trait_decorator() {
1895 use crate::types::Language;
1897 let class = make_class("Iterator", vec![], vec!["trait"]);
1898 assert!(
1899 is_trait_or_interface(&class, Language::Rust),
1900 "Rust class with 'trait' decorator should be detected as interface"
1901 );
1902 }
1903
1904 #[test]
1905 fn test_is_trait_or_interface_rust_plain_struct_not_trait() {
1906 use crate::types::Language;
1908 let class = make_class("MyStruct", vec![], vec![]);
1909 assert!(
1910 !is_trait_or_interface(&class, Language::Rust),
1911 "Plain Rust struct should not be detected as interface"
1912 );
1913 }
1914
1915 #[test]
1916 fn test_is_trait_or_interface_go_interface_suffix() {
1917 use crate::types::Language;
1919 let class = make_class("Reader", vec![], vec![]);
1920 assert!(
1921 is_trait_or_interface(&class, Language::Go),
1922 "Go class named 'Reader' (single-method interface pattern) should be detected"
1923 );
1924 }
1925
1926 #[test]
1927 fn test_is_trait_or_interface_go_non_interface() {
1928 use crate::types::Language;
1930 let class = make_class("Config", vec![], vec![]);
1931 assert!(
1932 !is_trait_or_interface(&class, Language::Go),
1933 "Go class named 'Config' should not be detected as interface"
1934 );
1935 }
1936
1937 #[test]
1938 fn test_is_trait_or_interface_go_interface_decorator() {
1939 use crate::types::Language;
1941 let class = make_class("Handler", vec![], vec!["interface"]);
1942 assert!(
1943 is_trait_or_interface(&class, Language::Go),
1944 "Go class with 'interface' decorator should be detected"
1945 );
1946 }
1947
1948 #[test]
1949 fn test_is_trait_or_interface_swift_protocol_decorator() {
1950 use crate::types::Language;
1952 let class = make_class("Codable", vec![], vec!["protocol"]);
1953 assert!(
1954 is_trait_or_interface(&class, Language::Swift),
1955 "Swift class with 'protocol' decorator should be detected as interface"
1956 );
1957 }
1958
1959 #[test]
1960 fn test_is_trait_or_interface_swift_protocol_suffix() {
1961 use crate::types::Language;
1963 let class = make_class("ViewProtocol", vec![], vec![]);
1964 assert!(
1965 is_trait_or_interface(&class, Language::Swift),
1966 "Swift class ending in 'Protocol' should be detected as interface"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_is_trait_or_interface_swift_delegate_suffix() {
1972 use crate::types::Language;
1974 let class = make_class("UITableViewDelegate", vec![], vec![]);
1975 assert!(
1976 is_trait_or_interface(&class, Language::Swift),
1977 "Swift class ending in 'Delegate' should be detected as interface"
1978 );
1979 }
1980
1981 #[test]
1982 fn test_is_trait_or_interface_swift_datasource_suffix() {
1983 use crate::types::Language;
1985 let class = make_class("UITableViewDataSource", vec![], vec![]);
1986 assert!(
1987 is_trait_or_interface(&class, Language::Swift),
1988 "Swift class ending in 'DataSource' should be detected as interface"
1989 );
1990 }
1991
1992 #[test]
1993 fn test_is_trait_or_interface_scala_trait_decorator() {
1994 use crate::types::Language;
1996 let class = make_class("Ordered", vec![], vec!["trait"]);
1997 assert!(
1998 is_trait_or_interface(&class, Language::Scala),
1999 "Scala class with 'trait' decorator should be detected as interface"
2000 );
2001 }
2002
2003 #[test]
2004 fn test_is_trait_or_interface_php_interface_decorator() {
2005 use crate::types::Language;
2007 let class = make_class("Countable", vec![], vec!["interface"]);
2008 assert!(
2009 is_trait_or_interface(&class, Language::Php),
2010 "PHP class with 'interface' decorator should be detected"
2011 );
2012 }
2013
2014 #[test]
2015 fn test_is_trait_or_interface_php_trait_decorator() {
2016 use crate::types::Language;
2018 let class = make_class("Loggable", vec![], vec!["trait"]);
2019 assert!(
2020 is_trait_or_interface(&class, Language::Php),
2021 "PHP class with 'trait' decorator should be detected as interface"
2022 );
2023 }
2024
2025 #[test]
2026 fn test_is_trait_or_interface_ruby_module_mixin() {
2027 use crate::types::Language;
2029 let class = make_class("Comparable", vec![], vec![]);
2030 assert!(
2031 is_trait_or_interface(&class, Language::Ruby),
2032 "Ruby class named 'Comparable' should be detected as interface/mixin"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_is_trait_or_interface_ruby_module_decorator() {
2038 use crate::types::Language;
2040 let class = make_class("Serializable", vec![], vec!["module"]);
2041 assert!(
2042 is_trait_or_interface(&class, Language::Ruby),
2043 "Ruby class with 'module' decorator should be detected as interface/mixin"
2044 );
2045 }
2046
2047 #[test]
2048 fn test_is_trait_or_interface_typescript_interface_decorator() {
2049 use crate::types::Language;
2051 let class = make_class("UserService", vec![], vec!["interface"]);
2052 assert!(
2053 is_trait_or_interface(&class, Language::TypeScript),
2054 "TypeScript class with 'interface' decorator should be detected"
2055 );
2056 }
2057
2058 #[test]
2059 fn test_is_trait_or_interface_java_i_prefix() {
2060 use crate::types::Language;
2062 let class = make_class("IRepository", vec![], vec![]);
2063 assert!(
2064 is_trait_or_interface(&class, Language::Java),
2065 "Java class with I-prefix should be detected as interface"
2066 );
2067 }
2068
2069 #[test]
2070 fn test_is_trait_or_interface_python_protocol_base() {
2071 use crate::types::Language;
2073 let class = make_class("Comparable", vec!["Protocol"], vec![]);
2074 assert!(
2075 is_trait_or_interface(&class, Language::Python),
2076 "Python class with Protocol base should be detected"
2077 );
2078 }
2079
2080 #[test]
2081 fn test_is_trait_or_interface_python_abc_base() {
2082 use crate::types::Language;
2084 let class = make_class("AbstractHandler", vec!["ABC"], vec![]);
2085 assert!(
2086 is_trait_or_interface(&class, Language::Python),
2087 "Python class with ABC base should be detected"
2088 );
2089 }
2090
2091 #[test]
2092 fn test_is_trait_or_interface_collect_functions_marks_trait_methods() {
2093 use crate::types::{ClassInfo, FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
2095
2096 let module_infos = vec![(
2097 PathBuf::from("lib.php"),
2098 ModuleInfo {
2099 file_path: PathBuf::from("lib.php"),
2100 language: Language::Php,
2101 docstring: None,
2102 imports: vec![],
2103 functions: vec![],
2104 classes: vec![ClassInfo {
2105 name: "Cacheable".to_string(),
2106 bases: vec![],
2107 docstring: None,
2108 methods: vec![FunctionInfo {
2109 name: "cache_key".to_string(),
2110 params: vec![],
2111 return_type: Some("string".to_string()),
2112 docstring: None,
2113 is_method: true,
2114 is_async: false,
2115 decorators: vec![],
2116 line_number: 5,
2117 }],
2118 fields: vec![],
2119 decorators: vec!["interface".to_string()],
2120 line_number: 3,
2121 }],
2122 constants: vec![],
2123 call_graph: IntraFileCallGraph::default(),
2124 },
2125 )];
2126
2127 let functions = collect_all_functions(&module_infos);
2128 assert_eq!(functions.len(), 1);
2129 assert!(
2130 functions[0].is_trait_method,
2131 "Methods of a PHP interface class should have is_trait_method=true"
2132 );
2133 }
2134
2135 #[test]
2140 fn test_framework_entry_file_nextjs() {
2141 use crate::types::Language;
2142 assert!(
2144 is_framework_entry_file(Path::new("app/dashboard/page.tsx"), Language::TypeScript),
2145 "page.tsx should be detected as Next.js framework entry"
2146 );
2147 assert!(
2148 is_framework_entry_file(Path::new("app/layout.tsx"), Language::TypeScript),
2149 "layout.tsx should be detected as Next.js framework entry"
2150 );
2151 assert!(
2152 is_framework_entry_file(Path::new("app/api/users/route.ts"), Language::TypeScript),
2153 "route.ts should be detected as Next.js framework entry"
2154 );
2155 assert!(
2156 is_framework_entry_file(Path::new("app/loading.tsx"), Language::TypeScript),
2157 "loading.tsx should be detected as Next.js framework entry"
2158 );
2159 assert!(
2160 is_framework_entry_file(Path::new("app/error.tsx"), Language::TypeScript),
2161 "error.tsx should be detected as Next.js framework entry"
2162 );
2163 assert!(
2164 is_framework_entry_file(Path::new("app/not-found.tsx"), Language::TypeScript),
2165 "not-found.tsx should be detected as Next.js framework entry"
2166 );
2167 assert!(
2168 is_framework_entry_file(Path::new("middleware.ts"), Language::TypeScript),
2169 "middleware.ts should be detected as Next.js framework entry"
2170 );
2171 }
2172
2173 #[test]
2174 fn test_framework_entry_file_django() {
2175 use crate::types::Language;
2176 assert!(
2177 is_framework_entry_file(Path::new("myapp/views.py"), Language::Python),
2178 "views.py should be detected as Django framework entry"
2179 );
2180 assert!(
2181 is_framework_entry_file(Path::new("myapp/models.py"), Language::Python),
2182 "models.py should be detected as Django framework entry"
2183 );
2184 assert!(
2185 is_framework_entry_file(Path::new("myapp/admin.py"), Language::Python),
2186 "admin.py should be detected as Django framework entry"
2187 );
2188 assert!(
2189 is_framework_entry_file(Path::new("myapp/serializers.py"), Language::Python),
2190 "serializers.py should be detected as Django framework entry"
2191 );
2192 assert!(
2193 is_framework_entry_file(Path::new("myapp/tasks.py"), Language::Python),
2194 "tasks.py should be detected as Celery framework entry"
2195 );
2196 assert!(
2197 is_framework_entry_file(Path::new("conftest.py"), Language::Python),
2198 "conftest.py should be detected as pytest framework entry"
2199 );
2200 }
2201
2202 #[test]
2203 fn test_framework_entry_file_rails() {
2204 use crate::types::Language;
2205 assert!(
2206 is_framework_entry_file(
2207 Path::new("app/controllers/users_controller.rb"),
2208 Language::Ruby
2209 ),
2210 "*_controller.rb in controllers/ should be detected as Rails framework entry"
2211 );
2212 assert!(
2213 is_framework_entry_file(Path::new("app/models/user.rb"), Language::Ruby),
2214 "*.rb in models/ should be detected as Rails framework entry"
2215 );
2216 assert!(
2217 is_framework_entry_file(
2218 Path::new("app/helpers/application_helper.rb"),
2219 Language::Ruby
2220 ),
2221 "*_helper.rb in helpers/ should be detected as Rails framework entry"
2222 );
2223 assert!(
2224 is_framework_entry_file(Path::new("config/routes.rb"), Language::Ruby),
2225 "routes.rb should be detected as Rails framework entry"
2226 );
2227 }
2228
2229 #[test]
2230 fn test_framework_entry_file_spring() {
2231 use crate::types::Language;
2232 assert!(
2233 is_framework_entry_file(Path::new("src/UserController.java"), Language::Java),
2234 "*Controller.java should be detected as Spring framework entry"
2235 );
2236 assert!(
2237 is_framework_entry_file(Path::new("src/UserService.java"), Language::Java),
2238 "*Service.java should be detected as Spring framework entry"
2239 );
2240 assert!(
2241 is_framework_entry_file(Path::new("src/UserRepository.java"), Language::Java),
2242 "*Repository.java should be detected as Spring framework entry"
2243 );
2244 assert!(
2245 is_framework_entry_file(Path::new("src/AppConfiguration.java"), Language::Java),
2246 "*Configuration.java should be detected as Spring framework entry"
2247 );
2248 assert!(
2250 is_framework_entry_file(Path::new("src/UserController.kt"), Language::Kotlin),
2251 "*Controller.kt should be detected as Spring/Kotlin framework entry"
2252 );
2253 assert!(
2255 is_framework_entry_file(Path::new("src/MainActivity.java"), Language::Java),
2256 "*Activity.java should be detected as Android framework entry"
2257 );
2258 assert!(
2259 is_framework_entry_file(Path::new("src/HomeFragment.kt"), Language::Kotlin),
2260 "*Fragment.kt should be detected as Android/Kotlin framework entry"
2261 );
2262 }
2263
2264 #[test]
2265 fn test_framework_entry_file_non_framework() {
2266 use crate::types::Language;
2267 assert!(
2268 !is_framework_entry_file(Path::new("src/utils.ts"), Language::TypeScript),
2269 "utils.ts should NOT be detected as framework entry"
2270 );
2271 assert!(
2272 !is_framework_entry_file(Path::new("src/helpers.py"), Language::Python),
2273 "helpers.py should NOT be detected as framework entry"
2274 );
2275 assert!(
2276 !is_framework_entry_file(Path::new("lib/parser.rb"), Language::Ruby),
2277 "parser.rb should NOT be detected as framework entry"
2278 );
2279 assert!(
2280 !is_framework_entry_file(Path::new("src/Utils.java"), Language::Java),
2281 "Utils.java should NOT be detected as framework entry"
2282 );
2283 assert!(
2284 !is_framework_entry_file(Path::new("src/random.go"), Language::Go),
2285 "random.go should NOT be detected as framework entry"
2286 );
2287 }
2288
2289 #[test]
2290 fn test_framework_directive_use_server() {
2291 let dir = std::env::temp_dir().join("tldr_test_framework_directive");
2293 std::fs::create_dir_all(&dir).unwrap();
2294 let file = dir.join("actions.ts");
2295 std::fs::write(&file, "'use server'\n\nexport async function createUser() {}\n").unwrap();
2296
2297 assert!(
2298 has_framework_directive(&file),
2299 "File with 'use server' directive should be detected"
2300 );
2301
2302 let file2 = dir.join("actions2.tsx");
2304 std::fs::write(&file2, "\"use server\";\n\nexport async function deleteUser() {}\n")
2305 .unwrap();
2306
2307 assert!(
2308 has_framework_directive(&file2),
2309 "File with \"use server\"; directive should be detected"
2310 );
2311
2312 let _ = std::fs::remove_dir_all(&dir);
2314 }
2315
2316 #[test]
2317 fn test_framework_directive_use_client() {
2318 let dir = std::env::temp_dir().join("tldr_test_framework_directive_client");
2319 std::fs::create_dir_all(&dir).unwrap();
2320 let file = dir.join("component.tsx");
2321 std::fs::write(
2322 &file,
2323 "'use client'\n\nimport React from 'react';\n\nexport function Button() {}\n",
2324 )
2325 .unwrap();
2326
2327 assert!(
2328 has_framework_directive(&file),
2329 "File with 'use client' directive should be detected"
2330 );
2331
2332 let _ = std::fs::remove_dir_all(&dir);
2334 }
2335
2336 #[test]
2337 fn test_framework_directive_absent() {
2338 let dir = std::env::temp_dir().join("tldr_test_framework_directive_absent");
2339 std::fs::create_dir_all(&dir).unwrap();
2340 let file = dir.join("utils.ts");
2341 std::fs::write(
2342 &file,
2343 "import { helper } from './helper';\n\nexport function doWork() {}\n",
2344 )
2345 .unwrap();
2346
2347 assert!(
2348 !has_framework_directive(&file),
2349 "File without framework directive should NOT be detected"
2350 );
2351
2352 let py_file = dir.join("views.py");
2354 std::fs::write(&py_file, "'use server'\ndef view(): pass\n").unwrap();
2355
2356 assert!(
2357 !has_framework_directive(&py_file),
2358 "Non-JS/TS file should NOT be detected even with directive-like content"
2359 );
2360
2361 let _ = std::fs::remove_dir_all(&dir);
2363 }
2364
2365 #[test]
2366 fn test_collect_functions_skips_framework_entries() {
2367 use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
2370
2371 let module_infos = vec![(
2372 PathBuf::from("app/dashboard/page.tsx"),
2373 ModuleInfo {
2374 file_path: PathBuf::from("app/dashboard/page.tsx"),
2375 language: Language::TypeScript,
2376 docstring: None,
2377 imports: vec![],
2378 functions: vec![
2379 FunctionInfo {
2380 name: "DashboardPage".to_string(),
2381 params: vec![],
2382 return_type: Some("JSX.Element".to_string()),
2383 docstring: None,
2384 is_method: false,
2385 is_async: false,
2386 decorators: vec![],
2387 line_number: 5,
2388 },
2389 FunctionInfo {
2390 name: "generateMetadata".to_string(),
2391 params: vec![],
2392 return_type: Some("Metadata".to_string()),
2393 docstring: None,
2394 is_method: false,
2395 is_async: true,
2396 decorators: vec![],
2397 line_number: 20,
2398 },
2399 FunctionInfo {
2401 name: "_privateHelper".to_string(),
2402 params: vec![],
2403 return_type: None,
2404 docstring: None,
2405 is_method: false,
2406 is_async: false,
2407 decorators: vec![],
2408 line_number: 30,
2409 },
2410 ],
2411 classes: vec![],
2412 constants: vec![],
2413 call_graph: IntraFileCallGraph::default(),
2414 },
2415 )];
2416
2417 let functions = collect_all_functions(&module_infos);
2418 assert_eq!(functions.len(), 3);
2419
2420 let dashboard = functions.iter().find(|f| f.name == "DashboardPage").unwrap();
2423 assert!(
2424 dashboard.has_decorator,
2425 "Public function in page.tsx should have has_decorator=true (framework entry)"
2426 );
2427 assert!(
2428 dashboard.is_public,
2429 "DashboardPage should be public"
2430 );
2431
2432 let metadata = functions.iter().find(|f| f.name == "generateMetadata").unwrap();
2434 assert!(
2435 metadata.has_decorator,
2436 "Public function in page.tsx should have has_decorator=true (framework entry)"
2437 );
2438
2439 let private_fn = functions.iter().find(|f| f.name == "_privateHelper").unwrap();
2441 assert!(
2442 !private_fn.has_decorator,
2443 "Private function in page.tsx should NOT have has_decorator=true"
2444 );
2445 assert!(
2446 !private_fn.is_public,
2447 "_privateHelper should not be public"
2448 );
2449
2450 let graph = ProjectCallGraph::new();
2452 let result = dead_code_analysis(&graph, &functions, None).unwrap();
2453
2454 assert!(
2456 !result
2457 .possibly_dead
2458 .iter()
2459 .any(|f| f.name == "DashboardPage"),
2460 "DashboardPage (framework entry) should not be in possibly_dead"
2461 );
2462 assert!(
2463 !result
2464 .possibly_dead
2465 .iter()
2466 .any(|f| f.name == "generateMetadata"),
2467 "generateMetadata (framework entry) should not be in possibly_dead"
2468 );
2469
2470 assert!(
2472 result
2473 .dead_functions
2474 .iter()
2475 .any(|f| f.name == "_privateHelper"),
2476 "_privateHelper should be in dead_functions"
2477 );
2478 }
2479}