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