1use crate::state::OciState;
8use crate::types::{DeadCodeReport, InternedString, SymbolKind, Visibility};
9use std::collections::{HashSet, VecDeque};
10
11pub struct DeadCodeAnalyzer;
13
14impl DeadCodeAnalyzer {
15 pub fn new() -> Self {
17 Self
18 }
19
20 pub fn analyze(&self, state: &OciState) -> DeadCodeReport {
28 let entry_points = self.identify_entry_points(state);
30
31 let reachable = self.compute_reachable(state, &entry_points);
33
34 let dead_symbols = self.find_dead_symbols(state, &reachable);
36
37 let potentially_live = self.identify_potentially_live(state, &reachable);
40
41 DeadCodeReport {
42 dead_symbols,
43 entry_points,
44 potentially_live,
45 }
46 }
47
48 fn identify_entry_points(&self, state: &OciState) -> Vec<InternedString> {
56 let mut entry_points = Vec::new();
57
58 for entry in state.symbols.iter() {
60 let scoped_name = *entry.key();
61 let symbol = entry.value();
62
63 if self.is_entry_point(state, symbol) {
65 entry_points.push(scoped_name);
66 }
67 }
68
69 entry_points
70 }
71
72 fn is_entry_point(&self, state: &OciState, symbol: &crate::types::SymbolDef) -> bool {
74 let name = state.resolve(symbol.name);
76 if name == "main" && matches!(symbol.kind, SymbolKind::Function) {
77 return true;
78 }
79
80 if symbol.attributes.iter().any(|attr| attr.contains("test")) {
82 return true;
83 }
84
85 if matches!(symbol.visibility, Visibility::Public) {
87 return true;
88 }
89
90 if matches!(symbol.visibility, Visibility::Crate) {
92 return true;
93 }
94
95 if matches!(symbol.kind, SymbolKind::Impl) {
97 return true;
98 }
99
100 if matches!(symbol.kind, SymbolKind::Method) {
102 if let Some(parent) = symbol.parent {
104 if let Some(parent_sym) = state.get_symbol(parent) {
105 if matches!(parent_sym.kind, SymbolKind::Impl) {
106 return true;
107 }
108 }
109 }
110 }
111
112 false
113 }
114
115 fn compute_reachable(
119 &self,
120 state: &OciState,
121 entry_points: &[InternedString],
122 ) -> HashSet<InternedString> {
123 let mut reachable = HashSet::new();
124 let mut queue = VecDeque::new();
125
126 for &entry in entry_points {
128 reachable.insert(entry);
129 queue.push_back(entry);
130 }
131
132 while let Some(current) = queue.pop_front() {
134 let callees = state.find_callees(current);
136
137 for call_edge in callees {
138 let callee_symbols = state.find_by_name(&call_edge.callee_name);
140
141 for callee_sym in callee_symbols {
142 let callee_scoped = callee_sym.scoped_name;
143
144 if reachable.insert(callee_scoped) {
146 queue.push_back(callee_scoped);
147 }
148 }
149 }
150
151 if let Some(symbol) = state.get_symbol(current) {
154 if let Some(parent) = symbol.parent {
155 if reachable.insert(parent) {
156 queue.push_back(parent);
157 }
158 }
159 }
160 }
161
162 reachable
163 }
164
165 fn find_dead_symbols(
167 &self,
168 state: &OciState,
169 reachable: &HashSet<InternedString>,
170 ) -> Vec<InternedString> {
171 let mut dead_symbols = Vec::new();
172
173 for entry in state.symbols.iter() {
174 let scoped_name = *entry.key();
175 let symbol = entry.value();
176
177 if reachable.contains(&scoped_name) {
179 continue;
180 }
181
182 if matches!(
184 symbol.kind,
185 SymbolKind::Module | SymbolKind::Field | SymbolKind::Variant
186 ) {
187 continue;
188 }
189
190 dead_symbols.push(scoped_name);
191 }
192
193 dead_symbols
194 }
195
196 fn identify_potentially_live(
203 &self,
204 state: &OciState,
205 reachable: &HashSet<InternedString>,
206 ) -> Vec<InternedString> {
207 let mut potentially_live = Vec::new();
208
209 for entry in state.symbols.iter() {
210 let scoped_name = *entry.key();
211 let symbol = entry.value();
212
213 if reachable.contains(&scoped_name) {
215 continue;
216 }
217
218 let has_special_attrs = symbol.attributes.iter().any(|attr| {
220 attr.contains("derive")
221 || attr.contains("macro")
222 || attr.contains("no_mangle")
223 || attr.contains("export_name")
224 || attr.contains("link_name")
225 || attr.contains("used")
226 });
227
228 if has_special_attrs {
229 potentially_live.push(scoped_name);
230 continue;
231 }
232
233 if matches!(symbol.kind, SymbolKind::Const | SymbolKind::Static) {
235 potentially_live.push(scoped_name);
236 continue;
237 }
238
239 if matches!(symbol.kind, SymbolKind::Macro) {
241 potentially_live.push(scoped_name);
242 }
243 }
244
245 potentially_live
246 }
247}
248
249impl Default for DeadCodeAnalyzer {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::state::create_state;
259 use crate::types::{Location, Signature, SymbolDef};
260 use std::path::PathBuf;
261
262 #[test]
263 fn test_entry_point_detection() {
264 let state = create_state(PathBuf::from("/test"));
265 let analyzer = DeadCodeAnalyzer::new();
266
267 let main_name = state.intern("main");
269 let main_scoped = state.intern("crate::main");
270 state.add_symbol(SymbolDef {
271 name: main_name,
272 scoped_name: main_scoped,
273 kind: SymbolKind::Function,
274 location: Location::new(PathBuf::from("/test/main.rs"), 0, 10),
275 signature: Some(Signature::default()),
276 visibility: Visibility::Private,
277 attributes: vec![],
278 doc_comment: None,
279 parent: None,
280 });
281
282 let test_name = state.intern("test_foo");
284 let test_scoped = state.intern("crate::test_foo");
285 state.add_symbol(SymbolDef {
286 name: test_name,
287 scoped_name: test_scoped,
288 kind: SymbolKind::Function,
289 location: Location::new(PathBuf::from("/test/main.rs"), 20, 30),
290 signature: Some(Signature::default()),
291 visibility: Visibility::Private,
292 attributes: vec!["test".to_string()],
293 doc_comment: None,
294 parent: None,
295 });
296
297 let pub_name = state.intern("public_api");
299 let pub_scoped = state.intern("crate::public_api");
300 state.add_symbol(SymbolDef {
301 name: pub_name,
302 scoped_name: pub_scoped,
303 kind: SymbolKind::Function,
304 location: Location::new(PathBuf::from("/test/main.rs"), 40, 50),
305 signature: Some(Signature::default()),
306 visibility: Visibility::Public,
307 attributes: vec![],
308 doc_comment: None,
309 parent: None,
310 });
311
312 let priv_name = state.intern("internal");
314 let priv_scoped = state.intern("crate::internal");
315 state.add_symbol(SymbolDef {
316 name: priv_name,
317 scoped_name: priv_scoped,
318 kind: SymbolKind::Function,
319 location: Location::new(PathBuf::from("/test/main.rs"), 60, 70),
320 signature: Some(Signature::default()),
321 visibility: Visibility::Private,
322 attributes: vec![],
323 doc_comment: None,
324 parent: None,
325 });
326
327 let entry_points = analyzer.identify_entry_points(&state);
328
329 assert_eq!(entry_points.len(), 3);
331 assert!(entry_points.contains(&main_scoped));
332 assert!(entry_points.contains(&test_scoped));
333 assert!(entry_points.contains(&pub_scoped));
334 assert!(!entry_points.contains(&priv_scoped));
335 }
336
337 #[test]
338 fn test_reachability_analysis() {
339 let state = create_state(PathBuf::from("/test"));
340 let analyzer = DeadCodeAnalyzer::new();
341
342 let main_scoped = state.intern("crate::main");
344 state.add_symbol(SymbolDef {
345 name: state.intern("main"),
346 scoped_name: main_scoped,
347 kind: SymbolKind::Function,
348 location: Location::new(PathBuf::from("/test/main.rs"), 0, 10),
349 signature: Some(Signature::default()),
350 visibility: Visibility::Private,
351 attributes: vec![],
352 doc_comment: None,
353 parent: None,
354 });
355
356 let helper_scoped = state.intern("crate::helper");
358 state.add_symbol(SymbolDef {
359 name: state.intern("helper"),
360 scoped_name: helper_scoped,
361 kind: SymbolKind::Function,
362 location: Location::new(PathBuf::from("/test/main.rs"), 20, 30),
363 signature: Some(Signature::default()),
364 visibility: Visibility::Private,
365 attributes: vec![],
366 doc_comment: None,
367 parent: None,
368 });
369
370 let dead_scoped = state.intern("crate::dead");
372 state.add_symbol(SymbolDef {
373 name: state.intern("dead"),
374 scoped_name: dead_scoped,
375 kind: SymbolKind::Function,
376 location: Location::new(PathBuf::from("/test/main.rs"), 40, 50),
377 signature: Some(Signature::default()),
378 visibility: Visibility::Private,
379 attributes: vec![],
380 doc_comment: None,
381 parent: None,
382 });
383
384 use crate::types::CallEdge;
386 state.add_call_edge(CallEdge {
387 caller: main_scoped,
388 callee_name: "helper".to_string(),
389 location: Location::new(PathBuf::from("/test/main.rs"), 5, 6),
390 is_method_call: false,
391 });
392
393 let report = analyzer.analyze(&state);
394
395 assert!(report.entry_points.contains(&main_scoped));
397
398 assert!(!report.dead_symbols.contains(&helper_scoped));
400
401 assert!(report.dead_symbols.contains(&dead_scoped));
403 }
404}