Skip to main content

ryo_analysis/query/
unused_symbol.rs

1//! Unused Symbol Checker - Detects symbols that become unused after mutations.
2//!
3//! Analyzes symbol usage patterns to identify dead code candidates
4//! using CodeGraphV2 and TypeFlowGraphV2.
5//!
6//! # Capabilities
7//!
8//! - Detect symbols with zero usages
9//! - Identify symbols that would become unused after deletions
10//! - Track usage count changes for impact assessment
11//!
12//! # Performance
13//!
14//! Target: < 5ms per symbol check.
15
16use super::{CodeGraphV2, TypeFlowGraphV2};
17use crate::symbol::SymbolRegistry;
18use crate::SymbolId;
19
20/// Result of unused symbol analysis.
21#[derive(Debug, Clone)]
22pub struct UnusedSymbolResult {
23    /// Symbols that are currently unused (zero references).
24    pub unused_symbols: Vec<UnusedSymbol>,
25
26    /// Symbols that would become unused if a deletion occurred.
27    pub would_become_unused: Vec<UnusedSymbol>,
28}
29
30impl UnusedSymbolResult {
31    /// Check if there are any unused symbols.
32    pub fn has_unused(&self) -> bool {
33        !self.unused_symbols.is_empty()
34    }
35
36    /// Check if there are symbols that would become unused.
37    pub fn has_potential_unused(&self) -> bool {
38        !self.would_become_unused.is_empty()
39    }
40
41    /// Get total count of issues.
42    pub fn issue_count(&self) -> usize {
43        self.unused_symbols.len() + self.would_become_unused.len()
44    }
45}
46
47/// Information about an unused symbol.
48#[derive(Debug, Clone)]
49pub struct UnusedSymbol {
50    /// The unused symbol.
51    pub symbol_id: SymbolId,
52
53    /// Reason why it's considered unused.
54    pub reason: String,
55
56    /// Number of remaining usages (0 for fully unused).
57    pub usage_count: usize,
58}
59
60/// Unused Symbol Checker using CodeGraphV2 and TypeFlowGraphV2.
61///
62/// Analyzes symbol usage patterns to detect dead code candidates.
63///
64/// # Example
65///
66/// ```rust,ignore
67/// let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, &registry);
68///
69/// // Check if a symbol is unused
70/// if checker.is_unused(symbol_id) {
71///     println!("Symbol has no references");
72/// }
73///
74/// // Find all unused symbols in a scope
75/// let result = checker.find_unused_in_scope(module_id);
76/// for unused in result.unused_symbols {
77///     println!("Unused: {:?}", unused.symbol_id);
78/// }
79/// ```
80pub struct UnusedSymbolChecker<'a> {
81    code_graph: &'a CodeGraphV2,
82    typeflow: &'a TypeFlowGraphV2,
83    registry: &'a SymbolRegistry,
84}
85
86impl<'a> UnusedSymbolChecker<'a> {
87    /// Create a new UnusedSymbolChecker.
88    pub fn new(
89        code_graph: &'a CodeGraphV2,
90        typeflow: &'a TypeFlowGraphV2,
91        registry: &'a SymbolRegistry,
92    ) -> Self {
93        Self {
94            code_graph,
95            typeflow,
96            registry,
97        }
98    }
99
100    /// Check if a symbol has no usages.
101    pub fn is_unused(&self, symbol_id: SymbolId) -> bool {
102        self.get_usage_count(symbol_id) == 0
103    }
104
105    /// Get the total reference count for a symbol.
106    ///
107    /// Combines call references (CodeGraphV2) and type references (TypeFlowGraphV2).
108    pub fn get_usage_count(&self, symbol_id: SymbolId) -> usize {
109        let call_refs = self.code_graph.reference_count(symbol_id);
110        let type_refs = self.typeflow.usage_count(symbol_id);
111        call_refs + type_refs
112    }
113
114    /// Check a single symbol for unused status.
115    pub fn check_symbol(&self, symbol_id: SymbolId) -> Option<UnusedSymbol> {
116        let usage_count = self.get_usage_count(symbol_id);
117
118        if usage_count == 0 {
119            let name = self
120                .registry
121                .path(symbol_id)
122                .map(|p| p.name())
123                .unwrap_or("unknown");
124
125            Some(UnusedSymbol {
126                symbol_id,
127                reason: format!("Symbol '{}' has no references", name),
128                usage_count: 0,
129            })
130        } else {
131            None
132        }
133    }
134
135    /// Find all unused symbols among the given candidates.
136    pub fn find_unused(&self, candidates: &[SymbolId]) -> UnusedSymbolResult {
137        let mut unused_symbols = Vec::new();
138
139        for &symbol_id in candidates {
140            if let Some(unused) = self.check_symbol(symbol_id) {
141                unused_symbols.push(unused);
142            }
143        }
144
145        UnusedSymbolResult {
146            unused_symbols,
147            would_become_unused: Vec::new(),
148        }
149    }
150
151    /// Check which symbols would become unused if a symbol is deleted.
152    ///
153    /// This is useful for cascade analysis during deletions.
154    pub fn would_become_unused_if_deleted(&self, to_delete: SymbolId) -> UnusedSymbolResult {
155        let mut would_become_unused = Vec::new();
156
157        // Find symbols that are only used by the symbol being deleted
158        // These are symbols where the only user is `to_delete`
159
160        // Get all types that `to_delete` uses (via TypeFlow)
161        let used_by_deleted: Vec<SymbolId> = self.typeflow.types_used_by(to_delete).collect();
162
163        for used_id in used_by_deleted {
164            // Count type users excluding the symbol being deleted
165            let remaining_users = self
166                .typeflow
167                .type_users(used_id)
168                .filter(|&user| user != to_delete)
169                .count();
170
171            let total_remaining = remaining_users;
172
173            if total_remaining == 0 {
174                let name = self
175                    .registry
176                    .path(used_id)
177                    .map(|p| p.name())
178                    .unwrap_or("unknown");
179
180                would_become_unused.push(UnusedSymbol {
181                    symbol_id: used_id,
182                    reason: format!(
183                        "Symbol '{}' would have no remaining references after deletion",
184                        name
185                    ),
186                    usage_count: 0,
187                });
188            }
189        }
190
191        UnusedSymbolResult {
192            unused_symbols: Vec::new(),
193            would_become_unused,
194        }
195    }
196
197    /// Analyze symbols affected by a mutation for unused status.
198    ///
199    /// Use this after mutations to detect newly unused symbols.
200    pub fn analyze_after_mutation(
201        &self,
202        affected_symbols: &[SymbolId],
203        deleted_symbols: &[SymbolId],
204    ) -> UnusedSymbolResult {
205        let mut unused_symbols = Vec::new();
206        let mut would_become_unused = Vec::new();
207
208        // Check affected symbols for unused status
209        for &symbol_id in affected_symbols {
210            // Skip deleted symbols
211            if deleted_symbols.contains(&symbol_id) {
212                continue;
213            }
214
215            if let Some(unused) = self.check_symbol(symbol_id) {
216                unused_symbols.push(unused);
217            }
218        }
219
220        // Check what would become unused due to deletions
221        for &deleted_id in deleted_symbols {
222            let result = self.would_become_unused_if_deleted(deleted_id);
223            would_become_unused.extend(result.would_become_unused);
224        }
225
226        UnusedSymbolResult {
227            unused_symbols,
228            would_become_unused,
229        }
230    }
231
232    /// Get a reference to the underlying CodeGraphV2.
233    pub fn code_graph(&self) -> &CodeGraphV2 {
234        self.code_graph
235    }
236
237    /// Get a reference to the underlying TypeFlowGraphV2.
238    pub fn typeflow(&self) -> &TypeFlowGraphV2 {
239        self.typeflow
240    }
241}
242
243// ============================================================================
244// Tests
245// ============================================================================
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::symbol::SymbolPath;
251    use crate::SymbolKind;
252
253    fn create_test_setup() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
254        let mut registry = SymbolRegistry::new();
255        let code_graph = CodeGraphV2::new();
256        let typeflow = TypeFlowGraphV2::new();
257
258        // Create some symbols
259        let _mod_id = registry
260            .register(SymbolPath::parse("test").unwrap(), SymbolKind::Mod)
261            .unwrap();
262
263        let _foo_id = registry
264            .register(SymbolPath::parse("test::Foo").unwrap(), SymbolKind::Struct)
265            .unwrap();
266
267        let _bar_id = registry
268            .register(SymbolPath::parse("test::Bar").unwrap(), SymbolKind::Struct)
269            .unwrap();
270
271        (code_graph, typeflow, registry)
272    }
273
274    #[test]
275    fn test_is_unused_with_no_references() {
276        let (code_graph, typeflow, registry) = create_test_setup();
277        let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, &registry);
278
279        // Get Foo symbol - should be unused since we didn't add any edges
280        let foo_id = registry.lookup_by_name("Foo").unwrap();
281
282        assert!(checker.is_unused(foo_id));
283        assert_eq!(checker.get_usage_count(foo_id), 0);
284    }
285
286    #[test]
287    fn test_find_unused_symbols() {
288        let (code_graph, typeflow, registry) = create_test_setup();
289        let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, &registry);
290
291        let foo_id = registry.lookup_by_name("Foo").unwrap();
292        let bar_id = registry.lookup_by_name("Bar").unwrap();
293
294        let result = checker.find_unused(&[foo_id, bar_id]);
295
296        // Both should be unused
297        assert_eq!(result.unused_symbols.len(), 2);
298        assert!(result.has_unused());
299    }
300}