ryo-analysis 0.1.0

Code graph and discovery engine for the RYO project
Documentation
//! Type Impact Checker - TypeFlowGraphV2-based type change impact analysis.
//!
//! Analyzes the impact of type changes across the codebase using TypeFlowGraphV2.
//! This enables pre-mutation validation without running `cargo check`.
//!
//! # Capabilities
//!
//! - Detect all usages of a changed type
//! - Identify affected trait bounds
//! - Find containing types (types with fields of the changed type)
//! - Check compatibility of type changes
//!
//! # Performance
//!
//! Target: < 10ms per symbol impact analysis.

use super::{TypeFlowGraphV2, TypeImpactV2, TypeNodeId, UsageContext};
use crate::SymbolId;

/// Result of type impact analysis.
#[derive(Debug, Clone)]
pub struct TypeImpactResult {
    /// The symbol that was changed.
    pub changed_symbol: SymbolId,

    /// Number of direct usages affected.
    pub direct_usage_count: usize,

    /// Number of trait bounds affected.
    pub bound_usage_count: usize,

    /// Types that contain this type as a field.
    pub containing_types: Vec<SymbolId>,

    /// Specific issues found during impact analysis.
    pub issues: Vec<TypeImpactIssue>,
}

impl TypeImpactResult {
    /// Check if there are any issues.
    pub fn has_issues(&self) -> bool {
        !self.issues.is_empty()
    }

    /// Check if this change has any impact.
    pub fn has_impact(&self) -> bool {
        self.direct_usage_count > 0
            || self.bound_usage_count > 0
            || !self.containing_types.is_empty()
    }
}

/// Specific issue found during type impact analysis.
#[derive(Debug, Clone)]
pub enum TypeImpactIssue {
    /// Type is used as a field in another type - struct literal may need update.
    FieldUsageInType {
        /// SymbolId of the containing type that owns the impacted field.
        container_type: SymbolId,
    },

    /// Type is used in trait bound - may break trait implementation.
    TraitBoundUsage {
        /// Number of trait-bound sites referencing the type.
        bound_count: usize,
    },

    /// Type is used in function parameter - callers may need update.
    ParameterUsage {
        /// Number of parameter-position usages.
        usage_count: usize,
    },

    /// Type is used in return position - callers may need update.
    ReturnTypeUsage {
        /// Number of return-position usages.
        usage_count: usize,
    },

    /// Type is used in impl block - impl may need update.
    ImplUsage {
        /// Number of impl blocks referencing the type.
        usage_count: usize,
    },
}

/// Type Impact Checker using TypeFlowGraphV2.
///
/// Provides fast analysis of type change impacts without running the compiler.
///
/// # Example
///
/// ```rust,ignore
/// let checker = TypeImpactChecker::new(&typeflow, &graph_checker);
///
/// // Analyze impact of changing a type
/// let result = checker.analyze_impact(changed_symbol_id);
///
/// if result.has_issues() {
///     for issue in &result.issues {
///         println!("Impact: {:?}", issue);
///     }
/// }
/// ```
pub struct TypeImpactChecker<'a> {
    typeflow: &'a TypeFlowGraphV2,
}

impl<'a> TypeImpactChecker<'a> {
    /// Create a new TypeImpactChecker.
    pub fn new(typeflow: &'a TypeFlowGraphV2) -> Self {
        Self { typeflow }
    }

    /// Analyze the impact of changing a type.
    ///
    /// Returns detailed information about all usages and potential issues.
    pub fn analyze_impact(&self, symbol_id: SymbolId) -> TypeImpactResult {
        let impact = self.typeflow.impact(symbol_id);
        let mut issues = Vec::new();

        // Analyze direct usages by context
        let (param_count, return_count, impl_count) = self.categorize_usages(&impact);

        if param_count > 0 {
            issues.push(TypeImpactIssue::ParameterUsage {
                usage_count: param_count,
            });
        }

        if return_count > 0 {
            issues.push(TypeImpactIssue::ReturnTypeUsage {
                usage_count: return_count,
            });
        }

        if impl_count > 0 {
            issues.push(TypeImpactIssue::ImplUsage {
                usage_count: impl_count,
            });
        }

        // Analyze trait bound usages
        if !impact.bound_usages.is_empty() {
            issues.push(TypeImpactIssue::TraitBoundUsage {
                bound_count: impact.bound_usages.len(),
            });
        }

        // Analyze containing types
        for container in &impact.containing_types {
            issues.push(TypeImpactIssue::FieldUsageInType {
                container_type: *container,
            });
        }

        TypeImpactResult {
            changed_symbol: symbol_id,
            direct_usage_count: impact.direct_usages.len(),
            bound_usage_count: impact.bound_usages.len(),
            containing_types: impact.containing_types,
            issues,
        }
    }

    /// Get all symbols that would be affected by changing the given type.
    ///
    /// This includes:
    /// - Types that use this type as a field
    /// - Types that have trait bounds involving this type
    /// - Functions with parameters/returns of this type
    pub fn affected_symbols(&self, symbol_id: SymbolId) -> Vec<SymbolId> {
        let impact = self.typeflow.impact(symbol_id);
        let mut affected = Vec::new();

        // Add containing types
        affected.extend(impact.containing_types.iter().copied());

        // Add resolved symbols from direct usages
        for usage_idx in &impact.direct_usages {
            if let Some(usage) = self.typeflow.get_usage(TypeNodeId::usage(*usage_idx)) {
                if let Some(resolved) = usage.resolved {
                    affected.push(resolved);
                }
            }
        }

        affected.sort();
        affected.dedup();
        affected
    }

    /// Categorize usages by their context.
    ///
    /// Returns (parameter_count, return_count, impl_count).
    fn categorize_usages(&self, impact: &TypeImpactV2) -> (usize, usize, usize) {
        let mut param_count = 0;
        let mut return_count = 0;
        let mut impl_count = 0;

        for usage_idx in &impact.direct_usages {
            if let Some(usage) = self.typeflow.get_usage(TypeNodeId::usage(*usage_idx)) {
                match usage.context {
                    UsageContext::ParamType => param_count += 1,
                    UsageContext::ReturnType => return_count += 1,
                    UsageContext::ImplTarget | UsageContext::ImplTrait => impl_count += 1,
                    _ => {}
                }
            }
        }

        (param_count, return_count, impl_count)
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::symbol::{SymbolPath, SymbolRegistry};
    use crate::SymbolKind;
    use crate::TypeDefKind;

    fn create_test_setup() -> (SymbolRegistry, TypeFlowGraphV2, SymbolId, SymbolId) {
        let mut registry = SymbolRegistry::new();
        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();

        let mut typeflow = TypeFlowGraphV2::new();

        // Add Foo as a definition
        typeflow.add_definition(foo_id, TypeDefKind::Struct);

        // Add Bar as a definition
        typeflow.add_definition(bar_id, TypeDefKind::Struct);

        // Bar contains Foo as a field
        typeflow.add_contains(bar_id, foo_id);

        (registry, typeflow, foo_id, bar_id)
    }

    #[test]
    fn test_analyze_impact_no_usages() {
        let (_, typeflow, _, bar_id) = create_test_setup();

        let checker = TypeImpactChecker::new(&typeflow);
        let result = checker.analyze_impact(bar_id);

        // Bar has no usages (nothing uses Bar as a field)
        assert_eq!(result.direct_usage_count, 0);
        assert!(result.containing_types.is_empty());
    }

    #[test]
    fn test_analyze_impact_with_containing_type() {
        let (_, typeflow, foo_id, bar_id) = create_test_setup();

        let checker = TypeImpactChecker::new(&typeflow);
        let result = checker.analyze_impact(foo_id);

        // Foo is contained in Bar
        assert!(result.containing_types.contains(&bar_id));
        assert!(result.has_impact());
    }

    #[test]
    fn test_affected_symbols() {
        let (_, typeflow, foo_id, bar_id) = create_test_setup();

        let checker = TypeImpactChecker::new(&typeflow);
        let affected = checker.affected_symbols(foo_id);

        // Bar is affected because it contains Foo
        assert!(affected.contains(&bar_id));
    }
}