Skip to main content

reliary_dead/
lib.rs

1/// Grammar-free dead code detection.
2/// Uses occurrence counting: an identifier is dead if total_occurrences == definition_occurrences.
3use std::collections::{HashMap, HashSet};
4
5/// Configuration for dead code analysis
6#[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
37/// Analyze a single file for dead identifiers
38pub 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    // Maps: identifier → (definition_lines, total_occurrence_lines)
42
43    for (i, line) in lines.iter().enumerate() {
44        // Find all identifiers in this line
45        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            // Check if this line is a definition site
51            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        // Dead: defined but never used outside its own definition line
74        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
110/// Analyze multiple files and return aggregated dead code results
111pub 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    // Dedup by name
117    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        // helper is used, main is used
131        let dead: Vec<_> = results.iter().filter(|c| c.name == "helper").collect();
132        assert!(dead.is_empty()); // helper is called
133    }
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        // constants with all-caps names get high confidence
158        let high: Vec<_> = results.iter().filter(|c| c.confidence == Confidence::High).collect();
159        assert!(!high.is_empty());
160    }
161}