Skip to main content

tldr_core/analysis/
dead.rs

1//! Dead code analysis (spec Section 2.2.3)
2//!
3//! Find functions that are never called (dead code).
4//!
5//! # Exclusion Patterns (not considered dead)
6//! - App entry: main, __main__, cli, app, run, start, create_app
7//! - Test: test_*, pytest_*, Test*, Benchmark*, setUp, tearDown
8//! - Lifecycle: onCreate, onStart, onDestroy, init, destroy, etc.
9//! - Handlers: handle*, Handle*, on_*, before_*, after_*
10//! - Hooks: load, configure, request, response, invoke, call, execute
11//! - HTTP: ServeHTTP, doGet, doPost, handler
12//! - Dunder methods (__init__, __str__, etc.)
13//! - Custom patterns from entry_points parameter
14//!
15//! # Performance
16//! - O(E + V) where E = edges, V = functions
17
18use std::collections::{HashMap, HashSet};
19use std::path::{Path, PathBuf};
20
21use crate::types::{DeadCodeReport, FunctionRef, ProjectCallGraph};
22use crate::TldrResult;
23
24// Re-export for convenience
25#[allow(unused_imports)]
26use super::refcount::is_rescued_by_refcount;
27
28/// Analyze dead (unreachable) code.
29///
30/// # Arguments
31/// * `call_graph` - Project call graph
32/// * `all_functions` - All functions in the project
33/// * `entry_points` - Optional custom entry point patterns
34///
35/// # Returns
36/// * `Ok(DeadCodeReport)` - Dead code analysis results
37pub fn dead_code_analysis(
38    call_graph: &ProjectCallGraph,
39    all_functions: &[FunctionRef],
40    entry_points: Option<&[String]>,
41) -> TldrResult<DeadCodeReport> {
42    // Build set of all functions that are called
43    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    // Build set of all functions that call others (callers are entry points)
53    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    // Find dead functions, classifying into "definitely dead" and "possibly dead"
62    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        // Skip if called by anyone
68        if called_functions.contains(func_ref) {
69            continue;
70        }
71
72        // Skip if matches entry point patterns
73        if is_entry_point_name(&func_ref.name, entry_points) {
74            continue;
75        }
76
77        // Skip dunder methods (__init__, __str__, etc.) - called implicitly by runtime
78        // Check both bare name and Class.method format (also supports Lua module:method)
79        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        // PHP magic methods (leading __ without trailing __)
88        // These are implicitly called by PHP runtime
89        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        // Skip trait/interface methods (they are called implicitly by the type system)
117        if func_ref.is_trait_method {
118            continue;
119        }
120
121        // Skip test functions (they are called by the test runner)
122        if func_ref.is_test {
123            continue;
124        }
125
126        // Skip decorated/annotated functions (they are called by frameworks)
127        if func_ref.has_decorator {
128            continue;
129        }
130
131        // Classify: public/exported but uncalled -> possibly dead (may be API surface)
132        // Private/unenriched and uncalled -> definitely dead
133        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
164/// Analyze dead (unreachable) code using reference counting instead of a call graph.
165///
166/// This is an alternative to `dead_code_analysis()` that uses identifier reference
167/// counts to determine liveness. A function with `ref_count > 1` is considered alive
168/// because it is referenced somewhere beyond its definition. A function with
169/// `ref_count == 1` (only definition) is dead, subject to the same exclusion patterns
170/// as the call-graph-based analysis.
171///
172/// Short names (< 3 characters) need a higher refcount threshold (>= 5) to be rescued,
173/// since collision-prone names like `i`, `j`, `id` inflate counts artificially.
174///
175/// # Arguments
176/// * `all_functions` - All functions in the project
177/// * `ref_counts` - Map of identifier name to occurrence count across codebase
178/// * `entry_points` - Optional custom entry point patterns
179///
180/// # Returns
181/// * `Ok(DeadCodeReport)` - Dead code analysis results (backward compatible)
182pub 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        // Skip if matches entry point patterns (C4)
193        if is_entry_point_name(&func_ref.name, entry_points) {
194            continue;
195        }
196
197        // Skip dunder methods (__init__, __str__, etc.) - called implicitly by runtime (C5)
198        // Check both bare name and Class.method format (also supports Lua module:method)
199        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        // PHP magic methods (leading __ without trailing __)
208        // These are implicitly called by PHP runtime
209        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        // Skip trait/interface methods (C6)
237        if func_ref.is_trait_method {
238            continue;
239        }
240
241        // Skip test functions (C7)
242        if func_ref.is_test {
243            continue;
244        }
245
246        // Skip decorated/annotated functions (C8)
247        if func_ref.has_decorator {
248            continue;
249        }
250
251        // Check refcount: if rescued by refcount (ref_count > 1, name >= 3 chars) -> alive (C2)
252        if is_rescued_by_refcount(&func_ref.name, ref_counts) {
253            continue;
254        }
255
256        // Not rescued -> classify by visibility (C9)
257        // Enrich with the actual ref_count for the output
258        let mut enriched = func_ref.clone();
259        // Look up by bare name (for Class.method, use the bare method name for refcount)
260        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
294/// Check if a function name matches entry point patterns
295fn is_entry_point_name(name: &str, custom_patterns: Option<&[String]>) -> bool {
296    // Standard entry point names
297    let standard_patterns = [
298        // Application entry points
299        "main",
300        "__main__",
301        "cli",
302        "app",
303        "run",
304        "start",
305        // Test setup/teardown
306        "setup",
307        "teardown",
308        "setUp",
309        "tearDown",
310        // Python ASGI/WSGI
311        "create_app",
312        "make_app",
313        // Go HTTP
314        "ServeHTTP",
315        "Handler",
316        "handler",
317        // C/system callbacks
318        "OnLoad",
319        "OnInit",
320        "OnExit",
321        // Android/Kotlin lifecycle
322        "onCreate",
323        "onStart",
324        "onStop",
325        "onResume",
326        "onPause",
327        "onDestroy",
328        "onBind",
329        "onClick",
330        "onCreateView",
331        // Java Servlet / Spring
332        "doGet",
333        "doPost",
334        "doPut",
335        "doDelete",
336        "init",
337        "destroy",
338        "service",
339        // Plugin/middleware hooks
340        "load",
341        "configure",
342        "request",
343        "response",
344        "error",
345        "invoke",
346        "call",
347        "execute",
348        // Next.js instrumentation hooks
349        "register",
350        "onRequestError",
351    ];
352
353    if standard_patterns.contains(&name) {
354        return true;
355    }
356
357    // Extract bare method name from "Class.method" or "module:method" format
358    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    // Test function patterns
370    if name.starts_with("test_") || name.starts_with("pytest_") {
371        return true;
372    }
373
374    // Test patterns on bare method name too
375    if bare_name != name && (bare_name.starts_with("test_") || bare_name.starts_with("pytest_")) {
376        return true;
377    }
378
379    // Go-style test functions (TestXxx, BenchmarkXxx, ExampleXxx)
380    if name.starts_with("Test") || name.starts_with("Benchmark") || name.starts_with("Example") {
381        return true;
382    }
383
384    // Java/Kotlin @Test annotation convention (methods starting with "test")
385    if bare_name.starts_with("test") {
386        return true;
387    }
388
389    // Prefix patterns for handlers/hooks across languages
390    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    // Check custom patterns
401    if let Some(patterns) = custom_patterns {
402        for pattern in patterns {
403            if name == pattern {
404                return true;
405            }
406            // Support simple glob patterns
407            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
425/// Build a human-readable signature string from function name, parameters, and return type.
426///
427/// Examples:
428/// - `build_signature("calculate", &["x", "y"], Some("int"))` -> `"calculate(x, y) -> int"`
429/// - `build_signature("helper", &[], None)` -> `"helper()"`
430fn 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
438/// Extract all functions from a project for dead code analysis.
439///
440/// This is a helper function that can be used to gather all functions
441/// from the AST extraction phase. It enriches FunctionRef with metadata
442/// from the AST (decorators, visibility, test status, trait context)
443/// to reduce false positives in dead code analysis.
444pub 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        // Add top-level functions
456        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        // Add class methods
481        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
519/// Check if a file path looks like a test file
520fn 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    // Common test file patterns across languages
525    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
539/// Check if a function name looks like a test function
540fn 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
548/// Check if any decorator indicates a test
549fn 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
556/// Infer visibility from function name based on language conventions.
557///
558/// This is a heuristic approach - not perfect, but vastly better than
559/// treating everything as private (which causes 95-100% FP rate).
560fn 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        // Python: no leading underscore = public (convention)
572        Language::Python => !bare_name.starts_with('_'),
573
574        // Go: uppercase first letter = exported
575        Language::Go => bare_name
576            .chars()
577            .next()
578            .map(|c| c.is_uppercase())
579            .unwrap_or(false),
580
581        // Rust: we can't tell from name alone, but `pub` functions are
582        // the majority in library crates. Without AST visibility info,
583        // treat non-underscore-prefixed as possibly public.
584        // The AST extraction code should set this more precisely.
585        Language::Rust => !bare_name.starts_with('_'),
586
587        // TypeScript/JavaScript: functions with decorators like @export
588        // or those not starting with _ are typically public
589        Language::TypeScript | Language::JavaScript => !bare_name.starts_with('_'),
590
591        // Java/Kotlin/C#/Scala: typically all non-private methods are public.
592        // Without explicit `private` keyword info, treat as public unless
593        // name starts with underscore or is clearly internal.
594        Language::Java | Language::Kotlin | Language::CSharp | Language::Scala => {
595            !bare_name.starts_with('_')
596        }
597
598        // C/C++: static functions are private; others are public.
599        // We can't tell from name, so treat as public by default.
600        Language::C | Language::Cpp => true,
601
602        // Ruby: methods after `private` keyword are private.
603        // Convention: leading underscore = private.
604        Language::Ruby => !bare_name.starts_with('_'),
605
606        // PHP: has explicit public/private/protected keywords.
607        // Convention: leading underscore = private.
608        Language::Php => !bare_name.starts_with('_'),
609
610        // Elixir: functions starting with _ are private (defp vs def)
611        Language::Elixir => !bare_name.starts_with('_'),
612
613        // Lua/Luau: local = private, module table = public
614        // Convention: _M:method = public (module API), _prefix = private
615        Language::Lua | Language::Luau => {
616            // _M:method is always public — _M is the module export table
617            if name.starts_with("_M:") || name.starts_with("_M.") {
618                return true;
619            }
620            // Extract method name after : (Lua method call syntax)
621            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        // OCaml: .mli files define public interface
630        // Convention: leading underscore = private
631        Language::Ocaml => !bare_name.starts_with('_'),
632
633        // Swift: default is internal, not public
634        Language::Swift => !bare_name.starts_with('_'),
635    }
636}
637
638/// Check if a class looks like a trait/interface/protocol/abstract class
639fn 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    // Check bases for common trait/interface patterns
648    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    // Check class decorators for abstract/interface/trait/protocol/module indicators.
658    // AST extractors tag ClassInfo with these decorators:
659    //   - PHP: "interface" for interfaces, "trait" for traits
660    //   - Scala: "trait" for traits (via inheritance extractor)
661    //   - Swift: "protocol" for protocols (via inheritance extractor)
662    //   - Ruby: "module" for modules used as mixins
663    //   - Rust: "trait" for trait items (when extracted by simple extractor)
664    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        // Rust: traits are extracted as "classes" by some AST extractor paths.
674        // The decorator check above handles cases where "trait" is set.
675        // Without a decorator, we cannot reliably distinguish traits from structs
676        // by name alone, so return false.
677        Language::Rust => false,
678
679        // Go: interfaces follow naming conventions.
680        // Common Go interfaces end in "-er" (Reader, Writer, Handler, Stringer)
681        // or have explicit "Interface" suffix.
682        Language::Go => {
683            // Explicit "Interface" suffix
684            if name.ends_with("Interface") {
685                return true;
686            }
687            // Common Go single-method interface pattern: capitalized name ending in "er"
688            // e.g., Reader, Writer, Closer, Handler, Stringer, Formatter
689            // Must be at least 3 chars and start uppercase to avoid false positives
690            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        // Java/Kotlin: interfaces are common
704        Language::Java | Language::Kotlin => {
705            // Check for interface-like naming convention (IFoo pattern)
706            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        // C#: interface naming convention (IFoo)
716        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        // Swift: protocols follow naming conventions.
727        // Common suffixes: "Protocol", "Delegate", "DataSource", "able"/"ible"
728        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        // Scala: traits use IFoo convention or end in common trait suffixes.
737        // The decorator check above handles the "trait" tag from the extractor.
738        Language::Scala => {
739            // IFoo convention (same as Java)
740            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        // PHP: interfaces and traits are tagged by the extractor with decorators
750        // ("interface" or "trait"), handled by the decorator check above.
751        // Additional naming convention: IFoo pattern
752        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        // Ruby: modules used as mixins/interfaces.
763        // Common naming patterns: ends in "able", "ible", or includes "Mixin"
764        Language::Ruby => {
765            name.ends_with("able") || name.ends_with("ible") || name.contains("Mixin")
766        }
767
768        _ => false,
769    }
770}
771
772/// Check if a file is a framework entry point (called by framework, not user code).
773///
774/// Functions in framework entry files are invoked by the framework runtime, not
775/// by user code. Their absence from the call graph doesn't mean they are dead.
776/// All exported/public functions in these files should be excluded from dead code analysis.
777fn 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            // Next.js App Router conventions
786            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            // SvelteKit conventions
832            || 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            // Nuxt conventions (files in pages/, layouts/, middleware/ dirs)
849            || (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            // Remix conventions
854            || path_str.contains("/routes/")
855            // Astro pages
856            || (path_str.contains("/pages/") && file_name.ends_with(".astro"))
857        }
858        Language::Python => {
859            // Django conventions
860            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                // Flask/FastAPI
871                || file_name == "wsgi.py"
872                || file_name == "asgi.py"
873                || file_name == "conftest.py"
874                // Celery
875                || file_name == "tasks.py"
876        }
877        Language::Ruby => {
878            // Rails conventions
879            (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            // Spring Boot conventions
891            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                // Android
902                || 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            // ASP.NET conventions
911            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            // Go HTTP handlers are typically in handler files
920            file_name == "main.go"
921                || file_name.ends_with("_handler.go")
922                || file_name.ends_with("_handlers.go")
923        }
924        Language::Php => {
925            // Laravel conventions
926            (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            // Phoenix conventions
936            (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            // SwiftUI / iOS conventions
944            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            // Play Framework conventions
951            (path_str.contains("/controllers/") && file_name.ends_with(".scala"))
952                || file_name == "routes"
953        }
954        _ => false,
955    }
956}
957
958/// Check if a file contains a framework directive that makes exports externally reachable.
959///
960/// React Server Components use `'use server'` and `'use client'` directives at the
961/// top of files. All exports from such files are framework entry points.
962fn has_framework_directive(path: &Path) -> bool {
963    // Only relevant for JS/TS files
964    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    // Read first few lines looking for directives
970    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            // Skip empty lines and comments
985            if !trimmed.is_empty()
986                && !trimmed.starts_with("//")
987                && !trimmed.starts_with("/*")
988                && !trimmed.starts_with('*')
989            {
990                // If we hit a non-directive, non-comment line, stop looking
991                // (directives must be at the top of the file)
992                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        // main calls process, process calls helper
1010        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        // 'unused' should be dead
1039        assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
1040        // 'main' is an entry point, so not dead
1041        assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
1042        // 'process' is called, so not dead
1043        assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
1044        // 'helper' is called, so not dead
1045        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(); // Empty graph
1051        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        // All are entry points or dunder methods
1061        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        // handle_request matches pattern
1076        assert!(!result
1077            .dead_functions
1078            .iter()
1079            .any(|f| f.name == "handle_request"));
1080        // process_event doesn't match
1081        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"), // entry point
1094            FunctionRef::new("a.py".into(), "test_x"), // entry point
1095        ];
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        // Go HTTP handler
1119        assert!(is_entry_point_name("ServeHTTP", None));
1120        assert!(is_entry_point_name("Handler", None));
1121        // Go test conventions
1122        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        // Class.method format should check bare method name
1162        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        // Not an entry point
1167        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    // =========================================================================
1180    // Tests for enriched FunctionRef metadata (dead code FP reduction)
1181    // =========================================================================
1182
1183    /// Helper to create an enriched FunctionRef with metadata
1184    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        // A public function that is never called should NOT be in dead_functions,
1208        // it should be in possibly_dead instead
1209        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        // Public uncalled function should NOT be in dead_functions
1215        assert!(
1216            !result.dead_functions.iter().any(|f| f.name == "pub_helper"),
1217            "Public uncalled function should not be in dead_functions"
1218        );
1219        // Should be in possibly_dead
1220        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        // A private function that is never called IS dead
1229        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        // Trait/interface methods should never be dead (they are implementations)
1259        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        // Decorated/annotated functions (e.g. @route, @command) should not be dead
1280        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        // Functions marked as is_test should not be dead
1307        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        // Test a realistic scenario with mixed public/private/trait/decorated
1332        let graph = ProjectCallGraph::new();
1333        let functions = vec![
1334            // Private uncalled -> dead
1335            enriched_func("_internal_cache", false, false, false, vec![]),
1336            // Public uncalled -> possibly_dead
1337            enriched_func("public_api_method", true, false, false, vec![]),
1338            // Trait method -> not dead at all
1339            enriched_func("Serialize.serialize", false, true, false, vec![]),
1340            // Decorated -> not dead
1341            enriched_func("handle_index", false, false, true, vec!["get"]),
1342            // Private + uncalled + no metadata -> dead
1343            enriched_func("_orphan", false, false, false, vec![]),
1344        ];
1345
1346        let result = dead_code_analysis(&graph, &functions, None).unwrap();
1347
1348        // Definitely dead: _internal_cache, _orphan (private, uncalled, no special metadata)
1349        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        // Possibly dead: public_api_method (public but uncalled)
1366        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        // Dead percentage should be based on "definitely dead" only
1376        // 2 dead out of 5 total = 40%
1377        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        // FunctionRef::new() should still work and default all new fields to false/empty
1387        // Unenriched functions should behave like the old behavior (private by default)
1388        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        // The report should have the possibly_dead and total_possibly_dead fields
1407        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        // If a public function IS called, it should appear in neither list
1415        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(), // must match enriched_func's file
1420            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        // Ensure the original test_dead_finds_uncalled logic still works.
1433        // FunctionRef::new creates unenriched refs (is_public=false),
1434        // so private uncalled functions should still be dead.
1435        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        // 'unused' is private (default) and uncalled -> dead
1446        assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
1447        // 'main' is an entry point -> not dead
1448        assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
1449        // 'process' is called -> not dead
1450        assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
1451        // 'helper' is called -> not dead
1452        assert!(!result.dead_functions.iter().any(|f| f.name == "helper"));
1453    }
1454
1455    // =========================================================================
1456    // T7-T12: Refcount-based dead code analysis tests
1457    // (Contracts C1-C3, C4/C5/C9 via refcount path)
1458    //
1459    // These tests define expected behavior for dead_code_analysis_refcount().
1460    // They are #[ignore] until the function is implemented in Phase P3.
1461    // =========================================================================
1462
1463    /// T7: A function with ref_count > 1 is rescued (alive) — not dead or possibly dead.
1464    /// Validates Contract C2: ref_count > 1 means ALIVE.
1465    #[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    /// T8: A private function with ref_count == 1 (only definition) is dead.
1491    /// Validates Contract C1: ref_count == 1 means DEAD (unless excluded).
1492    #[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    /// T9: Entry points, dunders, and test functions with ref_count == 1 are
1511    /// still excluded from dead code reports.
1512    /// Validates Contracts C4 (entry points), C5 (dunders), C7 (test functions).
1513    #[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            // "main" is an entry point (C4)
1522            enriched_func("main", false, false, false, vec![]),
1523            // "__init__" is a dunder method (C5)
1524            enriched_func("__init__", false, false, false, vec![]),
1525            // "test_something" matches test prefix pattern (C7 via entry point check)
1526            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    /// T10: Short names (< 3 chars) with low refcount are NOT rescued (collision-prone).
1552    /// Short names with very high refcount (>= 5) ARE rescued (clearly genuine usage).
1553    #[test]
1554    fn test_refcount_short_name_low_count_stays_dead() {
1555        let mut ref_counts = HashMap::new();
1556        // "fn" has 3 references but is only 2 characters — needs >= 5 to rescue
1557        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    /// T10b: Short names with high refcount (>= 5) ARE rescued — clearly not collisions.
1570    #[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    /// T11: Public uncalled functions go to possibly_dead, private uncalled go to dead.
1590    /// Validates Contract C9: visibility-based classification in refcount path.
1591    #[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            // Public, ref_count == 1 -> possibly_dead
1599            enriched_func("public_func", true, false, false, vec![]),
1600            // Private, ref_count == 1 -> dead_functions
1601            enriched_func("_private_func", false, false, false, vec![]),
1602        ];
1603
1604        let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
1605
1606        // Public uncalled -> possibly_dead
1607        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        // Private uncalled -> dead_functions
1620        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    /// T12: The original dead_code_analysis() with call graph still works correctly.
1637    /// Validates backward compatibility: refcount additions must not regress the
1638    /// existing call-graph-based dead code detection.
1639    #[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        // 'main' is entry point -> not dead
1653        assert!(
1654            !result.dead_functions.iter().any(|f| f.name == "main"),
1655            "main should not be dead (entry point)"
1656        );
1657        // 'process' is called -> not dead
1658        assert!(
1659            !result.dead_functions.iter().any(|f| f.name == "process"),
1660            "process should not be dead (called)"
1661        );
1662        // 'helper' is called -> not dead
1663        assert!(
1664            !result.dead_functions.iter().any(|f| f.name == "helper"),
1665            "helper should not be dead (called)"
1666        );
1667        // '_orphaned' is private, not called -> dead
1668        assert!(
1669            result.dead_functions.iter().any(|f| f.name == "_orphaned"),
1670            "_orphaned should be in dead_functions (private, uncalled)"
1671        );
1672        // 'public_orphan' is public, not called -> possibly_dead
1673        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        // Verify stats
1682        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        // 1 dead out of 5 = 20%
1692        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    // =========================================================================
1700    // Tests for enriched output fields (line, signature, ref_count)
1701    // =========================================================================
1702
1703    #[test]
1704    fn test_functionref_has_line_field() {
1705        // FunctionRef should have a line field for the start line number
1706        let func = FunctionRef::new("test.py".into(), "my_func");
1707        // Default should be 0 (unknown)
1708        assert_eq!(func.line, 0, "Default line should be 0");
1709
1710        // Should be settable
1711        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        // FunctionRef should have a signature field
1718        let func = FunctionRef::new("test.py".into(), "my_func");
1719        // Default should be empty string
1720        assert!(
1721            func.signature.is_empty(),
1722            "Default signature should be empty"
1723        );
1724
1725        // Should be settable
1726        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        // FunctionRef should include 'line' in its JSON serialization
1736        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        // FunctionRef should include 'signature' in its JSON serialization when non-empty
1760        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        // collect_all_functions should populate the line field from FunctionInfo.line_number
1788        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        // collect_all_functions should build a signature string from params and return_type
1824        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        // Signature should contain the function name and parameters
1852        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        // FunctionRef::new should default line to 0 and signature to empty
1871        let func = FunctionRef::new("test.py".into(), "func");
1872        assert_eq!(func.line, 0);
1873        assert_eq!(func.signature, "");
1874    }
1875
1876    // =========================================================================
1877    // Tests for is_trait_or_interface: multi-language interface detection
1878    // =========================================================================
1879
1880    /// Helper to create a ClassInfo with given name, bases, and decorators
1881    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        // Rust traits extracted with "trait" decorator should be detected
1896        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        // A plain Rust struct should NOT be detected as an interface
1907        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        // Go interfaces often end with "Interface" suffix or "er" suffix
1918        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        // A Go struct with a regular name should NOT be detected
1929        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        // Go interface explicitly tagged with decorator
1940        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        // Swift protocols tagged with "protocol" decorator should be detected
1951        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        // Swift protocols with "Protocol" suffix
1962        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        // Swift delegates (commonly protocols) with "Delegate" suffix
1973        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        // Swift data source protocols with "DataSource" suffix
1984        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        // Scala traits tagged with "trait" decorator
1995        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        // PHP interfaces tagged with "interface" decorator by extractor
2006        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        // PHP traits tagged with "trait" decorator by extractor
2017        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        // Ruby modules used as mixins - naming convention with "able" suffix
2028        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        // Ruby module explicitly tagged with decorator
2039        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        // TypeScript interface tagged with decorator (already partly handled)
2050        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        // Java interface with IFoo naming convention (already handled)
2061        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        // Python typing.Protocol base class (already handled)
2072        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        // Python ABC base class (already handled)
2083        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        // Integration test: collect_all_functions should mark methods of trait classes
2094        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    // =========================================================================
2136    // Tests for framework entry point detection (false positive reduction)
2137    // =========================================================================
2138
2139    #[test]
2140    fn test_framework_entry_file_nextjs() {
2141        use crate::types::Language;
2142        // Next.js App Router conventions
2143        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        // Kotlin equivalents
2249        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        // Android
2254        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        // Create a temp file with 'use server' directive
2292        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        // Also test double-quote variant
2303        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        // Cleanup
2313        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        // Cleanup
2333        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        // Non-JS file should not be detected
2353        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        // Cleanup
2362        let _ = std::fs::remove_dir_all(&dir);
2363    }
2364
2365    #[test]
2366    fn test_collect_functions_skips_framework_entries() {
2367        // Integration test: public functions from page.tsx should have has_decorator=true
2368        // so they are excluded from dead code analysis
2369        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                    // Private function should NOT get framework treatment
2400                    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        // DashboardPage is public (no leading underscore) and in a framework entry file
2421        // -> should have has_decorator = true (framework entry treatment)
2422        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        // generateMetadata is also public in a framework entry file
2433        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        // _privateHelper is private, so framework entry treatment should NOT apply
2440        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        // Now verify that public framework entry functions are NOT reported as dead
2451        let graph = ProjectCallGraph::new();
2452        let result = dead_code_analysis(&graph, &functions, None).unwrap();
2453
2454        // DashboardPage and generateMetadata should NOT be in possibly_dead
2455        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        // _privateHelper SHOULD be in dead_functions (private, uncalled, no framework treatment)
2471        assert!(
2472            result
2473                .dead_functions
2474                .iter()
2475                .any(|f| f.name == "_privateHelper"),
2476            "_privateHelper should be in dead_functions"
2477        );
2478    }
2479}