1use super::{CodeGraphV2, TypeFlowGraphV2};
17use crate::symbol::SymbolRegistry;
18use crate::SymbolId;
19use crate::SymbolKind;
20
21#[derive(Debug, Clone)]
23pub struct ReferenceIntegrityResult {
24 pub target_symbol: SymbolId,
26
27 pub issues: Vec<ReferenceIntegrityIssue>,
29}
30
31impl ReferenceIntegrityResult {
32 pub fn has_issues(&self) -> bool {
34 !self.issues.is_empty()
35 }
36
37 pub fn error_count(&self) -> usize {
39 self.issues.iter().filter(|i| i.is_error()).count()
40 }
41
42 pub fn warning_count(&self) -> usize {
44 self.issues.iter().filter(|i| !i.is_error()).count()
45 }
46}
47
48#[derive(Debug, Clone)]
50pub enum ReferenceIntegrityIssue {
51 DanglingReference {
53 referrer: SymbolId,
55 deleted_symbol: SymbolId,
57 reference_count: usize,
59 },
60
61 MissingFieldInLiteral {
63 location: SymbolId,
65 struct_type: SymbolId,
67 missing_field: String,
69 },
70
71 RemovedFieldInLiteral {
73 location: SymbolId,
75 struct_type: SymbolId,
77 removed_field: String,
79 },
80
81 IncompatibleMethodCall {
83 caller: SymbolId,
85 method: SymbolId,
87 expected_args: usize,
89 actual_args: usize,
91 },
92
93 RenameWouldBreakReferences {
95 symbol: SymbolId,
97 referrers: Vec<SymbolId>,
99 },
100
101 UnusedAfterMutation {
103 symbol: SymbolId,
105 },
106}
107
108impl ReferenceIntegrityIssue {
109 pub fn is_error(&self) -> bool {
111 !matches!(self, ReferenceIntegrityIssue::UnusedAfterMutation { .. })
112 }
113}
114
115pub struct ReferenceIntegrityChecker<'a> {
134 graph: &'a CodeGraphV2,
135 typeflow: &'a TypeFlowGraphV2,
136 registry: &'a SymbolRegistry,
137}
138
139impl<'a> ReferenceIntegrityChecker<'a> {
140 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 pub fn check_deletion_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
157 let mut issues = Vec::new();
158
159 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 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], deleted_symbol: symbol_id,
173 reference_count: all_referrers.len(),
174 });
175 }
176
177 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 pub fn check_rename_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
195 let mut issues = Vec::new();
196
197 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 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 let users: Vec<SymbolId> = self.typeflow.type_users(struct_id).collect();
228
229 for user_id in users {
230 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 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 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 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 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 if current_param_count != new_arg_count {
301 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, });
311 }
312 }
313
314 ReferenceIntegrityResult {
315 target_symbol: method_id,
316 issues,
317 }
318 }
319
320 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 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 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#[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 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 let create_config = builder
379 .add_symbol(
380 SymbolPath::parse("test::create_config").unwrap(),
381 SymbolKind::Function,
382 )
383 .unwrap();
384 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 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, ®istry);
411
412 let create_config_id = registry.lookup_by_name("create_config").unwrap();
414
415 let result = checker.check_deletion_impact(create_config_id);
416
417 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, ®istry);
426
427 let init_id = registry.lookup_by_name("init").unwrap();
429
430 let result = checker.check_deletion_impact(init_id);
431
432 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, ®istry);
440
441 let config_id = registry.lookup_by_name("Config").unwrap();
443
444 let result = checker.check_rename_impact(config_id);
445
446 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, ®istry);
454
455 let config_id = registry.lookup_by_name("Config").unwrap();
457
458 let result = checker.check_field_addition_impact(config_id, "timeout");
459
460 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, ®istry);
468
469 let create_config_id = registry.lookup_by_name("create_config").unwrap();
471
472 let referrers = checker.get_all_referrers(create_config_id);
473
474 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, ®istry);
483
484 let init_id = registry.lookup_by_name("init").unwrap();
486 assert!(checker.is_symbol_unused(init_id));
487
488 let create_config_id = registry.lookup_by_name("create_config").unwrap();
490 assert!(!checker.is_symbol_unused(create_config_id));
491 }
492}