use crate::analysis::types::{
DeadCodeInfo, UnreachableCode, UnusedExport, UnusedImport, UnusedSymbol, UnusedVariable,
};
use crate::parser::Language;
use crate::types::{Symbol, SymbolKind, Visibility};
use std::collections::{HashMap, HashSet};
pub struct DeadCodeDetector {
definitions: HashMap<String, DefinitionInfo>,
references: HashSet<String>,
imports: HashMap<String, ImportInfo>,
variables: HashMap<String, HashMap<String, VariableInfo>>,
files: Vec<FileInfo>,
}
#[derive(Debug, Clone)]
struct DefinitionInfo {
name: String,
kind: SymbolKind,
visibility: Visibility,
file_path: String,
line: u32,
is_entry_point: bool,
}
#[derive(Debug, Clone)]
struct ImportInfo {
name: String,
import_path: String,
file_path: String,
line: u32,
is_used: bool,
}
#[derive(Debug, Clone)]
struct VariableInfo {
name: String,
file_path: String,
line: u32,
scope: String,
is_used: bool,
}
#[derive(Debug, Clone)]
struct FileInfo {
path: String,
language: Language,
symbols: Vec<Symbol>,
}
impl DeadCodeDetector {
pub fn new() -> Self {
Self {
definitions: HashMap::new(),
references: HashSet::new(),
imports: HashMap::new(),
variables: HashMap::new(),
files: Vec::new(),
}
}
pub fn add_file(&mut self, file_path: &str, symbols: &[Symbol], language: Language) {
self.files.push(FileInfo {
path: file_path.to_owned(),
language,
symbols: symbols.to_vec(),
});
for symbol in symbols {
let is_entry_point = self.is_entry_point(symbol, language);
self.definitions.insert(
symbol.name.clone(),
DefinitionInfo {
name: symbol.name.clone(),
kind: symbol.kind,
visibility: symbol.visibility,
file_path: file_path.to_owned(),
line: symbol.start_line,
is_entry_point,
},
);
for call in &symbol.calls {
self.references.insert(call.clone());
}
if let Some(ref extends) = symbol.extends {
self.references.insert(extends.clone());
}
for implements in &symbol.implements {
self.references.insert(implements.clone());
}
if let Some(ref parent) = symbol.parent {
self.references.insert(parent.clone());
}
}
}
pub fn add_import(&mut self, name: &str, import_path: &str, file_path: &str, line: u32) {
self.imports.insert(
format!("{}:{}", file_path, name),
ImportInfo {
name: name.to_owned(),
import_path: import_path.to_owned(),
file_path: file_path.to_owned(),
line,
is_used: false,
},
);
}
pub fn add_variable(&mut self, name: &str, file_path: &str, line: u32, scope: &str) {
let scope_vars = self.variables.entry(scope.to_owned()).or_default();
scope_vars.insert(
name.to_owned(),
VariableInfo {
name: name.to_owned(),
file_path: file_path.to_owned(),
line,
scope: scope.to_owned(),
is_used: false,
},
);
}
pub fn mark_import_used(&mut self, name: &str, file_path: &str) {
let key = format!("{}:{}", file_path, name);
if let Some(import) = self.imports.get_mut(&key) {
import.is_used = true;
}
}
pub fn mark_variable_used(&mut self, name: &str, scope: &str) {
if let Some(scope_vars) = self.variables.get_mut(scope) {
if let Some(var) = scope_vars.get_mut(name) {
var.is_used = true;
}
}
}
fn is_entry_point(&self, symbol: &Symbol, language: Language) -> bool {
let name = &symbol.name;
if name == "main" || name == "init" || name == "__init__" {
return true;
}
if name.starts_with("test_")
|| name.starts_with("Test")
|| name.ends_with("_test")
|| name.ends_with("Test")
{
return true;
}
match language {
Language::Python => {
name.starts_with("__") && name.ends_with("__")
},
Language::JavaScript | Language::TypeScript => {
name.chars().next().is_some_and(|c| c.is_uppercase())
&& matches!(symbol.kind, SymbolKind::Class | SymbolKind::Function)
},
Language::Rust => {
matches!(symbol.visibility, Visibility::Public)
},
Language::Go => {
name.chars().next().is_some_and(|c| c.is_uppercase())
},
Language::Java | Language::Kotlin => {
name == "main"
|| matches!(symbol.visibility, Visibility::Public)
&& matches!(symbol.kind, SymbolKind::Method)
},
Language::Ruby => {
name == "initialize" || name.starts_with("before_") || name.starts_with("after_")
},
Language::Php => {
name.starts_with("__")
},
Language::Swift => {
name == "viewDidLoad" || name == "applicationDidFinishLaunching"
},
Language::Elixir => {
name == "start" || name.starts_with("handle_") || name == "child_spec"
},
_ => false,
}
}
pub fn detect(&self) -> DeadCodeInfo {
DeadCodeInfo {
unused_exports: self.find_unused_exports(),
unreachable_code: Vec::new(), unused_private: self.find_unused_private(),
unused_imports: self.find_unused_imports(),
unused_variables: self.find_unused_variables(),
}
}
fn find_unused_exports(&self) -> Vec<UnusedExport> {
let mut unused = Vec::new();
for (name, def) in &self.definitions {
if !matches!(def.visibility, Visibility::Public) {
continue;
}
if def.is_entry_point {
continue;
}
if self.references.contains(name) {
continue;
}
let confidence = self.calculate_confidence(def);
unused.push(UnusedExport {
name: name.clone(),
kind: format!("{:?}", def.kind),
file_path: def.file_path.clone(),
line: def.line,
confidence,
reason: "No references found in analyzed codebase".to_owned(),
});
}
unused
}
fn find_unused_private(&self) -> Vec<UnusedSymbol> {
let mut unused = Vec::new();
for (name, def) in &self.definitions {
if matches!(def.visibility, Visibility::Public) {
continue;
}
if def.is_entry_point {
continue;
}
if self.references.contains(name) {
continue;
}
unused.push(UnusedSymbol {
name: name.clone(),
kind: format!("{:?}", def.kind),
file_path: def.file_path.clone(),
line: def.line,
});
}
unused
}
fn find_unused_imports(&self) -> Vec<UnusedImport> {
self.imports
.values()
.filter(|import| !import.is_used)
.map(|import| UnusedImport {
name: import.name.clone(),
import_path: import.import_path.clone(),
file_path: import.file_path.clone(),
line: import.line,
})
.collect()
}
fn find_unused_variables(&self) -> Vec<UnusedVariable> {
let mut unused = Vec::new();
for (scope, vars) in &self.variables {
for var in vars.values() {
if !var.is_used {
if var.name.starts_with('_') {
continue;
}
unused.push(UnusedVariable {
name: var.name.clone(),
file_path: var.file_path.clone(),
line: var.line,
scope: Some(scope.clone()),
});
}
}
}
unused
}
fn calculate_confidence(&self, def: &DefinitionInfo) -> f32 {
let mut confidence: f32 = 0.5;
if matches!(def.visibility, Visibility::Private | Visibility::Internal) {
confidence += 0.3;
}
match def.kind {
SymbolKind::Function | SymbolKind::Method => confidence += 0.1,
SymbolKind::Class | SymbolKind::Struct => confidence += 0.05,
SymbolKind::Variable | SymbolKind::Constant => confidence += 0.15,
_ => {},
}
confidence.min(0.95)
}
}
impl Default for DeadCodeDetector {
fn default() -> Self {
Self::new()
}
}
pub fn detect_unreachable_code(
source: &str,
file_path: &str,
_language: Language,
) -> Vec<UnreachableCode> {
let mut unreachable = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let mut after_terminator = false;
let mut terminator_line = 0u32;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
let line_num = (i + 1) as u32;
if is_terminator(trimmed) {
after_terminator = true;
terminator_line = line_num;
continue;
}
if after_terminator {
if trimmed.is_empty()
|| trimmed.starts_with("//")
|| trimmed.starts_with('#')
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
|| trimmed == "}"
|| trimmed == ")"
|| trimmed == "]"
{
continue;
}
if trimmed.starts_with("case ")
|| trimmed.starts_with("default:")
|| trimmed.starts_with("else")
|| trimmed.starts_with("catch")
|| trimmed.starts_with("except")
|| trimmed.starts_with("rescue")
|| trimmed.starts_with("finally")
{
after_terminator = false;
continue;
}
unreachable.push(UnreachableCode {
file_path: file_path.to_owned(),
start_line: line_num,
end_line: line_num,
snippet: trimmed.to_owned(),
reason: format!("Code after terminator on line {}", terminator_line),
});
after_terminator = false;
}
}
unreachable
}
fn is_terminator(line: &str) -> bool {
let terminators = [
"return",
"return;",
"throw",
"raise",
"break",
"break;",
"continue",
"continue;",
"exit",
"exit(",
"die(",
"panic!",
"unreachable!",
];
for term in &terminators {
if line.starts_with(term) || line == *term {
return true;
}
}
if line.starts_with("return ") && line.ends_with(';') {
return true;
}
false
}
pub fn detect_dead_code(files: &[(String, Vec<Symbol>, Language)]) -> DeadCodeInfo {
let mut detector = DeadCodeDetector::new();
for (path, symbols, language) in files {
detector.add_file(path, symbols, *language);
}
detector.detect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Visibility;
fn make_symbol(name: &str, kind: SymbolKind, visibility: Visibility) -> Symbol {
Symbol {
name: name.to_owned(),
kind,
visibility,
start_line: 1,
end_line: 10,
..Default::default()
}
}
#[test]
fn test_unused_export_detection() {
let mut detector = DeadCodeDetector::new();
let symbols = [
make_symbol("used_func", SymbolKind::Function, Visibility::Public),
make_symbol("unused_func", SymbolKind::Function, Visibility::Public),
];
let caller = Symbol {
name: "caller".to_owned(),
kind: SymbolKind::Function,
visibility: Visibility::Private,
calls: vec!["used_func".to_owned()],
..Default::default()
};
detector.add_file("test.c", &[symbols[0].clone(), symbols[1].clone(), caller], Language::C);
let result = detector.detect();
assert!(result
.unused_exports
.iter()
.any(|e| e.name == "unused_func"));
assert!(!result.unused_exports.iter().any(|e| e.name == "used_func"));
}
#[test]
fn test_entry_point_not_flagged() {
let mut detector = DeadCodeDetector::new();
let symbols = vec![
make_symbol("main", SymbolKind::Function, Visibility::Public),
make_symbol("test_something", SymbolKind::Function, Visibility::Public),
make_symbol("__init__", SymbolKind::Method, Visibility::Public),
];
detector.add_file("test.py", &symbols, Language::Python);
let result = detector.detect();
assert!(!result.unused_exports.iter().any(|e| e.name == "main"));
assert!(!result
.unused_exports
.iter()
.any(|e| e.name == "test_something"));
assert!(!result.unused_exports.iter().any(|e| e.name == "__init__"));
}
#[test]
fn test_unreachable_code_detection() {
let source = r#"
fn example() {
let x = 1;
return x;
let y = 2; // unreachable
println!("{}", y);
}
"#;
let unreachable = detect_unreachable_code(source, "test.rs", Language::Rust);
assert!(!unreachable.is_empty());
assert!(unreachable.iter().any(|u| u.snippet.contains("let y")));
}
#[test]
fn test_is_terminator() {
assert!(is_terminator("return;"));
assert!(is_terminator("return x"));
assert!(is_terminator("throw new Error()"));
assert!(is_terminator("break;"));
assert!(is_terminator("continue;"));
assert!(is_terminator("panic!(\"error\")"));
assert!(!is_terminator("let x = 1;"));
assert!(!is_terminator("if (x) {"));
assert!(!is_terminator("// return"));
}
#[test]
fn test_unused_imports() {
let mut detector = DeadCodeDetector::new();
detector.add_import("used_import", "some/path", "test.ts", 1);
detector.add_import("unused_import", "other/path", "test.ts", 2);
detector.mark_import_used("used_import", "test.ts");
let result = detector.detect();
assert_eq!(result.unused_imports.len(), 1);
assert_eq!(result.unused_imports[0].name, "unused_import");
}
#[test]
fn test_underscore_variables_ignored() {
let mut detector = DeadCodeDetector::new();
detector.add_variable("_unused", "test.rs", 1, "main");
detector.add_variable("used", "test.rs", 2, "main");
detector.add_variable("not_used", "test.rs", 3, "main");
detector.mark_variable_used("used", "main");
let result = detector.detect();
assert!(!result.unused_variables.iter().any(|v| v.name == "_unused"));
assert!(result.unused_variables.iter().any(|v| v.name == "not_used"));
}
}