1use crate::declarations::{RubyFieldScope, extract_name_path};
2use crate::graph::RubyGraphSource;
3use crate::graph_support::{RubySemanticFacts, RubySource};
4use crate::mixins::{ruby_forward_mixin_specs, ruby_forward_superclass_targets};
5use brokk_bifrost_core::analyzer::model::RubyMethodDispatchMode;
6use brokk_bifrost_core::analyzer::type_relations::TypeRelationKind;
7use brokk_bifrost_core::analyzer::{BoundedDefinitionLookup, CodeUnit, ProjectFile};
8use brokk_bifrost_core::hash::{HashMap, HashSet};
9use std::cell::RefCell;
10use tree_sitter::Node;
11
12#[derive(Clone, Copy, PartialEq, Eq)]
13pub enum RubyTargetKind {
14 TypeOrConstant,
15 Method,
16 Field(RubyFieldScope),
17}
18
19pub struct RubyTargetSpec {
20 pub target: CodeUnit,
21 pub kind: RubyTargetKind,
22 pub member_name: String,
23 pub field_owner: Option<String>,
24}
25
26pub struct RubyFieldTarget {
27 pub owner: String,
28 pub scope: RubyFieldScope,
29 pub member: String,
30}
31
32impl RubyTargetSpec {
33 pub fn from_target(
34 graph: &RubyGraphSource<'_>,
35 ruby: &dyn RubySource,
36 target: &CodeUnit,
37 ) -> Option<Self> {
38 if target.is_field()
39 && let Some(field) = ruby_field_target(target)
40 {
41 return Some(Self {
42 target: target.clone(),
43 kind: RubyTargetKind::Field(field.scope),
44 member_name: field.member,
45 field_owner: Some(field.owner),
46 });
47 }
48 if target.is_class() || target.is_module() || target.is_field() {
49 return Some(Self {
50 target: target.clone(),
51 kind: RubyTargetKind::TypeOrConstant,
52 member_name: target.identifier().to_string(),
53 field_owner: None,
54 });
55 }
56 if target.is_function() {
57 let class_side_declaration = matches!(
58 ruby.method_dispatch_mode(target),
59 RubyMethodDispatchMode::Singleton | RubyMethodDispatchMode::ModuleFunction
60 );
61 if graph.index.parent_of(target).is_none() && class_side_declaration {
62 return None;
63 }
64 return Some(Self {
65 target: target.clone(),
66 kind: RubyTargetKind::Method,
67 member_name: target.identifier().to_string(),
68 field_owner: None,
69 });
70 }
71 None
72 }
73}
74
75pub fn ruby_field_target(target: &CodeUnit) -> Option<RubyFieldTarget> {
76 let member = target.identifier();
77 let short_name = target.short_name();
83 if member.starts_with("@@") {
84 let owner = short_name.strip_suffix(&format!(".{member}"))?;
85 return (!owner.is_empty()).then(|| RubyFieldTarget {
86 owner: owner.to_string(),
87 scope: RubyFieldScope::ClassVariable,
88 member: member.to_string(),
89 });
90 }
91 if member.starts_with('@') {
92 let singleton_suffix = format!(".$singleton.{member}");
93 if let Some(owner) = short_name.strip_suffix(&singleton_suffix) {
94 return (!owner.is_empty()).then(|| RubyFieldTarget {
95 owner: owner.to_string(),
96 scope: RubyFieldScope::SingletonClass,
97 member: member.to_string(),
98 });
99 }
100 let owner = short_name.strip_suffix(&format!(".{member}"))?;
101 return (!owner.is_empty()).then(|| RubyFieldTarget {
102 owner: owner.to_string(),
103 scope: RubyFieldScope::Instance,
104 member: member.to_string(),
105 });
106 }
107 None
108}
109
110#[derive(Clone, Copy, PartialEq, Eq)]
111pub enum ReceiverMode {
112 Instance,
113 Class,
114 TopLevel,
115}
116
117#[derive(Clone, Copy)]
118pub enum ExplicitReceiverLookup {
119 Bare,
120 ReceiverOnly,
121}
122
123#[derive(Clone)]
124pub struct ReceiverType {
125 pub owner_fq_name: String,
126 pub mode: ReceiverMode,
127}
128
129pub struct RubySemanticIndex<'a> {
130 pub graph: RubyGraphSource<'a>,
131 pub ruby: &'a dyn RubySource,
132 facts: Option<&'a RubySemanticFacts>,
133 target: Option<CodeUnit>,
134 forward_owner_facts: RefCell<HashMap<String, RubyForwardOwnerFacts>>,
135 pub factory_return_cache: RefCell<HashMap<FactoryInferenceKey, Option<String>>>,
136}
137
138#[derive(Clone, Default)]
139struct RubyForwardOwnerFacts {
140 ancestors: Vec<String>,
141 included: Vec<String>,
142 prepended: Vec<String>,
143 extended: Vec<String>,
144}
145
146impl<'a> RubySemanticIndex<'a> {
147 pub fn build(
148 graph: RubyGraphSource<'a>,
149 ruby: &'a dyn RubySource,
150 spec: &RubyTargetSpec,
151 ) -> Self {
152 Self::build_with_target(graph, ruby, Some(spec.target.clone()))
153 }
154
155 pub fn build_for_lookup(graph: RubyGraphSource<'a>, ruby: &'a dyn RubySource) -> Self {
156 Self::build_with_target(graph, ruby, None)
157 }
158
159 fn build_with_target(
160 graph: RubyGraphSource<'a>,
161 ruby: &'a dyn RubySource,
162 target: Option<CodeUnit>,
163 ) -> Self {
164 Self {
165 graph,
166 ruby,
167 facts: target.as_ref().map(|_| ruby.semantic_facts()),
168 target,
169 forward_owner_facts: RefCell::new(HashMap::default()),
170 factory_return_cache: RefCell::new(HashMap::default()),
171 }
172 }
173
174 pub fn visible_files_from(&self, file: &ProjectFile) -> HashSet<ProjectFile> {
175 let mut visible = HashSet::default();
176 visible.insert(file.clone());
177 if let Some(zeitwerk_files) =
178 crate::imports::ruby_zeitwerk_visible_files_for(self.ruby, file)
179 {
180 visible.extend(zeitwerk_files.iter().cloned());
181 }
182 let mut stack = crate::imports::ruby_required_files(self.ruby, file);
183 while let Some(next) = stack.pop() {
184 if !visible.insert(next.clone()) {
185 continue;
186 }
187 stack.extend(crate::imports::ruby_required_files(self.ruby, &next));
188 }
189 visible
190 }
191
192 pub fn visible_files_from_bounded(
199 &self,
200 file: &ProjectFile,
201 max_files: usize,
202 ) -> Option<HashSet<ProjectFile>> {
203 let mut visible = HashSet::default();
204 visible.insert(file.clone());
205 let mut stack = crate::imports::ruby_required_files(self.ruby, file);
206 while let Some(next) = stack.pop() {
207 if !visible.insert(next.clone()) {
208 continue;
209 }
210 if visible.len() > max_files {
211 return None;
212 }
213 stack.extend(crate::imports::ruby_required_files(self.ruby, &next));
214 }
215 Some(visible)
216 }
217
218 pub fn resolve_constant(
219 &self,
220 file: &ProjectFile,
221 visible_files: &HashSet<ProjectFile>,
222 lexical_stack: &[String],
223 node: Node<'_>,
224 source: &str,
225 ) -> Option<CodeUnit> {
226 let path = extract_name_path(node, source);
227 self.resolve_constant_path(
228 file,
229 visible_files,
230 lexical_stack,
231 &path.segments,
232 path.absolute,
233 true,
234 )
235 }
236
237 pub fn resolve_project_local_constant(
241 &self,
242 file: &ProjectFile,
243 visible_files: &HashSet<ProjectFile>,
244 lexical_stack: &[String],
245 node: Node<'_>,
246 source: &str,
247 ) -> Option<CodeUnit> {
248 let path = extract_name_path(node, source);
249 self.resolve_constant_path(
250 file,
251 visible_files,
252 lexical_stack,
253 &path.segments,
254 path.absolute,
255 false,
256 )
257 }
258
259 pub fn declares_constant_anywhere(
269 &self,
270 lexical_stack: &[String],
271 node: Node<'_>,
272 source: &str,
273 ) -> bool {
274 let path = extract_name_path(node, source);
275 let Some(candidates) =
276 constant_lookup_candidates(lexical_stack, &path.segments, path.absolute)
277 else {
278 return false;
279 };
280 candidates
281 .iter()
282 .any(|candidate| self.graph.index.definitions(candidate).next().is_some())
283 }
284
285 pub fn resolve_constant_name(
286 &self,
287 file: &ProjectFile,
288 visible_files: &HashSet<ProjectFile>,
289 lexical_stack: &[String],
290 name: &str,
291 ) -> Option<CodeUnit> {
292 self.resolve_constant_path(
293 file,
294 visible_files,
295 lexical_stack,
296 &[name.to_string()],
297 false,
298 true,
299 )
300 }
301
302 fn resolve_constant_path(
303 &self,
304 file: &ProjectFile,
305 visible_files: &HashSet<ProjectFile>,
306 lexical_stack: &[String],
307 segments: &[String],
308 absolute: bool,
309 include_autoload: bool,
310 ) -> Option<CodeUnit> {
311 let candidates = constant_lookup_candidates(lexical_stack, segments, absolute)?;
312
313 candidates.into_iter().find_map(|candidate| {
314 let autoload_files = if include_autoload {
315 crate::imports::ruby_autoload_visible_files_for_constant(self.ruby, &candidate)
316 } else {
317 HashSet::default()
318 };
319 self.graph.index.definitions(&candidate).find(|unit| {
320 visible_files.contains(unit.source())
321 || unit.source() == file
322 || autoload_files.contains(unit.source())
323 })
324 })
325 }
326
327 pub fn target_matches_constant(&self, unit: &CodeUnit) -> bool {
328 self.target
329 .as_ref()
330 .is_some_and(|target| unit == target || unit.fq_name() == target.fq_name())
331 }
332
333 pub fn resolve_method_candidates(
334 &self,
335 support: &dyn BoundedDefinitionLookup,
336 visible_files: &HashSet<ProjectFile>,
337 receiver: &ReceiverType,
338 member: &str,
339 ) -> Vec<CodeUnit> {
340 self.method_candidates(support, visible_files, receiver, member, None)
341 }
342
343 pub fn resolve_method_candidates_traced(
350 &self,
351 support: &dyn BoundedDefinitionLookup,
352 visible_files: &HashSet<ProjectFile>,
353 receiver: &ReceiverType,
354 member: &str,
355 find: &mut Option<RubyMethodFind>,
356 ) -> Vec<CodeUnit> {
357 self.method_candidates(support, visible_files, receiver, member, Some(find))
358 }
359
360 fn method_candidates(
361 &self,
362 support: &dyn BoundedDefinitionLookup,
363 visible_files: &HashSet<ProjectFile>,
364 receiver: &ReceiverType,
365 member: &str,
366 mut find: Option<&mut Option<RubyMethodFind>>,
367 ) -> Vec<CodeUnit> {
368 let visible_files: Vec<ProjectFile> = visible_files.iter().cloned().collect();
369 let mut seen = HashSet::default();
370 let mut push_owner = |owner: &str, mode: RubyMethodLookupMode, out: &mut Vec<CodeUnit>| {
371 for unit in support.fqn_direct_children(owner) {
372 if unit.is_function()
373 && unit.identifier() == member
374 && visible_files.contains(unit.source())
375 && ruby_method_lookup_mode_matches(self.ruby, &unit, mode)
376 && seen.insert(unit.clone())
377 {
378 out.push(unit);
379 }
380 }
381 };
382
383 match receiver.mode {
384 ReceiverMode::TopLevel => {
385 self.resolve_top_level_method_candidates(support, &visible_files, member)
386 }
387 ReceiverMode::Instance => {
388 for owner in self.forward_receiver_owner_lookup_order(
389 support,
390 &visible_files,
391 &receiver.owner_fq_name,
392 ) {
393 let mut prepended = Vec::new();
394 let mut prepended_from = None;
395 for mixin in self
396 .mixin_owners(
397 support,
398 &visible_files,
399 &owner,
400 TypeRelationKind::MixinPrepend,
401 )
402 .into_iter()
403 .rev()
404 {
405 push_owner(&mixin, RubyMethodLookupMode::InstanceMethod, &mut prepended);
406 if !prepended.is_empty() {
407 prepended_from = Some(mixin);
408 break;
409 }
410 }
411 if !prepended.is_empty() {
412 record_find(
413 &mut find,
414 &owner,
415 prepended_from,
416 Some(TypeRelationKind::MixinPrepend),
417 false,
418 );
419 return prepended;
420 }
421
422 let mut direct = Vec::new();
423 push_owner(&owner, RubyMethodLookupMode::InstanceMethod, &mut direct);
424 if !direct.is_empty() {
425 record_find(&mut find, &owner, None, None, false);
426 return direct;
427 }
428
429 let mut included = Vec::new();
430 let mut included_from = None;
431 for mixin in self
432 .mixin_owners(
433 support,
434 &visible_files,
435 &owner,
436 TypeRelationKind::MixinInclude,
437 )
438 .into_iter()
439 .rev()
440 {
441 push_owner(&mixin, RubyMethodLookupMode::InstanceMethod, &mut included);
442 if !included.is_empty() {
443 included_from = Some(mixin);
444 break;
445 }
446 }
447 if !included.is_empty() {
448 record_find(
449 &mut find,
450 &owner,
451 included_from,
452 Some(TypeRelationKind::MixinInclude),
453 false,
454 );
455 return included;
456 }
457 }
458 Vec::new()
459 }
460 ReceiverMode::Class => {
461 for owner in self.forward_receiver_owner_lookup_order(
462 support,
463 &visible_files,
464 &receiver.owner_fq_name,
465 ) {
466 let mut direct = Vec::new();
467 push_owner(&owner, RubyMethodLookupMode::SingletonMethod, &mut direct);
468 if !direct.is_empty() {
469 record_find(&mut find, &owner, None, None, true);
470 return direct;
471 }
472
473 let mut extended = Vec::new();
474 let mut extended_from = None;
475 for mixin in self
476 .mixin_owners(
477 support,
478 &visible_files,
479 &owner,
480 TypeRelationKind::MixinExtend,
481 )
482 .into_iter()
483 .rev()
484 {
485 push_owner(&mixin, RubyMethodLookupMode::InstanceMethod, &mut extended);
486 if !extended.is_empty() {
487 extended_from = Some(mixin);
488 break;
489 }
490 }
491 if !extended.is_empty() {
492 record_find(
493 &mut find,
494 &owner,
495 extended_from,
496 Some(TypeRelationKind::MixinExtend),
497 true,
498 );
499 return extended;
500 }
501 }
502 Vec::new()
503 }
504 }
505 }
506
507 pub fn resolve_bare_method_candidates(
508 &self,
509 support: &dyn BoundedDefinitionLookup,
510 visible_files: &HashSet<ProjectFile>,
511 receiver: &ReceiverType,
512 member: &str,
513 ) -> Vec<CodeUnit> {
514 self.bare_method_candidates(support, visible_files, receiver, member, None)
515 }
516
517 pub fn resolve_bare_method_candidates_traced(
524 &self,
525 support: &dyn BoundedDefinitionLookup,
526 visible_files: &HashSet<ProjectFile>,
527 receiver: &ReceiverType,
528 member: &str,
529 find: &mut Option<RubyMethodFind>,
530 ) -> Vec<CodeUnit> {
531 self.bare_method_candidates(support, visible_files, receiver, member, Some(find))
532 }
533
534 fn bare_method_candidates(
535 &self,
536 support: &dyn BoundedDefinitionLookup,
537 visible_files: &HashSet<ProjectFile>,
538 receiver: &ReceiverType,
539 member: &str,
540 find: Option<&mut Option<RubyMethodFind>>,
541 ) -> Vec<CodeUnit> {
542 let candidates = self.method_candidates(support, visible_files, receiver, member, find);
543 if !candidates.is_empty() || receiver.mode == ReceiverMode::TopLevel {
544 return candidates;
545 }
546 let visible_files: Vec<ProjectFile> = visible_files.iter().cloned().collect();
547 self.resolve_top_level_method_candidates(support, &visible_files, member)
548 }
549
550 pub fn direct_ancestor_owners(
554 &self,
555 support: &dyn BoundedDefinitionLookup,
556 visible_files: &[ProjectFile],
557 owner: &str,
558 ) -> Vec<String> {
559 if let Some(facts) = self.facts {
560 let mut direct: Vec<String> = facts
561 .ancestors
562 .get(owner)
563 .map(|items| items.iter().cloned().collect())
564 .unwrap_or_default();
565 direct.sort();
566 return direct;
567 }
568 self.forward_owner_facts(support, visible_files, owner)
569 .ancestors
570 }
571
572 pub fn mixin_owners_of(
575 &self,
576 support: &dyn BoundedDefinitionLookup,
577 visible_files: &[ProjectFile],
578 owner: &str,
579 kind: TypeRelationKind,
580 ) -> Vec<String> {
581 self.mixin_owners(support, visible_files, owner, kind)
582 }
583
584 pub fn owner_unit(
588 &self,
589 support: &dyn BoundedDefinitionLookup,
590 visible_files: &[ProjectFile],
591 owner: &str,
592 ) -> Option<CodeUnit> {
593 support.fqn(owner).into_iter().find(|unit| {
594 (unit.is_class() || unit.is_module())
595 && unit.fq_name() == owner
596 && visible_files.contains(unit.source())
597 })
598 }
599
600 fn resolve_top_level_method_candidates(
601 &self,
602 support: &dyn BoundedDefinitionLookup,
603 visible_files: &[ProjectFile],
604 member: &str,
605 ) -> Vec<CodeUnit> {
606 support
607 .file_identifier_in_files(visible_files, member)
608 .into_iter()
609 .filter(|unit| {
610 unit.is_function()
611 && unit.identifier() == member
612 && self.graph.index.parent_of(unit).is_none()
613 && !ruby_method_lookup_mode_matches(
614 self.ruby,
615 unit,
616 RubyMethodLookupMode::SingletonMethod,
617 )
618 })
619 .collect()
620 }
621
622 fn mixin_owners(
623 &self,
624 support: &dyn BoundedDefinitionLookup,
625 visible_files: &[ProjectFile],
626 owner: &str,
627 kind: TypeRelationKind,
628 ) -> Vec<String> {
629 if let Some(facts) = self.facts {
630 let index = match kind {
631 TypeRelationKind::MixinInclude => &facts.mixin_included_owners,
632 TypeRelationKind::MixinPrepend => &facts.mixin_prepended_owners,
633 TypeRelationKind::MixinExtend => &facts.mixin_class_owners,
634 _ => return Vec::new(),
635 };
636 return index.get(owner).cloned().unwrap_or_default();
637 }
638 let facts = self.forward_owner_facts(support, visible_files, owner);
639 match kind {
640 TypeRelationKind::MixinInclude => facts.included,
641 TypeRelationKind::MixinPrepend => facts.prepended,
642 TypeRelationKind::MixinExtend => facts.extended,
643 _ => Vec::new(),
644 }
645 }
646
647 pub fn ancestor_lookup_order(&self, owner: &str) -> Vec<String> {
648 let Some(facts) = self.facts else {
649 return Vec::new();
650 };
651 let mut out = Vec::new();
652 let mut visited = HashSet::default();
653 let mut stack: Vec<String> = facts
654 .ancestors
655 .get(owner)
656 .map(|items| items.iter().cloned().collect())
657 .unwrap_or_default();
658 while let Some(candidate) = stack.pop() {
659 if !visited.insert(candidate.clone()) {
660 continue;
661 }
662 out.push(candidate.clone());
663 if let Some(next) = facts.ancestors.get(&candidate) {
664 stack.extend(next.iter().cloned());
665 }
666 }
667 out
668 }
669
670 pub fn forward_ancestor_lookup_order(
671 &self,
672 support: &dyn BoundedDefinitionLookup,
673 visible_files: &[ProjectFile],
674 owner: &str,
675 ) -> Vec<String> {
676 if self.facts.is_some() {
677 return self.ancestor_lookup_order(owner);
678 }
679 let mut out = Vec::new();
680 let mut visited = HashSet::default();
681 let mut stack = self
682 .forward_owner_facts(support, visible_files, owner)
683 .ancestors;
684 stack.reverse();
685 while let Some(candidate) = stack.pop() {
686 if !visited.insert(candidate.clone()) {
687 continue;
688 }
689 out.push(candidate.clone());
690 let mut next = self
691 .forward_owner_facts(support, visible_files, &candidate)
692 .ancestors;
693 next.reverse();
694 stack.extend(next);
695 }
696 out
697 }
698
699 fn forward_receiver_owner_lookup_order(
700 &self,
701 support: &dyn BoundedDefinitionLookup,
702 visible_files: &[ProjectFile],
703 owner: &str,
704 ) -> Vec<String> {
705 let mut owners = vec![owner.to_string()];
706 owners.extend(self.forward_ancestor_lookup_order(support, visible_files, owner));
707 owners
708 }
709
710 fn forward_owner_facts(
711 &self,
712 support: &dyn BoundedDefinitionLookup,
713 visible_files: &[ProjectFile],
714 owner: &str,
715 ) -> RubyForwardOwnerFacts {
716 if let Some(cached) = self.forward_owner_facts.borrow().get(owner) {
717 return cached.clone();
718 }
719 let Some(owner_unit) = self.owner_unit(support, visible_files, owner) else {
720 self.forward_owner_facts
721 .borrow_mut()
722 .insert(owner.to_string(), RubyForwardOwnerFacts::default());
723 return RubyForwardOwnerFacts::default();
724 };
725
726 let specs = ruby_forward_mixin_specs(self.ruby, &owner_unit);
727 let mixin_names: HashSet<String> =
728 specs.iter().map(|spec| spec.raw_target.clone()).collect();
729 let mut facts = RubyForwardOwnerFacts::default();
730 for spec in specs {
731 let Some(target) =
732 self.resolve_forward_owner_name(support, visible_files, owner, &spec.raw_target)
733 else {
734 continue;
735 };
736 match spec.kind {
737 TypeRelationKind::MixinInclude => facts.included.push(target),
738 TypeRelationKind::MixinPrepend => facts.prepended.push(target),
739 TypeRelationKind::MixinExtend => facts.extended.push(target),
740 _ => {}
741 }
742 }
743 for raw in ruby_forward_superclass_targets(self.ruby, &owner_unit) {
744 if mixin_names.contains(&raw) {
745 continue;
746 }
747 if let Some(target) =
748 self.resolve_forward_owner_name(support, visible_files, owner, &raw)
749 {
750 facts.ancestors.push(target);
751 }
752 }
753 facts.ancestors.dedup();
754 facts.included.dedup();
755 facts.prepended.dedup();
756 facts.extended.dedup();
757 self.forward_owner_facts
758 .borrow_mut()
759 .insert(owner.to_string(), facts.clone());
760 facts
761 }
762
763 fn resolve_forward_owner_name(
764 &self,
765 support: &dyn BoundedDefinitionLookup,
766 visible_files: &[ProjectFile],
767 lexical_owner: &str,
768 raw: &str,
769 ) -> Option<String> {
770 let mut candidate_names = vec![raw.to_string()];
771 let mut prefix = lexical_owner;
772 while let Some((parent, _)) = prefix.rsplit_once('$') {
775 candidate_names.push(format!("{parent}${raw}"));
776 prefix = parent;
777 }
778 for candidate in candidate_names {
779 let mut matches = support.fqn(&candidate);
780 matches.retain(|unit| {
781 (unit.is_class() || unit.is_module()) && visible_files.contains(unit.source())
782 });
783 matches.sort();
784 matches.dedup();
785 if matches.len() == 1 {
786 return Some(matches.remove(0).fq_name());
787 }
788 }
789
790 let identifier = raw.rsplit('$').next().unwrap_or(raw); let mut matches = support.file_identifier_in_files(visible_files, identifier);
792 matches.retain(|unit| {
793 (unit.is_class() || unit.is_module()) && unit.identifier() == identifier
794 });
795 matches.sort();
796 matches.dedup();
797 (matches.len() == 1).then(|| matches.remove(0).fq_name())
798 }
799}
800
801#[derive(Debug, Clone, PartialEq, Eq)]
807pub struct RubyMethodFind {
808 pub reached_from: String,
811 pub owner: String,
814 pub mixin: Option<TypeRelationKind>,
817 pub class_side: bool,
819}
820
821fn record_find(
824 find: &mut Option<&mut Option<RubyMethodFind>>,
825 reached_from: &str,
826 mixin_owner: Option<String>,
827 mixin: Option<TypeRelationKind>,
828 class_side: bool,
829) {
830 let Some(slot) = find.as_deref_mut() else {
831 return;
832 };
833 debug_assert_eq!(
834 mixin_owner.is_some(),
835 mixin.is_some(),
836 "a mixin edge and the module it reaches are recorded together"
837 );
838 *slot = Some(RubyMethodFind {
839 owner: mixin_owner.unwrap_or_else(|| reached_from.to_owned()),
840 reached_from: reached_from.to_owned(),
841 mixin,
842 class_side,
843 });
844}
845
846#[derive(Clone, Copy)]
847pub enum RubyMethodLookupMode {
848 InstanceMethod,
849 SingletonMethod,
850}
851
852#[derive(Clone, Eq, Hash, PartialEq)]
853pub struct FactoryInferenceKey {
854 pub method: CodeUnit,
855 pub invocation_owner_fq_name: String,
856}
857
858pub struct FactoryInferenceFrame {
859 pub method: CodeUnit,
860 pub invocation_owner_fq_name: String,
861}
862
863pub enum FactoryMethodOutcome {
864 Owner(String),
865 Chain(Vec<FactoryInferenceFrame>),
866 Unknown,
867}
868
869pub fn ruby_method_lookup_mode_matches(
870 ruby: &dyn RubySource,
871 unit: &CodeUnit,
872 mode: RubyMethodLookupMode,
873) -> bool {
874 matches!(
875 (ruby.method_dispatch_mode(unit), mode),
876 (
877 RubyMethodDispatchMode::Instance,
878 RubyMethodLookupMode::InstanceMethod
879 ) | (
880 RubyMethodDispatchMode::Singleton,
881 RubyMethodLookupMode::SingletonMethod
882 ) | (RubyMethodDispatchMode::ModuleFunction, _)
883 )
884}
885
886fn constant_lookup_candidates(
887 lexical_stack: &[String],
888 segments: &[String],
889 absolute: bool,
890) -> Option<Vec<String>> {
891 if segments.is_empty() {
892 return None;
893 }
894
895 let name = segments.join("$");
896 let mut candidates = Vec::new();
897 if !absolute {
898 for owner in lexical_stack.iter().rev() {
899 candidates.push(format!("{owner}${name}"));
900 }
901 }
902 candidates.push(name);
903
904 let Some((constant_name, owner_segments)) = segments.split_last() else {
905 return Some(candidates);
906 };
907 if owner_segments.is_empty() {
908 if !absolute {
909 for owner in lexical_stack.iter().rev() {
910 candidates.push(format!("{owner}.{constant_name}"));
911 }
912 }
913 return Some(candidates);
914 }
915
916 let owner_name = owner_segments.join("$");
917 if !absolute {
918 for owner in lexical_stack.iter().rev() {
919 candidates.push(format!("{owner}${owner_name}.{constant_name}"));
920 }
921 }
922 candidates.push(format!("{owner_name}.{constant_name}"));
923
924 Some(candidates)
925}