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