Skip to main content

brokk_bifrost_ruby/graph/
extractor.rs

1use crate::declarations::{
2    RubyFieldScope, extract_name_segments, parse_ruby_tree, ruby_variable_field_name,
3};
4use crate::graph::hits::{
5    record_self_receiver_usage_hit, record_unproven_usage_hit, record_usage_hit,
6};
7use crate::graph::resolver::{
8    ExplicitReceiverLookup, FactoryInferenceFrame, FactoryInferenceKey, FactoryMethodOutcome,
9    ReceiverMode, ReceiverType, RubyMethodLookupMode, RubySemanticIndex, RubyTargetKind,
10    RubyTargetSpec, ruby_method_lookup_mode_matches,
11};
12use crate::graph::syntax::{
13    constant_hit_node, dynamic_dispatch_target_argument, is_call_method_identifier,
14    is_declaration_constant, is_declaration_identifier, method_receiver_mode, node_text,
15    symbol_or_string_value,
16};
17use brokk_bifrost_core::analyzer::model::Range;
18use brokk_bifrost_core::analyzer::tree_walk::{TreeWalkAction, walk_tree_iterative};
19use brokk_bifrost_core::analyzer::usages::local_inference::{
20    LocalInferenceConfig, LocalInferenceEngine,
21};
22use brokk_bifrost_core::analyzer::usages::model::UsageHit;
23use brokk_bifrost_core::analyzer::{
24    BoundedDefinitionLookup, CodeUnit, CodeUnitIndex, Language, ProjectFile,
25};
26use brokk_bifrost_core::hash::HashSet;
27use std::collections::BTreeSet;
28use tree_sitter::Node;
29pub struct RubyFileScan<'a> {
30    pub index: &'a dyn CodeUnitIndex,
31    pub semantic: &'a RubySemanticIndex<'a>,
32    pub support: &'a dyn BoundedDefinitionLookup,
33    pub file: &'a ProjectFile,
34    pub source: &'a str,
35    pub line_starts: &'a [usize],
36    pub visible_files: HashSet<ProjectFile>,
37    pub spec: &'a RubyTargetSpec,
38    pub hits: &'a mut BTreeSet<UsageHit>,
39    pub unproven_hits: &'a mut BTreeSet<UsageHit>,
40}
41
42impl RubyFileScan<'_> {
43    pub fn scan(&mut self, root: Node<'_>) {
44        let mut state = RubyWalkState {
45            scan: self,
46            locals: LocalInferenceEngine::new(LocalInferenceConfig::default()),
47            lexical_stack: Vec::new(),
48            method_stack: Vec::new(),
49            exits: Vec::new(),
50        };
51        walk_tree_iterative(
52            root,
53            &mut state,
54            |node, state| state.enter(node),
55            |state| state.exit(),
56        );
57    }
58}
59
60enum RubyExit {
61    Lexical,
62    Method,
63    LocalScope,
64}
65
66struct RubyWalkState<'a, 'b> {
67    scan: &'a mut RubyFileScan<'b>,
68    locals: LocalInferenceEngine<String>,
69    lexical_stack: Vec<String>,
70    method_stack: Vec<ReceiverMode>,
71    exits: Vec<RubyExit>,
72}
73
74impl RubyWalkState<'_, '_> {
75    fn enter(&mut self, node: Node<'_>) -> TreeWalkAction {
76        match node.kind() {
77            "class" | "module" => {
78                self.record_superclass_reference(node);
79                if let Some(owner) = self.type_owner(node) {
80                    self.lexical_stack.push(owner);
81                    self.exits.push(RubyExit::Lexical);
82                    self.record_reference(node);
83                    return TreeWalkAction::DescendWithExit;
84                }
85            }
86            "method" | "singleton_method" => {
87                self.locals.enter_scope();
88                self.seed_parameter_shadows(node);
89                self.method_stack.push(method_receiver_mode(node));
90                self.exits.push(RubyExit::Method);
91                return TreeWalkAction::DescendWithExit;
92            }
93            "singleton_class" => {
94                self.locals.enter_scope();
95                self.method_stack.push(ReceiverMode::Class);
96                self.exits.push(RubyExit::Method);
97                return TreeWalkAction::DescendWithExit;
98            }
99            "block" | "do_block" => {
100                self.locals.enter_scope();
101                self.exits.push(RubyExit::LocalScope);
102                return TreeWalkAction::DescendWithExit;
103            }
104            "assignment" => self.seed_assignment(node),
105            _ => {}
106        }
107        self.record_reference(node);
108        TreeWalkAction::Descend
109    }
110
111    fn exit(&mut self) {
112        match self.exits.pop() {
113            Some(RubyExit::Lexical) => {
114                self.lexical_stack.pop();
115            }
116            Some(RubyExit::Method) => {
117                self.method_stack.pop();
118                self.locals.exit_scope();
119            }
120            Some(RubyExit::LocalScope) => {
121                self.locals.exit_scope();
122            }
123            None => {}
124        }
125    }
126
127    fn type_owner(&self, node: Node<'_>) -> Option<String> {
128        ruby_type_owner(
129            self.scan.semantic,
130            self.scan.file,
131            &self.scan.visible_files,
132            &self.lexical_stack,
133            node,
134            self.scan.source,
135        )
136    }
137
138    fn record_reference(&mut self, node: Node<'_>) {
139        match self.scan.spec.kind {
140            RubyTargetKind::TypeOrConstant => self.record_constant_reference(node),
141            RubyTargetKind::Method => self.record_method_reference(node),
142            RubyTargetKind::Field(_) => self.record_field_reference(node),
143        }
144    }
145
146    fn record_superclass_reference(&mut self, node: Node<'_>) {
147        if self.scan.spec.kind != RubyTargetKind::TypeOrConstant {
148            return;
149        }
150        let Some(superclass) = node.child_by_field_name("superclass") else {
151            return;
152        };
153        let mut stack = vec![superclass];
154        while let Some(current) = stack.pop() {
155            self.record_constant_reference(current);
156            for index in (0..current.named_child_count()).rev() {
157                if let Some(child) = current.named_child(index) {
158                    stack.push(child);
159                }
160            }
161        }
162    }
163
164    fn record_field_reference(&mut self, node: Node<'_>) {
165        let Some(name) = ruby_variable_field_name(node, self.scan.source) else {
166            return;
167        };
168        if name != self.scan.spec.member_name || self.is_target_field_declaration_site(node) {
169            return;
170        }
171        let Some((owner, scope)) =
172            ruby_field_reference_owner_and_scope(&self.lexical_stack, &self.method_stack, node)
173        else {
174            return;
175        };
176        if self.field_reference_matches_target(&owner, scope) {
177            self.record_hit(node);
178        }
179    }
180
181    fn field_reference_matches_target(&self, owner: &str, scope: RubyFieldScope) -> bool {
182        if self.scan.spec.kind != RubyTargetKind::Field(scope) {
183            return false;
184        }
185        let Some(target_owner) = self.scan.spec.field_owner.as_deref() else {
186            return false;
187        };
188        match scope {
189            RubyFieldScope::ClassVariable => {
190                owner == target_owner
191                    || self
192                        .scan
193                        .semantic
194                        .ancestor_lookup_order(owner)
195                        .iter()
196                        .any(|ancestor| ancestor == target_owner)
197            }
198            RubyFieldScope::Instance | RubyFieldScope::SingletonClass => owner == target_owner,
199        }
200    }
201
202    fn is_target_field_declaration_site(&self, node: Node<'_>) -> bool {
203        let Some(parent) = node.parent() else {
204            return false;
205        };
206        if !matches!(parent.kind(), "assignment" | "operator_assignment")
207            || parent.child_by_field_name("left") != Some(node)
208        {
209            return false;
210        }
211        self.scan
212            .index
213            .ranges(&self.scan.spec.target)
214            .iter()
215            .any(|range| {
216                range.start_byte == parent.start_byte() && range.end_byte == parent.end_byte()
217            })
218    }
219
220    fn record_constant_reference(&mut self, node: Node<'_>) {
221        if crate::imports::is_ruby_autoload_symbol_argument(node, self.scan.source) {
222            self.record_autoload_symbol_constant_reference(node);
223            return;
224        }
225        if !matches!(node.kind(), "constant" | "scope_resolution") || is_declaration_constant(node)
226        {
227            return;
228        }
229        if let Some(unit) = self.scan.semantic.resolve_constant(
230            self.scan.file,
231            &self.scan.visible_files,
232            &self.lexical_stack,
233            node,
234            self.scan.source,
235        ) && self.scan.semantic.target_matches_constant(&unit)
236        {
237            self.record_hit(constant_hit_node(node));
238        }
239    }
240
241    fn record_autoload_symbol_constant_reference(&mut self, node: Node<'_>) {
242        let Some(name) = crate::imports::ruby_symbol_name(node, self.scan.source) else {
243            return;
244        };
245        if let Some(unit) = self.scan.semantic.resolve_constant_name(
246            self.scan.file,
247            &self.scan.visible_files,
248            &self.lexical_stack,
249            &name,
250        ) && self.scan.semantic.target_matches_constant(&unit)
251        {
252            self.record_hit(node);
253        }
254    }
255
256    fn record_method_reference(&mut self, node: Node<'_>) {
257        if node.kind() == "identifier" {
258            self.record_bare_identifier_method_reference(node);
259            return;
260        }
261        if node.kind() != "call" {
262            return;
263        }
264        if let Some((dispatched_member, dispatched_method)) =
265            dynamic_dispatch_target_argument(node, self.scan.source)
266            && dispatched_member == self.scan.spec.member_name
267        {
268            self.record_method_hit_for_call_receiver(
269                node,
270                dispatched_method,
271                ExplicitReceiverLookup::ReceiverOnly,
272            );
273            return;
274        }
275        if let Some(aliased_method) =
276            alias_method_target_argument(node, self.scan.source, &self.scan.spec.member_name)
277        {
278            // `alias_method :new, :old` inside a class references the class's own
279            // method — a same-owner site.
280            self.record_bare_method_hit_for_receiver(
281                &self.enclosing_receiver().unwrap_or_else(|| ReceiverType {
282                    owner_fq_name: String::new(),
283                    mode: ReceiverMode::TopLevel,
284                }),
285                aliased_method,
286                true,
287            );
288            return;
289        }
290        let Some(method) = node.child_by_field_name("method") else {
291            return;
292        };
293        if node_text(method, self.scan.source) != self.scan.spec.member_name {
294            return;
295        }
296        self.record_method_hit_for_call_receiver(node, method, ExplicitReceiverLookup::Bare);
297    }
298
299    fn record_bare_identifier_method_reference(&mut self, node: Node<'_>) {
300        let name = node_text(node, self.scan.source);
301        if name != self.scan.spec.member_name
302            || self.locals.is_shadowed(name)
303            || is_declaration_identifier(node)
304            || is_call_method_identifier(node)
305        {
306            return;
307        }
308        // A bare method-name reference resolves against the enclosing (implicit
309        // self) receiver — a same-owner site.
310        match self.enclosing_receiver() {
311            Some(receiver) => self.record_bare_method_hit_for_receiver(&receiver, node, true),
312            None => {
313                self.record_unproven_hit(node);
314            }
315        }
316    }
317
318    fn record_bare_method_hit_for_receiver(
319        &mut self,
320        receiver: &ReceiverType,
321        hit_node: Node<'_>,
322        same_owner: bool,
323    ) {
324        let candidates = self.scan.semantic.resolve_bare_method_candidates(
325            self.scan.support,
326            &self.scan.visible_files,
327            receiver,
328            &self.scan.spec.member_name,
329        );
330        self.record_method_hit_from_candidates(&candidates, hit_node, same_owner);
331    }
332
333    fn record_explicit_receiver_method_hit(
334        &mut self,
335        receiver: &ReceiverType,
336        hit_node: Node<'_>,
337        same_owner: bool,
338    ) {
339        let candidates = self.scan.semantic.resolve_method_candidates(
340            self.scan.support,
341            &self.scan.visible_files,
342            receiver,
343            &self.scan.spec.member_name,
344        );
345        self.record_method_hit_from_candidates(&candidates, hit_node, same_owner);
346    }
347
348    fn record_method_hit_for_call_receiver(
349        &mut self,
350        call: Node<'_>,
351        hit_node: Node<'_>,
352        explicit_receiver_lookup: ExplicitReceiverLookup,
353    ) {
354        let receiver_node = call.child_by_field_name("receiver");
355        // A `self.method` or implicit-self (bare) call is on the current instance
356        // / own class — a same-owner site (#1014 facet B). An explicit variable /
357        // constant receiver, even of the same type, is a different instance and
358        // stays external.
359        let same_owner = match receiver_node {
360            None => true,
361            Some(receiver) => receiver.kind() == "self",
362        };
363        let receiver = match receiver_node {
364            Some(receiver) => self.receiver_type(receiver),
365            None => self.enclosing_receiver(),
366        };
367        let Some(receiver) = receiver else {
368            self.record_unproven_hit(hit_node);
369            return;
370        };
371        match (receiver_node, explicit_receiver_lookup) {
372            (Some(_), ExplicitReceiverLookup::ReceiverOnly) => {
373                self.record_explicit_receiver_method_hit(&receiver, hit_node, same_owner);
374            }
375            _ => self.record_bare_method_hit_for_receiver(&receiver, hit_node, same_owner),
376        }
377    }
378
379    fn record_method_hit_from_candidates(
380        &mut self,
381        candidates: &[CodeUnit],
382        hit_node: Node<'_>,
383        same_owner: bool,
384    ) {
385        if candidates.iter().any(|candidate| {
386            candidate == &self.scan.spec.target
387                || candidate.fq_name() == self.scan.spec.target.fq_name()
388        }) {
389            if same_owner {
390                self.record_self_receiver_hit(hit_node);
391            } else {
392                self.record_hit(hit_node);
393            }
394        } else if candidates.is_empty() {
395            self.record_unproven_hit(hit_node);
396        }
397    }
398
399    fn receiver_type(&self, node: Node<'_>) -> Option<ReceiverType> {
400        ruby_receiver_type(
401            self.scan.semantic,
402            self.scan.file,
403            &self.scan.visible_files,
404            &self.lexical_stack,
405            &self.locals,
406            &self.method_stack,
407            node,
408            self.scan.source,
409        )
410    }
411
412    fn enclosing_receiver(&self) -> Option<ReceiverType> {
413        ruby_enclosing_receiver(&self.lexical_stack, &self.method_stack)
414    }
415
416    fn seed_assignment(&mut self, node: Node<'_>) {
417        ruby_seed_assignment(
418            self.scan.semantic,
419            self.scan.file,
420            &self.scan.visible_files,
421            &self.lexical_stack,
422            &self.method_stack,
423            &mut self.locals,
424            node,
425            self.scan.source,
426        );
427    }
428
429    fn seed_parameter_shadows(&mut self, node: Node<'_>) {
430        ruby_seed_parameter_shadows(&mut self.locals, node, self.scan.source);
431    }
432
433    fn record_hit(&mut self, node: Node<'_>) {
434        record_usage_hit(
435            self.scan.index,
436            self.scan.file,
437            self.scan.source,
438            self.scan.line_starts,
439            self.scan.hits,
440            node,
441        );
442    }
443
444    fn record_self_receiver_hit(&mut self, node: Node<'_>) {
445        record_self_receiver_usage_hit(
446            self.scan.index,
447            self.scan.file,
448            self.scan.source,
449            self.scan.line_starts,
450            self.scan.hits,
451            node,
452        );
453    }
454
455    fn record_unproven_hit(&mut self, node: Node<'_>) {
456        record_unproven_usage_hit(
457            self.scan.index,
458            self.scan.file,
459            self.scan.source,
460            self.scan.line_starts,
461            self.scan.unproven_hits,
462            node,
463        );
464    }
465}
466
467fn alias_method_target_argument<'tree>(
468    node: Node<'tree>,
469    source: &str,
470    member: &str,
471) -> Option<Node<'tree>> {
472    let method = node.child_by_field_name("method")?;
473    if node_text(method, source) != "alias_method" {
474        return None;
475    }
476    let arguments = node.child_by_field_name("arguments")?;
477    let mut cursor = arguments.walk();
478    let mut args = arguments.named_children(&mut cursor);
479    args.next()?;
480    let target_argument = args.next()?;
481    symbol_or_string_value(target_argument, source)
482        .is_some_and(|value| value == member)
483        .then_some(target_argument)
484}
485
486pub fn language_for_file(file: &ProjectFile) -> Language {
487    brokk_bifrost_core::analyzer::common::language_for_file(file)
488}
489
490pub fn first_precise(bindings: &LocalInferenceEngine<String>, symbol: &str) -> Option<String> {
491    bindings
492        .resolve_symbol(symbol)
493        .as_precise()
494        .and_then(|targets| targets.iter().next().cloned())
495}
496
497pub fn ruby_type_owner(
498    semantic: &RubySemanticIndex<'_>,
499    file: &ProjectFile,
500    visible_files: &HashSet<ProjectFile>,
501    lexical_stack: &[String],
502    node: Node<'_>,
503    source: &str,
504) -> Option<String> {
505    let name = node.child_by_field_name("name")?;
506    semantic
507        .resolve_constant(file, visible_files, lexical_stack, name, source)
508        .filter(|unit| unit.is_class() || unit.is_module())
509        .map(|unit| unit.fq_name())
510        .or_else(|| {
511            let mut segments = lexical_stack.to_vec();
512            segments.extend(extract_name_segments(name, source));
513            (!segments.is_empty()).then(|| segments.join("$"))
514        })
515}
516
517#[allow(clippy::too_many_arguments)]
518pub fn ruby_receiver_type(
519    semantic: &RubySemanticIndex<'_>,
520    file: &ProjectFile,
521    visible_files: &HashSet<ProjectFile>,
522    lexical_stack: &[String],
523    locals: &LocalInferenceEngine<String>,
524    method_stack: &[ReceiverMode],
525    node: Node<'_>,
526    source: &str,
527) -> Option<ReceiverType> {
528    match node.kind() {
529        "constant" | "scope_resolution" => {
530            let unit =
531                semantic.resolve_constant(file, visible_files, lexical_stack, node, source)?;
532            (unit.is_class() || unit.is_module()).then(|| ReceiverType {
533                owner_fq_name: unit.fq_name(),
534                mode: ReceiverMode::Class,
535            })
536        }
537        "identifier" => {
538            let name = node_text(node, source);
539            if let Some(owner_fq_name) = first_precise(locals, name) {
540                return Some(ReceiverType {
541                    owner_fq_name,
542                    mode: ReceiverMode::Instance,
543                });
544            }
545            // A known local whose type we can't pin is not a method call.
546            if locals.is_shadowed(name) {
547                return None;
548            }
549            // Otherwise a bare implicit-`self` method used as a receiver
550            // (`get_foo.v`): type it by `get_foo`'s inferred return instance.
551            let enclosing = ruby_enclosing_receiver(lexical_stack, method_stack)?;
552            ruby_method_return_receiver_type(semantic, visible_files, &enclosing, name)
553        }
554        "self" => ruby_enclosing_receiver(lexical_stack, method_stack),
555        "call" => ruby_constructed_receiver_type(
556            semantic,
557            file,
558            visible_files,
559            lexical_stack,
560            locals,
561            method_stack,
562            node,
563            source,
564        ),
565        _ => None,
566    }
567}
568
569#[allow(clippy::too_many_arguments)]
570pub fn ruby_constructed_receiver_type(
571    semantic: &RubySemanticIndex<'_>,
572    file: &ProjectFile,
573    visible_files: &HashSet<ProjectFile>,
574    lexical_stack: &[String],
575    locals: &LocalInferenceEngine<String>,
576    method_stack: &[ReceiverMode],
577    node: Node<'_>,
578    source: &str,
579) -> Option<ReceiverType> {
580    let method = node.child_by_field_name("method")?;
581    let method_name = node_text(method, source);
582    let receiver = node.child_by_field_name("receiver")?;
583
584    if method_name == "new" {
585        return ruby_new_receiver_type(
586            semantic,
587            file,
588            visible_files,
589            lexical_stack,
590            locals,
591            method_stack,
592            receiver,
593            source,
594        );
595    }
596
597    let class = ruby_receiver_type(
598        semantic,
599        file,
600        visible_files,
601        lexical_stack,
602        locals,
603        method_stack,
604        receiver,
605        source,
606    )?;
607    ruby_factory_return_receiver_type(semantic, visible_files, &class, method_name)
608}
609
610#[allow(clippy::too_many_arguments)]
611fn ruby_new_receiver_type(
612    semantic: &RubySemanticIndex<'_>,
613    file: &ProjectFile,
614    visible_files: &HashSet<ProjectFile>,
615    lexical_stack: &[String],
616    locals: &LocalInferenceEngine<String>,
617    method_stack: &[ReceiverMode],
618    receiver: Node<'_>,
619    source: &str,
620) -> Option<ReceiverType> {
621    let class = ruby_receiver_type(
622        semantic,
623        file,
624        visible_files,
625        lexical_stack,
626        locals,
627        method_stack,
628        receiver,
629        source,
630    )?;
631    (class.mode == ReceiverMode::Class).then_some(ReceiverType {
632        owner_fq_name: class.owner_fq_name,
633        mode: ReceiverMode::Instance,
634    })
635}
636
637fn ruby_factory_return_receiver_type(
638    semantic: &RubySemanticIndex<'_>,
639    visible_files: &HashSet<ProjectFile>,
640    class: &ReceiverType,
641    method_name: &str,
642) -> Option<ReceiverType> {
643    if class.mode != ReceiverMode::Class {
644        return None;
645    }
646    ruby_method_return_receiver_type(semantic, visible_files, class, method_name)
647}
648
649/// Type a call whose receiver is `receiver` and whose method is `method_name` by
650/// the method's inferred return instance (e.g. a factory method returning
651/// `Foo.new`). Works for both class receivers (`Klass.make.x`) and the enclosing
652/// instance receiver of a bare call (`get_foo.x`).
653fn ruby_method_return_receiver_type(
654    semantic: &RubySemanticIndex<'_>,
655    visible_files: &HashSet<ProjectFile>,
656    receiver: &ReceiverType,
657    method_name: &str,
658) -> Option<ReceiverType> {
659    // The one reader of the analyzer's global definition index on this path, and
660    // the reason `RubyGraphSource::definitions` is a callback: the index builds
661    // on first access, and the diagnostics pass builds a `RubySemanticIndex`
662    // that never reaches here.
663    let mut candidates = Vec::new();
664    (semantic.graph.definitions)(&mut |support| {
665        candidates =
666            semantic.resolve_method_candidates(support, visible_files, receiver, method_name);
667    });
668    candidates.into_iter().find_map(|candidate| {
669        ruby_infer_method_return_instance_owner(semantic, candidate, receiver.owner_fq_name.clone())
670            .map(|owner_fq_name| ReceiverType {
671                owner_fq_name,
672                mode: ReceiverMode::Instance,
673            })
674    })
675}
676
677fn ruby_infer_method_return_instance_owner(
678    semantic: &RubySemanticIndex<'_>,
679    method_unit: CodeUnit,
680    invocation_owner_fq_name: String,
681) -> Option<String> {
682    let start = FactoryInferenceFrame {
683        method: method_unit,
684        invocation_owner_fq_name,
685    };
686    let start_key = start.key();
687    if let Some(cached) = semantic.factory_return_cache.borrow().get(&start_key) {
688        return cached.clone();
689    }
690
691    let mut stack = vec![start];
692    let mut visited = HashSet::default();
693    while let Some(frame) = stack.pop() {
694        let key = frame.key();
695        if let Some(cached) = semantic.factory_return_cache.borrow().get(&key) {
696            if cached.is_some() {
697                semantic
698                    .factory_return_cache
699                    .borrow_mut()
700                    .insert(start_key, cached.clone());
701                return cached.clone();
702            }
703            continue;
704        }
705        if !visited.insert(key.clone()) {
706            semantic.factory_return_cache.borrow_mut().insert(key, None);
707            continue;
708        }
709
710        match ruby_factory_method_outcome(semantic, &frame) {
711            FactoryMethodOutcome::Owner(owner) => {
712                semantic
713                    .factory_return_cache
714                    .borrow_mut()
715                    .insert(key, Some(owner.clone()));
716                semantic
717                    .factory_return_cache
718                    .borrow_mut()
719                    .insert(start_key, Some(owner.clone()));
720                return Some(owner);
721            }
722            FactoryMethodOutcome::Chain(next) => {
723                if next.is_empty() {
724                    semantic.factory_return_cache.borrow_mut().insert(key, None);
725                } else {
726                    stack.extend(next.into_iter().rev());
727                }
728            }
729            FactoryMethodOutcome::Unknown => {
730                semantic.factory_return_cache.borrow_mut().insert(key, None);
731            }
732        }
733    }
734    semantic
735        .factory_return_cache
736        .borrow_mut()
737        .insert(start_key, None);
738    None
739}
740
741impl FactoryInferenceFrame {
742    fn key(&self) -> FactoryInferenceKey {
743        FactoryInferenceKey {
744            method: self.method.clone(),
745            invocation_owner_fq_name: self.invocation_owner_fq_name.clone(),
746        }
747    }
748}
749
750fn ruby_factory_method_outcome(
751    semantic: &RubySemanticIndex<'_>,
752    frame: &FactoryInferenceFrame,
753) -> FactoryMethodOutcome {
754    let Some(owner) = semantic.graph.index.parent_of(&frame.method) else {
755        return FactoryMethodOutcome::Unknown;
756    };
757    let Ok(source) = semantic
758        .graph
759        .index
760        .project()
761        .read_source(frame.method.source())
762    else {
763        return FactoryMethodOutcome::Unknown;
764    };
765    let Some(tree) = parse_ruby_tree(&source) else {
766        return FactoryMethodOutcome::Unknown;
767    };
768    let ranges = semantic.graph.index.ranges(&frame.method);
769    let Some(node) = ruby_method_node_for_ranges(tree.root_node(), &ranges, &source) else {
770        return FactoryMethodOutcome::Unknown;
771    };
772    let Some(expression) = ruby_tail_expression(node) else {
773        return FactoryMethodOutcome::Unknown;
774    };
775    let expression = if expression.kind() == "assignment" {
776        let Some(right) = expression.child_by_field_name("right") else {
777            return FactoryMethodOutcome::Unknown;
778        };
779        right
780    } else {
781        expression
782    };
783    if expression.kind() == "identifier"
784        && node_text(expression, &source) == "new"
785        && !ruby_method_declares_local(node, "new", &source)
786        && ruby_method_lookup_mode_matches(
787            semantic.ruby,
788            &frame.method,
789            RubyMethodLookupMode::SingletonMethod,
790        )
791    {
792        return ruby_bare_new_outcome(semantic, frame);
793    }
794    if expression.kind() != "call"
795        || expression
796            .child_by_field_name("method")
797            .is_none_or(|method| node_text(method, &source) != "new")
798    {
799        return FactoryMethodOutcome::Unknown;
800    }
801    if expression.child_by_field_name("receiver").is_none()
802        && ruby_method_lookup_mode_matches(
803            semantic.ruby,
804            &frame.method,
805            RubyMethodLookupMode::SingletonMethod,
806        )
807    {
808        return ruby_bare_new_outcome(semantic, frame);
809    }
810    let Some(receiver) = expression.child_by_field_name("receiver") else {
811        return FactoryMethodOutcome::Unknown;
812    };
813    ruby_factory_new_receiver_outcome(
814        semantic,
815        frame.method.source(),
816        &source,
817        &owner.fq_name(),
818        &frame.invocation_owner_fq_name,
819        receiver,
820    )
821}
822
823fn ruby_bare_new_outcome(
824    semantic: &RubySemanticIndex<'_>,
825    frame: &FactoryInferenceFrame,
826) -> FactoryMethodOutcome {
827    let visible_files = semantic.visible_files_from(frame.method.source());
828    let receiver = ReceiverType {
829        owner_fq_name: frame.invocation_owner_fq_name.clone(),
830        mode: ReceiverMode::Class,
831    };
832    let mut overrides = Vec::new();
833    (semantic.graph.definitions)(&mut |support| {
834        overrides = semantic.resolve_method_candidates(support, &visible_files, &receiver, "new");
835    });
836    if overrides.is_empty() {
837        return FactoryMethodOutcome::Owner(frame.invocation_owner_fq_name.clone());
838    }
839    FactoryMethodOutcome::Chain(
840        overrides
841            .into_iter()
842            .map(|method| FactoryInferenceFrame {
843                method,
844                invocation_owner_fq_name: frame.invocation_owner_fq_name.clone(),
845            })
846            .collect(),
847    )
848}
849
850fn ruby_method_declares_local(method: Node<'_>, name: &str, source: &str) -> bool {
851    if method
852        .child_by_field_name("parameters")
853        .is_some_and(|parameters| {
854            let mut cursor = parameters.walk();
855            parameters
856                .named_children(&mut cursor)
857                .any(|parameter| ruby_parameter_declares_name(parameter, name, source))
858        })
859    {
860        return true;
861    }
862    let mut stack: Vec<_> = method.child_by_field_name("body").into_iter().collect();
863    while let Some(node) = stack.pop() {
864        if matches!(node.kind(), "assignment" | "operator_assignment")
865            && node
866                .child_by_field_name("left")
867                .is_some_and(|left| ruby_assignment_declares_name(left, name, source))
868        {
869            return true;
870        }
871        if node.id() != method.id()
872            && matches!(
873                node.kind(),
874                "method" | "singleton_method" | "class" | "module" | "singleton_class"
875            )
876        {
877            continue;
878        }
879        for index in (0..node.named_child_count()).rev() {
880            if let Some(child) = node.named_child(index) {
881                stack.push(child);
882            }
883        }
884    }
885    false
886}
887
888fn ruby_parameter_declares_name(node: Node<'_>, name: &str, source: &str) -> bool {
889    match node.kind() {
890        "identifier" => node_text(node, source) == name,
891        "optional_parameter"
892        | "keyword_parameter"
893        | "splat_parameter"
894        | "hash_splat_parameter"
895        | "block_parameter" => node
896            .child_by_field_name("name")
897            .is_some_and(|binding| node_text(binding, source) == name),
898        "destructured_parameter" => {
899            let mut cursor = node.walk();
900            node.named_children(&mut cursor)
901                .any(|child| ruby_parameter_declares_name(child, name, source))
902        }
903        _ => false,
904    }
905}
906
907fn ruby_assignment_declares_name(node: Node<'_>, name: &str, source: &str) -> bool {
908    match node.kind() {
909        "identifier" => node_text(node, source) == name,
910        "left_assignment_list" | "destructured_left_assignment" | "rest_assignment" => {
911            let mut cursor = node.walk();
912            node.named_children(&mut cursor)
913                .any(|child| ruby_assignment_declares_name(child, name, source))
914        }
915        _ => false,
916    }
917}
918
919pub fn ruby_method_node_for_ranges<'tree>(
920    root: Node<'tree>,
921    ranges: &[Range],
922    source: &str,
923) -> Option<Node<'tree>> {
924    let mut stack = vec![root];
925    while let Some(node) = stack.pop() {
926        if ranges
927            .iter()
928            .any(|range| range.start_byte == node.start_byte() && range.end_byte == node.end_byte())
929        {
930            if matches!(node.kind(), "method" | "singleton_method" | "call") {
931                return Some(node);
932            }
933            if let Some(call) = ruby_synthetic_method_macro_call(node, source) {
934                return Some(call);
935            }
936        }
937        for index in (0..node.named_child_count()).rev() {
938            if let Some(child) = node.named_child(index) {
939                stack.push(child);
940            }
941        }
942    }
943    None
944}
945
946fn ruby_synthetic_method_macro_call<'tree>(node: Node<'tree>, source: &str) -> Option<Node<'tree>> {
947    if !matches!(node.kind(), "simple_symbol" | "string") {
948        return None;
949    }
950    let arguments = node.parent()?;
951    if arguments.kind() != "argument_list" {
952        return None;
953    }
954    let call = arguments.parent()?;
955    if call.kind() != "call" {
956        return None;
957    }
958    let method = call.child_by_field_name("method")?;
959    matches!(
960        node_text(method, source),
961        "attr_accessor" | "attr_reader" | "attr_writer" | "alias_method"
962    )
963    .then_some(call)
964}
965
966fn ruby_factory_new_receiver_outcome(
967    semantic: &RubySemanticIndex<'_>,
968    file: &ProjectFile,
969    source: &str,
970    owner_fq_name: &str,
971    invocation_owner_fq_name: &str,
972    receiver: Node<'_>,
973) -> FactoryMethodOutcome {
974    let lexical_stack = ruby_lexical_stack_for_owner(owner_fq_name);
975    match receiver.kind() {
976        "self" => FactoryMethodOutcome::Owner(invocation_owner_fq_name.to_string()),
977        "constant" | "scope_resolution" => {
978            let visible_files = semantic.visible_files_from(file);
979            semantic
980                .resolve_constant(file, &visible_files, &lexical_stack, receiver, source)
981                .filter(|unit| unit.is_class() || unit.is_module())
982                .map(|unit| FactoryMethodOutcome::Owner(unit.fq_name()))
983                .unwrap_or(FactoryMethodOutcome::Unknown)
984        }
985        "call" => ruby_factory_chained_call_outcome(
986            semantic,
987            file,
988            source,
989            &lexical_stack,
990            invocation_owner_fq_name,
991            receiver,
992        ),
993        _ => FactoryMethodOutcome::Unknown,
994    }
995}
996
997fn ruby_factory_chained_call_outcome(
998    semantic: &RubySemanticIndex<'_>,
999    file: &ProjectFile,
1000    source: &str,
1001    lexical_stack: &[String],
1002    invocation_owner_fq_name: &str,
1003    call: Node<'_>,
1004) -> FactoryMethodOutcome {
1005    let Some(method) = call.child_by_field_name("method") else {
1006        return FactoryMethodOutcome::Unknown;
1007    };
1008    let method_name = node_text(method, source);
1009    let Some(receiver) = call.child_by_field_name("receiver") else {
1010        return FactoryMethodOutcome::Unknown;
1011    };
1012    let Some(owner_fq_name) = ruby_factory_class_receiver_owner(
1013        semantic,
1014        file,
1015        source,
1016        lexical_stack,
1017        invocation_owner_fq_name,
1018        receiver,
1019    ) else {
1020        return FactoryMethodOutcome::Unknown;
1021    };
1022    let visible_files = semantic.visible_files_from(file);
1023    let class = ReceiverType {
1024        owner_fq_name: owner_fq_name.clone(),
1025        mode: ReceiverMode::Class,
1026    };
1027    let mut candidates = Vec::new();
1028    (semantic.graph.definitions)(&mut |support| {
1029        candidates =
1030            semantic.resolve_method_candidates(support, &visible_files, &class, method_name);
1031    });
1032    let frames = candidates
1033        .into_iter()
1034        .map(|method| FactoryInferenceFrame {
1035            method,
1036            invocation_owner_fq_name: owner_fq_name.clone(),
1037        })
1038        .collect();
1039    FactoryMethodOutcome::Chain(frames)
1040}
1041
1042fn ruby_factory_class_receiver_owner(
1043    semantic: &RubySemanticIndex<'_>,
1044    file: &ProjectFile,
1045    source: &str,
1046    lexical_stack: &[String],
1047    invocation_owner_fq_name: &str,
1048    receiver: Node<'_>,
1049) -> Option<String> {
1050    match receiver.kind() {
1051        "self" => Some(invocation_owner_fq_name.to_string()),
1052        "constant" | "scope_resolution" => {
1053            let visible_files = semantic.visible_files_from(file);
1054            semantic
1055                .resolve_constant(file, &visible_files, lexical_stack, receiver, source)
1056                .filter(|unit| unit.is_class() || unit.is_module())
1057                .map(|unit| unit.fq_name())
1058        }
1059        _ => None,
1060    }
1061}
1062
1063fn ruby_tail_expression(node: Node<'_>) -> Option<Node<'_>> {
1064    let body = node.child_by_field_name("body")?;
1065    let mut cursor = body.walk();
1066    body.named_children(&mut cursor).last()
1067}
1068
1069fn ruby_lexical_stack_for_owner(owner_fq_name: &str) -> Vec<String> {
1070    let segments: Vec<_> = owner_fq_name
1071        // fqname-M4: builds the ruby lexical-scope stack from a `&str` owner fq-name (no CodeUnit/fq at this call site)
1072        .split('$')
1073        .filter(|segment| !segment.is_empty())
1074        .collect();
1075    (1..=segments.len())
1076        .map(|end| segments[..end].join("$"))
1077        .collect()
1078}
1079
1080pub fn ruby_enclosing_receiver(
1081    lexical_stack: &[String],
1082    method_stack: &[ReceiverMode],
1083) -> Option<ReceiverType> {
1084    if lexical_stack.is_empty() {
1085        return Some(ReceiverType {
1086            owner_fq_name: String::new(),
1087            mode: ReceiverMode::TopLevel,
1088        });
1089    }
1090
1091    let owner_fq_name = lexical_stack.last()?.clone();
1092    let mode = method_stack
1093        .last()
1094        .copied()
1095        .unwrap_or(ReceiverMode::Instance);
1096    Some(ReceiverType {
1097        owner_fq_name,
1098        mode,
1099    })
1100}
1101
1102pub fn ruby_field_reference_owner_and_scope(
1103    lexical_stack: &[String],
1104    method_stack: &[ReceiverMode],
1105    node: Node<'_>,
1106) -> Option<(String, RubyFieldScope)> {
1107    match node.kind() {
1108        "class_variable" => lexical_stack
1109            .last()
1110            .cloned()
1111            .map(|owner| (owner, RubyFieldScope::ClassVariable)),
1112        "instance_variable" => {
1113            let owner = lexical_stack.last()?.clone();
1114            let scope = match method_stack.last().copied() {
1115                Some(ReceiverMode::Instance) => RubyFieldScope::Instance,
1116                Some(ReceiverMode::Class | ReceiverMode::TopLevel) | None => {
1117                    RubyFieldScope::SingletonClass
1118                }
1119            };
1120            Some((owner, scope))
1121        }
1122        _ => None,
1123    }
1124}
1125
1126#[allow(clippy::too_many_arguments)]
1127pub fn ruby_seed_assignment(
1128    semantic: &RubySemanticIndex<'_>,
1129    file: &ProjectFile,
1130    visible_files: &HashSet<ProjectFile>,
1131    lexical_stack: &[String],
1132    method_stack: &[ReceiverMode],
1133    locals: &mut LocalInferenceEngine<String>,
1134    node: Node<'_>,
1135    source: &str,
1136) {
1137    let Some(left) = node.child_by_field_name("left") else {
1138        return;
1139    };
1140    if left.kind() != "identifier" {
1141        return;
1142    }
1143    let name = node_text(left, source);
1144    if name.is_empty() {
1145        return;
1146    }
1147    let resolved = node
1148        .child_by_field_name("right")
1149        .and_then(|right| {
1150            ruby_receiver_type(
1151                semantic,
1152                file,
1153                visible_files,
1154                lexical_stack,
1155                locals,
1156                method_stack,
1157                right,
1158                source,
1159            )
1160        })
1161        .filter(|receiver| receiver.mode == ReceiverMode::Instance)
1162        .map(|receiver| receiver.owner_fq_name);
1163    match resolved {
1164        Some(owner) => locals.seed_symbol(name.to_string(), owner),
1165        None => locals.declare_shadow(name.to_string()),
1166    }
1167}
1168
1169pub fn ruby_seed_parameter_shadows(
1170    locals: &mut LocalInferenceEngine<String>,
1171    node: Node<'_>,
1172    source: &str,
1173) {
1174    if let Some(parameters) = node.child_by_field_name("parameters") {
1175        let mut stack = vec![parameters];
1176        while let Some(current) = stack.pop() {
1177            if current.kind() == "identifier" {
1178                let name = node_text(current, source);
1179                if !name.is_empty() {
1180                    locals.declare_shadow(name.to_string());
1181                }
1182                continue;
1183            }
1184            for index in (0..current.named_child_count()).rev() {
1185                if let Some(child) = current.named_child(index) {
1186                    stack.push(child);
1187                }
1188            }
1189        }
1190    }
1191}