Skip to main content

omni_index/analysis/
dead_code.rs

1//! Dead code analysis via global reachability.
2//!
3//! This module performs whole-program analysis to identify potentially dead code
4//! by building a reachability graph from entry points (main functions, tests, public APIs)
5//! and marking all symbols that are transitively called from those entry points.
6
7use crate::state::OciState;
8use crate::types::{DeadCodeReport, InternedString, SymbolKind, Visibility};
9use std::collections::{HashSet, VecDeque};
10
11/// Analyzes code to detect potentially dead (unreachable) symbols.
12pub struct DeadCodeAnalyzer;
13
14impl DeadCodeAnalyzer {
15    /// Creates a new dead code analyzer.
16    pub fn new() -> Self {
17        Self
18    }
19
20    /// Performs dead code analysis on the entire codebase.
21    ///
22    /// This works by:
23    /// 1. Identifying entry points (main, tests, public symbols, trait impls)
24    /// 2. Performing BFS from entry points following call edges
25    /// 3. Marking all reachable symbols as live
26    /// 4. Reporting unreachable symbols as potentially dead
27    pub fn analyze(&self, state: &OciState) -> DeadCodeReport {
28        // Step 1: Identify all entry points
29        let entry_points = self.identify_entry_points(state);
30
31        // Step 2: Perform reachability analysis from entry points
32        let reachable = self.compute_reachable(state, &entry_points);
33
34        // Step 3: Identify dead symbols (symbols not in reachable set)
35        let dead_symbols = self.find_dead_symbols(state, &reachable);
36
37        // Step 4: Identify potentially live symbols (conservative estimation)
38        // These are symbols that might be used through dynamic dispatch, FFI, etc.
39        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    /// Identifies entry points for reachability analysis.
49    ///
50    /// Entry points include:
51    /// - Functions named "main"
52    /// - Functions with #[test] attribute
53    /// - Public symbols (pub/pub(crate))
54    /// - Trait implementations
55    fn identify_entry_points(&self, state: &OciState) -> Vec<InternedString> {
56        let mut entry_points = Vec::new();
57
58        // Iterate over all symbols
59        for entry in state.symbols.iter() {
60            let scoped_name = *entry.key();
61            let symbol = entry.value();
62
63            // Check if this is an entry point
64            if self.is_entry_point(state, symbol) {
65                entry_points.push(scoped_name);
66            }
67        }
68
69        entry_points
70    }
71
72    /// Determines if a symbol is an entry point.
73    fn is_entry_point(&self, state: &OciState, symbol: &crate::types::SymbolDef) -> bool {
74        // 1. Functions named "main" are always entry points
75        let name = state.resolve(symbol.name);
76        if name == "main" && matches!(symbol.kind, SymbolKind::Function) {
77            return true;
78        }
79
80        // 2. Functions with #[test] attribute
81        if symbol.attributes.iter().any(|attr| attr.contains("test")) {
82            return true;
83        }
84
85        // 3. Public symbols are entry points (can be called from outside)
86        if matches!(symbol.visibility, Visibility::Public) {
87            return true;
88        }
89
90        // 4. pub(crate) symbols are also entry points (reachable within crate)
91        if matches!(symbol.visibility, Visibility::Crate) {
92            return true;
93        }
94
95        // 5. Trait implementations are entry points (can be called through trait objects)
96        if matches!(symbol.kind, SymbolKind::Impl) {
97            return true;
98        }
99
100        // 6. Methods in trait impls are entry points
101        if matches!(symbol.kind, SymbolKind::Method) {
102            // Check if parent is an impl
103            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    /// Computes the set of reachable symbols from the given entry points.
116    ///
117    /// Uses BFS to traverse the call graph.
118    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        // Initialize with entry points
127        for &entry in entry_points {
128            reachable.insert(entry);
129            queue.push_back(entry);
130        }
131
132        // BFS traversal
133        while let Some(current) = queue.pop_front() {
134            // Find all callees of the current symbol
135            let callees = state.find_callees(current);
136
137            for call_edge in callees {
138                // Resolve callee name to scoped symbols
139                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 we haven't seen this symbol yet, mark it as reachable
145                    if reachable.insert(callee_scoped) {
146                        queue.push_back(callee_scoped);
147                    }
148                }
149            }
150
151            // Also mark parent symbols as reachable
152            // (if a method is reachable, its struct/impl should be too)
153            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    /// Finds symbols that are not reachable (potentially dead).
166    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            // Skip if reachable
178            if reachable.contains(&scoped_name) {
179                continue;
180            }
181
182            // Skip certain symbol kinds that are not meaningful for dead code analysis
183            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    /// Identifies symbols that are potentially live through non-standard mechanisms.
197    ///
198    /// These include:
199    /// - Symbols with macro attributes (might be called by macro expansion)
200    /// - Symbols with derive attributes (might be used by generated code)
201    /// - Constants and statics (might be used through references)
202    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            // Skip if already reachable
214            if reachable.contains(&scoped_name) {
215                continue;
216            }
217
218            // Check for attributes that might indicate dynamic usage
219            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            // Constants and statics might be used through references
234            if matches!(symbol.kind, SymbolKind::Const | SymbolKind::Static) {
235                potentially_live.push(scoped_name);
236                continue;
237            }
238
239            // Macros are often used in ways not tracked by call graph
240            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        // Add a main function
268        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        // Add a test function
283        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        // Add a public function
298        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        // Add a private function (not an entry point)
313        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        // Should have 3 entry points: main, test, and public
330        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        // Add main function
343        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        // Add helper function (called by main)
357        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        // Add dead function (not called)
371        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        // Add call edge: main -> helper
385        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        // main is entry point
396        assert!(report.entry_points.contains(&main_scoped));
397
398        // helper is reachable
399        assert!(!report.dead_symbols.contains(&helper_scoped));
400
401        // dead is not reachable
402        assert!(report.dead_symbols.contains(&dead_scoped));
403    }
404}