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 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 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 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 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 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 label: CallLabel::new(""),
436 relation: CallRelation::Call,
437 children,
438 })
439 }
440
441 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 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}