Skip to main content

diffkit/
graph.rs

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