Skip to main content

ryo_analysis/query/
reference_integrity.rs

1//! Reference Integrity Checker - Validates reference consistency after mutations.
2//!
3//! Checks that no dangling references exist after symbol deletion, renaming,
4//! or structural changes (field additions/removals).
5//!
6//! # Capabilities
7//!
8//! - Detect references to deleted/renamed symbols
9//! - Find struct literals with missing field assignments
10//! - Check call site compatibility after method signature changes
11//!
12//! # Performance
13//!
14//! Target: < 10ms per mutation check.
15
16use super::{CodeGraphV2, TypeFlowGraphV2};
17use crate::symbol::SymbolRegistry;
18use crate::SymbolId;
19use crate::SymbolKind;
20
21/// Result of reference integrity analysis.
22#[derive(Debug, Clone)]
23pub struct ReferenceIntegrityResult {
24    /// The symbol being analyzed.
25    pub target_symbol: SymbolId,
26
27    /// Issues found during analysis.
28    pub issues: Vec<ReferenceIntegrityIssue>,
29}
30
31impl ReferenceIntegrityResult {
32    /// Check if there are any issues.
33    pub fn has_issues(&self) -> bool {
34        !self.issues.is_empty()
35    }
36
37    /// Get the count of errors (not warnings).
38    pub fn error_count(&self) -> usize {
39        self.issues.iter().filter(|i| i.is_error()).count()
40    }
41
42    /// Get the count of warnings.
43    pub fn warning_count(&self) -> usize {
44        self.issues.iter().filter(|i| !i.is_error()).count()
45    }
46}
47
48/// Specific issue found during reference integrity analysis.
49#[derive(Debug, Clone)]
50pub enum ReferenceIntegrityIssue {
51    /// Reference to a symbol that will be deleted.
52    DanglingReference {
53        /// The symbol with the dangling reference.
54        referrer: SymbolId,
55        /// The deleted symbol being referenced.
56        deleted_symbol: SymbolId,
57        /// Number of references found.
58        reference_count: usize,
59    },
60
61    /// Struct literal missing required fields after field addition.
62    MissingFieldInLiteral {
63        /// Location of the struct literal (containing function/method).
64        location: SymbolId,
65        /// The struct type.
66        struct_type: SymbolId,
67        /// The added field that's missing.
68        missing_field: String,
69    },
70
71    /// Struct literal has field that will be deleted.
72    RemovedFieldInLiteral {
73        /// Location of the struct literal (containing function/method).
74        location: SymbolId,
75        /// The struct type.
76        struct_type: SymbolId,
77        /// The field being removed.
78        removed_field: String,
79    },
80
81    /// Method call with incompatible arguments after signature change.
82    IncompatibleMethodCall {
83        /// The caller with incompatible call.
84        caller: SymbolId,
85        /// The method being called.
86        method: SymbolId,
87        /// Expected argument count.
88        expected_args: usize,
89        /// Actual argument count at call site.
90        actual_args: usize,
91    },
92
93    /// Symbol rename would break references.
94    RenameWouldBreakReferences {
95        /// The symbol being renamed.
96        symbol: SymbolId,
97        /// Referrers that would break.
98        referrers: Vec<SymbolId>,
99    },
100
101    /// Unused symbol after mutation (warning).
102    UnusedAfterMutation {
103        /// The symbol that became unused.
104        symbol: SymbolId,
105    },
106}
107
108impl ReferenceIntegrityIssue {
109    /// Check if this issue is an error (vs warning).
110    pub fn is_error(&self) -> bool {
111        !matches!(self, ReferenceIntegrityIssue::UnusedAfterMutation { .. })
112    }
113}
114
115/// Reference Integrity Checker using CodeGraphV2.
116///
117/// Validates that mutations don't leave dangling references or break
118/// existing code that depends on the changed symbols.
119///
120/// # Example
121///
122/// ```rust,ignore
123/// let checker = ReferenceIntegrityChecker::new(&code_graph, &registry);
124///
125/// // Check impact of deleting a symbol
126/// let result = checker.check_deletion_impact(symbol_to_delete);
127/// if result.has_issues() {
128///     for issue in &result.issues {
129///         println!("Issue: {:?}", issue);
130///     }
131/// }
132/// ```
133pub struct ReferenceIntegrityChecker<'a> {
134    graph: &'a CodeGraphV2,
135    typeflow: &'a TypeFlowGraphV2,
136    registry: &'a SymbolRegistry,
137}
138
139impl<'a> ReferenceIntegrityChecker<'a> {
140    /// Create a new ReferenceIntegrityChecker.
141    pub fn new(
142        graph: &'a CodeGraphV2,
143        typeflow: &'a TypeFlowGraphV2,
144        registry: &'a SymbolRegistry,
145    ) -> Self {
146        Self {
147            graph,
148            typeflow,
149            registry,
150        }
151    }
152
153    /// Check the impact of deleting a symbol.
154    ///
155    /// Returns all references that would become dangling.
156    pub fn check_deletion_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
157        let mut issues = Vec::new();
158
159        // Find all symbols that reference the to-be-deleted symbol
160        let referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
161        let callers: Vec<SymbolId> = self.graph.callers_of(symbol_id).collect();
162
163        // Combine all references
164        let mut all_referrers: Vec<SymbolId> = referrers.clone();
165        all_referrers.extend(callers.iter().copied());
166        all_referrers.sort();
167        all_referrers.dedup();
168
169        if !all_referrers.is_empty() {
170            issues.push(ReferenceIntegrityIssue::DanglingReference {
171                referrer: all_referrers[0], // Representative referrer
172                deleted_symbol: symbol_id,
173                reference_count: all_referrers.len(),
174            });
175        }
176
177        // Check if this symbol has children (struct fields, enum variants, methods)
178        // If so, deleting the parent affects all children
179        let children: Vec<SymbolId> = self.graph.children_of(symbol_id).collect();
180        for child_id in children {
181            let child_result = self.check_deletion_impact(child_id);
182            issues.extend(child_result.issues);
183        }
184
185        ReferenceIntegrityResult {
186            target_symbol: symbol_id,
187            issues,
188        }
189    }
190
191    /// Check the impact of renaming a symbol.
192    ///
193    /// Returns all locations that would need to be updated.
194    pub fn check_rename_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
195        let mut issues = Vec::new();
196
197        // Find all referrers (users + callers)
198        let mut referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
199        referrers.extend(self.graph.callers_of(symbol_id));
200        referrers.sort();
201        referrers.dedup();
202
203        if !referrers.is_empty() {
204            issues.push(ReferenceIntegrityIssue::RenameWouldBreakReferences {
205                symbol: symbol_id,
206                referrers: referrers.clone(),
207            });
208        }
209
210        ReferenceIntegrityResult {
211            target_symbol: symbol_id,
212            issues,
213        }
214    }
215
216    /// Check if adding a field would affect struct literals.
217    ///
218    /// This checks all callers that might construct the struct.
219    pub fn check_field_addition_impact(
220        &self,
221        struct_id: SymbolId,
222        field_name: &str,
223    ) -> ReferenceIntegrityResult {
224        let mut issues = Vec::new();
225
226        // Find all functions that use (construct) this struct
227        let users: Vec<SymbolId> = self.typeflow.type_users(struct_id).collect();
228
229        for user_id in users {
230            // Check if the user is a function that might construct the struct
231            if let Some(kind) = self.registry.kind(user_id) {
232                if matches!(kind, SymbolKind::Function | SymbolKind::Method) {
233                    issues.push(ReferenceIntegrityIssue::MissingFieldInLiteral {
234                        location: user_id,
235                        struct_type: struct_id,
236                        missing_field: field_name.to_string(),
237                    });
238                }
239            }
240        }
241
242        ReferenceIntegrityResult {
243            target_symbol: struct_id,
244            issues,
245        }
246    }
247
248    /// Check if removing a field would affect struct literals.
249    pub fn check_field_removal_impact(
250        &self,
251        struct_id: SymbolId,
252        field_name: &str,
253    ) -> ReferenceIntegrityResult {
254        let mut issues = Vec::new();
255
256        // Find all functions that use (construct) this struct
257        let users: Vec<SymbolId> = self.typeflow.type_users(struct_id).collect();
258
259        for user_id in users {
260            if let Some(kind) = self.registry.kind(user_id) {
261                if matches!(kind, SymbolKind::Function | SymbolKind::Method) {
262                    issues.push(ReferenceIntegrityIssue::RemovedFieldInLiteral {
263                        location: user_id,
264                        struct_type: struct_id,
265                        removed_field: field_name.to_string(),
266                    });
267                }
268            }
269        }
270
271        ReferenceIntegrityResult {
272            target_symbol: struct_id,
273            issues,
274        }
275    }
276
277    /// Check if a method signature change would break call sites.
278    ///
279    /// Compares expected argument count against actual call sites.
280    pub fn check_method_signature_change(
281        &self,
282        method_id: SymbolId,
283        new_arg_count: usize,
284    ) -> ReferenceIntegrityResult {
285        let mut issues = Vec::new();
286
287        // Get current parameter count
288        let current_param_count = self
289            .graph
290            .children_of(method_id)
291            .filter(|child_id| {
292                self.registry
293                    .kind(*child_id)
294                    .map(|k| matches!(k, SymbolKind::Parameter))
295                    .unwrap_or(false)
296            })
297            .count();
298
299        // Only check if the count actually changed
300        if current_param_count != new_arg_count {
301            // Find all callers
302            let callers: Vec<SymbolId> = self.graph.callers_of(method_id).collect();
303
304            for caller_id in callers {
305                issues.push(ReferenceIntegrityIssue::IncompatibleMethodCall {
306                    caller: caller_id,
307                    method: method_id,
308                    expected_args: new_arg_count,
309                    actual_args: current_param_count, // Assuming caller uses current count
310                });
311            }
312        }
313
314        ReferenceIntegrityResult {
315            target_symbol: method_id,
316            issues,
317        }
318    }
319
320    /// Get all referrers of a symbol (type_users + callers).
321    pub fn get_all_referrers(&self, symbol_id: SymbolId) -> Vec<SymbolId> {
322        let mut referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
323        referrers.extend(self.graph.callers_of(symbol_id));
324        referrers.sort();
325        referrers.dedup();
326        referrers
327    }
328
329    /// Check if a symbol is unused (no call references and no type references).
330    pub fn is_symbol_unused(&self, symbol_id: SymbolId) -> bool {
331        self.graph.reference_count(symbol_id) == 0
332            && self.typeflow.type_users(symbol_id).next().is_none()
333    }
334
335    /// Get total reference count for a symbol (calls + type references).
336    pub fn reference_count(&self, symbol_id: SymbolId) -> usize {
337        self.graph.reference_count(symbol_id) + self.typeflow.usage_count(symbol_id)
338    }
339}
340
341// ============================================================================
342// Tests
343// ============================================================================
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::query::{GraphBuilderV2, TypeFlowGraphV2};
349    use crate::symbol::SymbolPath;
350
351    fn create_test_setup() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
352        let mut registry = SymbolRegistry::new();
353        let mut builder = GraphBuilderV2::new(&mut registry);
354
355        // Create a struct with fields
356        let config = builder
357            .add_symbol(
358                SymbolPath::parse("test::Config").unwrap(),
359                SymbolKind::Struct,
360            )
361            .unwrap();
362        let name_field = builder
363            .add_symbol(
364                SymbolPath::parse("test::Config::name").unwrap(),
365                SymbolKind::Field,
366            )
367            .unwrap();
368        let value_field = builder
369            .add_symbol(
370                SymbolPath::parse("test::Config::value").unwrap(),
371                SymbolKind::Field,
372            )
373            .unwrap();
374        builder.add_contains(config, name_field);
375        builder.add_contains(config, value_field);
376
377        // Create a function that uses Config
378        let create_config = builder
379            .add_symbol(
380                SymbolPath::parse("test::create_config").unwrap(),
381                SymbolKind::Function,
382            )
383            .unwrap();
384        // create_config -> Config type usage registered in TypeFlow below
385
386        // Create another function that calls create_config
387        let init = builder
388            .add_symbol(
389                SymbolPath::parse("test::init").unwrap(),
390                SymbolKind::Function,
391            )
392            .unwrap();
393        builder.add_call(init, create_config);
394
395        let graph = builder.build();
396        let mut typeflow = TypeFlowGraphV2::new();
397        // Register create_config -> Config type usage in TypeFlow
398        typeflow.add_usage(
399            crate::query::UsageContext::ReturnType,
400            crate::query::RefKind::Owned,
401            Some(config),
402            Some(create_config),
403        );
404        (graph, typeflow, registry)
405    }
406
407    #[test]
408    fn test_check_deletion_impact_with_references() {
409        let (graph, typeflow, registry) = create_test_setup();
410        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
411
412        // Find create_config function
413        let create_config_id = registry.lookup_by_name("create_config").unwrap();
414
415        let result = checker.check_deletion_impact(create_config_id);
416
417        // Should have dangling reference issue (init calls create_config)
418        assert!(result.has_issues());
419        assert!(result.error_count() > 0);
420    }
421
422    #[test]
423    fn test_check_deletion_impact_no_references() {
424        let (graph, typeflow, registry) = create_test_setup();
425        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
426
427        // Find init function (no one calls it)
428        let init_id = registry.lookup_by_name("init").unwrap();
429
430        let result = checker.check_deletion_impact(init_id);
431
432        // Should have no issues (init is not referenced by anyone)
433        assert!(!result.has_issues());
434    }
435
436    #[test]
437    fn test_check_rename_impact() {
438        let (graph, typeflow, registry) = create_test_setup();
439        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
440
441        // Find Config struct
442        let config_id = registry.lookup_by_name("Config").unwrap();
443
444        let result = checker.check_rename_impact(config_id);
445
446        // Should have rename issue (create_config uses Config)
447        assert!(result.has_issues());
448    }
449
450    #[test]
451    fn test_check_field_addition_impact() {
452        let (graph, typeflow, registry) = create_test_setup();
453        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
454
455        // Find Config struct
456        let config_id = registry.lookup_by_name("Config").unwrap();
457
458        let result = checker.check_field_addition_impact(config_id, "timeout");
459
460        // Should have issue (create_config constructs Config)
461        assert!(result.has_issues());
462    }
463
464    #[test]
465    fn test_get_all_referrers() {
466        let (graph, typeflow, registry) = create_test_setup();
467        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
468
469        // Find create_config function
470        let create_config_id = registry.lookup_by_name("create_config").unwrap();
471
472        let referrers = checker.get_all_referrers(create_config_id);
473
474        // init should be in the referrers
475        let init_id = registry.lookup_by_name("init").unwrap();
476        assert!(referrers.contains(&init_id));
477    }
478
479    #[test]
480    fn test_is_symbol_unused() {
481        let (graph, typeflow, registry) = create_test_setup();
482        let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, &registry);
483
484        // init is unused (no callers)
485        let init_id = registry.lookup_by_name("init").unwrap();
486        assert!(checker.is_symbol_unused(init_id));
487
488        // create_config is used by init
489        let create_config_id = registry.lookup_by_name("create_config").unwrap();
490        assert!(!checker.is_symbol_unused(create_config_id));
491    }
492}