1use std::collections::{HashMap, HashSet};
4
5#[derive(Debug, Clone)]
7pub struct DeadConfig {
8 pub min_name_len: usize,
9 pub test_file_patterns: Vec<String>,
10}
11
12impl Default for DeadConfig {
13 fn default() -> Self {
14 Self {
15 min_name_len: 3,
16 test_file_patterns: vec!["test".to_string(), "spec".to_string(), "mock".to_string()],
17 }
18 }
19}
20
21#[derive(Debug, Clone)]
22pub struct DeadCandidate {
23 pub name: String,
24 pub file: String,
25 pub line: usize,
26 pub confidence: Confidence,
27 pub reason: String,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub enum Confidence {
32 High,
33 Medium,
34 Low,
35}
36
37pub fn analyze_file(file: &str, content: &str, config: &DeadConfig) -> Vec<DeadCandidate> {
39 let lines: Vec<&str> = content.lines().collect();
40 let mut identifiers: HashMap<String, (Vec<usize>, Vec<usize>)> = HashMap::new();
41 for (i, line) in lines.iter().enumerate() {
44 for token in line.split(|c: char| !c.is_alphanumeric() && c != '_') {
46 if token.len() < config.min_name_len { continue; }
47 if !token.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') { continue; }
48 if !token.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { continue; }
49
50 let t = line.trim();
52 let is_def = t.starts_with("fn ") || t.starts_with("def ")
53 || t.starts_with("class ") || t.starts_with("struct ")
54 || t.starts_with("enum ") || t.starts_with("trait ")
55 || t.starts_with("const ") || t.starts_with("let ")
56 || t.starts_with("var ") || t.starts_with("function ")
57 || t.starts_with("public ") || t.starts_with("private ")
58 || line.contains(&format!("fn {}", token))
59 || line.contains(&format!("def {}", token));
60
61 let entry = identifiers.entry(token.to_string()).or_insert((Vec::new(), Vec::new()));
62 if is_def {
63 entry.0.push(i);
64 }
65 entry.1.push(i);
66 }
67 }
68
69 let mut candidates = Vec::new();
70 let is_test_file = config.test_file_patterns.iter().any(|p| file.contains(p));
71
72 for (name, (def_lines, total_lines)) in identifiers {
73 if def_lines.is_empty() { continue; }
75 let def_only = def_lines.iter().all(|def_line| {
76 total_lines.iter().all(|&tl| tl == *def_line)
77 });
78 if !def_only { continue; }
79
80 let confidence = if is_test_file {
81 Confidence::Low
82 } else if name.chars().all(|c| c.is_ascii_uppercase() || c == '_') && name.len() >= 5 {
83 Confidence::High
84 } else if name.len() >= 5 {
85 Confidence::Medium
86 } else {
87 Confidence::Low
88 };
89
90 let reason = match &confidence {
91 Confidence::High => format!("{} — exported but never imported", name),
92 Confidence::Medium => format!("{} — defined but never referenced outside definition", name),
93 Confidence::Low => format!("{} — potentially dead (test file)", name),
94 };
95
96 for &dl in &def_lines {
97 candidates.push(DeadCandidate {
98 name: name.clone(),
99 file: file.to_string(),
100 line: dl + 1,
101 confidence: confidence.clone(),
102 reason: reason.clone(),
103 });
104 }
105 }
106
107 candidates
108}
109
110pub fn analyze_files(files: &[(String, String)], config: &DeadConfig) -> Vec<DeadCandidate> {
112 let mut all = Vec::new();
113 for (file, content) in files {
114 all.extend(analyze_file(file, content, config));
115 }
116 let mut seen = HashSet::new();
118 all.into_iter().filter(|c| seen.insert(c.name.clone())).collect()
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_dead_function() {
127 let content = "fn helper() {}\nfn main() { helper(); }";
128 let config = DeadConfig::default();
129 let results = analyze_file("test.rs", content, &config);
130 let dead: Vec<_> = results.iter().filter(|c| c.name == "helper").collect();
132 assert!(dead.is_empty()); }
134
135 #[test]
136 fn test_unused_function() {
137 let content = "fn dead_func() {}\nfn main() {}";
138 let config = DeadConfig::default();
139 let results = analyze_file("test.rs", content, &config);
140 assert!(results.iter().any(|c| c.name == "dead_func"));
141 }
142
143 #[test]
144 fn test_test_file_low_confidence() {
145 let content = "fn helper() {}\n";
146 let config = DeadConfig::default();
147 let results = analyze_file("test_helper.rs", content, &config);
148 assert!(!results.is_empty());
149 assert_eq!(results[0].confidence, Confidence::Low);
150 }
151
152 #[test]
153 fn test_high_confidence_constant() {
154 let content = "const API_KEY = 123;\n";
155 let config = DeadConfig::default();
156 let results = analyze_file("config.rs", content, &config);
157 let high: Vec<_> = results.iter().filter(|c| c.confidence == Confidence::High).collect();
159 assert!(!high.is_empty());
160 }
161}