use super::patterns::detect_dynamic_patterns;
use crate::config::Config;
use crate::core::{CallGraph, Confidence, DeadSymbol, DynamicPatternKind};
pub fn score_dead_symbols(
mut dead_symbols: Vec<DeadSymbol>,
call_graph: &CallGraph,
config: &Config,
) -> Vec<DeadSymbol> {
let file_has_eval: std::collections::HashSet<_> = call_graph
.files
.values()
.filter(|f| f.has_dynamic_eval)
.map(|f| f.id)
.collect();
let has_global_dynamic = detect_dynamic_patterns(call_graph);
for dead in &mut dead_symbols {
let mut score = dead.confidence_score as i32;
if dead.symbol.has_decorators {
score -= 20;
}
if dead.symbol.exported {
score -= 10;
}
if file_has_eval.contains(&dead.symbol.file_id) {
score -= 30;
}
if has_global_dynamic {
score -= 15;
}
if matches!(dead.reason, crate::core::DeadnessReason::Transitive { .. }) {
score -= 5;
}
if is_type_only(&dead.symbol.kind) {
score += 5;
}
if dead.symbol.name.starts_with('_') && !dead.symbol.name.starts_with("__") {
score += 5;
}
for pattern in &call_graph.dynamic_patterns {
if pattern.affected_symbols.contains(&dead.symbol.id) {
match pattern.kind {
DynamicPatternKind::Eval | DynamicPatternKind::FunctionConstructor => {
score -= 40; }
DynamicPatternKind::Reflect => {
score -= 30;
}
DynamicPatternKind::BracketAccess | DynamicPatternKind::StringPropertyAccess => {
score -= 20;
}
DynamicPatternKind::ObjectIteration => {
score -= 15;
}
DynamicPatternKind::DynamicImport | DynamicPatternKind::DynamicRequire => {
score -= 25;
}
}
}
}
if dead.symbol.name == "default" {
score -= 10;
}
if dead.symbol.kind == crate::core::SymbolKind::Method {
score -= 5;
}
dead.confidence_score = score.clamp(0, 100) as u8;
dead.confidence = Confidence::from_score(dead.confidence_score);
}
if !config.analysis.include_types {
dead_symbols.retain(|d| !is_type_only(&d.symbol.kind));
}
dead_symbols
}
fn is_type_only(kind: &crate::core::SymbolKind) -> bool {
matches!(
kind,
crate::core::SymbolKind::Type | crate::core::SymbolKind::Interface
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{DeadnessReason, FileId, Location, SymbolId, SymbolKind, TrackedSymbol};
use std::path::PathBuf;
fn make_dead_symbol(name: &str, exported: bool, has_decorators: bool) -> DeadSymbol {
let mut symbol = TrackedSymbol::new(
SymbolId::new(0),
name.to_string(),
SymbolKind::Function,
Location::new(PathBuf::from("test.ts"), 0, 10, 1, 1),
FileId::new(0),
);
symbol.exported = exported;
symbol.has_decorators = has_decorators;
DeadSymbol::new(
symbol,
100,
DeadnessReason::Unreachable {
explanation: "never referenced".to_string(),
},
)
}
#[test]
fn test_base_confidence() {
let dead = make_dead_symbol("foo", false, false);
assert_eq!(dead.confidence_score, 100);
assert_eq!(dead.confidence, Confidence::High);
}
#[test]
fn test_exported_reduces_confidence() {
let graph = CallGraph::new();
let config = Config::default();
let dead_symbols = vec![make_dead_symbol("foo", true, false)];
let scored = score_dead_symbols(dead_symbols, &graph, &config);
assert_eq!(scored[0].confidence_score, 90);
}
#[test]
fn test_decorators_reduce_confidence() {
let graph = CallGraph::new();
let config = Config::default();
let dead_symbols = vec![make_dead_symbol("foo", false, true)];
let scored = score_dead_symbols(dead_symbols, &graph, &config);
assert_eq!(scored[0].confidence_score, 80);
}
#[test]
fn test_combined_penalties() {
let graph = CallGraph::new();
let config = Config::default();
let dead_symbols = vec![make_dead_symbol("foo", true, true)];
let scored = score_dead_symbols(dead_symbols, &graph, &config);
assert_eq!(scored[0].confidence_score, 70);
assert_eq!(scored[0].confidence, Confidence::Medium);
}
#[test]
fn test_private_convention_bonus() {
let graph = CallGraph::new();
let config = Config::default();
let dead_symbols = vec![make_dead_symbol("_privateHelper", false, false)];
let scored = score_dead_symbols(dead_symbols, &graph, &config);
assert_eq!(scored[0].confidence_score, 100);
}
}