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
25/// Identity of a callee subtree for sibling-call presentation. Every source
26/// call remains visible; only the last equivalent call expands this subtree.
27#[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                    // A direct target outside the analyzed graph is a normal external
107                    // leaf, not incomplete resolution. Keep verbose diagnostics focused
108                    // on call sites whose candidate completeness needs explanation.
109                    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    /// Return one deterministic root for every source component in the graph.
141    /// Functions with no incoming call are natural roots. A closed recursive
142    /// component has no such function, so its earliest source definition is
143    /// used as that component's presentation root.
144    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    /// Resolve an entry to all compiler-proven call contexts of the same
266    /// source function. Ordinary name ambiguity remains an error.
267    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    /// Resolve only definitions whose bodies belong to `file`. This keeps an
315    /// explicit entry in file mode from silently selecting an equally named
316    /// function in another project file.
317    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    /// A function's local call shape, without expanding callees. Dynamic
406    /// dispatch candidates remain part of the shape because changing the
407    /// candidate set is itself a call-graph change.
408    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            // Declaration labels are only visible when the function itself is
442            // selected as an entry. They must not propagate to every caller.
443            label: CallLabel::new(""),
444            relation: CallRelation::Call,
445            children,
446        })
447    }
448
449    /// Return every function in this graph that can reach one of `targets`.
450    /// This graph-level pass is linear in the call graph and avoids first
451    /// materializing a potentially exponential call tree.
452    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                // Static-looking receiver names can be matched without type inference.
843                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}