1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Reachability-based dead-code detection.
//!
//! `cargo`'s `dead_code` lint misses unused `pub` items and false-positives on
//! `cfg`-gated cross-platform code (a Linux-only helper looks "never used" on a
//! macOS build). This analysis works on source *text* instead: it counts every
//! identifier occurrence across the whole tree — including code behind inactive
//! `cfg`s — so a symbol referenced only from another platform's branch is still
//! seen as live. A symbol whose name appears **only at its own definition(s)**
//! is reported as dead.
//!
//! This is deliberately conservative (it under-reports rather than over-reports
//! dead code): if a name appears anywhere else, we treat it as reachable. Names
//! that are dispatched by trait/std machinery (`new`, `fmt`, `from`, …) and
//! entry points (`main`, `#[test]`) are excluded.
use anyhow::Result;
use regex::Regex;
use serde::Serialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use walkdir::WalkDir;
fn def_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(
r"^(?P<indent>\s*)(?P<vis>pub(?:\([^)]*\))?\s+)?(?:async\s+|unsafe\s+|const\s+|extern\s+\S+\s+)*(?P<kind>fn|struct|enum|trait|type|const|static)\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)",
)
.expect("valid def regex")
})
}
fn ident_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"[A-Za-z_][A-Za-z0-9_]*").expect("valid ident regex"))
}
/// Names dispatched by trait/std machinery or otherwise reachable without an
/// explicit textual reference — excluded to avoid false positives.
const DISPATCHED: &[&str] = &[
"new",
"default",
"fmt",
"from",
"into",
"try_from",
"try_into",
"drop",
"clone",
"eq",
"ne",
"hash",
"cmp",
"partial_cmp",
"deref",
"deref_mut",
"as_ref",
"as_mut",
"next",
"poll",
"add",
"sub",
"mul",
"div",
"index",
"call",
"main",
"run",
"execute",
"handle",
"visit",
];
/// A symbol that appears to be dead (referenced nowhere but its definition).
#[derive(Debug, Clone, Serialize)]
pub struct DeadSymbol {
pub name: String,
pub kind: String,
pub path: String,
pub line: usize,
pub is_pub: bool,
/// True if the definition sits under a `#[cfg(...)]` — likely intentional
/// platform/feature code rather than truly dead; surfaced but de-ranked.
pub cfg_gated: bool,
}
struct Def {
name: String,
kind: String,
path: String,
line: usize,
is_pub: bool,
cfg_gated: bool,
}
pub struct DeadCodeAnalyzer {
root: PathBuf,
}
impl DeadCodeAnalyzer {
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
}
}
pub fn find(&self) -> Result<Vec<DeadSymbol>> {
let mut defs: Vec<Def> = Vec::new();
let mut occ: HashMap<String, usize> = HashMap::new();
// Definitions come from `src/` (the shippable code we might delete), but
// *references* are counted across tests and examples too — otherwise a
// `pub fn` exercised only by its (relocated) unit test looks dead.
let src_root = self.root.join("src");
let ref_roots = ["src", "tests", "examples"];
for dir in ref_roots {
let dir_path = self.root.join(dir);
let is_src = dir == "src";
for entry in WalkDir::new(&dir_path).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p.extension().is_some_and(|e| e == "rs") {
let Ok(text) = std::fs::read_to_string(p) else {
continue;
};
if is_src {
let rel = p
.strip_prefix(&src_root)
.unwrap_or(p)
.to_string_lossy()
.replace('\\', "/");
collect_defs(&text, &rel, &mut defs);
}
for m in ident_re().find_iter(&text) {
*occ.entry(m.as_str().to_string()).or_default() += 1;
}
}
}
}
// How many definitions share each name (each contributes one textual
// occurrence at its own signature).
let mut def_count: HashMap<&str, usize> = HashMap::new();
for d in &defs {
*def_count.entry(d.name.as_str()).or_default() += 1;
}
let mut dead: Vec<DeadSymbol> = defs
.iter()
.filter(|d| {
let name = d.name.as_str();
if DISPATCHED.contains(&name) || name.starts_with("__") {
return false;
}
// Occurrences beyond the definition signatures themselves.
let total = occ.get(name).copied().unwrap_or(0);
let self_refs = def_count.get(name).copied().unwrap_or(1);
total <= self_refs
})
.map(|d| DeadSymbol {
name: d.name.clone(),
kind: d.kind.clone(),
path: d.path.clone(),
line: d.line,
is_pub: d.is_pub,
cfg_gated: d.cfg_gated,
})
.collect();
// Real dead code (not cfg-gated) first; then by kind for stable output.
dead.sort_by(|a, b| {
a.cfg_gated
.cmp(&b.cfg_gated)
.then_with(|| a.path.cmp(&b.path))
.then_with(|| a.line.cmp(&b.line))
});
Ok(dead)
}
}
fn collect_defs(text: &str, rel_path: &str, out: &mut Vec<Def>) {
let lines: Vec<&str> = text.lines().collect();
for (i, line) in lines.iter().enumerate() {
let Some(c) = def_re().captures(line) else {
continue;
};
// A preceding attribute line containing `cfg(` marks platform/feature gating.
let cfg_gated = i > 0 && {
let prev = lines[i - 1].trim_start();
prev.starts_with("#[cfg(") || prev.starts_with("#[cfg_attr(")
};
out.push(Def {
name: c["name"].to_string(),
kind: c["kind"].to_string(),
path: rel_path.to_string(),
line: i + 1,
is_pub: c.name("vis").is_some(),
cfg_gated,
});
}
}