use super::{CodeGraphV2, TypeFlowGraphV2};
use crate::symbol::SymbolRegistry;
use crate::SymbolId;
#[derive(Debug, Clone)]
pub struct UnusedSymbolResult {
pub unused_symbols: Vec<UnusedSymbol>,
pub would_become_unused: Vec<UnusedSymbol>,
}
impl UnusedSymbolResult {
pub fn has_unused(&self) -> bool {
!self.unused_symbols.is_empty()
}
pub fn has_potential_unused(&self) -> bool {
!self.would_become_unused.is_empty()
}
pub fn issue_count(&self) -> usize {
self.unused_symbols.len() + self.would_become_unused.len()
}
}
#[derive(Debug, Clone)]
pub struct UnusedSymbol {
pub symbol_id: SymbolId,
pub reason: String,
pub usage_count: usize,
}
pub struct UnusedSymbolChecker<'a> {
code_graph: &'a CodeGraphV2,
typeflow: &'a TypeFlowGraphV2,
registry: &'a SymbolRegistry,
}
impl<'a> UnusedSymbolChecker<'a> {
pub fn new(
code_graph: &'a CodeGraphV2,
typeflow: &'a TypeFlowGraphV2,
registry: &'a SymbolRegistry,
) -> Self {
Self {
code_graph,
typeflow,
registry,
}
}
pub fn is_unused(&self, symbol_id: SymbolId) -> bool {
self.get_usage_count(symbol_id) == 0
}
pub fn get_usage_count(&self, symbol_id: SymbolId) -> usize {
let call_refs = self.code_graph.reference_count(symbol_id);
let type_refs = self.typeflow.usage_count(symbol_id);
call_refs + type_refs
}
pub fn check_symbol(&self, symbol_id: SymbolId) -> Option<UnusedSymbol> {
let usage_count = self.get_usage_count(symbol_id);
if usage_count == 0 {
let name = self
.registry
.path(symbol_id)
.map(|p| p.name())
.unwrap_or("unknown");
Some(UnusedSymbol {
symbol_id,
reason: format!("Symbol '{}' has no references", name),
usage_count: 0,
})
} else {
None
}
}
pub fn find_unused(&self, candidates: &[SymbolId]) -> UnusedSymbolResult {
let mut unused_symbols = Vec::new();
for &symbol_id in candidates {
if let Some(unused) = self.check_symbol(symbol_id) {
unused_symbols.push(unused);
}
}
UnusedSymbolResult {
unused_symbols,
would_become_unused: Vec::new(),
}
}
pub fn would_become_unused_if_deleted(&self, to_delete: SymbolId) -> UnusedSymbolResult {
let mut would_become_unused = Vec::new();
let used_by_deleted: Vec<SymbolId> = self.typeflow.types_used_by(to_delete).collect();
for used_id in used_by_deleted {
let remaining_users = self
.typeflow
.type_users(used_id)
.filter(|&user| user != to_delete)
.count();
let total_remaining = remaining_users;
if total_remaining == 0 {
let name = self
.registry
.path(used_id)
.map(|p| p.name())
.unwrap_or("unknown");
would_become_unused.push(UnusedSymbol {
symbol_id: used_id,
reason: format!(
"Symbol '{}' would have no remaining references after deletion",
name
),
usage_count: 0,
});
}
}
UnusedSymbolResult {
unused_symbols: Vec::new(),
would_become_unused,
}
}
pub fn analyze_after_mutation(
&self,
affected_symbols: &[SymbolId],
deleted_symbols: &[SymbolId],
) -> UnusedSymbolResult {
let mut unused_symbols = Vec::new();
let mut would_become_unused = Vec::new();
for &symbol_id in affected_symbols {
if deleted_symbols.contains(&symbol_id) {
continue;
}
if let Some(unused) = self.check_symbol(symbol_id) {
unused_symbols.push(unused);
}
}
for &deleted_id in deleted_symbols {
let result = self.would_become_unused_if_deleted(deleted_id);
would_become_unused.extend(result.would_become_unused);
}
UnusedSymbolResult {
unused_symbols,
would_become_unused,
}
}
pub fn code_graph(&self) -> &CodeGraphV2 {
self.code_graph
}
pub fn typeflow(&self) -> &TypeFlowGraphV2 {
self.typeflow
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbol::SymbolPath;
use crate::SymbolKind;
fn create_test_setup() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
let mut registry = SymbolRegistry::new();
let code_graph = CodeGraphV2::new();
let typeflow = TypeFlowGraphV2::new();
let _mod_id = registry
.register(SymbolPath::parse("test").unwrap(), SymbolKind::Mod)
.unwrap();
let _foo_id = registry
.register(SymbolPath::parse("test::Foo").unwrap(), SymbolKind::Struct)
.unwrap();
let _bar_id = registry
.register(SymbolPath::parse("test::Bar").unwrap(), SymbolKind::Struct)
.unwrap();
(code_graph, typeflow, registry)
}
#[test]
fn test_is_unused_with_no_references() {
let (code_graph, typeflow, registry) = create_test_setup();
let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, ®istry);
let foo_id = registry.lookup_by_name("Foo").unwrap();
assert!(checker.is_unused(foo_id));
assert_eq!(checker.get_usage_count(foo_id), 0);
}
#[test]
fn test_find_unused_symbols() {
let (code_graph, typeflow, registry) = create_test_setup();
let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, ®istry);
let foo_id = registry.lookup_by_name("Foo").unwrap();
let bar_id = registry.lookup_by_name("Bar").unwrap();
let result = checker.find_unused(&[foo_id, bar_id]);
assert_eq!(result.unused_symbols.len(), 2);
assert!(result.has_unused());
}
}