1use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
2
3use crate::model::{
4 CallLabel, CallNode, CallRelation, CallSite, CallSyntax, CallTarget, DispatchCandidate,
5 DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, SymbolId,
6};
7
8#[derive(Clone, Debug, Default)]
9pub struct ProgramGraph {
10 functions: BTreeMap<SymbolId, FunctionInfo>,
11 functions_by_name: BTreeMap<(crate::model::LanguageId, String), BTreeSet<SymbolId>>,
12 facts: Vec<LanguageFact>,
13 source_files: BTreeSet<std::path::PathBuf>,
14 declared_roots: BTreeSet<SymbolId>,
15}
16
17struct Expansion<'a> {
18 max_depth: usize,
19 file: Option<&'a std::path::Path>,
20 relevant: Option<&'a BTreeSet<SymbolId>>,
21 context: Option<&'a BTreeSet<SymbolId>>,
22 boundary_marker: Option<&'a str>,
23}
24
25#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
28enum CallExpansionKey {
29 Direct(SymbolId),
30 Dynamic(SymbolId, Vec<SymbolId>, DispatchResolution),
31}
32
33impl ProgramGraph {
34 pub fn from_files(files: impl IntoIterator<Item = FileAnalysis>) -> Result<Self, String> {
35 let mut graph = Self::default();
36 for file in files {
37 graph.source_files.extend(file.source_files);
38 graph.declared_roots.extend(file.roots);
39 for function in file.functions {
40 graph.source_files.insert(function.span.file.clone());
41 if graph.functions.contains_key(&function.id) {
42 return Err(format!("duplicate symbol: {}", function.id));
43 }
44 graph
45 .functions_by_name
46 .entry((function.id.language.clone(), function.id.name.clone()))
47 .or_default()
48 .insert(function.id.clone());
49 graph.functions.insert(function.id.clone(), function);
50 }
51 graph.facts.extend(file.facts);
52 }
53 Ok(graph)
54 }
55
56 pub fn functions(&self) -> &BTreeMap<SymbolId, FunctionInfo> {
57 &self.functions
58 }
59
60 pub fn facts(&self) -> &[LanguageFact] {
61 &self.facts
62 }
63
64 pub fn resolution_diagnostics(&self) -> Vec<String> {
65 let mut diagnostics = BTreeSet::new();
66 for function in self.functions.values() {
67 for call in &function.calls {
68 match &call.target {
69 CallTarget::Dynamic {
70 resolution,
71 evidence,
72 unresolved_reasons,
73 ..
74 } => {
75 let reasons = unresolved_reasons
76 .iter()
77 .map(ToString::to_string)
78 .collect::<Vec<_>>()
79 .join(", ");
80 diagnostics.insert(format!(
81 "{}:{}:{}: {}: {:?}, evidence={evidence}{}",
82 call.span.file.display(),
83 call.span.start_line,
84 call.span.start_column,
85 call.label.default,
86 resolution,
87 if reasons.is_empty() {
88 String::new()
89 } else {
90 format!(", unresolved={reasons}")
91 }
92 ));
93 }
94 CallTarget::Indirect { signature, reason } => {
95 diagnostics.insert(format!(
96 "{}:{}:{}: {}: indirect{}, unresolved={reason}",
97 call.span.file.display(),
98 call.span.start_line,
99 call.span.start_column,
100 call.label.default,
101 signature
102 .as_ref()
103 .map_or_else(String::new, |signature| format!(" ({signature})")),
104 ));
105 }
106 CallTarget::Direct(_) | CallTarget::Unresolved => {}
110 }
111 }
112 }
113 diagnostics.into_iter().collect()
114 }
115
116 pub fn source_files(&self) -> BTreeSet<std::path::PathBuf> {
117 self.source_files.clone()
118 }
119
120 pub fn analyzes_file(&self, file: &std::path::Path) -> bool {
121 self.source_files
122 .iter()
123 .any(|analyzed| same_file(analyzed, file))
124 }
125
126 pub fn has_functions_in_file(&self, file: &std::path::Path) -> bool {
127 self.functions
128 .values()
129 .any(|function| same_file(&function.span.file, file))
130 }
131
132 pub fn public_symbols(&self) -> BTreeSet<SymbolId> {
133 self.functions
134 .values()
135 .filter(|function| function.public)
136 .map(|function| function.id.clone())
137 .collect()
138 }
139
140 pub fn inferred_roots(&self) -> BTreeSet<SymbolId> {
145 if !self.declared_roots.is_empty() {
146 return self
147 .declared_roots
148 .iter()
149 .filter(|root| self.functions.contains_key(*root))
150 .cloned()
151 .collect();
152 }
153 let mut incoming = self
154 .functions
155 .keys()
156 .cloned()
157 .map(|symbol| (symbol, 0usize))
158 .collect::<BTreeMap<_, _>>();
159 let mut adjacency = BTreeMap::<SymbolId, BTreeSet<SymbolId>>::new();
160
161 for (caller, function) in &self.functions {
162 for call in &function.calls {
163 for target in self.call_targets(caller, call) {
164 if self.functions.contains_key(&target) {
165 *incoming.entry(target.clone()).or_default() += 1;
166 adjacency.entry(caller.clone()).or_default().insert(target);
167 }
168 }
169 }
170 }
171
172 let natural = incoming
173 .iter()
174 .filter_map(|(symbol, count)| (*count == 0).then_some(symbol.clone()))
175 .collect::<BTreeSet<_>>();
176 let mut covered = BTreeSet::new();
177 for root in &natural {
178 mark_reachable(root, &adjacency, &mut covered);
179 }
180
181 let mut roots = natural;
182 while let Some(uncovered) = self
183 .functions
184 .values()
185 .filter(|function| !covered.contains(&function.id))
186 .min_by_key(|function| {
187 (
188 function.span.file.clone(),
189 function.span.start_line,
190 function.span.start_column,
191 function.id.clone(),
192 )
193 })
194 .map(|function| function.id.clone())
195 {
196 roots.insert(uncovered.clone());
197 mark_reachable(&uncovered, &adjacency, &mut covered);
198 }
199 roots
200 }
201
202 pub fn roots_in_file(&self, file: &std::path::Path) -> BTreeSet<SymbolId> {
203 let functions = self
204 .functions
205 .values()
206 .filter(|function| same_file(&function.span.file, file))
207 .map(|function| function.id.clone())
208 .collect::<BTreeSet<_>>();
209 if functions.is_empty() {
210 return BTreeSet::new();
211 }
212
213 let mut incoming = functions
214 .iter()
215 .cloned()
216 .map(|symbol| (symbol, 0usize))
217 .collect::<BTreeMap<_, _>>();
218 let mut adjacency = BTreeMap::<SymbolId, BTreeSet<SymbolId>>::new();
219 for caller in &functions {
220 let Some(function) = self.functions.get(caller) else {
221 continue;
222 };
223 for call in &function.calls {
224 for target in self.call_targets(caller, call) {
225 if functions.contains(&target) {
226 *incoming.entry(target.clone()).or_default() += 1;
227 adjacency.entry(caller.clone()).or_default().insert(target);
228 }
229 }
230 }
231 }
232
233 let natural = incoming
234 .iter()
235 .filter_map(|(symbol, count)| (*count == 0).then_some(symbol.clone()))
236 .collect::<BTreeSet<_>>();
237 let mut covered = BTreeSet::new();
238 for root in &natural {
239 mark_reachable(root, &adjacency, &mut covered);
240 }
241 let mut roots = natural;
242 while let Some(uncovered) = functions.iter().find(|symbol| !covered.contains(*symbol)) {
243 roots.insert(uncovered.clone());
244 mark_reachable(uncovered, &adjacency, &mut covered);
245 }
246 roots
247 }
248
249 pub fn resolve_entry(&self, entry: &str) -> Result<Option<SymbolId>, String> {
250 let mut matches = self.resolve_entries(entry)?;
251 match matches.len() {
252 0 => Ok(None),
253 1 => Ok(matches.pop()),
254 _ => Err(format!(
255 "entry `{entry}` resolves to multiple call contexts: {}",
256 matches
257 .iter()
258 .map(ToString::to_string)
259 .collect::<Vec<_>>()
260 .join(", ")
261 )),
262 }
263 }
264
265 pub fn resolve_entries(&self, entry: &str) -> Result<Vec<SymbolId>, String> {
268 let normalized_entry = entry.replace('.', "::");
269 let match_generic_base = !normalized_entry.contains('<');
270 let mut matches = self
271 .functions
272 .iter()
273 .filter(|(id, function)| {
274 let qualified = id.qualified_parts().join("::");
275 let qualified_context_base = strip_context_suffix(&qualified);
276 let name_context_base = strip_context_suffix(&id.name);
277 let display = callable_prefix(&function.label.default);
278 id.to_string() == entry
279 || id.short_name() == entry
280 || id.name == entry
281 || qualified == normalized_entry
282 || qualified.ends_with(&format!("::{normalized_entry}"))
283 || display == entry
284 || display == normalized_entry
285 || (match_generic_base
286 && [qualified_context_base, name_context_base, display]
287 .into_iter()
288 .flat_map(callable_aliases)
289 .any(|alias| {
290 alias == entry
291 || alias == normalized_entry
292 || alias.ends_with(&format!("::{normalized_entry}"))
293 }))
294 })
295 .map(|(id, _)| id.clone())
296 .collect::<Vec<_>>();
297 matches.sort();
298 matches.dedup();
299
300 if matches.len() <= 1 || self.same_contextual_function(&matches) {
301 Ok(matches)
302 } else {
303 Err(format!(
304 "entry `{entry}` is ambiguous: {}",
305 matches
306 .iter()
307 .map(ToString::to_string)
308 .collect::<Vec<_>>()
309 .join(", ")
310 ))
311 }
312 }
313
314 pub fn resolve_entries_in_file(
318 &self,
319 entry: &str,
320 file: &std::path::Path,
321 ) -> Result<Vec<SymbolId>, String> {
322 let matches = self.resolve_entries(entry)?;
323 Ok(matches
324 .into_iter()
325 .filter(|symbol| {
326 self.functions
327 .get(symbol)
328 .is_some_and(|function| same_file(&function.span.file, file))
329 })
330 .collect())
331 }
332
333 fn same_contextual_function(&self, symbols: &[SymbolId]) -> bool {
334 let Some((first_symbol, remaining)) = symbols.split_first() else {
335 return true;
336 };
337 let Some(first) = self.functions.get(first_symbol) else {
338 return false;
339 };
340 let first_display = strip_generic_arguments(callable_prefix(&first.label.default));
341 remaining.iter().all(|symbol| {
342 let Some(function) = self.functions.get(symbol) else {
343 return false;
344 };
345 strip_generic_arguments(callable_prefix(&function.label.default)) == first_display
346 && function.span == first.span
347 })
348 }
349
350 pub fn build_call_tree(&self, entry: &SymbolId, max_depth: usize) -> Option<CallNode> {
351 self.build_call_tree_in_file(entry, max_depth, None)
352 }
353
354 pub fn build_call_tree_in_file(
355 &self,
356 entry: &SymbolId,
357 max_depth: usize,
358 file: Option<&std::path::Path>,
359 ) -> Option<CallNode> {
360 self.functions.get(entry)?;
361 let expansion = Expansion {
362 max_depth,
363 file,
364 relevant: None,
365 context: None,
366 boundary_marker: None,
367 };
368 Some(self.expand(
369 entry,
370 None,
371 CallRelation::Call,
372 0,
373 &expansion,
374 &mut HashSet::new(),
375 ))
376 }
377
378 pub(crate) fn build_diff_call_tree(
379 &self,
380 entry: &SymbolId,
381 max_depth: usize,
382 file: Option<&std::path::Path>,
383 relevant: &BTreeSet<SymbolId>,
384 context: &BTreeSet<SymbolId>,
385 boundary_marker: &str,
386 ) -> Option<CallNode> {
387 self.functions.get(entry)?;
388 let expansion = Expansion {
389 max_depth,
390 file,
391 relevant: Some(relevant),
392 context: Some(context),
393 boundary_marker: Some(boundary_marker),
394 };
395 Some(self.expand(
396 entry,
397 None,
398 CallRelation::Call,
399 0,
400 &expansion,
401 &mut HashSet::new(),
402 ))
403 }
404
405 pub(crate) fn local_call_shape(
409 &self,
410 symbol: &SymbolId,
411 file: Option<&std::path::Path>,
412 ) -> Option<CallNode> {
413 let function = self.functions.get(symbol)?;
414 let expansion = Expansion {
415 max_depth: 2,
416 file,
417 relevant: None,
418 context: None,
419 boundary_marker: None,
420 };
421 let mut visiting = HashSet::from([symbol.clone()]);
422 let children = function
423 .calls
424 .iter()
425 .map(|call| {
426 let dynamic = matches!(call.target, CallTarget::Dynamic { .. });
427 let mut node = self.expand_call(symbol, call, 1, &expansion, true, &mut visiting);
428 if !dynamic {
429 node.children.clear();
430 } else {
431 for candidate in &mut node.children {
432 candidate.children.clear();
433 }
434 }
435 node
436 })
437 .collect();
438 Some(CallNode {
439 key: symbol.to_string(),
440 callsite: None,
441 label: CallLabel::new(""),
444 relation: CallRelation::Call,
445 children,
446 })
447 }
448
449 pub(crate) fn symbols_reaching(
453 &self,
454 targets: &BTreeSet<SymbolId>,
455 file: Option<&std::path::Path>,
456 ) -> BTreeSet<SymbolId> {
457 let included = |symbol: &SymbolId| {
458 self.functions.get(symbol).is_some_and(|function| {
459 file.is_none_or(|file| same_file(&function.span.file, file))
460 })
461 };
462 let mut reverse = BTreeMap::<SymbolId, BTreeSet<SymbolId>>::new();
463 for (caller, function) in &self.functions {
464 if !included(caller) {
465 continue;
466 }
467 for call in &function.calls {
468 for target in self.call_targets(caller, call) {
469 if included(&target) {
470 reverse.entry(target).or_default().insert(caller.clone());
471 }
472 }
473 }
474 }
475
476 let mut reaching = BTreeSet::new();
477 let mut pending = targets
478 .iter()
479 .filter(|target| included(target))
480 .cloned()
481 .collect::<VecDeque<_>>();
482 while let Some(symbol) = pending.pop_front() {
483 if !reaching.insert(symbol.clone()) {
484 continue;
485 }
486 if let Some(callers) = reverse.get(&symbol) {
487 pending.extend(callers.iter().cloned());
488 }
489 }
490 reaching
491 }
492
493 pub(crate) fn dispatch_context_symbols(
494 &self,
495 file: Option<&std::path::Path>,
496 ) -> BTreeSet<SymbolId> {
497 let dispatchers = self
498 .functions
499 .iter()
500 .filter(|(_, function)| {
501 file.is_none_or(|file| same_file(&function.span.file, file))
502 && function
503 .calls
504 .iter()
505 .any(|call| matches!(call.target, CallTarget::Dynamic { .. }))
506 })
507 .map(|(symbol, _)| symbol.clone())
508 .collect();
509 self.symbols_reaching(&dispatchers, file)
510 }
511
512 #[allow(clippy::too_many_arguments)]
513 fn expand(
514 &self,
515 symbol: &SymbolId,
516 callsite_label: Option<&CallLabel>,
517 relation: CallRelation,
518 depth: usize,
519 expansion: &Expansion<'_>,
520 visiting: &mut HashSet<SymbolId>,
521 ) -> CallNode {
522 let function = self
523 .functions
524 .get(symbol)
525 .expect("expanded symbols must exist in the function index");
526 let label = callsite_label
527 .cloned()
528 .unwrap_or_else(|| function.label.clone());
529
530 if depth >= expansion.max_depth {
531 let children = expansion
532 .boundary_marker
533 .filter(|_| {
534 expansion
535 .relevant
536 .is_some_and(|relevant| relevant.contains(symbol))
537 })
538 .map(|marker| CallNode {
539 key: format!("{symbol}#{marker}"),
540 callsite: None,
541 label: CallLabel::new(""),
542 relation: CallRelation::Call,
543 children: Vec::new(),
544 })
545 .into_iter()
546 .collect();
547 return CallNode {
548 key: symbol.to_string(),
549 callsite: None,
550 label,
551 relation,
552 children,
553 };
554 }
555
556 if !visiting.insert(symbol.clone()) {
557 return CallNode {
558 key: symbol.to_string(),
559 callsite: None,
560 label,
561 relation: CallRelation::BackEdge,
562 children: Vec::new(),
563 };
564 }
565
566 let last_expansions = function
567 .calls
568 .iter()
569 .enumerate()
570 .filter_map(|(index, call)| {
571 self.call_expansion_key(symbol, call)
572 .map(|key| (key, index))
573 })
574 .collect::<BTreeMap<_, _>>();
575 let children = function
576 .calls
577 .iter()
578 .enumerate()
579 .map(|(index, call)| {
580 let expand_target = self
581 .call_expansion_key(symbol, call)
582 .is_none_or(|key| last_expansions.get(&key) == Some(&index));
583 self.expand_call(symbol, call, depth + 1, expansion, expand_target, visiting)
584 })
585 .collect();
586
587 visiting.remove(symbol);
588 CallNode {
589 key: symbol.to_string(),
590 callsite: None,
591 label,
592 relation,
593 children,
594 }
595 }
596
597 fn expand_call(
598 &self,
599 caller: &SymbolId,
600 call: &CallSite,
601 depth: usize,
602 expansion: &Expansion<'_>,
603 expand_target: bool,
604 visiting: &mut HashSet<SymbolId>,
605 ) -> CallNode {
606 match &call.target {
607 CallTarget::Dynamic {
608 dispatch,
609 candidates,
610 resolution,
611 ..
612 } => {
613 let mut node = self.expand_dynamic_call(
614 dispatch,
615 &call.label,
616 candidates,
617 *resolution,
618 depth,
619 expansion,
620 expand_target,
621 visiting,
622 );
623 node.callsite = Some(call.id.clone());
624 node
625 }
626 CallTarget::Direct(target) => {
627 let mut node = self.expand_direct_call(
628 target.clone(),
629 &call.label,
630 CallRelation::Call,
631 depth,
632 expansion,
633 expand_target,
634 visiting,
635 );
636 node.callsite = Some(call.id.clone());
637 node
638 }
639 CallTarget::Indirect { .. } => CallNode {
640 key: format!("{}://indirect/{}", caller.language, call.id.0),
641 callsite: Some(call.id.clone()),
642 label: call.label.with_suffix(" [indirect]"),
643 relation: CallRelation::Call,
644 children: Vec::new(),
645 },
646 CallTarget::Unresolved => {
647 let target = self.resolve_call(caller, &call.syntax);
648 if let Some(target) = target {
649 let mut node = self.expand_direct_call(
650 target,
651 &call.label,
652 CallRelation::Call,
653 depth,
654 expansion,
655 expand_target,
656 visiting,
657 );
658 node.callsite = Some(call.id.clone());
659 node
660 } else {
661 CallNode {
662 key: format!("{}://?{}", caller.language, call.syntax.key_fragment()),
663 callsite: Some(call.id.clone()),
664 label: call.label.clone(),
665 relation: CallRelation::Call,
666 children: Vec::new(),
667 }
668 }
669 }
670 }
671 }
672
673 #[allow(clippy::too_many_arguments)]
674 fn expand_direct_call(
675 &self,
676 target: SymbolId,
677 label: &CallLabel,
678 relation: CallRelation,
679 depth: usize,
680 expansion: &Expansion<'_>,
681 expand_target: bool,
682 visiting: &mut HashSet<SymbolId>,
683 ) -> CallNode {
684 let can_expand = self.functions.get(&target).is_some_and(|function| {
685 expansion
686 .file
687 .is_none_or(|file| same_file(&function.span.file, file))
688 }) && expansion.relevant.is_none_or(|relevant| {
689 relevant.contains(&target)
690 || expansion
691 .context
692 .is_some_and(|context| context.contains(&target))
693 });
694 if can_expand && expand_target {
695 self.expand(&target, Some(label), relation, depth, expansion, visiting)
696 } else {
697 CallNode {
698 key: target.to_string(),
699 callsite: None,
700 label: label.clone(),
701 relation,
702 children: Vec::new(),
703 }
704 }
705 }
706
707 #[allow(clippy::too_many_arguments)]
708 fn expand_dynamic_call(
709 &self,
710 dispatch: &SymbolId,
711 label: &CallLabel,
712 candidates: &[DispatchCandidate],
713 resolution: DispatchResolution,
714 depth: usize,
715 expansion: &Expansion<'_>,
716 expand_target: bool,
717 visiting: &mut HashSet<SymbolId>,
718 ) -> CallNode {
719 let children = if !expand_target
720 || depth >= expansion.max_depth
721 || resolution == DispatchResolution::Unresolved
722 {
723 Vec::new()
724 } else {
725 candidates
726 .iter()
727 .map(|candidate| {
728 self.expand_direct_call(
729 candidate.target.clone(),
730 &candidate.label,
731 CallRelation::DispatchCandidate,
732 depth + 1,
733 expansion,
734 true,
735 visiting,
736 )
737 })
738 .chain(
739 (resolution == DispatchResolution::Partial).then(|| CallNode {
740 key: format!("{dispatch}#unresolved"),
741 callsite: None,
742 label: CallLabel::new("… unresolved targets"),
743 relation: CallRelation::DispatchCandidate,
744 children: Vec::new(),
745 }),
746 )
747 .collect()
748 };
749
750 let label = match resolution {
751 DispatchResolution::Complete => label.clone(),
752 DispatchResolution::Partial => label.with_suffix(" [partial]"),
753 DispatchResolution::Unresolved => label.with_suffix(" [unresolved]"),
754 };
755
756 CallNode {
757 key: dispatch.to_string(),
758 callsite: None,
759 label,
760 relation: CallRelation::Call,
761 children,
762 }
763 }
764
765 fn call_targets(&self, caller: &SymbolId, call: &CallSite) -> Vec<SymbolId> {
766 match &call.target {
767 CallTarget::Direct(target) => vec![target.clone()],
768 CallTarget::Dynamic { candidates, .. } => candidates
769 .iter()
770 .map(|candidate| candidate.target.clone())
771 .collect(),
772 CallTarget::Indirect { .. } => Vec::new(),
773 CallTarget::Unresolved => self
774 .resolve_call(caller, &call.syntax)
775 .into_iter()
776 .collect(),
777 }
778 }
779
780 fn call_expansion_key(&self, caller: &SymbolId, call: &CallSite) -> Option<CallExpansionKey> {
781 match &call.target {
782 CallTarget::Direct(target) => Some(CallExpansionKey::Direct(target.clone())),
783 CallTarget::Unresolved => self
784 .resolve_call(caller, &call.syntax)
785 .map(CallExpansionKey::Direct),
786 CallTarget::Dynamic {
787 dispatch,
788 candidates,
789 resolution,
790 ..
791 } => Some(CallExpansionKey::Dynamic(
792 dispatch.clone(),
793 candidates
794 .iter()
795 .map(|candidate| candidate.target.clone())
796 .collect(),
797 *resolution,
798 )),
799 CallTarget::Indirect { .. } => None,
800 }
801 }
802
803 fn resolve_call(&self, caller: &SymbolId, call: &CallSyntax) -> Option<SymbolId> {
804 let mut preferred = Vec::new();
805
806 match call {
807 CallSyntax::SelfMethod(method) => {
808 if let Some(container) = &caller.container {
809 preferred.extend(self.named_symbols(&caller.language, method).filter(
810 |candidate| {
811 candidate.module == caller.module
812 && candidate.container.as_ref() == Some(container)
813 },
814 ));
815 }
816 }
817 CallSyntax::Path(parts) if !parts.is_empty() => {
818 let name = parts.last().expect("non-empty path");
819 if parts.first().is_some_and(|part| part == "Self") {
820 if let Some(container) = &caller.container {
821 preferred.extend(self.named_symbols(&caller.language, name).filter(
822 |candidate| {
823 candidate.module == caller.module
824 && candidate.container.as_ref() == Some(container)
825 },
826 ));
827 }
828 } else if parts.len() == 1 {
829 preferred.extend(self.named_symbols(&caller.language, name).filter(
830 |candidate| {
831 candidate.module == caller.module && candidate.container.is_none()
832 },
833 ));
834 } else {
835 preferred.extend(
836 self.named_symbols(&caller.language, name)
837 .filter(|candidate| path_suffix_matches(candidate, parts)),
838 );
839 }
840 }
841 CallSyntax::Method { receiver, method } => {
842 if receiver.chars().next().is_some_and(char::is_uppercase) {
844 preferred.extend(self.named_symbols(&caller.language, method).filter(
845 |candidate| {
846 candidate
847 .container
848 .as_deref()
849 .is_some_and(|container| base_container(container) == receiver)
850 },
851 ));
852 }
853 }
854 CallSyntax::Path(_) => {}
855 }
856
857 let preferred = unique(preferred);
858 if preferred.len() == 1 {
859 return preferred.into_iter().next().cloned();
860 }
861 if preferred.len() > 1 {
862 return None;
863 }
864
865 let name = match call {
866 CallSyntax::Path(parts) => parts.last()?,
867 CallSyntax::SelfMethod(method) | CallSyntax::Method { method, .. } => method,
868 };
869 let fallback = unique(self.named_symbols(&caller.language, name));
870 (fallback.len() == 1).then(|| fallback[0].clone())
871 }
872
873 fn named_symbols<'a>(
874 &'a self,
875 language: &crate::model::LanguageId,
876 name: &str,
877 ) -> impl Iterator<Item = &'a SymbolId> {
878 self.functions_by_name
879 .get(&(language.clone(), name.to_owned()))
880 .into_iter()
881 .flatten()
882 }
883}
884
885fn mark_reachable(
886 root: &SymbolId,
887 adjacency: &BTreeMap<SymbolId, BTreeSet<SymbolId>>,
888 covered: &mut BTreeSet<SymbolId>,
889) {
890 if !covered.insert(root.clone()) {
891 return;
892 }
893 if let Some(targets) = adjacency.get(root) {
894 for target in targets {
895 mark_reachable(target, adjacency, covered);
896 }
897 }
898}
899
900fn same_file(left: &std::path::Path, right: &std::path::Path) -> bool {
901 left == right
902 || left
903 .canonicalize()
904 .ok()
905 .zip(right.canonicalize().ok())
906 .is_some_and(|(left, right)| left == right)
907}
908
909fn callable_prefix(label: &str) -> &str {
910 let mut depth = 0usize;
911 for (index, character) in label.char_indices().rev() {
912 match character {
913 ')' => depth += 1,
914 '(' => {
915 let Some(next_depth) = depth.checked_sub(1) else {
916 return label;
917 };
918 depth = next_depth;
919 if depth == 0 {
920 return &label[..index];
921 }
922 }
923 _ => {}
924 }
925 }
926 label
927}
928
929fn strip_context_suffix(value: &str) -> &str {
930 value.split_once("#ctx[").map_or(value, |(base, _)| base)
931}
932
933fn strip_generic_arguments(value: &str) -> String {
934 let mut stripped = String::with_capacity(value.len());
935 let mut depth = 0usize;
936 for character in value.chars() {
937 match character {
938 '<' => depth += 1,
939 '>' if depth > 0 => depth -= 1,
940 _ if depth == 0 => stripped.push(character),
941 _ => {}
942 }
943 }
944 stripped
945}
946
947fn callable_aliases(value: &str) -> Vec<String> {
948 if let Some((self_type, trait_name, method)) = qualified_trait_callable(value) {
949 let self_type = strip_generic_arguments(self_type);
950 let trait_name = strip_generic_arguments(trait_name);
951 let method = strip_generic_arguments(method);
952 return vec![
953 format!("{self_type} as {trait_name}::{method}"),
954 format!("{self_type}::{method}"),
955 format!("{trait_name}::{method}"),
956 ];
957 }
958 let callable = strip_generic_arguments(value);
959 let mut aliases = vec![callable.clone()];
960 if let Some((self_type, trait_method)) = callable.split_once(" as ")
961 && let Some((_, method)) = trait_method.rsplit_once("::")
962 {
963 aliases.push(format!("{self_type}::{method}"));
964 aliases.push(trait_method.to_owned());
965 }
966 aliases
967}
968
969fn qualified_trait_callable(value: &str) -> Option<(&str, &str, &str)> {
970 if !value.starts_with('<') {
971 return None;
972 }
973 let mut depth = 0usize;
974 let mut qualification_end = None;
975 for (index, character) in value.char_indices() {
976 match character {
977 '<' => depth += 1,
978 '>' => {
979 depth = depth.checked_sub(1)?;
980 if depth == 0 {
981 qualification_end = Some(index);
982 break;
983 }
984 }
985 _ => {}
986 }
987 }
988 let qualification_end = qualification_end?;
989 let qualification = &value[1..qualification_end];
990 let method = value[qualification_end + 1..].strip_prefix("::")?;
991 let (self_type, trait_name) = qualification.rsplit_once(" as ")?;
992 Some((self_type, trait_name, method))
993}
994
995fn unique<'a>(values: impl IntoIterator<Item = &'a SymbolId>) -> Vec<&'a SymbolId> {
996 values
997 .into_iter()
998 .collect::<BTreeSet<_>>()
999 .into_iter()
1000 .collect()
1001}
1002
1003fn base_container(container: &str) -> &str {
1004 container
1005 .split_once(" as ")
1006 .map_or(container, |(base, _)| base)
1007}
1008
1009fn path_suffix_matches(candidate: &SymbolId, raw: &[String]) -> bool {
1010 let raw = raw
1011 .iter()
1012 .filter(|part| !matches!(part.as_str(), "crate" | "self" | "super"))
1013 .map(String::as_str)
1014 .collect::<Vec<_>>();
1015 let candidate = candidate.qualified_parts();
1016 raw.len() <= candidate.len() && candidate[candidate.len() - raw.len()..] == raw
1017}