Skip to main content

ryo_analysis/query/
type_impact.rs

1//! Type Impact Checker - TypeFlowGraphV2-based type change impact analysis.
2//!
3//! Analyzes the impact of type changes across the codebase using TypeFlowGraphV2.
4//! This enables pre-mutation validation without running `cargo check`.
5//!
6//! # Capabilities
7//!
8//! - Detect all usages of a changed type
9//! - Identify affected trait bounds
10//! - Find containing types (types with fields of the changed type)
11//! - Check compatibility of type changes
12//!
13//! # Performance
14//!
15//! Target: < 10ms per symbol impact analysis.
16
17use super::{TypeFlowGraphV2, TypeImpactV2, TypeNodeId, UsageContext};
18use crate::SymbolId;
19
20/// Result of type impact analysis.
21#[derive(Debug, Clone)]
22pub struct TypeImpactResult {
23    /// The symbol that was changed.
24    pub changed_symbol: SymbolId,
25
26    /// Number of direct usages affected.
27    pub direct_usage_count: usize,
28
29    /// Number of trait bounds affected.
30    pub bound_usage_count: usize,
31
32    /// Types that contain this type as a field.
33    pub containing_types: Vec<SymbolId>,
34
35    /// Specific issues found during impact analysis.
36    pub issues: Vec<TypeImpactIssue>,
37}
38
39impl TypeImpactResult {
40    /// Check if there are any issues.
41    pub fn has_issues(&self) -> bool {
42        !self.issues.is_empty()
43    }
44
45    /// Check if this change has any impact.
46    pub fn has_impact(&self) -> bool {
47        self.direct_usage_count > 0
48            || self.bound_usage_count > 0
49            || !self.containing_types.is_empty()
50    }
51}
52
53/// Specific issue found during type impact analysis.
54#[derive(Debug, Clone)]
55pub enum TypeImpactIssue {
56    /// Type is used as a field in another type - struct literal may need update.
57    FieldUsageInType {
58        /// SymbolId of the containing type that owns the impacted field.
59        container_type: SymbolId,
60    },
61
62    /// Type is used in trait bound - may break trait implementation.
63    TraitBoundUsage {
64        /// Number of trait-bound sites referencing the type.
65        bound_count: usize,
66    },
67
68    /// Type is used in function parameter - callers may need update.
69    ParameterUsage {
70        /// Number of parameter-position usages.
71        usage_count: usize,
72    },
73
74    /// Type is used in return position - callers may need update.
75    ReturnTypeUsage {
76        /// Number of return-position usages.
77        usage_count: usize,
78    },
79
80    /// Type is used in impl block - impl may need update.
81    ImplUsage {
82        /// Number of impl blocks referencing the type.
83        usage_count: usize,
84    },
85}
86
87/// Type Impact Checker using TypeFlowGraphV2.
88///
89/// Provides fast analysis of type change impacts without running the compiler.
90///
91/// # Example
92///
93/// ```rust,ignore
94/// let checker = TypeImpactChecker::new(&typeflow, &graph_checker);
95///
96/// // Analyze impact of changing a type
97/// let result = checker.analyze_impact(changed_symbol_id);
98///
99/// if result.has_issues() {
100///     for issue in &result.issues {
101///         println!("Impact: {:?}", issue);
102///     }
103/// }
104/// ```
105pub struct TypeImpactChecker<'a> {
106    typeflow: &'a TypeFlowGraphV2,
107}
108
109impl<'a> TypeImpactChecker<'a> {
110    /// Create a new TypeImpactChecker.
111    pub fn new(typeflow: &'a TypeFlowGraphV2) -> Self {
112        Self { typeflow }
113    }
114
115    /// Analyze the impact of changing a type.
116    ///
117    /// Returns detailed information about all usages and potential issues.
118    pub fn analyze_impact(&self, symbol_id: SymbolId) -> TypeImpactResult {
119        let impact = self.typeflow.impact(symbol_id);
120        let mut issues = Vec::new();
121
122        // Analyze direct usages by context
123        let (param_count, return_count, impl_count) = self.categorize_usages(&impact);
124
125        if param_count > 0 {
126            issues.push(TypeImpactIssue::ParameterUsage {
127                usage_count: param_count,
128            });
129        }
130
131        if return_count > 0 {
132            issues.push(TypeImpactIssue::ReturnTypeUsage {
133                usage_count: return_count,
134            });
135        }
136
137        if impl_count > 0 {
138            issues.push(TypeImpactIssue::ImplUsage {
139                usage_count: impl_count,
140            });
141        }
142
143        // Analyze trait bound usages
144        if !impact.bound_usages.is_empty() {
145            issues.push(TypeImpactIssue::TraitBoundUsage {
146                bound_count: impact.bound_usages.len(),
147            });
148        }
149
150        // Analyze containing types
151        for container in &impact.containing_types {
152            issues.push(TypeImpactIssue::FieldUsageInType {
153                container_type: *container,
154            });
155        }
156
157        TypeImpactResult {
158            changed_symbol: symbol_id,
159            direct_usage_count: impact.direct_usages.len(),
160            bound_usage_count: impact.bound_usages.len(),
161            containing_types: impact.containing_types,
162            issues,
163        }
164    }
165
166    /// Get all symbols that would be affected by changing the given type.
167    ///
168    /// This includes:
169    /// - Types that use this type as a field
170    /// - Types that have trait bounds involving this type
171    /// - Functions with parameters/returns of this type
172    pub fn affected_symbols(&self, symbol_id: SymbolId) -> Vec<SymbolId> {
173        let impact = self.typeflow.impact(symbol_id);
174        let mut affected = Vec::new();
175
176        // Add containing types
177        affected.extend(impact.containing_types.iter().copied());
178
179        // Add resolved symbols from direct usages
180        for usage_idx in &impact.direct_usages {
181            if let Some(usage) = self.typeflow.get_usage(TypeNodeId::usage(*usage_idx)) {
182                if let Some(resolved) = usage.resolved {
183                    affected.push(resolved);
184                }
185            }
186        }
187
188        affected.sort();
189        affected.dedup();
190        affected
191    }
192
193    /// Categorize usages by their context.
194    ///
195    /// Returns (parameter_count, return_count, impl_count).
196    fn categorize_usages(&self, impact: &TypeImpactV2) -> (usize, usize, usize) {
197        let mut param_count = 0;
198        let mut return_count = 0;
199        let mut impl_count = 0;
200
201        for usage_idx in &impact.direct_usages {
202            if let Some(usage) = self.typeflow.get_usage(TypeNodeId::usage(*usage_idx)) {
203                match usage.context {
204                    UsageContext::ParamType => param_count += 1,
205                    UsageContext::ReturnType => return_count += 1,
206                    UsageContext::ImplTarget | UsageContext::ImplTrait => impl_count += 1,
207                    _ => {}
208                }
209            }
210        }
211
212        (param_count, return_count, impl_count)
213    }
214}
215
216// ============================================================================
217// Tests
218// ============================================================================
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::symbol::{SymbolPath, SymbolRegistry};
224    use crate::SymbolKind;
225    use crate::TypeDefKind;
226
227    fn create_test_setup() -> (SymbolRegistry, TypeFlowGraphV2, SymbolId, SymbolId) {
228        let mut registry = SymbolRegistry::new();
229        let foo_id = registry
230            .register(SymbolPath::parse("test::Foo").unwrap(), SymbolKind::Struct)
231            .unwrap();
232        let bar_id = registry
233            .register(SymbolPath::parse("test::Bar").unwrap(), SymbolKind::Struct)
234            .unwrap();
235
236        let mut typeflow = TypeFlowGraphV2::new();
237
238        // Add Foo as a definition
239        typeflow.add_definition(foo_id, TypeDefKind::Struct);
240
241        // Add Bar as a definition
242        typeflow.add_definition(bar_id, TypeDefKind::Struct);
243
244        // Bar contains Foo as a field
245        typeflow.add_contains(bar_id, foo_id);
246
247        (registry, typeflow, foo_id, bar_id)
248    }
249
250    #[test]
251    fn test_analyze_impact_no_usages() {
252        let (_, typeflow, _, bar_id) = create_test_setup();
253
254        let checker = TypeImpactChecker::new(&typeflow);
255        let result = checker.analyze_impact(bar_id);
256
257        // Bar has no usages (nothing uses Bar as a field)
258        assert_eq!(result.direct_usage_count, 0);
259        assert!(result.containing_types.is_empty());
260    }
261
262    #[test]
263    fn test_analyze_impact_with_containing_type() {
264        let (_, typeflow, foo_id, bar_id) = create_test_setup();
265
266        let checker = TypeImpactChecker::new(&typeflow);
267        let result = checker.analyze_impact(foo_id);
268
269        // Foo is contained in Bar
270        assert!(result.containing_types.contains(&bar_id));
271        assert!(result.has_impact());
272    }
273
274    #[test]
275    fn test_affected_symbols() {
276        let (_, typeflow, foo_id, bar_id) = create_test_setup();
277
278        let checker = TypeImpactChecker::new(&typeflow);
279        let affected = checker.affected_symbols(foo_id);
280
281        // Bar is affected because it contains Foo
282        assert!(affected.contains(&bar_id));
283    }
284}