ryo_analysis/query/
unused_symbol.rs1use super::{CodeGraphV2, TypeFlowGraphV2};
17use crate::symbol::SymbolRegistry;
18use crate::SymbolId;
19
20#[derive(Debug, Clone)]
22pub struct UnusedSymbolResult {
23 pub unused_symbols: Vec<UnusedSymbol>,
25
26 pub would_become_unused: Vec<UnusedSymbol>,
28}
29
30impl UnusedSymbolResult {
31 pub fn has_unused(&self) -> bool {
33 !self.unused_symbols.is_empty()
34 }
35
36 pub fn has_potential_unused(&self) -> bool {
38 !self.would_become_unused.is_empty()
39 }
40
41 pub fn issue_count(&self) -> usize {
43 self.unused_symbols.len() + self.would_become_unused.len()
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct UnusedSymbol {
50 pub symbol_id: SymbolId,
52
53 pub reason: String,
55
56 pub usage_count: usize,
58}
59
60pub struct UnusedSymbolChecker<'a> {
81 code_graph: &'a CodeGraphV2,
82 typeflow: &'a TypeFlowGraphV2,
83 registry: &'a SymbolRegistry,
84}
85
86impl<'a> UnusedSymbolChecker<'a> {
87 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 pub fn is_unused(&self, symbol_id: SymbolId) -> bool {
102 self.get_usage_count(symbol_id) == 0
103 }
104
105 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 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 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 pub fn would_become_unused_if_deleted(&self, to_delete: SymbolId) -> UnusedSymbolResult {
155 let mut would_become_unused = Vec::new();
156
157 let used_by_deleted: Vec<SymbolId> = self.typeflow.types_used_by(to_delete).collect();
162
163 for used_id in used_by_deleted {
164 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 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 for &symbol_id in affected_symbols {
210 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 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 pub fn code_graph(&self) -> &CodeGraphV2 {
234 self.code_graph
235 }
236
237 pub fn typeflow(&self) -> &TypeFlowGraphV2 {
239 self.typeflow
240 }
241}
242
243#[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 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, ®istry);
278
279 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, ®istry);
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 assert_eq!(result.unused_symbols.len(), 2);
298 assert!(result.has_unused());
299 }
300}