use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::types::{DeadCodeReport, FunctionRef, ProjectCallGraph};
use crate::TldrResult;
#[allow(unused_imports)]
use super::refcount::is_rescued_by_refcount;
pub fn dead_code_analysis(
call_graph: &ProjectCallGraph,
all_functions: &[FunctionRef],
entry_points: Option<&[String]>,
) -> TldrResult<DeadCodeReport> {
let mut called_functions: HashSet<FunctionRef> = HashSet::new();
for edge in call_graph.edges() {
called_functions.insert(FunctionRef::new(
edge.dst_file.clone(),
edge.dst_func.clone(),
));
}
let mut callers: HashSet<FunctionRef> = HashSet::new();
for edge in call_graph.edges() {
callers.insert(FunctionRef::new(
edge.src_file.clone(),
edge.src_func.clone(),
));
}
let mut dead_functions: Vec<FunctionRef> = Vec::new();
let mut possibly_dead: Vec<FunctionRef> = Vec::new();
let mut by_file: HashMap<PathBuf, Vec<String>> = HashMap::new();
for func_ref in all_functions {
if called_functions.contains(func_ref) {
continue;
}
if is_entry_point_name(&func_ref.name, entry_points) {
continue;
}
let bare_name = if func_ref.name.contains('.') {
func_ref.name.rsplit('.').next().unwrap_or(&func_ref.name)
} else if func_ref.name.contains(':') {
func_ref.name.rsplit(':').next().unwrap_or(&func_ref.name)
} else {
&func_ref.name
};
static PHP_MAGIC: &[&str] = &[
"__construct",
"__destruct",
"__call",
"__callStatic",
"__get",
"__set",
"__isset",
"__unset",
"__sleep",
"__wakeup",
"__serialize",
"__unserialize",
"__toString",
"__invoke",
"__set_state",
"__clone",
"__debugInfo",
];
if PHP_MAGIC.contains(&bare_name) {
continue;
}
if bare_name.starts_with("__") && bare_name.ends_with("__") {
continue;
}
if func_ref.is_trait_method {
continue;
}
if func_ref.is_test {
continue;
}
if func_ref.has_decorator {
continue;
}
if func_ref.is_public {
possibly_dead.push(func_ref.clone());
} else {
dead_functions.push(func_ref.clone());
by_file
.entry(func_ref.file.clone())
.or_default()
.push(func_ref.name.clone());
}
}
let total_dead = dead_functions.len();
let total_possibly_dead = possibly_dead.len();
let total_functions = all_functions.len();
let dead_percentage = if total_functions > 0 {
(total_dead as f64 / total_functions as f64) * 100.0
} else {
0.0
};
Ok(DeadCodeReport {
dead_functions,
possibly_dead,
by_file,
total_dead,
total_possibly_dead,
total_functions,
dead_percentage,
})
}
pub fn dead_code_analysis_refcount(
all_functions: &[FunctionRef],
ref_counts: &HashMap<String, usize>,
entry_points: Option<&[String]>,
) -> TldrResult<DeadCodeReport> {
let mut dead_functions: Vec<FunctionRef> = Vec::new();
let mut possibly_dead: Vec<FunctionRef> = Vec::new();
let mut by_file: HashMap<PathBuf, Vec<String>> = HashMap::new();
for func_ref in all_functions {
if is_entry_point_name(&func_ref.name, entry_points) {
continue;
}
let bare_name = if func_ref.name.contains('.') {
func_ref.name.rsplit('.').next().unwrap_or(&func_ref.name)
} else if func_ref.name.contains(':') {
func_ref.name.rsplit(':').next().unwrap_or(&func_ref.name)
} else {
&func_ref.name
};
static PHP_MAGIC: &[&str] = &[
"__construct",
"__destruct",
"__call",
"__callStatic",
"__get",
"__set",
"__isset",
"__unset",
"__sleep",
"__wakeup",
"__serialize",
"__unserialize",
"__toString",
"__invoke",
"__set_state",
"__clone",
"__debugInfo",
];
if PHP_MAGIC.contains(&bare_name) {
continue;
}
if bare_name.starts_with("__") && bare_name.ends_with("__") {
continue;
}
if func_ref.is_trait_method {
continue;
}
if func_ref.is_test {
continue;
}
if func_ref.has_decorator {
continue;
}
if is_rescued_by_refcount(&func_ref.name, ref_counts) {
continue;
}
let mut enriched = func_ref.clone();
let lookup_name = bare_name;
enriched.ref_count = ref_counts.get(lookup_name).copied().unwrap_or(0) as u32;
if func_ref.is_public {
possibly_dead.push(enriched);
} else {
by_file
.entry(func_ref.file.clone())
.or_default()
.push(func_ref.name.clone());
dead_functions.push(enriched);
}
}
let total_dead = dead_functions.len();
let total_possibly_dead = possibly_dead.len();
let total_functions = all_functions.len();
let dead_percentage = if total_functions > 0 {
(total_dead as f64 / total_functions as f64) * 100.0
} else {
0.0
};
Ok(DeadCodeReport {
dead_functions,
possibly_dead,
by_file,
total_dead,
total_possibly_dead,
total_functions,
dead_percentage,
})
}
fn is_entry_point_name(name: &str, custom_patterns: Option<&[String]>) -> bool {
let standard_patterns = [
"main",
"__main__",
"cli",
"app",
"run",
"start",
"setup",
"teardown",
"setUp",
"tearDown",
"create_app",
"make_app",
"ServeHTTP",
"Handler",
"handler",
"OnLoad",
"OnInit",
"OnExit",
"onCreate",
"onStart",
"onStop",
"onResume",
"onPause",
"onDestroy",
"onBind",
"onClick",
"onCreateView",
"doGet",
"doPost",
"doPut",
"doDelete",
"init",
"destroy",
"service",
"load",
"configure",
"request",
"response",
"error",
"invoke",
"call",
"execute",
"register",
"onRequestError",
];
if standard_patterns.contains(&name) {
return true;
}
let bare_name = if name.contains('.') {
name.rsplit('.').next().unwrap_or(name)
} else if name.contains(':') {
name.rsplit(':').next().unwrap_or(name)
} else {
name
};
if bare_name != name && standard_patterns.contains(&bare_name) {
return true;
}
if name.starts_with("test_") || name.starts_with("pytest_") {
return true;
}
if bare_name != name && (bare_name.starts_with("test_") || bare_name.starts_with("pytest_")) {
return true;
}
if name.starts_with("Test") || name.starts_with("Benchmark") || name.starts_with("Example") {
return true;
}
if bare_name.starts_with("test") {
return true;
}
if bare_name.starts_with("handle") || bare_name.starts_with("Handle") {
return true;
}
if bare_name.starts_with("on_")
|| bare_name.starts_with("before_")
|| bare_name.starts_with("after_")
{
return true;
}
if let Some(patterns) = custom_patterns {
for pattern in patterns {
if name == pattern {
return true;
}
if pattern.ends_with('*') {
let prefix = pattern.trim_end_matches('*');
if name.starts_with(prefix) {
return true;
}
}
if pattern.starts_with('*') {
let suffix = pattern.trim_start_matches('*');
if name.ends_with(suffix) {
return true;
}
}
}
}
false
}
fn build_signature(name: &str, params: &[String], return_type: Option<&str>) -> String {
let params_str = params.join(", ");
match return_type {
Some(rt) if !rt.is_empty() => format!("{}({}) -> {}", name, params_str, rt),
_ => format!("{}({})", name, params_str),
}
}
pub fn collect_all_functions(
module_infos: &[(PathBuf, crate::types::ModuleInfo)],
) -> Vec<FunctionRef> {
let mut functions = Vec::new();
for (file_path, info) in module_infos {
let language = info.language;
let is_test_file = is_test_file_path(file_path);
let is_framework_entry =
is_framework_entry_file(file_path, language) || has_framework_directive(file_path);
for func in &info.functions {
let is_public =
infer_visibility_from_name(&func.name, language, !func.decorators.is_empty(), &func.decorators);
let has_decorator =
!func.decorators.is_empty() || (is_framework_entry && is_public);
let is_test = is_test_file
|| is_test_function_name(&func.name)
|| has_test_decorator(&func.decorators);
let signature = build_signature(&func.name, &func.params, func.return_type.as_deref());
functions.push(FunctionRef {
file: file_path.clone(),
name: func.name.clone(),
line: func.line_number,
signature,
ref_count: 0,
is_public,
is_test,
is_trait_method: false,
has_decorator,
decorator_names: func.decorators.clone(),
});
}
for class in &info.classes {
let is_trait = is_trait_or_interface(class, language);
for method in &class.methods {
let full_name = format!("{}.{}", class.name, method.name);
let is_public = infer_visibility_from_name(
&method.name,
language,
!method.decorators.is_empty(),
&method.decorators,
);
let has_decorator =
!method.decorators.is_empty() || (is_framework_entry && is_public);
let is_test = is_test_file
|| is_test_function_name(&method.name)
|| has_test_decorator(&method.decorators);
let signature =
build_signature(&method.name, &method.params, method.return_type.as_deref());
functions.push(FunctionRef {
file: file_path.clone(),
name: full_name,
line: method.line_number,
signature,
ref_count: 0,
is_public,
is_test,
is_trait_method: is_trait,
has_decorator,
decorator_names: method.decorators.clone(),
});
}
}
}
functions
}
fn is_test_file_path(path: &Path) -> bool {
let path_str = path.to_string_lossy();
let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
file_name.starts_with("test_")
|| file_name.ends_with("_test")
|| file_name.ends_with("_tests")
|| file_name.ends_with("_spec")
|| file_name.starts_with("Test")
|| file_name.ends_with("Test")
|| file_name.ends_with("Tests")
|| file_name.ends_with("Spec")
|| path_str.contains("/test/")
|| path_str.contains("/tests/")
|| path_str.contains("/spec/")
|| path_str.contains("/__tests__/")
}
fn is_test_function_name(name: &str) -> bool {
let bare = name.rsplit('.').next().unwrap_or(name);
bare.starts_with("test_")
|| bare.starts_with("Test")
|| bare.starts_with("Benchmark")
|| bare.starts_with("Example")
}
fn has_test_decorator(decorators: &[String]) -> bool {
decorators.iter().any(|d| {
let lower = d.to_lowercase();
lower == "test" || lower == "pytest.mark.parametrize" || lower.starts_with("test")
})
}
fn infer_visibility_from_name(
name: &str,
language: crate::types::Language,
_has_decorator: bool,
_decorators: &[String],
) -> bool {
use crate::types::Language;
let bare_name = name.rsplit('.').next().unwrap_or(name);
match language {
Language::Python => !bare_name.starts_with('_'),
Language::Go => bare_name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false),
Language::Rust => !bare_name.starts_with('_'),
Language::TypeScript | Language::JavaScript => !bare_name.starts_with('_'),
Language::Java | Language::Kotlin | Language::CSharp | Language::Scala => {
!bare_name.starts_with('_')
}
Language::C | Language::Cpp => true,
Language::Ruby => !bare_name.starts_with('_'),
Language::Php => !bare_name.starts_with('_'),
Language::Elixir => !bare_name.starts_with('_'),
Language::Lua | Language::Luau => {
if name.starts_with("_M:") || name.starts_with("_M.") {
return true;
}
let lua_bare = if let Some(pos) = bare_name.find(':') {
&bare_name[pos + 1..]
} else {
bare_name
};
!lua_bare.starts_with('_')
}
Language::Ocaml => !bare_name.starts_with('_'),
Language::Swift => !bare_name.starts_with('_'),
}
}
fn is_trait_or_interface(
class: &crate::types::ClassInfo,
language: crate::types::Language,
) -> bool {
use crate::types::Language;
let name = &class.name;
let has_abstract_base = class
.bases
.iter()
.any(|b| b == "ABC" || b == "ABCMeta" || b == "Protocol" || b == "Interface");
if has_abstract_base {
return true;
}
let has_type_decorator = class.decorators.iter().any(|d| {
d == "abstract" || d == "interface" || d == "protocol" || d == "trait" || d == "module"
});
if has_type_decorator {
return true;
}
match language {
Language::Rust => false,
Language::Go => {
if name.ends_with("Interface") {
return true;
}
if name.len() >= 3
&& name.ends_with("er")
&& name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
return true;
}
false
}
Language::Java | Language::Kotlin => {
name.starts_with('I')
&& name.len() > 1
&& name
.chars()
.nth(1)
.map(|c| c.is_uppercase())
.unwrap_or(false)
}
Language::CSharp => {
name.starts_with('I')
&& name.len() > 1
&& name
.chars()
.nth(1)
.map(|c| c.is_uppercase())
.unwrap_or(false)
}
Language::Swift => {
name.ends_with("Protocol")
|| name.ends_with("Delegate")
|| name.ends_with("DataSource")
|| name.ends_with("able")
|| name.ends_with("ible")
}
Language::Scala => {
name.starts_with('I')
&& name.len() > 1
&& name
.chars()
.nth(1)
.map(|c| c.is_uppercase())
.unwrap_or(false)
}
Language::Php => {
name.starts_with('I')
&& name.len() > 1
&& name
.chars()
.nth(1)
.map(|c| c.is_uppercase())
.unwrap_or(false)
}
Language::Ruby => {
name.ends_with("able") || name.ends_with("ible") || name.contains("Mixin")
}
_ => false,
}
}
fn is_framework_entry_file(path: &Path, language: crate::types::Language) -> bool {
use crate::types::Language;
let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
let path_str = path.to_string_lossy();
match language {
Language::TypeScript | Language::JavaScript => {
matches!(
file_name,
"page.tsx"
| "page.ts"
| "page.jsx"
| "page.js"
| "layout.tsx"
| "layout.ts"
| "layout.jsx"
| "layout.js"
| "route.tsx"
| "route.ts"
| "route.jsx"
| "route.js"
| "loading.tsx"
| "loading.ts"
| "loading.jsx"
| "loading.js"
| "error.tsx"
| "error.ts"
| "error.jsx"
| "error.js"
| "not-found.tsx"
| "not-found.ts"
| "not-found.jsx"
| "not-found.js"
| "template.tsx"
| "template.ts"
| "template.jsx"
| "template.js"
| "default.tsx"
| "default.ts"
| "default.jsx"
| "default.js"
| "middleware.ts"
| "middleware.js"
| "manifest.ts"
| "manifest.js"
| "opengraph-image.tsx"
| "opengraph-image.ts"
| "sitemap.ts"
| "sitemap.js"
| "robots.ts"
| "robots.js"
)
|| matches!(
file_name,
"+page.svelte"
| "+layout.svelte"
| "+error.svelte"
| "+page.ts"
| "+page.js"
| "+page.server.ts"
| "+page.server.js"
| "+layout.ts"
| "+layout.js"
| "+layout.server.ts"
| "+layout.server.js"
| "+server.ts"
| "+server.js"
)
|| (path_str.contains("/pages/") && file_name.ends_with(".vue"))
|| (path_str.contains("/layouts/") && file_name.ends_with(".vue"))
|| (path_str.contains("/middleware/")
&& (file_name.ends_with(".ts") || file_name.ends_with(".js")))
|| path_str.contains("/routes/")
|| (path_str.contains("/pages/") && file_name.ends_with(".astro"))
}
Language::Python => {
file_name == "views.py"
|| file_name == "admin.py"
|| file_name == "urls.py"
|| file_name == "models.py"
|| file_name == "forms.py"
|| file_name == "serializers.py"
|| file_name == "signals.py"
|| file_name == "apps.py"
|| file_name == "middleware.py"
|| file_name == "context_processors.py"
|| file_name == "wsgi.py"
|| file_name == "asgi.py"
|| file_name == "conftest.py"
|| file_name == "tasks.py"
}
Language::Ruby => {
(path_str.contains("/controllers/") && file_name.ends_with("_controller.rb"))
|| (path_str.contains("/models/") && file_name.ends_with(".rb"))
|| (path_str.contains("/helpers/") && file_name.ends_with("_helper.rb"))
|| (path_str.contains("/mailers/") && file_name.ends_with("_mailer.rb"))
|| (path_str.contains("/jobs/") && file_name.ends_with("_job.rb"))
|| (path_str.contains("/channels/") && file_name.ends_with("_channel.rb"))
|| file_name == "application.rb"
|| file_name == "routes.rb"
|| file_name == "schema.rb"
}
Language::Java | Language::Kotlin => {
file_name.ends_with("Controller.java")
|| file_name.ends_with("Controller.kt")
|| file_name.ends_with("Service.java")
|| file_name.ends_with("Service.kt")
|| file_name.ends_with("Repository.java")
|| file_name.ends_with("Repository.kt")
|| file_name.ends_with("Configuration.java")
|| file_name.ends_with("Configuration.kt")
|| file_name.ends_with("Application.java")
|| file_name.ends_with("Application.kt")
|| file_name.ends_with("Activity.java")
|| file_name.ends_with("Activity.kt")
|| file_name.ends_with("Fragment.java")
|| file_name.ends_with("Fragment.kt")
|| file_name.ends_with("ViewModel.java")
|| file_name.ends_with("ViewModel.kt")
}
Language::CSharp => {
file_name.ends_with("Controller.cs")
|| file_name.ends_with("Hub.cs")
|| file_name.ends_with("Middleware.cs")
|| (path_str.contains("/Pages/") && file_name.ends_with(".cshtml.cs"))
|| file_name == "Program.cs"
|| file_name == "Startup.cs"
}
Language::Go => {
file_name == "main.go"
|| file_name.ends_with("_handler.go")
|| file_name.ends_with("_handlers.go")
}
Language::Php => {
(path_str.contains("/Controllers/") && file_name.ends_with(".php"))
|| (path_str.contains("/Middleware/") && file_name.ends_with(".php"))
|| (path_str.contains("/Models/") && file_name.ends_with(".php"))
|| (path_str.contains("/Providers/") && file_name.ends_with(".php"))
|| file_name == "routes.php"
|| file_name == "web.php"
|| file_name == "api.php"
}
Language::Elixir => {
(path_str.contains("/controllers/") && file_name.ends_with("_controller.ex"))
|| (path_str.contains("/live/") && file_name.ends_with("_live.ex"))
|| (path_str.contains("/channels/") && file_name.ends_with("_channel.ex"))
|| file_name == "router.ex"
|| file_name == "endpoint.ex"
}
Language::Swift => {
file_name.ends_with("View.swift")
|| file_name.ends_with("ViewController.swift")
|| file_name.ends_with("App.swift")
|| file_name.ends_with("Delegate.swift")
}
Language::Scala => {
(path_str.contains("/controllers/") && file_name.ends_with(".scala"))
|| file_name == "routes"
}
_ => false,
}
}
fn has_framework_directive(path: &Path) -> bool {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !matches!(ext, "ts" | "tsx" | "js" | "jsx" | "mjs") {
return false;
}
if let Ok(content) = std::fs::read_to_string(path) {
for line in content.lines().take(5) {
let trimmed = line.trim();
if trimmed == r#""use server""#
|| trimmed == r#"'use server'"#
|| trimmed == r#""use server";"#
|| trimmed == r#"'use server';"#
|| trimmed == r#""use client""#
|| trimmed == r#"'use client'"#
|| trimmed == r#""use client";"#
|| trimmed == r#"'use client';"#
{
return true;
}
if !trimmed.is_empty()
&& !trimmed.starts_with("//")
&& !trimmed.starts_with("/*")
&& !trimmed.starts_with('*')
{
if !trimmed.starts_with('"') && !trimmed.starts_with('\'') {
break;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::CallEdge;
fn create_test_graph() -> ProjectCallGraph {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: "main.py".into(),
src_func: "main".to_string(),
dst_file: "main.py".into(),
dst_func: "process".to_string(),
});
graph.add_edge(CallEdge {
src_file: "main.py".into(),
src_func: "process".to_string(),
dst_file: "utils.py".into(),
dst_func: "helper".to_string(),
});
graph
}
#[test]
fn test_dead_finds_uncalled() {
let graph = create_test_graph();
let functions = vec![
FunctionRef::new("main.py".into(), "main"),
FunctionRef::new("main.py".into(), "process"),
FunctionRef::new("utils.py".into(), "helper"),
FunctionRef::new("utils.py".into(), "unused"),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
assert!(!result.dead_functions.iter().any(|f| f.name == "helper"));
}
#[test]
fn test_dead_excludes_entry_points() {
let graph = ProjectCallGraph::new(); let functions = vec![
FunctionRef::new("main.py".into(), "main"),
FunctionRef::new("test.py".into(), "test_something"),
FunctionRef::new("setup.py".into(), "setup"),
FunctionRef::new("utils.py".into(), "__init__"),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(result.dead_functions.is_empty());
}
#[test]
fn test_dead_custom_entry_points() {
let graph = ProjectCallGraph::new();
let functions = vec![
FunctionRef::new("handler.py".into(), "handle_request"),
FunctionRef::new("handler.py".into(), "process_event"),
];
let custom = vec!["handle_*".to_string()];
let result = dead_code_analysis(&graph, &functions, Some(&custom)).unwrap();
assert!(!result
.dead_functions
.iter()
.any(|f| f.name == "handle_request"));
assert!(result
.dead_functions
.iter()
.any(|f| f.name == "process_event"));
}
#[test]
fn test_dead_percentage() {
let graph = ProjectCallGraph::new();
let functions = vec![
FunctionRef::new("a.py".into(), "dead1"),
FunctionRef::new("a.py".into(), "dead2"),
FunctionRef::new("a.py".into(), "main"), FunctionRef::new("a.py".into(), "test_x"), ];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert_eq!(result.total_dead, 2);
assert_eq!(result.total_functions, 4);
assert!((result.dead_percentage - 50.0).abs() < 0.01);
}
#[test]
fn test_is_entry_point_name() {
assert!(is_entry_point_name("main", None));
assert!(is_entry_point_name("test_something", None));
assert!(is_entry_point_name("setup", None));
assert!(!is_entry_point_name("helper", None));
let custom = vec!["handler_*".to_string()];
assert!(is_entry_point_name("handler_request", Some(&custom)));
assert!(!is_entry_point_name("process_request", Some(&custom)));
}
#[test]
fn test_entry_point_go_patterns() {
assert!(is_entry_point_name("ServeHTTP", None));
assert!(is_entry_point_name("Handler", None));
assert!(is_entry_point_name("TestUserLogin", None));
assert!(is_entry_point_name("BenchmarkSort", None));
assert!(is_entry_point_name("ExampleParse", None));
}
#[test]
fn test_entry_point_android_lifecycle() {
assert!(is_entry_point_name("onCreate", None));
assert!(is_entry_point_name("onStart", None));
assert!(is_entry_point_name("onDestroy", None));
assert!(is_entry_point_name("onClick", None));
assert!(is_entry_point_name("onBind", None));
}
#[test]
fn test_entry_point_plugin_hooks() {
assert!(is_entry_point_name("load", None));
assert!(is_entry_point_name("configure", None));
assert!(is_entry_point_name("request", None));
assert!(is_entry_point_name("invoke", None));
assert!(is_entry_point_name("execute", None));
}
#[test]
fn test_entry_point_handler_prefix() {
assert!(is_entry_point_name("handleRequest", None));
assert!(is_entry_point_name("handle_event", None));
assert!(is_entry_point_name("HandleConnection", None));
}
#[test]
fn test_entry_point_hook_prefix() {
assert!(is_entry_point_name("on_message", None));
assert!(is_entry_point_name("before_request", None));
assert!(is_entry_point_name("after_response", None));
}
#[test]
fn test_entry_point_class_method_format() {
assert!(is_entry_point_name("MyServlet.doGet", None));
assert!(is_entry_point_name("Activity.onCreate", None));
assert!(is_entry_point_name("Server.handleRequest", None));
assert!(is_entry_point_name("TestSuite.test_login", None));
assert!(!is_entry_point_name("Utils.compute", None));
}
#[test]
fn test_entry_point_java_servlet() {
assert!(is_entry_point_name("doGet", None));
assert!(is_entry_point_name("doPost", None));
assert!(is_entry_point_name("init", None));
assert!(is_entry_point_name("destroy", None));
assert!(is_entry_point_name("service", None));
}
fn enriched_func(
name: &str,
is_public: bool,
is_trait_method: bool,
has_decorator: bool,
decorator_names: Vec<&str>,
) -> FunctionRef {
FunctionRef {
file: PathBuf::from("test.rs"),
name: name.to_string(),
line: 0,
signature: String::new(),
ref_count: 0,
is_public,
is_test: false,
is_trait_method,
has_decorator,
decorator_names: decorator_names.into_iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn test_public_uncalled_is_possibly_dead_not_dead() {
let graph = ProjectCallGraph::new();
let functions = vec![enriched_func("pub_helper", true, false, false, vec![])];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
!result.dead_functions.iter().any(|f| f.name == "pub_helper"),
"Public uncalled function should not be in dead_functions"
);
assert!(
result.possibly_dead.iter().any(|f| f.name == "pub_helper"),
"Public uncalled function should be in possibly_dead"
);
}
#[test]
fn test_private_uncalled_is_dead() {
let graph = ProjectCallGraph::new();
let functions = vec![enriched_func(
"_private_helper",
false,
false,
false,
vec![],
)];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
result
.dead_functions
.iter()
.any(|f| f.name == "_private_helper"),
"Private uncalled function should be in dead_functions"
);
assert!(
!result
.possibly_dead
.iter()
.any(|f| f.name == "_private_helper"),
"Private uncalled function should not be in possibly_dead"
);
}
#[test]
fn test_trait_method_not_dead() {
let graph = ProjectCallGraph::new();
let functions = vec![
enriched_func("serialize", false, true, false, vec![]),
enriched_func("deserialize", true, true, false, vec![]),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
result.dead_functions.is_empty(),
"Trait methods should never be in dead_functions"
);
assert!(
result.possibly_dead.is_empty(),
"Trait methods should never be in possibly_dead"
);
}
#[test]
fn test_decorated_function_not_dead() {
let graph = ProjectCallGraph::new();
let functions = vec![
enriched_func("index", false, false, true, vec!["route"]),
enriched_func(
"admin_panel",
true,
false,
true,
vec!["route", "login_required"],
),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
result.dead_functions.is_empty(),
"Decorated functions should not be in dead_functions"
);
assert!(
result.possibly_dead.is_empty(),
"Decorated functions should not be in possibly_dead"
);
}
#[test]
fn test_test_function_not_dead() {
let graph = ProjectCallGraph::new();
let functions = vec![FunctionRef {
file: PathBuf::from("test.rs"),
name: "unusual_test_name".to_string(),
line: 0,
signature: String::new(),
ref_count: 0,
is_public: false,
is_test: true,
is_trait_method: false,
has_decorator: false,
decorator_names: vec![],
}];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
result.dead_functions.is_empty(),
"Test functions should not be dead"
);
}
#[test]
fn test_mixed_enrichment_filtering() {
let graph = ProjectCallGraph::new();
let functions = vec![
enriched_func("_internal_cache", false, false, false, vec![]),
enriched_func("public_api_method", true, false, false, vec![]),
enriched_func("Serialize.serialize", false, true, false, vec![]),
enriched_func("handle_index", false, false, true, vec!["get"]),
enriched_func("_orphan", false, false, false, vec![]),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert_eq!(
result.total_dead,
2,
"Should have exactly 2 definitely-dead functions, got: {:?}",
result
.dead_functions
.iter()
.map(|f| &f.name)
.collect::<Vec<_>>()
);
assert!(result
.dead_functions
.iter()
.any(|f| f.name == "_internal_cache"));
assert!(result.dead_functions.iter().any(|f| f.name == "_orphan"));
assert_eq!(
result.total_possibly_dead, 1,
"Should have exactly 1 possibly-dead function"
);
assert!(result
.possibly_dead
.iter()
.any(|f| f.name == "public_api_method"));
assert!(
(result.dead_percentage - 40.0).abs() < 0.01,
"Dead percentage should be 40%, got {}",
result.dead_percentage
);
}
#[test]
fn test_unenriched_functionref_backwards_compat() {
let func = FunctionRef::new("test.py".into(), "some_func");
assert!(!func.is_public);
assert!(!func.is_test);
assert!(!func.is_trait_method);
assert!(!func.has_decorator);
assert!(func.decorator_names.is_empty());
}
#[test]
fn test_dead_code_report_has_possibly_dead_field() {
let graph = ProjectCallGraph::new();
let functions = vec![
enriched_func("pub_func", true, false, false, vec![]),
enriched_func("_priv_func", false, false, false, vec![]),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert_eq!(result.total_possibly_dead, 1);
assert_eq!(result.total_dead, 1);
assert_eq!(result.total_functions, 2);
}
#[test]
fn test_called_public_function_not_in_any_dead_list() {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: "main.rs".into(),
src_func: "main".to_string(),
dst_file: "test.rs".into(), dst_func: "pub_helper".to_string(),
});
let functions = vec![enriched_func("pub_helper", true, false, false, vec![])];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(result.dead_functions.is_empty());
assert!(result.possibly_dead.is_empty());
}
#[test]
fn test_old_tests_still_pass_with_new_fields() {
let graph = create_test_graph();
let functions = vec![
FunctionRef::new("main.py".into(), "main"),
FunctionRef::new("main.py".into(), "process"),
FunctionRef::new("utils.py".into(), "helper"),
FunctionRef::new("utils.py".into(), "unused"),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(result.dead_functions.iter().any(|f| f.name == "unused"));
assert!(!result.dead_functions.iter().any(|f| f.name == "main"));
assert!(!result.dead_functions.iter().any(|f| f.name == "process"));
assert!(!result.dead_functions.iter().any(|f| f.name == "helper"));
}
#[test]
fn test_refcount_no_cg_rescues() {
let mut ref_counts = HashMap::new();
ref_counts.insert("process_data".to_string(), 3);
let functions = vec![enriched_func("process_data", false, false, false, vec![])];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
!result
.dead_functions
.iter()
.any(|f| f.name == "process_data"),
"Function with ref_count=3 should NOT be in dead_functions"
);
assert!(
!result
.possibly_dead
.iter()
.any(|f| f.name == "process_data"),
"Function with ref_count=3 should NOT be in possibly_dead"
);
}
#[test]
fn test_refcount_no_cg_confirms_dead() {
let mut ref_counts = HashMap::new();
ref_counts.insert("_unused_helper".to_string(), 1);
let functions = vec![enriched_func("_unused_helper", false, false, false, vec![])];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
result
.dead_functions
.iter()
.any(|f| f.name == "_unused_helper"),
"_unused_helper with ref_count=1 should be in dead_functions"
);
}
#[test]
fn test_refcount_exclusions_apply() {
let mut ref_counts = HashMap::new();
ref_counts.insert("main".to_string(), 1);
ref_counts.insert("__init__".to_string(), 1);
ref_counts.insert("test_something".to_string(), 1);
let functions = vec![
enriched_func("main", false, false, false, vec![]),
enriched_func("__init__", false, false, false, vec![]),
enriched_func("test_something", false, false, false, vec![]),
];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
result.dead_functions.is_empty(),
"Entry points, dunders, and test functions should NOT be in dead_functions, got: {:?}",
result
.dead_functions
.iter()
.map(|f| &f.name)
.collect::<Vec<_>>()
);
assert!(
result.possibly_dead.is_empty(),
"Entry points, dunders, and test functions should NOT be in possibly_dead, got: {:?}",
result
.possibly_dead
.iter()
.map(|f| &f.name)
.collect::<Vec<_>>()
);
}
#[test]
fn test_refcount_short_name_low_count_stays_dead() {
let mut ref_counts = HashMap::new();
ref_counts.insert("fn".to_string(), 3);
let functions = vec![enriched_func("fn", false, false, false, vec![])];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
result.dead_functions.iter().any(|f| f.name == "fn"),
"Short name 'fn' (2 chars) with count=3 should be in dead_functions (needs >= 5)"
);
}
#[test]
fn test_refcount_short_name_high_count_rescued() {
let mut ref_counts = HashMap::new();
ref_counts.insert("cn".to_string(), 50);
let functions = vec![enriched_func("cn", false, false, false, vec![])];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
!result.dead_functions.iter().any(|f| f.name == "cn"),
"Short name 'cn' (2 chars) with count=50 should NOT be dead (rescued at >= 5)"
);
assert!(
!result.possibly_dead.iter().any(|f| f.name == "cn"),
"Short name 'cn' (2 chars) with count=50 should NOT be possibly_dead"
);
}
#[test]
fn test_refcount_public_vs_private() {
let mut ref_counts = HashMap::new();
ref_counts.insert("public_func".to_string(), 1);
ref_counts.insert("_private_func".to_string(), 1);
let functions = vec![
enriched_func("public_func", true, false, false, vec![]),
enriched_func("_private_func", false, false, false, vec![]),
];
let result = dead_code_analysis_refcount(&functions, &ref_counts, None).unwrap();
assert!(
result.possibly_dead.iter().any(|f| f.name == "public_func"),
"Public function with ref_count=1 should be in possibly_dead"
);
assert!(
!result
.dead_functions
.iter()
.any(|f| f.name == "public_func"),
"Public function should NOT be in dead_functions"
);
assert!(
result
.dead_functions
.iter()
.any(|f| f.name == "_private_func"),
"Private function with ref_count=1 should be in dead_functions"
);
assert!(
!result
.possibly_dead
.iter()
.any(|f| f.name == "_private_func"),
"Private function should NOT be in possibly_dead"
);
}
#[test]
fn test_backward_compat_cg() {
let graph = create_test_graph();
let functions = vec![
FunctionRef::new("main.py".into(), "main"),
FunctionRef::new("main.py".into(), "process"),
FunctionRef::new("utils.py".into(), "helper"),
FunctionRef::new("utils.py".into(), "_orphaned"),
enriched_func("public_orphan", true, false, false, vec![]),
];
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
!result.dead_functions.iter().any(|f| f.name == "main"),
"main should not be dead (entry point)"
);
assert!(
!result.dead_functions.iter().any(|f| f.name == "process"),
"process should not be dead (called)"
);
assert!(
!result.dead_functions.iter().any(|f| f.name == "helper"),
"helper should not be dead (called)"
);
assert!(
result.dead_functions.iter().any(|f| f.name == "_orphaned"),
"_orphaned should be in dead_functions (private, uncalled)"
);
assert!(
result
.possibly_dead
.iter()
.any(|f| f.name == "public_orphan"),
"public_orphan should be in possibly_dead (public, uncalled)"
);
assert_eq!(
result.total_dead, 1,
"Should have 1 definitely dead function"
);
assert_eq!(
result.total_possibly_dead, 1,
"Should have 1 possibly dead function"
);
assert_eq!(result.total_functions, 5, "Should have 5 total functions");
assert!(
(result.dead_percentage - 20.0).abs() < 0.01,
"Dead percentage should be 20%, got {}",
result.dead_percentage
);
}
#[test]
fn test_functionref_has_line_field() {
let func = FunctionRef::new("test.py".into(), "my_func");
assert_eq!(func.line, 0, "Default line should be 0");
let func_with_line = FunctionRef { line: 42, ..func };
assert_eq!(func_with_line.line, 42);
}
#[test]
fn test_functionref_has_signature_field() {
let func = FunctionRef::new("test.py".into(), "my_func");
assert!(
func.signature.is_empty(),
"Default signature should be empty"
);
let func_with_sig = FunctionRef {
signature: "def my_func(x, y)".to_string(),
..func
};
assert_eq!(func_with_sig.signature, "def my_func(x, y)");
}
#[test]
fn test_functionref_line_serializes_in_json() {
let func = FunctionRef {
file: PathBuf::from("test.py"),
name: "my_func".to_string(),
line: 42,
signature: String::new(),
ref_count: 0,
is_public: false,
is_test: false,
is_trait_method: false,
has_decorator: false,
decorator_names: vec![],
};
let json = serde_json::to_string(&func).unwrap();
assert!(
json.contains("\"line\":42"),
"JSON should contain line field, got: {}",
json
);
}
#[test]
fn test_functionref_signature_serializes_in_json() {
let func = FunctionRef {
file: PathBuf::from("test.py"),
name: "my_func".to_string(),
line: 10,
signature: "def my_func(x: int, y: int) -> int".to_string(),
ref_count: 0,
is_public: false,
is_test: false,
is_trait_method: false,
has_decorator: false,
decorator_names: vec![],
};
let json = serde_json::to_string(&func).unwrap();
assert!(
json.contains("\"signature\""),
"JSON should contain signature field, got: {}",
json
);
assert!(
json.contains("my_func(x: int"),
"JSON should contain signature content"
);
}
#[test]
fn test_collect_all_functions_carries_line_number() {
use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
let module_infos = vec![(
PathBuf::from("test.py"),
ModuleInfo {
file_path: PathBuf::from("test.py"),
language: Language::Python,
docstring: None,
imports: vec![],
functions: vec![FunctionInfo {
name: "my_func".to_string(),
params: vec!["x".to_string(), "y".to_string()],
return_type: Some("int".to_string()),
docstring: None,
is_method: false,
is_async: false,
decorators: vec![],
line_number: 42,
}],
classes: vec![],
constants: vec![],
call_graph: IntraFileCallGraph::default(),
},
)];
let functions = collect_all_functions(&module_infos);
assert_eq!(functions.len(), 1);
assert_eq!(
functions[0].line, 42,
"line should be populated from FunctionInfo.line_number"
);
}
#[test]
fn test_collect_all_functions_builds_signature() {
use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
let module_infos = vec![(
PathBuf::from("test.py"),
ModuleInfo {
file_path: PathBuf::from("test.py"),
language: Language::Python,
docstring: None,
imports: vec![],
functions: vec![FunctionInfo {
name: "calculate".to_string(),
params: vec!["x".to_string(), "y".to_string()],
return_type: Some("int".to_string()),
docstring: None,
is_method: false,
is_async: false,
decorators: vec![],
line_number: 10,
}],
classes: vec![],
constants: vec![],
call_graph: IntraFileCallGraph::default(),
},
)];
let functions = collect_all_functions(&module_infos);
assert_eq!(functions.len(), 1);
assert!(
!functions[0].signature.is_empty(),
"Signature should be populated, got empty"
);
assert!(
functions[0].signature.contains("calculate"),
"Signature should contain function name, got: {}",
functions[0].signature
);
assert!(
functions[0].signature.contains("x"),
"Signature should contain parameter names, got: {}",
functions[0].signature
);
}
#[test]
fn test_functionref_new_defaults_line_and_signature() {
let func = FunctionRef::new("test.py".into(), "func");
assert_eq!(func.line, 0);
assert_eq!(func.signature, "");
}
fn make_class(name: &str, bases: Vec<&str>, decorators: Vec<&str>) -> crate::types::ClassInfo {
crate::types::ClassInfo {
name: name.to_string(),
bases: bases.into_iter().map(|s| s.to_string()).collect(),
docstring: None,
methods: vec![],
fields: vec![],
decorators: decorators.into_iter().map(|s| s.to_string()).collect(),
line_number: 1,
}
}
#[test]
fn test_is_trait_or_interface_rust_trait_decorator() {
use crate::types::Language;
let class = make_class("Iterator", vec![], vec!["trait"]);
assert!(
is_trait_or_interface(&class, Language::Rust),
"Rust class with 'trait' decorator should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_rust_plain_struct_not_trait() {
use crate::types::Language;
let class = make_class("MyStruct", vec![], vec![]);
assert!(
!is_trait_or_interface(&class, Language::Rust),
"Plain Rust struct should not be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_go_interface_suffix() {
use crate::types::Language;
let class = make_class("Reader", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Go),
"Go class named 'Reader' (single-method interface pattern) should be detected"
);
}
#[test]
fn test_is_trait_or_interface_go_non_interface() {
use crate::types::Language;
let class = make_class("Config", vec![], vec![]);
assert!(
!is_trait_or_interface(&class, Language::Go),
"Go class named 'Config' should not be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_go_interface_decorator() {
use crate::types::Language;
let class = make_class("Handler", vec![], vec!["interface"]);
assert!(
is_trait_or_interface(&class, Language::Go),
"Go class with 'interface' decorator should be detected"
);
}
#[test]
fn test_is_trait_or_interface_swift_protocol_decorator() {
use crate::types::Language;
let class = make_class("Codable", vec![], vec!["protocol"]);
assert!(
is_trait_or_interface(&class, Language::Swift),
"Swift class with 'protocol' decorator should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_swift_protocol_suffix() {
use crate::types::Language;
let class = make_class("ViewProtocol", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Swift),
"Swift class ending in 'Protocol' should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_swift_delegate_suffix() {
use crate::types::Language;
let class = make_class("UITableViewDelegate", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Swift),
"Swift class ending in 'Delegate' should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_swift_datasource_suffix() {
use crate::types::Language;
let class = make_class("UITableViewDataSource", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Swift),
"Swift class ending in 'DataSource' should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_scala_trait_decorator() {
use crate::types::Language;
let class = make_class("Ordered", vec![], vec!["trait"]);
assert!(
is_trait_or_interface(&class, Language::Scala),
"Scala class with 'trait' decorator should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_php_interface_decorator() {
use crate::types::Language;
let class = make_class("Countable", vec![], vec!["interface"]);
assert!(
is_trait_or_interface(&class, Language::Php),
"PHP class with 'interface' decorator should be detected"
);
}
#[test]
fn test_is_trait_or_interface_php_trait_decorator() {
use crate::types::Language;
let class = make_class("Loggable", vec![], vec!["trait"]);
assert!(
is_trait_or_interface(&class, Language::Php),
"PHP class with 'trait' decorator should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_ruby_module_mixin() {
use crate::types::Language;
let class = make_class("Comparable", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Ruby),
"Ruby class named 'Comparable' should be detected as interface/mixin"
);
}
#[test]
fn test_is_trait_or_interface_ruby_module_decorator() {
use crate::types::Language;
let class = make_class("Serializable", vec![], vec!["module"]);
assert!(
is_trait_or_interface(&class, Language::Ruby),
"Ruby class with 'module' decorator should be detected as interface/mixin"
);
}
#[test]
fn test_is_trait_or_interface_typescript_interface_decorator() {
use crate::types::Language;
let class = make_class("UserService", vec![], vec!["interface"]);
assert!(
is_trait_or_interface(&class, Language::TypeScript),
"TypeScript class with 'interface' decorator should be detected"
);
}
#[test]
fn test_is_trait_or_interface_java_i_prefix() {
use crate::types::Language;
let class = make_class("IRepository", vec![], vec![]);
assert!(
is_trait_or_interface(&class, Language::Java),
"Java class with I-prefix should be detected as interface"
);
}
#[test]
fn test_is_trait_or_interface_python_protocol_base() {
use crate::types::Language;
let class = make_class("Comparable", vec!["Protocol"], vec![]);
assert!(
is_trait_or_interface(&class, Language::Python),
"Python class with Protocol base should be detected"
);
}
#[test]
fn test_is_trait_or_interface_python_abc_base() {
use crate::types::Language;
let class = make_class("AbstractHandler", vec!["ABC"], vec![]);
assert!(
is_trait_or_interface(&class, Language::Python),
"Python class with ABC base should be detected"
);
}
#[test]
fn test_is_trait_or_interface_collect_functions_marks_trait_methods() {
use crate::types::{ClassInfo, FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
let module_infos = vec![(
PathBuf::from("lib.php"),
ModuleInfo {
file_path: PathBuf::from("lib.php"),
language: Language::Php,
docstring: None,
imports: vec![],
functions: vec![],
classes: vec![ClassInfo {
name: "Cacheable".to_string(),
bases: vec![],
docstring: None,
methods: vec![FunctionInfo {
name: "cache_key".to_string(),
params: vec![],
return_type: Some("string".to_string()),
docstring: None,
is_method: true,
is_async: false,
decorators: vec![],
line_number: 5,
}],
fields: vec![],
decorators: vec!["interface".to_string()],
line_number: 3,
}],
constants: vec![],
call_graph: IntraFileCallGraph::default(),
},
)];
let functions = collect_all_functions(&module_infos);
assert_eq!(functions.len(), 1);
assert!(
functions[0].is_trait_method,
"Methods of a PHP interface class should have is_trait_method=true"
);
}
#[test]
fn test_framework_entry_file_nextjs() {
use crate::types::Language;
assert!(
is_framework_entry_file(Path::new("app/dashboard/page.tsx"), Language::TypeScript),
"page.tsx should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/layout.tsx"), Language::TypeScript),
"layout.tsx should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/api/users/route.ts"), Language::TypeScript),
"route.ts should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/loading.tsx"), Language::TypeScript),
"loading.tsx should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/error.tsx"), Language::TypeScript),
"error.tsx should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/not-found.tsx"), Language::TypeScript),
"not-found.tsx should be detected as Next.js framework entry"
);
assert!(
is_framework_entry_file(Path::new("middleware.ts"), Language::TypeScript),
"middleware.ts should be detected as Next.js framework entry"
);
}
#[test]
fn test_framework_entry_file_django() {
use crate::types::Language;
assert!(
is_framework_entry_file(Path::new("myapp/views.py"), Language::Python),
"views.py should be detected as Django framework entry"
);
assert!(
is_framework_entry_file(Path::new("myapp/models.py"), Language::Python),
"models.py should be detected as Django framework entry"
);
assert!(
is_framework_entry_file(Path::new("myapp/admin.py"), Language::Python),
"admin.py should be detected as Django framework entry"
);
assert!(
is_framework_entry_file(Path::new("myapp/serializers.py"), Language::Python),
"serializers.py should be detected as Django framework entry"
);
assert!(
is_framework_entry_file(Path::new("myapp/tasks.py"), Language::Python),
"tasks.py should be detected as Celery framework entry"
);
assert!(
is_framework_entry_file(Path::new("conftest.py"), Language::Python),
"conftest.py should be detected as pytest framework entry"
);
}
#[test]
fn test_framework_entry_file_rails() {
use crate::types::Language;
assert!(
is_framework_entry_file(
Path::new("app/controllers/users_controller.rb"),
Language::Ruby
),
"*_controller.rb in controllers/ should be detected as Rails framework entry"
);
assert!(
is_framework_entry_file(Path::new("app/models/user.rb"), Language::Ruby),
"*.rb in models/ should be detected as Rails framework entry"
);
assert!(
is_framework_entry_file(
Path::new("app/helpers/application_helper.rb"),
Language::Ruby
),
"*_helper.rb in helpers/ should be detected as Rails framework entry"
);
assert!(
is_framework_entry_file(Path::new("config/routes.rb"), Language::Ruby),
"routes.rb should be detected as Rails framework entry"
);
}
#[test]
fn test_framework_entry_file_spring() {
use crate::types::Language;
assert!(
is_framework_entry_file(Path::new("src/UserController.java"), Language::Java),
"*Controller.java should be detected as Spring framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/UserService.java"), Language::Java),
"*Service.java should be detected as Spring framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/UserRepository.java"), Language::Java),
"*Repository.java should be detected as Spring framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/AppConfiguration.java"), Language::Java),
"*Configuration.java should be detected as Spring framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/UserController.kt"), Language::Kotlin),
"*Controller.kt should be detected as Spring/Kotlin framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/MainActivity.java"), Language::Java),
"*Activity.java should be detected as Android framework entry"
);
assert!(
is_framework_entry_file(Path::new("src/HomeFragment.kt"), Language::Kotlin),
"*Fragment.kt should be detected as Android/Kotlin framework entry"
);
}
#[test]
fn test_framework_entry_file_non_framework() {
use crate::types::Language;
assert!(
!is_framework_entry_file(Path::new("src/utils.ts"), Language::TypeScript),
"utils.ts should NOT be detected as framework entry"
);
assert!(
!is_framework_entry_file(Path::new("src/helpers.py"), Language::Python),
"helpers.py should NOT be detected as framework entry"
);
assert!(
!is_framework_entry_file(Path::new("lib/parser.rb"), Language::Ruby),
"parser.rb should NOT be detected as framework entry"
);
assert!(
!is_framework_entry_file(Path::new("src/Utils.java"), Language::Java),
"Utils.java should NOT be detected as framework entry"
);
assert!(
!is_framework_entry_file(Path::new("src/random.go"), Language::Go),
"random.go should NOT be detected as framework entry"
);
}
#[test]
fn test_framework_directive_use_server() {
let dir = std::env::temp_dir().join("tldr_test_framework_directive");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("actions.ts");
std::fs::write(&file, "'use server'\n\nexport async function createUser() {}\n").unwrap();
assert!(
has_framework_directive(&file),
"File with 'use server' directive should be detected"
);
let file2 = dir.join("actions2.tsx");
std::fs::write(&file2, "\"use server\";\n\nexport async function deleteUser() {}\n")
.unwrap();
assert!(
has_framework_directive(&file2),
"File with \"use server\"; directive should be detected"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_framework_directive_use_client() {
let dir = std::env::temp_dir().join("tldr_test_framework_directive_client");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("component.tsx");
std::fs::write(
&file,
"'use client'\n\nimport React from 'react';\n\nexport function Button() {}\n",
)
.unwrap();
assert!(
has_framework_directive(&file),
"File with 'use client' directive should be detected"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_framework_directive_absent() {
let dir = std::env::temp_dir().join("tldr_test_framework_directive_absent");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("utils.ts");
std::fs::write(
&file,
"import { helper } from './helper';\n\nexport function doWork() {}\n",
)
.unwrap();
assert!(
!has_framework_directive(&file),
"File without framework directive should NOT be detected"
);
let py_file = dir.join("views.py");
std::fs::write(&py_file, "'use server'\ndef view(): pass\n").unwrap();
assert!(
!has_framework_directive(&py_file),
"Non-JS/TS file should NOT be detected even with directive-like content"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_collect_functions_skips_framework_entries() {
use crate::types::{FunctionInfo, IntraFileCallGraph, Language, ModuleInfo};
let module_infos = vec![(
PathBuf::from("app/dashboard/page.tsx"),
ModuleInfo {
file_path: PathBuf::from("app/dashboard/page.tsx"),
language: Language::TypeScript,
docstring: None,
imports: vec![],
functions: vec![
FunctionInfo {
name: "DashboardPage".to_string(),
params: vec![],
return_type: Some("JSX.Element".to_string()),
docstring: None,
is_method: false,
is_async: false,
decorators: vec![],
line_number: 5,
},
FunctionInfo {
name: "generateMetadata".to_string(),
params: vec![],
return_type: Some("Metadata".to_string()),
docstring: None,
is_method: false,
is_async: true,
decorators: vec![],
line_number: 20,
},
FunctionInfo {
name: "_privateHelper".to_string(),
params: vec![],
return_type: None,
docstring: None,
is_method: false,
is_async: false,
decorators: vec![],
line_number: 30,
},
],
classes: vec![],
constants: vec![],
call_graph: IntraFileCallGraph::default(),
},
)];
let functions = collect_all_functions(&module_infos);
assert_eq!(functions.len(), 3);
let dashboard = functions.iter().find(|f| f.name == "DashboardPage").unwrap();
assert!(
dashboard.has_decorator,
"Public function in page.tsx should have has_decorator=true (framework entry)"
);
assert!(
dashboard.is_public,
"DashboardPage should be public"
);
let metadata = functions.iter().find(|f| f.name == "generateMetadata").unwrap();
assert!(
metadata.has_decorator,
"Public function in page.tsx should have has_decorator=true (framework entry)"
);
let private_fn = functions.iter().find(|f| f.name == "_privateHelper").unwrap();
assert!(
!private_fn.has_decorator,
"Private function in page.tsx should NOT have has_decorator=true"
);
assert!(
!private_fn.is_public,
"_privateHelper should not be public"
);
let graph = ProjectCallGraph::new();
let result = dead_code_analysis(&graph, &functions, None).unwrap();
assert!(
!result
.possibly_dead
.iter()
.any(|f| f.name == "DashboardPage"),
"DashboardPage (framework entry) should not be in possibly_dead"
);
assert!(
!result
.possibly_dead
.iter()
.any(|f| f.name == "generateMetadata"),
"generateMetadata (framework entry) should not be in possibly_dead"
);
assert!(
result
.dead_functions
.iter()
.any(|f| f.name == "_privateHelper"),
"_privateHelper should be in dead_functions"
);
}
}