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 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 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 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 if locals.is_shadowed(name) {
547 return None;
548 }
549 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
649fn ruby_method_return_receiver_type(
654 semantic: &RubySemanticIndex<'_>,
655 visible_files: &HashSet<ProjectFile>,
656 receiver: &ReceiverType,
657 method_name: &str,
658) -> Option<ReceiverType> {
659 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 .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}