ryo-analysis 0.1.0

Code graph and discovery engine for the RYO project
Documentation
//! Unused Symbol Checker - Detects symbols that become unused after mutations.
//!
//! Analyzes symbol usage patterns to identify dead code candidates
//! using CodeGraphV2 and TypeFlowGraphV2.
//!
//! # Capabilities
//!
//! - Detect symbols with zero usages
//! - Identify symbols that would become unused after deletions
//! - Track usage count changes for impact assessment
//!
//! # Performance
//!
//! Target: < 5ms per symbol check.

use super::{CodeGraphV2, TypeFlowGraphV2};
use crate::symbol::SymbolRegistry;
use crate::SymbolId;

/// Result of unused symbol analysis.
#[derive(Debug, Clone)]
pub struct UnusedSymbolResult {
    /// Symbols that are currently unused (zero references).
    pub unused_symbols: Vec<UnusedSymbol>,

    /// Symbols that would become unused if a deletion occurred.
    pub would_become_unused: Vec<UnusedSymbol>,
}

impl UnusedSymbolResult {
    /// Check if there are any unused symbols.
    pub fn has_unused(&self) -> bool {
        !self.unused_symbols.is_empty()
    }

    /// Check if there are symbols that would become unused.
    pub fn has_potential_unused(&self) -> bool {
        !self.would_become_unused.is_empty()
    }

    /// Get total count of issues.
    pub fn issue_count(&self) -> usize {
        self.unused_symbols.len() + self.would_become_unused.len()
    }
}

/// Information about an unused symbol.
#[derive(Debug, Clone)]
pub struct UnusedSymbol {
    /// The unused symbol.
    pub symbol_id: SymbolId,

    /// Reason why it's considered unused.
    pub reason: String,

    /// Number of remaining usages (0 for fully unused).
    pub usage_count: usize,
}

/// Unused Symbol Checker using CodeGraphV2 and TypeFlowGraphV2.
///
/// Analyzes symbol usage patterns to detect dead code candidates.
///
/// # Example
///
/// ```rust,ignore
/// let checker = UnusedSymbolChecker::new(&code_graph, &typeflow, &registry);
///
/// // Check if a symbol is unused
/// if checker.is_unused(symbol_id) {
///     println!("Symbol has no references");
/// }
///
/// // Find all unused symbols in a scope
/// let result = checker.find_unused_in_scope(module_id);
/// for unused in result.unused_symbols {
///     println!("Unused: {:?}", unused.symbol_id);
/// }
/// ```
pub struct UnusedSymbolChecker<'a> {
    code_graph: &'a CodeGraphV2,
    typeflow: &'a TypeFlowGraphV2,
    registry: &'a SymbolRegistry,
}

impl<'a> UnusedSymbolChecker<'a> {
    /// Create a new UnusedSymbolChecker.
    pub fn new(
        code_graph: &'a CodeGraphV2,
        typeflow: &'a TypeFlowGraphV2,
        registry: &'a SymbolRegistry,
    ) -> Self {
        Self {
            code_graph,
            typeflow,
            registry,
        }
    }

    /// Check if a symbol has no usages.
    pub fn is_unused(&self, symbol_id: SymbolId) -> bool {
        self.get_usage_count(symbol_id) == 0
    }

    /// Get the total reference count for a symbol.
    ///
    /// Combines call references (CodeGraphV2) and type references (TypeFlowGraphV2).
    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
    }

    /// Check a single symbol for unused status.
    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
        }
    }

    /// Find all unused symbols among the given candidates.
    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(),
        }
    }

    /// Check which symbols would become unused if a symbol is deleted.
    ///
    /// This is useful for cascade analysis during deletions.
    pub fn would_become_unused_if_deleted(&self, to_delete: SymbolId) -> UnusedSymbolResult {
        let mut would_become_unused = Vec::new();

        // Find symbols that are only used by the symbol being deleted
        // These are symbols where the only user is `to_delete`

        // Get all types that `to_delete` uses (via TypeFlow)
        let used_by_deleted: Vec<SymbolId> = self.typeflow.types_used_by(to_delete).collect();

        for used_id in used_by_deleted {
            // Count type users excluding the symbol being 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,
        }
    }

    /// Analyze symbols affected by a mutation for unused status.
    ///
    /// Use this after mutations to detect newly unused symbols.
    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();

        // Check affected symbols for unused status
        for &symbol_id in affected_symbols {
            // Skip deleted symbols
            if deleted_symbols.contains(&symbol_id) {
                continue;
            }

            if let Some(unused) = self.check_symbol(symbol_id) {
                unused_symbols.push(unused);
            }
        }

        // Check what would become unused due to deletions
        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,
        }
    }

    /// Get a reference to the underlying CodeGraphV2.
    pub fn code_graph(&self) -> &CodeGraphV2 {
        self.code_graph
    }

    /// Get a reference to the underlying TypeFlowGraphV2.
    pub fn typeflow(&self) -> &TypeFlowGraphV2 {
        self.typeflow
    }
}

// ============================================================================
// Tests
// ============================================================================

#[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();

        // Create some symbols
        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, &registry);

        // Get Foo symbol - should be unused since we didn't add any edges
        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, &registry);

        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]);

        // Both should be unused
        assert_eq!(result.unused_symbols.len(), 2);
        assert!(result.has_unused());
    }
}