1use crate::{GraphStore, Located, RefKind, Reference};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeSet;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Resolution {
24 Local,
26 UniqueGlobal,
28 Ambiguous,
30 ReceiverUnknown,
37 Unresolved,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ResolvedReference {
44 pub reference: Located<Reference>,
46 pub target: Option<String>,
49 pub resolution: Resolution,
51}
52
53pub fn resolve_references_to(store: &dyn GraphStore, name: &str) -> Vec<ResolvedReference> {
56 let def_paths: BTreeSet<String> = store
57 .definitions(name)
58 .into_iter()
59 .map(|d| d.path)
60 .collect();
61
62 store
63 .references_to(name)
64 .into_iter()
65 .map(|located| {
66 let (target, resolution) = if def_paths.contains(&located.path) {
67 (Some(located.path.clone()), Resolution::Local)
71 } else if def_paths.is_empty() {
72 (None, Resolution::Unresolved)
76 } else if located.item.kind == RefKind::Method {
77 (None, Resolution::ReceiverUnknown)
83 } else if def_paths.len() == 1 {
84 (def_paths.iter().next().cloned(), Resolution::UniqueGlobal)
85 } else {
86 (None, Resolution::Ambiguous)
87 };
88 ResolvedReference {
89 reference: located,
90 target,
91 resolution,
92 }
93 })
94 .collect()
95}
96
97pub fn resolved_callers_of(store: &dyn GraphStore, defining_path: &str, name: &str) -> Vec<String> {
101 let mut callers: Vec<String> = resolve_references_to(store, name)
102 .into_iter()
103 .filter(|r| r.target.as_deref() == Some(defining_path))
104 .filter_map(|r| r.reference.item.from)
105 .collect();
106 callers.sort();
107 callers.dedup();
108 callers
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
114pub struct ImpactNode {
115 pub name: String,
116 pub path: String,
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ImpactReport {
124 pub reached: Vec<ImpactNode>,
126 pub ambiguous_edges: usize,
131 pub receiver_unknown_edges: usize,
136 pub truncated: bool,
142}
143
144pub fn resolved_impact(
167 store: &dyn GraphStore,
168 name: &str,
169 max_depth: Option<usize>,
170) -> ImpactReport {
171 let seeds: Vec<ImpactNode> = store
172 .definitions(name)
173 .into_iter()
174 .map(|d| ImpactNode {
175 name: name.to_string(),
176 path: d.path,
177 })
178 .collect();
179
180 let mut visited: BTreeSet<ImpactNode> = seeds.iter().cloned().collect();
181 let mut frontier: Vec<ImpactNode> = seeds.clone();
182 let mut report = ImpactReport::default();
183 let mut depth = 0usize;
184
185 while !frontier.is_empty() {
186 if max_depth.is_some_and(|max| depth >= max) {
187 report.truncated = true;
188 break;
189 }
190 depth += 1;
191
192 let mut next: Vec<ImpactNode> = Vec::new();
193 for node in &frontier {
194 for resolved in resolve_references_to(store, &node.name) {
195 match resolved.resolution {
196 Resolution::Ambiguous => report.ambiguous_edges += 1,
198 Resolution::ReceiverUnknown => report.receiver_unknown_edges += 1,
199 _ if resolved.target.as_deref() != Some(node.path.as_str()) => {}
201 _ => {
202 let Some(from) = resolved.reference.item.from else {
203 continue; };
205 let caller = ImpactNode {
206 name: from,
207 path: resolved.reference.path,
208 };
209 if visited.insert(caller.clone()) {
210 next.push(caller);
211 }
212 }
213 }
214 }
215 }
216 frontier = next;
217 }
218
219 for seed in &seeds {
220 visited.remove(seed);
221 }
222 report.reached = visited.into_iter().collect();
223 report
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::{InMemoryGraphStore, build_rust};
230
231 fn view() -> InMemoryGraphStore {
232 let mut s = InMemoryGraphStore::new();
233 s.insert(
235 "a.rs",
236 build_rust("fn foo() {}\nfn ca() { foo(); }\nfn only() {}"),
237 );
238 s.insert("b.rs", build_rust("fn foo() {}\nfn cb() { foo(); }"));
239 s.insert("c.rs", build_rust("fn cc() { only(); }"));
241 s.insert("d.rs", build_rust("fn cd() { foo(); }"));
243 s
244 }
245
246 #[test]
247 fn local_definition_wins() {
248 let s = view();
249 let resolved = resolve_references_to(&s, "foo");
250 let ca = resolved
252 .iter()
253 .find(|r| r.reference.item.from.as_deref() == Some("ca"))
254 .unwrap();
255 assert_eq!(ca.resolution, Resolution::Local);
256 assert_eq!(ca.target.as_deref(), Some("a.rs"));
257 let cb = resolved
258 .iter()
259 .find(|r| r.reference.item.from.as_deref() == Some("cb"))
260 .unwrap();
261 assert_eq!(cb.target.as_deref(), Some("b.rs"));
262 }
263
264 #[test]
265 fn unique_global_resolves() {
266 let s = view();
267 let resolved = resolve_references_to(&s, "only");
268 assert_eq!(resolved.len(), 1);
269 assert_eq!(resolved[0].resolution, Resolution::UniqueGlobal);
270 assert_eq!(resolved[0].target.as_deref(), Some("a.rs"));
271 }
272
273 #[test]
274 fn multiple_defs_without_a_local_are_ambiguous() {
275 let s = view();
276 let cd = resolve_references_to(&s, "foo")
277 .into_iter()
278 .find(|r| r.reference.item.from.as_deref() == Some("cd"))
279 .unwrap();
280 assert_eq!(cd.resolution, Resolution::Ambiguous);
281 assert_eq!(cd.target, None);
282 }
283
284 #[test]
285 fn missing_definition_is_unresolved() {
286 let s = view();
287 let mut s = s;
288 s.insert("e.rs", build_rust("fn ce() { ghost(); }"));
289 let resolved = resolve_references_to(&s, "ghost");
290 assert_eq!(resolved.len(), 1);
291 assert_eq!(resolved[0].resolution, Resolution::Unresolved);
292 assert_eq!(resolved[0].target, None);
293 }
294
295 fn std_method_collision() -> InMemoryGraphStore {
301 let mut s = InMemoryGraphStore::new();
302 s.insert(
303 "crates/core/src/merge.rs",
304 build_rust("fn merge() { let _ = ours.keys().chain(theirs.keys()); }"),
305 );
306 s.insert(
307 "crates/graph/src/store.rs",
308 build_rust("fn chain() -> u8 { 0 }"),
309 );
310 s
311 }
312
313 #[test]
314 fn a_method_call_does_not_resolve_to_a_same_named_free_function() {
315 let s = std_method_collision();
316 let r = resolve_references_to(&s, "chain");
317 let call = r
318 .iter()
319 .find(|r| r.reference.path.contains("merge.rs"))
320 .expect("the .chain() call is recorded");
321 assert_eq!(call.resolution, Resolution::ReceiverUnknown);
322 assert_eq!(call.target, None, "must not claim the test fixture");
323 }
324
325 #[test]
326 fn the_collision_no_longer_bridges_the_impact_closure() {
327 let report = resolved_impact(&std_method_collision(), "chain", None);
330 assert!(
331 !names_of(&report).contains(&"merge"),
332 "must not cross into the other crate: {report:?}"
333 );
334 assert_eq!(report.receiver_unknown_edges, 1, "and must say so");
335 }
336
337 #[test]
338 fn a_free_call_still_resolves_unique_global() {
339 let mut s = InMemoryGraphStore::new();
341 s.insert("a.rs", build_rust("fn only() {}"));
342 s.insert("b.rs", build_rust("fn cb() { only(); }"));
343 let r = resolve_references_to(&s, "only");
344 assert_eq!(r[0].resolution, Resolution::UniqueGlobal);
345 assert_eq!(r[0].target.as_deref(), Some("a.rs"));
346 }
347
348 #[test]
349 fn a_method_call_still_resolves_locally() {
350 let mut s = InMemoryGraphStore::new();
353 s.insert(
354 "a.rs",
355 build_rust("fn helper() {}\nfn caller() { self.helper(); }"),
356 );
357 let r = resolve_references_to(&s, "helper");
358 let call = r.iter().find(|r| r.reference.item.from.is_some()).unwrap();
359 assert_eq!(call.resolution, Resolution::Local);
360 assert_eq!(call.target.as_deref(), Some("a.rs"));
361 }
362
363 #[test]
364 fn a_path_call_is_not_treated_as_a_method_call() {
365 let mut s = InMemoryGraphStore::new();
367 s.insert("a.rs", build_rust("fn parse() {}"));
368 s.insert("b.rs", build_rust("fn cb() { util::parse(); }"));
369 let r = resolve_references_to(&s, "parse");
370 let call = r.iter().find(|r| r.reference.path == "b.rs").unwrap();
371 assert_eq!(call.resolution, Resolution::UniqueGlobal);
372 }
373
374 #[test]
375 fn receiver_unknown_is_distinct_from_unresolved() {
376 let mut s = InMemoryGraphStore::new();
379 s.insert("a.rs", build_rust("fn c() { x.ghost(); }"));
380 let r = resolve_references_to(&s, "ghost");
381 assert_eq!(r[0].resolution, Resolution::Unresolved);
382 }
383
384 fn bridged() -> InMemoryGraphStore {
390 let mut s = InMemoryGraphStore::new();
391 s.insert(
392 "a.rs",
393 build_rust(
394 "fn leaf_a() {}\n\
395 fn helper() { leaf_a(); }\n\
396 fn top_a() { helper(); }",
397 ),
398 );
399 s.insert(
400 "b.rs",
401 build_rust(
402 "fn leaf_b() {}\n\
403 fn helper() { leaf_b(); }\n\
404 fn top_b() { helper(); }",
405 ),
406 );
407 s
408 }
409
410 fn names_of(report: &ImpactReport) -> Vec<&str> {
411 report.reached.iter().map(|n| n.name.as_str()).collect()
412 }
413
414 #[test]
415 fn name_matched_impact_merges_the_two_subgraphs() {
416 let s = bridged();
419 assert!(s.impact("leaf_a").contains(&"top_b".to_string()));
420 }
421
422 #[test]
423 fn resolved_impact_does_not_cross_an_ambiguous_name() {
424 let s = bridged();
425 let report = resolved_impact(&s, "leaf_a", None);
426 assert!(names_of(&report).contains(&"helper"), "{report:?}");
427 assert!(names_of(&report).contains(&"top_a"), "{report:?}");
428 assert!(
429 !names_of(&report).contains(&"top_b"),
430 "must not reach the other subgraph: {report:?}"
431 );
432 assert!(!names_of(&report).contains(&"leaf_b"), "{report:?}");
433 }
434
435 #[test]
436 fn resolved_impact_carries_a_defining_path_for_every_node() {
437 let report = resolved_impact(&bridged(), "leaf_a", None);
438 assert!(!report.reached.is_empty());
439 assert!(
440 report.reached.iter().all(|n| n.path == "a.rs"),
441 "{report:?}"
442 );
443 }
444
445 #[test]
446 fn resolved_impact_excludes_the_seed() {
447 let report = resolved_impact(&bridged(), "leaf_a", None);
448 assert!(!names_of(&report).contains(&"leaf_a"));
449 }
450
451 #[test]
452 fn resolved_impact_counts_ambiguous_edges_it_declined_to_follow() {
453 let mut s = bridged();
458 s.insert("c.rs", build_rust("fn outsider() { helper(); }"));
459 let report = resolved_impact(&s, "leaf_a", None);
460 assert!(
461 report.ambiguous_edges > 0,
462 "an unattributable edge must be reported: {report:?}"
463 );
464 assert!(
465 !names_of(&report).contains(&"outsider"),
466 "and must not be traversed: {report:?}"
467 );
468 }
469
470 #[test]
471 fn resolved_impact_reports_no_ambiguity_when_every_name_is_unique() {
472 let mut s = InMemoryGraphStore::new();
473 s.insert("a.rs", build_rust("fn leaf() {}\nfn mid() { leaf(); }"));
474 let report = resolved_impact(&s, "leaf", None);
475 assert_eq!(report.ambiguous_edges, 0);
476 assert_eq!(names_of(&report), vec!["mid"]);
477 assert!(!report.truncated);
478 }
479
480 #[test]
481 fn resolved_impact_survives_cycles() {
482 let mut s = InMemoryGraphStore::new();
483 s.insert("cyc.rs", build_rust("fn a() { b(); }\nfn b() { a(); }"));
484 let report = resolved_impact(&s, "a", None);
485 assert_eq!(names_of(&report), vec!["b"], "terminates, seed excluded");
486 }
487
488 #[test]
489 fn resolved_impact_respects_max_depth() {
490 let mut s = InMemoryGraphStore::new();
491 s.insert(
492 "a.rs",
493 build_rust("fn l() {}\nfn m() { l(); }\nfn t() { m(); }"),
494 );
495 let one = resolved_impact(&s, "l", Some(1));
496 assert_eq!(names_of(&one), vec!["m"], "one hop only");
497 assert!(one.truncated, "a capped walk must say so");
498
499 let two = resolved_impact(&s, "l", Some(2));
503 assert_eq!(names_of(&two), vec!["m", "t"]);
504 assert!(two.truncated, "t was reached but never explored");
505
506 let deep = resolved_impact(&s, "l", Some(9));
509 assert_eq!(names_of(&deep), vec!["m", "t"]);
510 assert!(!deep.truncated);
511 assert!(!resolved_impact(&s, "l", None).truncated);
512 }
513
514 #[test]
515 fn resolved_impact_on_an_undefined_name_is_empty() {
516 let report = resolved_impact(&bridged(), "ghost", None);
517 assert!(report.reached.is_empty());
518 assert_eq!(report.ambiguous_edges, 0);
519 }
520
521 #[test]
522 fn resolved_impact_walks_every_definition_of_an_ambiguous_seed() {
523 let report = resolved_impact(&bridged(), "helper", None);
526 let mut pairs: Vec<(&str, &str)> = report
527 .reached
528 .iter()
529 .map(|n| (n.name.as_str(), n.path.as_str()))
530 .collect();
531 pairs.sort();
532 assert_eq!(pairs, vec![("top_a", "a.rs"), ("top_b", "b.rs")]);
533 }
534
535 #[test]
536 fn resolved_callers_disambiguates_by_defining_path() {
537 let s = view();
538 assert!(s.callers_of("foo").contains(&"ca".to_string()));
540 assert!(s.callers_of("foo").contains(&"cb".to_string()));
541 assert_eq!(
543 resolved_callers_of(&s, "a.rs", "foo"),
544 vec!["ca".to_string()]
545 );
546 assert_eq!(
547 resolved_callers_of(&s, "b.rs", "foo"),
548 vec!["cb".to_string()]
549 );
550 }
551}