1use crate::{GraphStore, Import, Located, RefKind, Reference};
28use serde::{Deserialize, Serialize};
29use std::collections::{BTreeMap, BTreeSet};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Resolution {
34 Qualified,
40 Local,
42 UniqueGlobal,
44 Imported,
47 Ambiguous,
49 ReceiverUnknown,
56 Unresolved,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ResolvedReference {
63 pub reference: Located<Reference>,
65 pub target: Option<String>,
68 pub resolution: Resolution,
70}
71
72pub fn resolve_references_to(store: &dyn GraphStore, name: &str) -> Vec<ResolvedReference> {
75 let defs = store.definitions(name);
76 let def_paths: BTreeSet<String> = defs.iter().map(|d| d.path.clone()).collect();
77 let references = store.references_to(name);
78
79 let mut imports_by_path: BTreeMap<String, Vec<Import>> = BTreeMap::new();
83 if def_paths.len() > 1 {
84 for located in &references {
85 if !imports_by_path.contains_key(&located.path) {
86 imports_by_path.insert(located.path.clone(), store.imports_in_file(&located.path));
87 }
88 }
89 }
90
91 references
92 .into_iter()
93 .map(|located| {
94 let qualified: BTreeSet<&str> = match located.item.qualifier.as_deref() {
105 Some(q) => defs
106 .iter()
107 .filter(|d| d.item.owner.as_deref() == Some(q))
108 .map(|d| d.path.as_str())
109 .collect(),
110 None => BTreeSet::new(),
111 };
112
113 let (target, resolution) = if qualified.len() == 1 {
114 (
115 qualified.iter().next().map(|p| (*p).to_string()),
116 Resolution::Qualified,
117 )
118 } else if !qualified.is_empty() {
119 (None, Resolution::Ambiguous)
122 } else if def_paths.contains(&located.path) {
123 (Some(located.path.clone()), Resolution::Local)
127 } else if def_paths.is_empty() {
128 (None, Resolution::Unresolved)
132 } else if located.item.kind == RefKind::Method {
133 (None, Resolution::ReceiverUnknown)
139 } else if def_paths.len() == 1 {
140 (def_paths.iter().next().cloned(), Resolution::UniqueGlobal)
141 } else if let Some(path) = imports_by_path
142 .get(&located.path)
143 .and_then(|imports| imported_target(imports, name, &located.path, &def_paths))
144 {
145 (Some(path), Resolution::Imported)
146 } else {
147 (None, Resolution::Ambiguous)
148 };
149 ResolvedReference {
150 reference: located,
151 target,
152 resolution,
153 }
154 })
155 .collect()
156}
157
158fn imported_target(
166 imports: &[Import],
167 name: &str,
168 from_path: &str,
169 candidates: &BTreeSet<String>,
170) -> Option<String> {
171 let Some(import) = imports.iter().find(|i| i.name == name) else {
172 return included_target(imports, from_path, candidates);
176 };
177
178 let matched: Vec<&str> = if import.depth > 0 {
179 let anchor = relative_anchor(from_path, import.depth, &import.path)?;
182 candidates
183 .iter()
184 .filter(|path| path_under(path, &anchor))
185 .map(String::as_str)
186 .collect()
187 } else {
188 if import.path.is_empty() {
189 return None;
190 }
191 candidates
192 .iter()
193 .filter(|path| path_ends_with_module(path, &import.path))
194 .map(String::as_str)
195 .collect()
196 };
197
198 match matched.as_slice() {
199 [] => None,
200 [only] => Some((*only).to_string()),
201 several => nearest_to(several, from_path),
204 }
205}
206
207fn included_target(
215 imports: &[Import],
216 from_path: &str,
217 candidates: &BTreeSet<String>,
218) -> Option<String> {
219 let included: Vec<&Import> = imports
220 .iter()
221 .filter(|i| i.brings_whole_file() && !i.path.is_empty())
222 .collect();
223 if included.is_empty() {
224 return None;
225 }
226 let matched: Vec<&str> = candidates
227 .iter()
228 .filter(|path| {
229 included
230 .iter()
231 .any(|import| path_ends_with_module(path, &import.path))
232 })
233 .map(String::as_str)
234 .collect();
235 match matched.as_slice() {
236 [] => None,
237 [only] => Some((*only).to_string()),
238 several => nearest_to(several, from_path),
239 }
240}
241
242fn relative_anchor(from_path: &str, depth: usize, module: &[String]) -> Option<Vec<String>> {
248 let mut components: Vec<&str> = from_path.split('/').filter(|c| !c.is_empty()).collect();
249 components.pop()?; for _ in 1..depth {
251 components.pop()?;
252 }
253 let mut anchor: Vec<String> = components.into_iter().map(str::to_string).collect();
254 anchor.extend(module.iter().cloned());
255 Some(anchor)
256}
257
258fn path_under(file: &str, anchor: &[String]) -> bool {
260 let mut components: Vec<&str> = file.split('/').filter(|c| !c.is_empty()).collect();
261 if let Some(last) = components.pop() {
262 let stem = last.rsplit_once('.').map_or(last, |(stem, _)| stem);
263 if stem != "__init__" {
265 components.push(stem);
266 }
267 }
268 components.len() >= anchor.len()
269 && components
270 .iter()
271 .zip(anchor)
272 .all(|(component, segment)| same_segment(component, segment))
273}
274
275fn nearest_to(candidates: &[&str], from_path: &str) -> Option<String> {
281 let from: Vec<&str> = from_path.split('/').filter(|c| !c.is_empty()).collect();
282 let shared = |path: &str| {
283 path.split('/')
284 .filter(|c| !c.is_empty())
285 .zip(from.iter())
286 .take_while(|(component, own)| same_segment(component, own))
287 .count()
288 };
289 let best = candidates.iter().map(|path| shared(path)).max()?;
290 if best == 0 {
291 return None;
292 }
293 let mut winners = candidates.iter().filter(|path| shared(path) == best);
294 let only = winners.next()?;
295 winners.next().is_none().then(|| (*only).to_string())
296}
297
298fn path_ends_with_module(file: &str, module: &[String]) -> bool {
310 let mut components: Vec<&str> = file
311 .split('/')
312 .filter(|c| !c.is_empty() && *c != "src")
313 .collect();
314 if let Some(last) = components.pop() {
315 let stem = last.rsplit_once('.').map_or(last, |(stem, _)| stem);
316 if !matches!(stem, "mod" | "lib" | "main" | "__init__" | "index") {
317 components.push(stem);
318 }
319 }
320 components.len() >= module.len()
321 && components[components.len() - module.len()..]
322 .iter()
323 .zip(module)
324 .all(|(component, segment)| same_segment(component, segment))
325}
326
327fn same_segment(component: &str, segment: &str) -> bool {
329 component.len() == segment.len()
330 && component
331 .bytes()
332 .zip(segment.bytes())
333 .all(|(a, b)| a == b || (a == b'-' || a == b'_') && (b == b'-' || b == b'_'))
334}
335
336pub fn resolved_callers_of(store: &dyn GraphStore, defining_path: &str, name: &str) -> Vec<String> {
340 let mut callers: Vec<String> = resolve_references_to(store, name)
341 .into_iter()
342 .filter(|r| r.target.as_deref() == Some(defining_path))
343 .filter_map(|r| r.reference.item.from)
344 .collect();
345 callers.sort();
346 callers.dedup();
347 callers
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
353pub struct ImpactNode {
354 pub name: String,
355 pub path: String,
358}
359
360#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
362pub struct ImpactReport {
363 pub reached: Vec<ImpactNode>,
365 pub ambiguous_edges: usize,
370 pub receiver_unknown_edges: usize,
375 pub value_edges: usize,
385 pub truncated: bool,
391}
392
393pub fn resolved_impact(
416 store: &dyn GraphStore,
417 name: &str,
418 max_depth: Option<usize>,
419) -> ImpactReport {
420 let seeds: Vec<ImpactNode> = store
421 .definitions(name)
422 .into_iter()
423 .map(|d| ImpactNode {
424 name: name.to_string(),
425 path: d.path,
426 })
427 .collect();
428
429 let mut visited: BTreeSet<ImpactNode> = seeds.iter().cloned().collect();
430 let mut frontier: Vec<ImpactNode> = seeds.clone();
431 let mut report = ImpactReport::default();
432 let mut depth = 0usize;
433
434 while !frontier.is_empty() {
435 if max_depth.is_some_and(|max| depth >= max) {
436 report.truncated = true;
437 break;
438 }
439 depth += 1;
440
441 let mut next: Vec<ImpactNode> = Vec::new();
442 for node in &frontier {
443 for resolved in resolve_references_to(store, &node.name) {
444 if resolved.reference.item.kind == RefKind::Value {
447 report.value_edges += 1;
448 continue;
449 }
450 match resolved.resolution {
451 Resolution::Ambiguous => report.ambiguous_edges += 1,
453 Resolution::ReceiverUnknown => report.receiver_unknown_edges += 1,
454 _ if resolved.target.as_deref() != Some(node.path.as_str()) => {}
456 _ => {
457 let Some(from) = resolved.reference.item.from else {
458 continue; };
460 let caller = ImpactNode {
461 name: from,
462 path: resolved.reference.path,
463 };
464 if visited.insert(caller.clone()) {
465 next.push(caller);
466 }
467 }
468 }
469 }
470 }
471 frontier = next;
472 }
473
474 for seed in &seeds {
475 visited.remove(seed);
476 }
477 report.reached = visited.into_iter().collect();
478 report
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::{InMemoryGraphStore, build_rust};
485
486 fn view() -> InMemoryGraphStore {
487 let mut s = InMemoryGraphStore::new();
488 s.insert(
490 "a.rs",
491 build_rust("fn foo() {}\nfn ca() { foo(); }\nfn only() {}"),
492 );
493 s.insert("b.rs", build_rust("fn foo() {}\nfn cb() { foo(); }"));
494 s.insert("c.rs", build_rust("fn cc() { only(); }"));
496 s.insert("d.rs", build_rust("fn cd() { foo(); }"));
498 s
499 }
500
501 #[test]
502 fn local_definition_wins() {
503 let s = view();
504 let resolved = resolve_references_to(&s, "foo");
505 let ca = resolved
507 .iter()
508 .find(|r| r.reference.item.from.as_deref() == Some("ca"))
509 .unwrap();
510 assert_eq!(ca.resolution, Resolution::Local);
511 assert_eq!(ca.target.as_deref(), Some("a.rs"));
512 let cb = resolved
513 .iter()
514 .find(|r| r.reference.item.from.as_deref() == Some("cb"))
515 .unwrap();
516 assert_eq!(cb.target.as_deref(), Some("b.rs"));
517 }
518
519 #[test]
520 fn unique_global_resolves() {
521 let s = view();
522 let resolved = resolve_references_to(&s, "only");
523 assert_eq!(resolved.len(), 1);
524 assert_eq!(resolved[0].resolution, Resolution::UniqueGlobal);
525 assert_eq!(resolved[0].target.as_deref(), Some("a.rs"));
526 }
527
528 #[test]
529 fn multiple_defs_without_a_local_are_ambiguous() {
530 let s = view();
531 let cd = resolve_references_to(&s, "foo")
532 .into_iter()
533 .find(|r| r.reference.item.from.as_deref() == Some("cd"))
534 .unwrap();
535 assert_eq!(cd.resolution, Resolution::Ambiguous);
536 assert_eq!(cd.target, None);
537 }
538
539 #[test]
540 fn missing_definition_is_unresolved() {
541 let s = view();
542 let mut s = s;
543 s.insert("e.rs", build_rust("fn ce() { ghost(); }"));
544 let resolved = resolve_references_to(&s, "ghost");
545 assert_eq!(resolved.len(), 1);
546 assert_eq!(resolved[0].resolution, Resolution::Unresolved);
547 assert_eq!(resolved[0].target, None);
548 }
549
550 fn std_method_collision() -> InMemoryGraphStore {
556 let mut s = InMemoryGraphStore::new();
557 s.insert(
558 "crates/core/src/merge.rs",
559 build_rust("fn merge() { let _ = ours.keys().chain(theirs.keys()); }"),
560 );
561 s.insert(
562 "crates/graph/src/store.rs",
563 build_rust("fn chain() -> u8 { 0 }"),
564 );
565 s
566 }
567
568 #[test]
569 fn a_method_call_does_not_resolve_to_a_same_named_free_function() {
570 let s = std_method_collision();
571 let r = resolve_references_to(&s, "chain");
572 let call = r
573 .iter()
574 .find(|r| r.reference.path.contains("merge.rs"))
575 .expect("the .chain() call is recorded");
576 assert_eq!(call.resolution, Resolution::ReceiverUnknown);
577 assert_eq!(call.target, None, "must not claim the test fixture");
578 }
579
580 #[test]
581 fn the_collision_no_longer_bridges_the_impact_closure() {
582 let report = resolved_impact(&std_method_collision(), "chain", None);
585 assert!(
586 !names_of(&report).contains(&"merge"),
587 "must not cross into the other crate: {report:?}"
588 );
589 assert_eq!(report.receiver_unknown_edges, 1, "and must say so");
590 }
591
592 #[test]
593 fn a_free_call_still_resolves_unique_global() {
594 let mut s = InMemoryGraphStore::new();
596 s.insert("a.rs", build_rust("fn only() {}"));
597 s.insert("b.rs", build_rust("fn cb() { only(); }"));
598 let r = resolve_references_to(&s, "only");
599 assert_eq!(r[0].resolution, Resolution::UniqueGlobal);
600 assert_eq!(r[0].target.as_deref(), Some("a.rs"));
601 }
602
603 #[test]
604 fn a_method_call_still_resolves_locally() {
605 let mut s = InMemoryGraphStore::new();
608 s.insert(
609 "a.rs",
610 build_rust("fn helper() {}\nfn caller() { self.helper(); }"),
611 );
612 let r = resolve_references_to(&s, "helper");
613 let call = r.iter().find(|r| r.reference.item.from.is_some()).unwrap();
614 assert_eq!(call.resolution, Resolution::Local);
615 assert_eq!(call.target.as_deref(), Some("a.rs"));
616 }
617
618 #[test]
619 fn a_path_call_is_not_treated_as_a_method_call() {
620 let mut s = InMemoryGraphStore::new();
622 s.insert("a.rs", build_rust("fn parse() {}"));
623 s.insert("b.rs", build_rust("fn cb() { util::parse(); }"));
624 let r = resolve_references_to(&s, "parse");
625 let call = r.iter().find(|r| r.reference.path == "b.rs").unwrap();
626 assert_eq!(call.resolution, Resolution::UniqueGlobal);
627 }
628
629 #[test]
630 fn receiver_unknown_is_distinct_from_unresolved() {
631 let mut s = InMemoryGraphStore::new();
634 s.insert("a.rs", build_rust("fn c() { x.ghost(); }"));
635 let r = resolve_references_to(&s, "ghost");
636 assert_eq!(r[0].resolution, Resolution::Unresolved);
637 }
638
639 fn bridged() -> InMemoryGraphStore {
645 let mut s = InMemoryGraphStore::new();
646 s.insert(
647 "a.rs",
648 build_rust(
649 "fn leaf_a() {}\n\
650 fn helper() { leaf_a(); }\n\
651 fn top_a() { helper(); }",
652 ),
653 );
654 s.insert(
655 "b.rs",
656 build_rust(
657 "fn leaf_b() {}\n\
658 fn helper() { leaf_b(); }\n\
659 fn top_b() { helper(); }",
660 ),
661 );
662 s
663 }
664
665 fn names_of(report: &ImpactReport) -> Vec<&str> {
666 report.reached.iter().map(|n| n.name.as_str()).collect()
667 }
668
669 #[test]
670 fn name_matched_impact_merges_the_two_subgraphs() {
671 let s = bridged();
674 assert!(s.impact("leaf_a").contains(&"top_b".to_string()));
675 }
676
677 #[test]
678 fn resolved_impact_does_not_cross_an_ambiguous_name() {
679 let s = bridged();
680 let report = resolved_impact(&s, "leaf_a", None);
681 assert!(names_of(&report).contains(&"helper"), "{report:?}");
682 assert!(names_of(&report).contains(&"top_a"), "{report:?}");
683 assert!(
684 !names_of(&report).contains(&"top_b"),
685 "must not reach the other subgraph: {report:?}"
686 );
687 assert!(!names_of(&report).contains(&"leaf_b"), "{report:?}");
688 }
689
690 #[test]
691 fn resolved_impact_carries_a_defining_path_for_every_node() {
692 let report = resolved_impact(&bridged(), "leaf_a", None);
693 assert!(!report.reached.is_empty());
694 assert!(
695 report.reached.iter().all(|n| n.path == "a.rs"),
696 "{report:?}"
697 );
698 }
699
700 #[test]
701 fn resolved_impact_excludes_the_seed() {
702 let report = resolved_impact(&bridged(), "leaf_a", None);
703 assert!(!names_of(&report).contains(&"leaf_a"));
704 }
705
706 #[test]
707 fn resolved_impact_counts_ambiguous_edges_it_declined_to_follow() {
708 let mut s = bridged();
713 s.insert("c.rs", build_rust("fn outsider() { helper(); }"));
714 let report = resolved_impact(&s, "leaf_a", None);
715 assert!(
716 report.ambiguous_edges > 0,
717 "an unattributable edge must be reported: {report:?}"
718 );
719 assert!(
720 !names_of(&report).contains(&"outsider"),
721 "and must not be traversed: {report:?}"
722 );
723 }
724
725 #[test]
726 fn resolved_impact_reports_no_ambiguity_when_every_name_is_unique() {
727 let mut s = InMemoryGraphStore::new();
728 s.insert("a.rs", build_rust("fn leaf() {}\nfn mid() { leaf(); }"));
729 let report = resolved_impact(&s, "leaf", None);
730 assert_eq!(report.ambiguous_edges, 0);
731 assert_eq!(names_of(&report), vec!["mid"]);
732 assert!(!report.truncated);
733 }
734
735 #[test]
736 fn resolved_impact_survives_cycles() {
737 let mut s = InMemoryGraphStore::new();
738 s.insert("cyc.rs", build_rust("fn a() { b(); }\nfn b() { a(); }"));
739 let report = resolved_impact(&s, "a", None);
740 assert_eq!(names_of(&report), vec!["b"], "terminates, seed excluded");
741 }
742
743 #[test]
744 fn resolved_impact_respects_max_depth() {
745 let mut s = InMemoryGraphStore::new();
746 s.insert(
747 "a.rs",
748 build_rust("fn l() {}\nfn m() { l(); }\nfn t() { m(); }"),
749 );
750 let one = resolved_impact(&s, "l", Some(1));
751 assert_eq!(names_of(&one), vec!["m"], "one hop only");
752 assert!(one.truncated, "a capped walk must say so");
753
754 let two = resolved_impact(&s, "l", Some(2));
758 assert_eq!(names_of(&two), vec!["m", "t"]);
759 assert!(two.truncated, "t was reached but never explored");
760
761 let deep = resolved_impact(&s, "l", Some(9));
764 assert_eq!(names_of(&deep), vec!["m", "t"]);
765 assert!(!deep.truncated);
766 assert!(!resolved_impact(&s, "l", None).truncated);
767 }
768
769 #[test]
770 fn resolved_impact_on_an_undefined_name_is_empty() {
771 let report = resolved_impact(&bridged(), "ghost", None);
772 assert!(report.reached.is_empty());
773 assert_eq!(report.ambiguous_edges, 0);
774 }
775
776 #[test]
777 fn resolved_impact_walks_every_definition_of_an_ambiguous_seed() {
778 let report = resolved_impact(&bridged(), "helper", None);
781 let mut pairs: Vec<(&str, &str)> = report
782 .reached
783 .iter()
784 .map(|n| (n.name.as_str(), n.path.as_str()))
785 .collect();
786 pairs.sort();
787 assert_eq!(pairs, vec![("top_a", "a.rs"), ("top_b", "b.rs")]);
788 }
789
790 #[test]
791 fn resolved_callers_disambiguates_by_defining_path() {
792 let s = view();
793 assert!(s.callers_of("foo").contains(&"ca".to_string()));
795 assert!(s.callers_of("foo").contains(&"cb".to_string()));
796 assert_eq!(
798 resolved_callers_of(&s, "a.rs", "foo"),
799 vec!["ca".to_string()]
800 );
801 assert_eq!(
802 resolved_callers_of(&s, "b.rs", "foo"),
803 vec!["cb".to_string()]
804 );
805 }
806
807 fn two_gets() -> InMemoryGraphStore {
812 let mut s = InMemoryGraphStore::new();
813 s.insert(
814 "a.rs",
815 build_rust("struct Alpha; impl Alpha { fn get() {} }"),
816 );
817 s.insert("b.rs", build_rust("struct Beta; impl Beta { fn get() {} }"));
818 s
819 }
820
821 #[test]
822 fn a_qualified_reference_resolves_to_the_matching_owner() {
823 let mut s = two_gets();
824 s.insert("c.rs", build_rust("fn caller() { Beta::get(); }"));
825 let r = resolve_references_to(&s, "get")
826 .into_iter()
827 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
828 .expect("call recorded");
829 assert_eq!(r.resolution, Resolution::Qualified);
830 assert_eq!(r.target.as_deref(), Some("b.rs"));
831 }
832
833 #[test]
834 fn an_unqualified_reference_to_an_overloaded_name_stays_ambiguous() {
835 let mut s = two_gets();
836 s.insert("c.rs", build_rust("fn plain() { get(); }"));
837 let r = resolve_references_to(&s, "get")
838 .into_iter()
839 .find(|r| r.reference.item.from.as_deref() == Some("plain"))
840 .expect("call recorded");
841 assert_eq!(r.resolution, Resolution::Ambiguous);
842 assert_eq!(r.target, None);
843 }
844
845 #[test]
846 fn a_qualified_reference_beats_a_local_definition_of_the_same_name() {
847 let mut s = two_gets();
850 s.insert(
851 "c.rs",
852 build_rust("fn get() {}\nfn caller() { Beta::get(); }"),
853 );
854 let r = resolve_references_to(&s, "get")
855 .into_iter()
856 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
857 .expect("call recorded");
858 assert_eq!(r.resolution, Resolution::Qualified);
859 assert_eq!(r.target.as_deref(), Some("b.rs"));
860 }
861
862 #[test]
863 fn a_qualifier_that_matches_no_owner_falls_back_to_the_existing_ladder() {
864 let mut s = InMemoryGraphStore::new();
868 s.insert("a.rs", build_rust("fn helper() {}"));
869 s.insert("b.rs", build_rust("fn caller() { some_module::helper(); }"));
870 let r = resolve_references_to(&s, "helper")
871 .into_iter()
872 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
873 .expect("call recorded");
874 assert_eq!(r.resolution, Resolution::UniqueGlobal);
875 assert_eq!(r.target.as_deref(), Some("a.rs"));
876 }
877
878 #[test]
879 fn resolved_impact_follows_a_qualified_edge_it_used_to_decline() {
880 let mut s = two_gets();
881 s.insert("c.rs", build_rust("fn caller() { Beta::get(); }"));
882 let report = resolved_impact(&s, "get", None);
883 assert!(
884 names_of(&report).contains(&"caller"),
885 "qualified edge must be traversed: {report:?}"
886 );
887 assert_eq!(
888 report.ambiguous_edges, 0,
889 "and must not be counted as declined: {report:?}"
890 );
891 }
892
893 #[test]
896 fn resolved_impact_counts_a_value_reference_instead_of_traversing_it() {
897 let mut s = InMemoryGraphStore::new();
904 s.insert("a.rs", build_rust("fn helper() {}"));
905 s.insert("b.rs", build_rust("fn g() { register(helper); }"));
906
907 let report = resolved_impact(&s, "helper", None);
908 assert!(
909 !names_of(&report).contains(&"g"),
910 "must not be traversed: {report:?}"
911 );
912 assert_eq!(report.value_edges, 1, "must be reported: {report:?}");
913 assert_eq!(report.ambiguous_edges, 0, "{report:?}");
914 }
915
916 #[test]
917 fn a_plain_call_is_not_counted_as_a_value_edge() {
918 let mut s = InMemoryGraphStore::new();
919 s.insert("a.rs", build_rust("fn helper() {}"));
920 s.insert("b.rs", build_rust("fn g() { helper(); }"));
921 let report = resolved_impact(&s, "helper", None);
922 assert!(names_of(&report).contains(&"g"), "{report:?}");
923 assert_eq!(report.value_edges, 0, "{report:?}");
924 }
925
926 #[test]
929 fn a_typed_receiver_resolves_to_its_own_type() {
930 let mut s = two_gets();
934 s.insert(
935 "c.rs",
936 build_rust("fn caller() { let b = Beta::new(); b.get(); }"),
937 );
938 let r = resolve_references_to(&s, "get")
939 .into_iter()
940 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
941 .expect("call recorded");
942 assert_eq!(r.resolution, Resolution::Qualified);
943 assert_eq!(r.target.as_deref(), Some("b.rs"));
944 assert_eq!(
945 r.reference.item.kind,
946 RefKind::Method,
947 "still a method call: the shape is what guards #223"
948 );
949 }
950
951 #[test]
952 fn an_untyped_receiver_still_declines_and_is_counted() {
953 let mut s = two_gets();
954 s.insert(
955 "c.rs",
956 build_rust("fn caller() { let b = make(); b.get(); }"),
957 );
958 let r = resolve_references_to(&s, "get")
959 .into_iter()
960 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
961 .expect("call recorded");
962 assert_eq!(r.resolution, Resolution::ReceiverUnknown);
963
964 let report = resolved_impact(&s, "get", None);
965 assert!(
966 report.receiver_unknown_edges > 0,
967 "still reported: {report:?}"
968 );
969 }
970
971 #[test]
972 fn a_receiver_typed_as_something_outside_the_view_falls_through() {
973 let mut s = InMemoryGraphStore::new();
977 s.insert("a.rs", build_rust("fn len() -> usize { 0 }"));
978 s.insert(
979 "b.rs",
980 build_rust("fn caller() { let s = String::new(); s.len(); }"),
981 );
982 let r = resolve_references_to(&s, "len")
983 .into_iter()
984 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
985 .expect("call recorded");
986 assert_eq!(r.resolution, Resolution::ReceiverUnknown);
989 }
990
991 fn two_makes(caller_file: &str, caller_src: &str) -> InMemoryGraphStore {
995 let mut s = InMemoryGraphStore::new();
996 s.insert("src/model.rs", build_rust("fn make() {}"));
997 s.insert("src/other.rs", build_rust("fn make() {}"));
998 s.insert(caller_file, build_rust(caller_src));
999 s
1000 }
1001
1002 #[test]
1003 fn an_import_picks_the_module_it_names() {
1004 let s = two_makes(
1005 "src/app.rs",
1006 "use crate::model::make;\nfn caller() { make(); }",
1007 );
1008 let r = resolve_references_to(&s, "make")
1009 .into_iter()
1010 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1011 .expect("call recorded");
1012 assert_eq!(r.resolution, Resolution::Imported);
1013 assert_eq!(r.target.as_deref(), Some("src/model.rs"));
1014 }
1015
1016 #[test]
1017 fn without_the_import_the_same_call_stays_ambiguous() {
1018 let s = two_makes("src/app.rs", "fn caller() { make(); }");
1020 let r = resolve_references_to(&s, "make")
1021 .into_iter()
1022 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1023 .expect("call recorded");
1024 assert_eq!(r.resolution, Resolution::Ambiguous);
1025 }
1026
1027 #[test]
1028 fn an_import_that_does_not_narrow_falls_through() {
1029 let mut s = InMemoryGraphStore::new();
1033 s.insert("a/src/model.rs", build_rust("fn make() {}"));
1034 s.insert("b/src/model.rs", build_rust("fn make() {}"));
1035 s.insert(
1036 "c/src/app.rs",
1037 build_rust("use crate::model::make;\nfn caller() { make(); }"),
1038 );
1039 let r = resolve_references_to(&s, "make")
1040 .into_iter()
1041 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1042 .expect("call recorded");
1043 assert_eq!(r.resolution, Resolution::Ambiguous);
1044 assert_eq!(r.target, None);
1045 }
1046
1047 #[test]
1048 fn an_import_matches_a_module_directory_through_mod_rs() {
1049 let mut s = InMemoryGraphStore::new();
1051 s.insert("src/model/mod.rs", build_rust("fn make() {}"));
1052 s.insert("src/other.rs", build_rust("fn make() {}"));
1053 s.insert(
1054 "src/app.rs",
1055 build_rust("use crate::model::make;\nfn caller() { make(); }"),
1056 );
1057 let r = resolve_references_to(&s, "make")
1058 .into_iter()
1059 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1060 .expect("call recorded");
1061 assert_eq!(r.target.as_deref(), Some("src/model/mod.rs"));
1062 }
1063
1064 #[test]
1065 fn a_unique_global_still_resolves_without_consulting_imports() {
1066 let mut s = InMemoryGraphStore::new();
1069 s.insert("src/model.rs", build_rust("fn only() {}"));
1070 s.insert("src/app.rs", build_rust("fn caller() { only(); }"));
1071 let r = resolve_references_to(&s, "only")
1072 .into_iter()
1073 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1074 .expect("call recorded");
1075 assert_eq!(r.resolution, Resolution::UniqueGlobal);
1076 }
1077
1078 #[test]
1079 fn resolved_impact_follows_an_imported_edge() {
1080 let s = two_makes(
1081 "src/app.rs",
1082 "use crate::model::make;\nfn caller() { make(); }",
1083 );
1084 let report = resolved_impact(&s, "make", None);
1085 assert!(names_of(&report).contains(&"caller"), "{report:?}");
1086 }
1087
1088 #[test]
1089 fn an_import_of_a_crate_root_matches_the_crate_directory() {
1090 let mut s = InMemoryGraphStore::new();
1095 s.insert("crates/gonzalo-cli/src/lib.rs", build_rust("fn list() {}"));
1096 s.insert(
1097 "crates/gonzalo-graph/src/store.rs",
1098 build_rust("fn list() {}"),
1099 );
1100 s.insert(
1101 "crates/gonzalo-cli/src/main.rs",
1102 build_rust("use gonzalo_cli::list;\nfn caller() { list(); }"),
1103 );
1104 let r = resolve_references_to(&s, "list")
1105 .into_iter()
1106 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1107 .expect("call recorded");
1108 assert_eq!(r.resolution, Resolution::Imported);
1109 assert_eq!(r.target.as_deref(), Some("crates/gonzalo-cli/src/lib.rs"));
1110 }
1111
1112 #[test]
1113 fn a_hyphenated_directory_matches_an_underscored_module() {
1114 let mut s = InMemoryGraphStore::new();
1115 s.insert("my-pkg/src/model.rs", build_rust("fn make() {}"));
1116 s.insert("other/src/model.rs", build_rust("fn make() {}"));
1117 s.insert(
1118 "app.rs",
1119 build_rust("use my_pkg::model::make;\nfn caller() { make(); }"),
1120 );
1121 let r = resolve_references_to(&s, "make")
1122 .into_iter()
1123 .find(|r| r.reference.item.from.as_deref() == Some("caller"))
1124 .expect("call recorded");
1125 assert_eq!(r.target.as_deref(), Some("my-pkg/src/model.rs"));
1126 }
1127
1128 fn py(src: &str) -> crate::CodeGraph {
1131 crate::build(crate::Language::Python, src)
1132 }
1133
1134 fn java(src: &str) -> crate::CodeGraph {
1135 crate::build(crate::Language::Java, src)
1136 }
1137
1138 fn resolved_from(s: &InMemoryGraphStore, name: &str, caller: &str) -> ResolvedReference {
1139 resolve_references_to(s, name)
1140 .into_iter()
1141 .find(|r| r.reference.item.from.as_deref() == Some(caller))
1142 .expect("call recorded")
1143 }
1144
1145 #[test]
1146 fn a_relative_import_anchors_to_the_referencing_package() {
1147 let mut s = InMemoryGraphStore::new();
1151 s.insert(
1152 "ReferenceCode/src/cs5260/DataTypes/Action.py",
1153 py("def Action():\n pass\n"),
1154 );
1155 s.insert(
1156 "WorldTraderSim/src/WorldTraderSim/DataTypes/Action.py",
1157 py("def Action():\n pass\n"),
1158 );
1159 s.insert(
1160 "ReferenceCode/src/cs5260/Examples/HW2_1.py",
1161 py("from ..DataTypes import Action\ndef run():\n Action()\n"),
1162 );
1163
1164 let r = resolved_from(&s, "Action", "run");
1165 assert_eq!(r.resolution, Resolution::Imported);
1166 assert_eq!(
1167 r.target.as_deref(),
1168 Some("ReferenceCode/src/cs5260/DataTypes/Action.py")
1169 );
1170 }
1171
1172 #[test]
1173 fn a_single_dot_import_anchors_to_the_files_own_package() {
1174 let mut s = InMemoryGraphStore::new();
1175 s.insert("a/pkg/util.py", py("def helper():\n pass\n"));
1176 s.insert("b/pkg/util.py", py("def helper():\n pass\n"));
1177 s.insert(
1178 "a/pkg/main.py",
1179 py("from .util import helper\ndef run():\n helper()\n"),
1180 );
1181
1182 let r = resolved_from(&s, "helper", "run");
1183 assert_eq!(r.target.as_deref(), Some("a/pkg/util.py"));
1184 }
1185
1186 #[test]
1187 fn an_absolute_import_matching_several_copies_prefers_the_nearest() {
1188 let mut s = InMemoryGraphStore::new();
1192 for tree in ["assignment1", "assignment2"] {
1193 s.insert(
1194 &format!("{tree}/src/main/java/edu/vandy/util/Helper.java"),
1195 java("class Helper { static void go() {} }"),
1196 );
1197 }
1198 s.insert(
1199 "assignment2/src/main/java/edu/vandy/app/Main.java",
1200 java("import static edu.vandy.util.Helper.go;\nclass Main { void run() { go(); } }"),
1201 );
1202
1203 let r = resolved_from(&s, "go", "run");
1204 assert_eq!(r.resolution, Resolution::Imported);
1205 assert_eq!(
1206 r.target.as_deref(),
1207 Some("assignment2/src/main/java/edu/vandy/util/Helper.java")
1208 );
1209 }
1210
1211 #[test]
1212 fn two_equally_near_copies_stay_ambiguous() {
1213 let mut s = InMemoryGraphStore::new();
1215 for tree in ["one", "two"] {
1216 s.insert(
1217 &format!("{tree}/src/main/java/edu/vandy/util/Helper.java"),
1218 java("class Helper { static void go() {} }"),
1219 );
1220 }
1221 s.insert(
1222 "apps/src/main/java/edu/vandy/app/Main.java",
1223 java("import static edu.vandy.util.Helper.go;\nclass Main { void run() { go(); } }"),
1224 );
1225
1226 let r = resolved_from(&s, "go", "run");
1227 assert_eq!(r.resolution, Resolution::Ambiguous);
1228 assert_eq!(r.target, None);
1229 }
1230
1231 #[test]
1232 fn a_relative_import_that_anchors_nowhere_falls_through() {
1233 let mut s = InMemoryGraphStore::new();
1236 s.insert("util.py", py("def helper():\n pass\n"));
1237 s.insert(
1238 "main.py",
1239 py("from ...deep import helper\ndef run():\n helper()\n"),
1240 );
1241 let r = resolved_from(&s, "helper", "run");
1242 assert_eq!(r.resolution, Resolution::UniqueGlobal);
1243 }
1244
1245 fn c(src: &str) -> crate::CodeGraph {
1248 crate::build(crate::Language::C, src)
1249 }
1250
1251 #[test]
1252 fn an_include_narrows_an_ambiguous_reference() {
1253 let mut s = InMemoryGraphStore::new();
1256 s.insert("engine/render/pipeline.c", c("void draw(void) {}"));
1257 s.insert("tools/preview/pipeline.c", c("void draw(void) {}"));
1258 s.insert(
1259 "app/main.c",
1260 c("#include \"engine/render/pipeline.h\"\nvoid run(void) { draw(); }"),
1261 );
1262
1263 let r = resolved_from(&s, "draw", "run");
1264 assert_eq!(r.resolution, Resolution::Imported);
1265 assert_eq!(r.target.as_deref(), Some("engine/render/pipeline.c"));
1266 }
1267
1268 #[test]
1269 fn without_the_include_the_same_c_call_stays_ambiguous() {
1270 let mut s = InMemoryGraphStore::new();
1271 s.insert("engine/render/pipeline.c", c("void draw(void) {}"));
1272 s.insert("tools/preview/pipeline.c", c("void draw(void) {}"));
1273 s.insert("app/main.c", c("void run(void) { draw(); }"));
1274
1275 assert_eq!(
1276 resolved_from(&s, "draw", "run").resolution,
1277 Resolution::Ambiguous
1278 );
1279 }
1280
1281 #[test]
1282 fn an_include_fitting_several_candidates_falls_through() {
1283 let mut s = InMemoryGraphStore::new();
1285 s.insert("a/pipeline.c", c("void draw(void) {}"));
1286 s.insert("b/pipeline.c", c("void draw(void) {}"));
1287 s.insert(
1288 "app/main.c",
1289 c("#include \"pipeline.h\"\nvoid run(void) { draw(); }"),
1290 );
1291
1292 assert_eq!(
1293 resolved_from(&s, "draw", "run").resolution,
1294 Resolution::Ambiguous
1295 );
1296 }
1297
1298 #[test]
1299 fn a_system_include_matches_nothing_in_the_view() {
1300 let mut s = InMemoryGraphStore::new();
1301 s.insert("a/x.c", c("void printf_impl(void) {}"));
1302 s.insert("b/x.c", c("void printf_impl(void) {}"));
1303 s.insert(
1304 "app/main.c",
1305 c("#include <stdio.h>\nvoid run(void) { printf_impl(); }"),
1306 );
1307
1308 assert_eq!(
1309 resolved_from(&s, "printf_impl", "run").resolution,
1310 Resolution::Ambiguous,
1311 "stdio is not in the view"
1312 );
1313 }
1314
1315 #[test]
1316 fn a_named_import_still_wins_where_a_language_has_one() {
1317 let s = two_makes(
1320 "src/app.rs",
1321 "use crate::model::make;\nfn caller() { make(); }",
1322 );
1323 assert_eq!(
1324 resolved_from(&s, "make", "caller").target.as_deref(),
1325 Some("src/model.rs")
1326 );
1327 }
1328}