1use crate::imports::parse_ruby_require_call;
2use crate::mixins::{encode_mixin_relation, encode_superclass_relation, raw_mixin_specs_for_type};
3use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentId, SegmentKind, segment_interner};
4use brokk_bifrost_core::analyzer::model::{
5 CodeUnitType, DispatchExtensibility, RubyMethodDispatchMode, SignatureMetadata,
6};
7use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
8use brokk_bifrost_core::analyzer::structural::materialization::{
9 GenerationKind, MaterializationRecord,
10};
11use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, node_range, walk_named_tree_preorder};
12use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
13use brokk_bifrost_core::hash::HashSet;
14use tree_sitter::{Node, Parser, Tree};
15
16fn ruby_segment(text: &str, kind: SegmentKind) -> SegmentId {
18 segment_interner().intern(text, kind)
19}
20
21fn ruby_type_chain_fq(segments: &[String]) -> FqName {
33 let mut fq = FqName::new();
34 for segment in segments {
35 fq.push(ruby_segment(segment, SegmentKind::Nested));
36 }
37 fq
38}
39
40fn ruby_member_fq(type_segments: &[String], name: &str) -> FqName {
44 ruby_type_chain_fq(type_segments).with_pushed(ruby_segment(name, SegmentKind::Member))
45}
46
47pub fn parse_ruby_tree(source: &str) -> Option<Tree> {
49 let mut parser = Parser::new();
50 parser
51 .set_language(&tree_sitter_ruby::LANGUAGE.into())
52 .expect("failed to load ruby parser");
53 parser.parse(source, None)
54}
55
56pub fn ruby_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
58 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
59}
60
61pub struct RubyVisitor<'a> {
69 pub file: &'a ProjectFile,
70 pub source: &'a str,
71 pub parsed: &'a mut ParsedFile,
72}
73
74struct RubyWork<'tree> {
79 node: Node<'tree>,
80 segments: Vec<String>,
81 parent: Option<CodeUnit>,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum RubyFieldScope {
86 Instance,
87 ClassVariable,
88 SingletonClass,
89}
90
91fn push_named_children<'tree>(
94 node: Node<'tree>,
95 segments: &[String],
96 parent: Option<&CodeUnit>,
97 stack: &mut Vec<RubyWork<'tree>>,
98) {
99 let mut cursor = node.walk();
100 let children: Vec<_> = node.named_children(&mut cursor).collect();
101 for child in children.into_iter().rev() {
102 stack.push(RubyWork {
103 node: child,
104 segments: segments.to_vec(),
105 parent: parent.cloned(),
106 });
107 }
108}
109
110impl RubyVisitor<'_> {
111 pub fn visit_program(&mut self, root: Node<'_>) {
112 let mut stack = Vec::new();
113 push_named_children(root, &[], None, &mut stack);
114 while let Some(work) = stack.pop() {
115 self.visit_statement(work.node, &work.segments, work.parent.as_ref(), &mut stack);
116 }
117 }
118
119 fn visit_statement<'tree>(
120 &mut self,
121 node: Node<'tree>,
122 segments: &[String],
123 parent: Option<&CodeUnit>,
124 stack: &mut Vec<RubyWork<'tree>>,
125 ) {
126 match node.kind() {
127 "class" => self.visit_class_like(node, segments, parent, false, stack),
128 "module" => self.visit_class_like(node, segments, parent, true, stack),
129 "singleton_class" => {
130 if let Some(body) = node.child_by_field_name("body") {
132 push_named_children(body, segments, parent, stack);
133 }
134 }
135 "method" | "singleton_method" => self.visit_method(node, segments, parent),
136 "assignment" | "operator_assignment" => {
137 self.visit_assignment(node, segments, parent, None)
138 }
139 "call" => self.visit_call(node, segments, parent),
140 kind if is_descendable_container(kind) => {
141 push_named_children(node, segments, parent, stack);
142 }
143 _ => {}
144 }
145 }
146
147 fn visit_class_like<'tree>(
148 &mut self,
149 node: Node<'tree>,
150 segments: &[String],
151 parent: Option<&CodeUnit>,
152 is_module: bool,
153 stack: &mut Vec<RubyWork<'tree>>,
154 ) {
155 let Some(name_node) = node.child_by_field_name("name") else {
156 return;
157 };
158 let name_segments = extract_name_segments(name_node, self.source);
159 if name_segments.is_empty() {
160 return;
161 }
162
163 let mut new_segments = segments.to_vec();
164 new_segments.extend(name_segments);
165 let short_name = new_segments.join("$");
166
167 let kind = if is_module {
168 CodeUnitType::Module
169 } else {
170 CodeUnitType::Class
171 };
172 let code_unit = CodeUnit::new_fq(
173 self.file.clone(),
174 kind,
175 String::new(),
176 short_name,
177 ruby_type_chain_fq(&new_segments),
178 );
179 self.parsed
180 .replace_code_unit(code_unit.clone(), node, self.source, parent.cloned(), None);
181 self.parsed
182 .add_signature(code_unit.clone(), first_line(node, self.source));
183
184 let mut owner_relations = extract_ruby_supertypes(node, self.source)
185 .into_iter()
186 .map(|target| {
187 let encoded = encode_superclass_relation(&target);
188 (target, encoded)
189 })
190 .collect::<Vec<_>>();
191 owner_relations.extend(raw_mixin_specs_for_type(node, self.source).into_iter().map(
192 |spec| {
193 let encoded = encode_mixin_relation(&spec);
194 (spec.raw_target, encoded)
195 },
196 ));
197 if !owner_relations.is_empty() {
198 self.parsed.set_raw_supertypes(
199 code_unit.clone(),
200 owner_relations
201 .iter()
202 .map(|(target, _)| target.clone())
203 .collect(),
204 );
205 self.parsed.set_supertype_lookup_paths(
206 code_unit.clone(),
207 owner_relations
208 .into_iter()
209 .map(|(_, encoded)| encoded)
210 .collect(),
211 );
212 }
213
214 self.visit_scope_field_assignments(
215 node,
216 &new_segments,
217 Some(&code_unit),
218 RubyFieldScope::SingletonClass,
219 );
220 if let Some(body) = node.child_by_field_name("body") {
221 push_named_children(body, &new_segments, Some(&code_unit), stack);
222 }
223 }
224
225 fn visit_method(&mut self, node: Node<'_>, segments: &[String], parent: Option<&CodeUnit>) {
226 let Some(name_node) = node.child_by_field_name("name") else {
227 return;
228 };
229 let name = ruby_node_text(name_node, self.source).trim();
230 if name.is_empty() {
231 return;
232 }
233 let short_name = member_short_name(segments, name);
234 let signature = node
235 .child_by_field_name("parameters")
236 .map(|params| ruby_node_text(params, self.source).trim().to_string());
237 let code_unit = CodeUnit::with_signature_and_fq(
238 self.file.clone(),
239 CodeUnitType::Function,
240 String::new(),
241 short_name,
242 signature,
243 false,
244 ruby_member_fq(segments, name),
245 );
246 self.parsed
247 .replace_code_unit(code_unit.clone(), node, self.source, parent.cloned(), None);
248 self.parsed.set_ruby_method_dispatch_mode(
249 code_unit.clone(),
250 ruby_method_dispatch_mode(node, self.source),
251 );
252 self.parsed.add_signature_with_metadata(
253 code_unit,
254 ruby_signature_metadata(first_line(node, self.source), node, self.source),
255 );
256 self.visit_scope_field_assignments(node, segments, parent, ruby_method_field_scope(node));
260 }
261
262 fn visit_assignment(
263 &mut self,
264 node: Node<'_>,
265 segments: &[String],
266 parent: Option<&CodeUnit>,
267 field_scope: Option<RubyFieldScope>,
268 ) {
269 let Some(left) = node.child_by_field_name("left") else {
270 return;
271 };
272 if let Some(field_scope) = ruby_field_scope_for_assignment_left(left, segments, field_scope)
273 {
274 self.visit_variable_field_assignment(node, left, segments, parent, field_scope);
275 return;
276 }
277 if !matches!(left.kind(), "constant" | "scope_resolution") {
279 return;
280 }
281 let name_path = extract_name_path(left, self.source);
282 if name_path.segments.is_empty() {
283 return;
284 }
285 let short_name = assignment_constant_short_name(segments, &name_path);
286 let code_unit = CodeUnit::new_fq(
287 self.file.clone(),
288 CodeUnitType::Field,
289 String::new(),
290 short_name,
291 assignment_constant_fq(segments, &name_path),
292 );
293 self.parsed
294 .replace_code_unit(code_unit.clone(), node, self.source, parent.cloned(), None);
295 self.parsed.add_signature(
296 code_unit,
297 ruby_node_text(node, self.source).trim().to_string(),
298 );
299 }
300
301 fn visit_scope_field_assignments(
302 &mut self,
303 node: Node<'_>,
304 segments: &[String],
305 parent: Option<&CodeUnit>,
306 field_scope: RubyFieldScope,
307 ) {
308 let mut stack = vec![node];
309 while let Some(current) = stack.pop() {
310 if current != node
311 && matches!(
312 current.kind(),
313 "class" | "module" | "method" | "singleton_method" | "singleton_class"
314 )
315 {
316 continue;
317 }
318 if matches!(current.kind(), "assignment" | "operator_assignment") {
319 self.visit_assignment(current, segments, parent, Some(field_scope));
320 continue;
321 }
322 for index in (0..current.named_child_count()).rev() {
323 if let Some(child) = current.named_child(index) {
324 stack.push(child);
325 }
326 }
327 }
328 }
329
330 fn visit_variable_field_assignment(
331 &mut self,
332 node: Node<'_>,
333 left: Node<'_>,
334 segments: &[String],
335 parent: Option<&CodeUnit>,
336 field_scope: RubyFieldScope,
337 ) {
338 let Some(short_name) = ruby_field_short_name(segments, left, self.source, field_scope)
339 else {
340 return;
341 };
342 let fq = ruby_field_fq(segments, left, self.source, field_scope).unwrap_or_default();
343 let code_unit = CodeUnit::new_fq(
344 self.file.clone(),
345 CodeUnitType::Field,
346 String::new(),
347 short_name,
348 fq,
349 );
350 if self
351 .parsed
352 .first_range_start(&code_unit)
353 .is_some_and(|start| start <= node.start_byte())
354 {
355 return;
356 }
357 self.parsed
358 .replace_code_unit(code_unit.clone(), node, self.source, parent.cloned(), None);
359 self.parsed.add_signature(
360 code_unit,
361 ruby_node_text(node, self.source).trim().to_string(),
362 );
363 }
364
365 fn visit_call(&mut self, node: Node<'_>, segments: &[String], parent: Option<&CodeUnit>) {
366 let Some(method) = node.child_by_field_name("method") else {
367 return;
368 };
369 let method_name = ruby_node_text(method, self.source).trim();
370 match method_name {
371 "require" | "require_relative" | "load" | "autoload" => {
372 if let Some(info) = parse_ruby_require_call(node, self.source) {
373 self.parsed.import_statements.push(info.raw_snippet.clone());
374 self.parsed.imports.push(info);
375 }
376 }
377 "attr_accessor" | "attr_reader" | "attr_writer" => {
378 self.visit_attr_macro(node, method_name, segments, parent);
379 }
380 "alias_method" => {
381 self.visit_alias_method(node, segments, parent);
382 }
383 _ => {}
384 }
385 }
386
387 fn visit_attr_macro(
388 &mut self,
389 node: Node<'_>,
390 method_name: &str,
391 segments: &[String],
392 parent: Option<&CodeUnit>,
393 ) {
394 let Some(parent) = parent else {
396 return;
397 };
398 let Some(arguments) = node.child_by_field_name("arguments") else {
399 return;
400 };
401 let mut cursor = arguments.walk();
402 let mut dynamic_argument_seen = false;
403 for arg in arguments.named_children(&mut cursor) {
404 let Some(name) = literal_symbol_or_string_name(arg, self.source) else {
405 dynamic_argument_seen = true;
409 continue;
410 };
411 let member_name = attr_field_member_name(node, &name);
412 let field_name = format!("@{name}");
413 let code_unit = CodeUnit::new_fq(
414 self.file.clone(),
415 CodeUnitType::Field,
416 String::new(),
417 member_short_name(segments, &member_name),
418 ruby_scoped_field_fq(segments, &field_name, method_is_singleton_context(node)),
419 );
420 self.parsed.replace_code_unit(
421 code_unit.clone(),
422 node,
423 self.source,
424 Some(parent.clone()),
425 None,
426 );
427 self.parsed
428 .record_materialization(MaterializationRecord::GeneratedDeclaration {
429 site: node_range(node),
430 argument: node_range(arg),
431 kind: GenerationKind::AccessorMacro,
432 unit: code_unit.clone(),
433 });
434 self.parsed.add_signature(
435 code_unit,
436 ruby_node_text(node, self.source).trim().to_string(),
437 );
438 if matches!(method_name, "attr_accessor" | "attr_reader") {
439 self.add_member_function(
440 node,
441 arg,
442 segments,
443 parent,
444 &name,
445 GenerationKind::AccessorMacro,
446 );
447 }
448 if matches!(method_name, "attr_accessor" | "attr_writer") {
449 self.add_member_function(
450 node,
451 arg,
452 segments,
453 parent,
454 &format!("{name}="),
455 GenerationKind::AccessorMacro,
456 );
457 }
458 }
459 if dynamic_argument_seen {
460 self.parsed
461 .record_materialization(MaterializationRecord::DynamicGenerationSite {
462 site: node_range(node),
463 kind: GenerationKind::AccessorMacro,
464 });
465 }
466 }
467
468 fn visit_alias_method(
469 &mut self,
470 node: Node<'_>,
471 segments: &[String],
472 parent: Option<&CodeUnit>,
473 ) {
474 let Some(parent) = parent else {
475 return;
476 };
477 let Some(arguments) = node.child_by_field_name("arguments") else {
478 return;
479 };
480 let mut cursor = arguments.walk();
481 let Some(alias_arg) = arguments.named_children(&mut cursor).next() else {
482 return;
483 };
484 let Some(alias_name) = literal_symbol_or_string_name(alias_arg, self.source) else {
485 self.parsed
489 .record_materialization(MaterializationRecord::DynamicGenerationSite {
490 site: node_range(node),
491 kind: GenerationKind::AliasMacro,
492 });
493 return;
494 };
495 self.add_member_function(
496 node,
497 alias_arg,
498 segments,
499 parent,
500 &alias_name,
501 GenerationKind::AliasMacro,
502 );
503 }
504
505 fn add_member_function(
506 &mut self,
507 signature_node: Node<'_>,
508 range_node: Node<'_>,
509 segments: &[String],
510 parent: &CodeUnit,
511 name: &str,
512 generation: GenerationKind,
513 ) {
514 let code_unit = CodeUnit::new_fq(
515 self.file.clone(),
516 CodeUnitType::Function,
517 String::new(),
518 member_short_name(segments, name),
519 ruby_member_fq(segments, name),
520 );
521 self.parsed.replace_code_unit(
522 code_unit.clone(),
523 range_node,
524 self.source,
525 Some(parent.clone()),
526 None,
527 );
528 self.parsed
529 .record_materialization(MaterializationRecord::GeneratedDeclaration {
530 site: node_range(signature_node),
531 argument: node_range(range_node),
532 kind: generation,
533 unit: code_unit.clone(),
534 });
535 self.parsed.set_ruby_method_dispatch_mode(
536 code_unit.clone(),
537 ruby_method_dispatch_mode(signature_node, self.source),
538 );
539 self.parsed.add_signature(
540 code_unit,
541 ruby_node_text(signature_node, self.source)
542 .trim()
543 .to_string(),
544 );
545 }
546}
547
548fn member_short_name(segments: &[String], name: &str) -> String {
550 if segments.is_empty() {
551 name.to_string()
552 } else {
553 format!("{}.{}", segments.join("$"), name)
554 }
555}
556
557fn attr_field_member_name(node: Node<'_>, name: &str) -> String {
558 if method_is_singleton_context(node) {
559 format!("$singleton.@{name}")
560 } else {
561 format!("@{name}")
562 }
563}
564
565pub fn ruby_variable_field_name(node: Node<'_>, source: &str) -> Option<String> {
566 if !matches!(node.kind(), "instance_variable" | "class_variable") {
567 return None;
568 }
569 let name = ruby_node_text(node, source).trim();
570 (!name.is_empty()).then(|| name.to_string())
571}
572
573fn ruby_field_member_name(node: Node<'_>, source: &str, scope: RubyFieldScope) -> Option<String> {
578 let name = ruby_variable_field_name(node, source)?;
579 Some(match scope {
580 RubyFieldScope::Instance | RubyFieldScope::ClassVariable => name,
581 RubyFieldScope::SingletonClass => format!("$singleton.{name}"),
582 })
583}
584
585pub fn ruby_field_short_name(
586 segments: &[String],
587 node: Node<'_>,
588 source: &str,
589 scope: RubyFieldScope,
590) -> Option<String> {
591 if segments.is_empty() {
592 return None;
593 }
594 let member = ruby_field_member_name(node, source, scope)?;
595 Some(member_short_name(segments, &member))
596}
597
598fn ruby_field_fq(
600 segments: &[String],
601 node: Node<'_>,
602 source: &str,
603 scope: RubyFieldScope,
604) -> Option<FqName> {
605 if segments.is_empty() {
606 return None;
607 }
608 let name = ruby_variable_field_name(node, source)?;
609 Some(ruby_scoped_field_fq(
610 segments,
611 &name,
612 scope == RubyFieldScope::SingletonClass,
613 ))
614}
615
616fn ruby_scoped_field_fq(segments: &[String], name: &str, singleton: bool) -> FqName {
619 let mut fq = ruby_type_chain_fq(segments);
620 if singleton {
621 fq.push(ruby_segment("$singleton", SegmentKind::Package));
622 }
623 fq.push(ruby_segment(name, SegmentKind::Member));
624 fq
625}
626
627pub fn ruby_field_scope_for_assignment_left(
628 left: Node<'_>,
629 segments: &[String],
630 current_scope: Option<RubyFieldScope>,
631) -> Option<RubyFieldScope> {
632 if segments.is_empty() {
633 return None;
634 }
635 match left.kind() {
636 "class_variable" => Some(RubyFieldScope::ClassVariable),
637 "instance_variable" => Some(current_scope.unwrap_or(RubyFieldScope::SingletonClass)),
638 _ => None,
639 }
640}
641
642fn ruby_method_field_scope(node: Node<'_>) -> RubyFieldScope {
643 if method_is_singleton_context(node) {
644 RubyFieldScope::SingletonClass
645 } else {
646 RubyFieldScope::Instance
647 }
648}
649
650fn ruby_method_dispatch_mode(node: Node<'_>, source: &str) -> RubyMethodDispatchMode {
651 if module_function_applies_to_method(node, source) {
652 RubyMethodDispatchMode::ModuleFunction
653 } else if method_is_singleton_context(node) {
654 RubyMethodDispatchMode::Singleton
655 } else {
656 RubyMethodDispatchMode::Instance
657 }
658}
659
660fn method_is_singleton_context(node: Node<'_>) -> bool {
661 if node.kind() == "singleton_method" {
662 return true;
663 }
664 let mut parent = node.parent();
665 while let Some(current) = parent {
666 if current.kind() == "singleton_class" {
667 return true;
668 }
669 if matches!(current.kind(), "class" | "module") {
670 break;
671 }
672 parent = current.parent();
673 }
674 false
675}
676
677fn module_function_applies_to_method(node: Node<'_>, source: &str) -> bool {
678 if node.kind() != "method" {
679 return false;
680 }
681 let Some(name_node) = node.child_by_field_name("name") else {
682 return false;
683 };
684 let method_name = ruby_node_text(name_node, source).trim();
685 let Some(module) = enclosing_module_for_module_function(node) else {
686 return false;
687 };
688 let Some(body) = module.child_by_field_name("body") else {
689 return false;
690 };
691
692 let mut bare_module_function_active = false;
693 let mut stack = vec![body];
694 while let Some(current) = stack.pop() {
695 if current != body
696 && matches!(
697 current.kind(),
698 "class" | "module" | "method" | "singleton_method"
699 )
700 {
701 continue;
702 }
703 if current.kind() == "identifier"
704 && current.start_byte() < node.start_byte()
705 && ruby_node_text(current, source).trim() == "module_function"
706 {
707 bare_module_function_active = true;
708 continue;
709 }
710 if current.kind() == "call"
711 && let Some(method) = current.child_by_field_name("method")
712 && ruby_node_text(method, source).trim() == "module_function"
713 {
714 let mut names = module_function_names(current, source);
715 if names.next().is_none() {
716 if current.start_byte() < node.start_byte() {
717 bare_module_function_active = true;
718 }
719 } else if module_function_names(current, source).any(|name| name == method_name) {
720 return true;
721 }
722 continue;
723 }
724 for index in (0..current.named_child_count()).rev() {
725 if let Some(child) = current.named_child(index) {
726 stack.push(child);
727 }
728 }
729 }
730 bare_module_function_active
731}
732
733fn enclosing_module_for_module_function(node: Node<'_>) -> Option<Node<'_>> {
734 let mut parent = node.parent();
735 while let Some(current) = parent {
736 match current.kind() {
737 "module" => return Some(current),
738 "class" => return None,
739 _ => parent = current.parent(),
740 }
741 }
742 None
743}
744
745fn module_function_names<'a>(node: Node<'_>, source: &'a str) -> impl Iterator<Item = String> + 'a {
746 let mut names = Vec::new();
747 if let Some(arguments) = node.child_by_field_name("arguments") {
748 let mut cursor = arguments.walk();
749 for arg in arguments.named_children(&mut cursor) {
750 if let Some(name) = literal_symbol_or_string_name(arg, source) {
751 names.push(name);
752 }
753 }
754 }
755 names.into_iter()
756}
757
758fn assignment_constant_short_name(lexical_segments: &[String], name_path: &RubyNamePath) -> String {
759 let Some((name, owner_segments)) = name_path.segments.split_last() else {
760 return String::new();
761 };
762 if owner_segments.is_empty() {
763 return member_short_name(lexical_segments, name);
764 }
765 if name_path.absolute || owner_segments.len() > 1 || lexical_segments.is_empty() {
766 return member_short_name(owner_segments, name);
767 }
768
769 let mut resolved_owner = Vec::new();
770 resolved_owner.extend_from_slice(lexical_segments);
771 resolved_owner.extend_from_slice(owner_segments);
772 member_short_name(&resolved_owner, name)
773}
774
775fn assignment_constant_fq(lexical_segments: &[String], name_path: &RubyNamePath) -> FqName {
782 let Some((name, owner_segments)) = name_path.segments.split_last() else {
783 return FqName::new();
784 };
785 if owner_segments.is_empty() {
786 return ruby_member_fq(lexical_segments, name);
787 }
788 if name_path.absolute || owner_segments.len() > 1 || lexical_segments.is_empty() {
789 return ruby_member_fq(owner_segments, name);
790 }
791
792 let mut resolved_owner = Vec::new();
793 resolved_owner.extend_from_slice(lexical_segments);
794 resolved_owner.extend_from_slice(owner_segments);
795 ruby_member_fq(&resolved_owner, name)
796}
797
798pub struct RubyNamePath {
799 pub segments: Vec<String>,
800 pub absolute: bool,
801}
802
803pub fn extract_name_segments(name_node: Node<'_>, source: &str) -> Vec<String> {
808 extract_name_path(name_node, source).segments
809}
810
811pub fn extract_name_path(name_node: Node<'_>, source: &str) -> RubyNamePath {
812 match name_node.kind() {
813 "scope_resolution" => {
814 let mut path = name_node
815 .child_by_field_name("scope")
816 .map(|scope| extract_name_path(scope, source))
817 .unwrap_or_else(|| RubyNamePath {
818 segments: Vec::new(),
819 absolute: true,
820 });
821 if let Some(name) = name_node.child_by_field_name("name") {
822 path.segments.extend(extract_name_segments(name, source));
823 }
824 path
825 }
826 _ => {
827 let text = ruby_node_text(name_node, source).trim();
828 let segments = if text.is_empty() {
829 Vec::new()
830 } else {
831 vec![text.to_string()]
832 };
833 RubyNamePath {
834 segments,
835 absolute: false,
836 }
837 }
838 }
839}
840
841pub fn qualified_internal_name(node: Node<'_>, source: &str) -> Option<String> {
844 let segments = extract_name_segments(node, source);
845 (!segments.is_empty()).then(|| segments.join("$"))
846}
847
848fn extract_ruby_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
851 let mut supertypes = Vec::new();
852
853 if let Some(superclass) = node.child_by_field_name("superclass") {
854 let mut cursor = superclass.walk();
855 if let Some(expr) = superclass.named_children(&mut cursor).next()
856 && let Some(name) = qualified_internal_name(expr, source)
857 {
858 supertypes.push(name);
859 }
860 }
861
862 supertypes
863}
864
865fn literal_symbol_or_string_name(node: Node<'_>, source: &str) -> Option<String> {
868 if !matches!(node.kind(), "simple_symbol" | "string") {
869 return None;
870 }
871 let text = ruby_node_text(node, source).trim();
872 let stripped = text
873 .strip_prefix(':')
874 .unwrap_or(text)
875 .trim_matches(['"', '\'']);
876 (!stripped.is_empty()).then(|| stripped.to_string())
877}
878
879fn first_line(node: Node<'_>, source: &str) -> String {
881 ruby_node_text(node, source)
882 .lines()
883 .map(str::trim)
884 .find(|line| !line.is_empty())
885 .unwrap_or_default()
886 .to_string()
887}
888
889fn ruby_signature_metadata(signature: String, node: Node<'_>, source: &str) -> SignatureMetadata {
890 let Some(parameters_node) = node.child_by_field_name("parameters") else {
891 return SignatureMetadata::new(signature, Vec::new())
892 .with_dispatch_extensibility(DispatchExtensibility::Open);
893 };
894 let mut cursor = parameters_node.walk();
895 let labels = parameters_node
896 .named_children(&mut cursor)
897 .filter_map(|child| ruby_parameter_label_node(child))
898 .map(|label_node| ruby_node_text(label_node, source).trim().to_string())
899 .filter(|label| !label.is_empty())
900 .collect();
901 SignatureMetadata::with_parameter_labels(signature, labels)
902 .with_dispatch_extensibility(DispatchExtensibility::Open)
903}
904
905fn ruby_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
906 match node.kind() {
907 "identifier" => Some(node),
908 "optional_parameter"
909 | "keyword_parameter"
910 | "splat_parameter"
911 | "hash_splat_parameter"
912 | "block_parameter" => node
913 .child_by_field_name("name")
914 .or_else(|| first_identifier_descendant(node)),
915 _ => None,
916 }
917}
918
919fn first_identifier_descendant(node: Node<'_>) -> Option<Node<'_>> {
920 let mut stack = vec![node];
921 while let Some(current) = stack.pop() {
922 if current.kind() == "identifier" {
923 return Some(current);
924 }
925 for index in (0..current.named_child_count()).rev() {
926 if let Some(child) = current.named_child(index) {
927 stack.push(child);
928 }
929 }
930 }
931 None
932}
933
934pub fn is_descendable_container(kind: &str) -> bool {
938 matches!(
939 kind,
940 "if" | "unless"
941 | "elsif"
942 | "else"
943 | "while"
944 | "until"
945 | "for"
946 | "case"
947 | "case_match"
948 | "when"
949 | "in_clause"
950 | "begin"
951 | "body_statement"
952 | "do"
953 | "do_block"
954 | "block"
955 | "then"
956 | "ensure"
957 | "rescue"
958 | "parenthesized_statements"
959 | "begin_block"
960 | "end_block"
961 )
962}
963
964pub fn collect_ruby_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
965 walk_named_tree_preorder(node, true, |node| {
966 if matches!(node.kind(), "identifier" | "constant") {
967 let text = ruby_node_text(node, source).trim();
968 if !text.is_empty() {
969 identifiers.insert(text.to_string());
970 }
971 }
972 WalkControl::Continue
973 });
974}