use super::transitive::find_transitive_dead;
use crate::config::Config;
use crate::core::{CallGraph, DeadSymbol, DeadnessReason, SymbolId, TrackedSymbol};
use std::collections::{HashSet, VecDeque};
pub fn find_dead_symbols(call_graph: &CallGraph, config: &Config) -> Vec<DeadSymbol> {
let reachable = mark_reachable_symbols(call_graph);
let unreachable: Vec<_> = call_graph
.symbols
.values()
.filter(|s| !reachable.contains(&s.id))
.filter(|s| !config.should_ignore_symbol(&s.name))
.cloned()
.collect();
let (directly_dead, transitively_dead) = find_transitive_dead(&unreachable, call_graph);
let mut dead_symbols = Vec::new();
for symbol in directly_dead {
dead_symbols.push(create_dead_symbol(symbol, call_graph));
}
for (symbol, chain, killed_by) in transitively_dead {
dead_symbols.push(create_transitive_dead_symbol(symbol, chain, killed_by));
}
dead_symbols.sort_by(|a, b| {
let file_cmp = a.symbol.location.file_path.cmp(&b.symbol.location.file_path);
if file_cmp != std::cmp::Ordering::Equal {
return file_cmp;
}
a.symbol.location.line.cmp(&b.symbol.location.line)
});
dead_symbols
}
fn mark_reachable_symbols(call_graph: &CallGraph) -> HashSet<SymbolId> {
let mut reachable = HashSet::new();
let mut queue: VecDeque<SymbolId> = VecDeque::new();
for &entry_id in &call_graph.entry_points {
if !reachable.contains(&entry_id) {
queue.push_back(entry_id);
reachable.insert(entry_id);
}
}
while let Some(current_id) = queue.pop_front() {
for &ref_id in call_graph.get_outgoing_refs(current_id) {
if !reachable.contains(&ref_id) {
reachable.insert(ref_id);
queue.push_back(ref_id);
}
}
}
reachable
}
fn create_dead_symbol(symbol: TrackedSymbol, call_graph: &CallGraph) -> DeadSymbol {
let reason = if symbol.exported {
DeadnessReason::UnusedExport
} else if is_type_symbol(&symbol) {
DeadnessReason::UnusedType
} else {
let explanation = generate_unreachable_explanation(&symbol, call_graph);
DeadnessReason::Unreachable { explanation }
};
let base_confidence = 100u8;
DeadSymbol::new(symbol, base_confidence, reason)
}
fn create_transitive_dead_symbol(
symbol: TrackedSymbol,
chain: Vec<SymbolId>,
killed_by: SymbolId,
) -> DeadSymbol {
let base_confidence = 95u8;
DeadSymbol::transitive(symbol, base_confidence, chain, killed_by)
}
fn is_type_symbol(symbol: &TrackedSymbol) -> bool {
matches!(
symbol.kind,
crate::core::SymbolKind::Type
| crate::core::SymbolKind::Interface
)
}
fn generate_unreachable_explanation(symbol: &TrackedSymbol, call_graph: &CallGraph) -> String {
let incoming = call_graph.get_incoming_refs(symbol.id);
if incoming.is_empty() {
if symbol.exported {
"exported but never imported".to_string()
} else {
"never referenced".to_string()
}
} else {
format!("referenced only by {} dead symbol(s)", incoming.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{FileId, Location, SymbolKind};
use std::path::PathBuf;
fn make_symbol(id: u32, name: &str, kind: SymbolKind) -> TrackedSymbol {
TrackedSymbol::new(
SymbolId::new(id),
name.to_string(),
kind,
Location::new(PathBuf::from("test.ts"), 0, 10, 1, 1),
FileId::new(0),
)
}
#[test]
fn test_entry_point_is_not_dead() {
let mut graph = CallGraph::new();
let mut entry = make_symbol(0, "main", SymbolKind::Function);
entry.is_entry_point = true;
graph.add_symbol(entry);
graph.mark_entry_point(SymbolId::new(0));
let config = Config::default();
let dead = find_dead_symbols(&graph, &config);
assert!(dead.is_empty());
}
#[test]
fn test_unreferenced_is_dead() {
let mut graph = CallGraph::new();
let mut entry = make_symbol(0, "main", SymbolKind::Function);
entry.is_entry_point = true;
graph.add_symbol(entry);
graph.mark_entry_point(SymbolId::new(0));
let orphan = make_symbol(1, "orphan", SymbolKind::Function);
graph.add_symbol(orphan);
let config = Config::default();
let dead = find_dead_symbols(&graph, &config);
assert_eq!(dead.len(), 1);
assert_eq!(dead[0].symbol.name, "orphan");
}
#[test]
fn test_referenced_from_entry_is_alive() {
let mut graph = CallGraph::new();
let mut entry = make_symbol(0, "main", SymbolKind::Function);
entry.is_entry_point = true;
graph.add_symbol(entry);
graph.mark_entry_point(SymbolId::new(0));
let helper = make_symbol(1, "helper", SymbolKind::Function);
graph.add_symbol(helper);
graph.add_reference(crate::core::SymbolReference::new(
SymbolId::new(0),
SymbolId::new(1),
crate::core::ReferenceKind::Call,
Location::new(PathBuf::from("test.ts"), 5, 15, 2, 1),
));
let config = Config::default();
let dead = find_dead_symbols(&graph, &config);
assert!(dead.is_empty());
}
}