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.imports.push(info);
374 }
375 }
376 "attr_accessor" | "attr_reader" | "attr_writer" => {
377 self.visit_attr_macro(node, method_name, segments, parent);
378 }
379 "alias_method" => {
380 self.visit_alias_method(node, segments, parent);
381 }
382 _ => {}
383 }
384 }
385
386 fn visit_attr_macro(
387 &mut self,
388 node: Node<'_>,
389 method_name: &str,
390 segments: &[String],
391 parent: Option<&CodeUnit>,
392 ) {
393 let Some(parent) = parent else {
395 return;
396 };
397 let Some(arguments) = node.child_by_field_name("arguments") else {
398 return;
399 };
400 let mut cursor = arguments.walk();
401 let mut dynamic_argument_seen = false;
402 for arg in arguments.named_children(&mut cursor) {
403 let Some(name) = literal_symbol_or_string_name(arg, self.source) else {
404 dynamic_argument_seen = true;
408 continue;
409 };
410 let member_name = attr_field_member_name(node, &name);
411 let field_name = format!("@{name}");
412 let code_unit = CodeUnit::new_fq(
413 self.file.clone(),
414 CodeUnitType::Field,
415 String::new(),
416 member_short_name(segments, &member_name),
417 ruby_scoped_field_fq(segments, &field_name, method_is_singleton_context(node)),
418 );
419 self.parsed.replace_code_unit(
420 code_unit.clone(),
421 node,
422 self.source,
423 Some(parent.clone()),
424 None,
425 );
426 self.parsed
427 .record_materialization(MaterializationRecord::GeneratedDeclaration {
428 site: node_range(node),
429 argument: node_range(arg),
430 kind: GenerationKind::AccessorMacro,
431 unit: code_unit.clone(),
432 });
433 self.parsed.add_signature(
434 code_unit,
435 ruby_node_text(node, self.source).trim().to_string(),
436 );
437 if matches!(method_name, "attr_accessor" | "attr_reader") {
438 self.add_member_function(
439 node,
440 arg,
441 segments,
442 parent,
443 &name,
444 GenerationKind::AccessorMacro,
445 );
446 }
447 if matches!(method_name, "attr_accessor" | "attr_writer") {
448 self.add_member_function(
449 node,
450 arg,
451 segments,
452 parent,
453 &format!("{name}="),
454 GenerationKind::AccessorMacro,
455 );
456 }
457 }
458 if dynamic_argument_seen {
459 self.parsed
460 .record_materialization(MaterializationRecord::DynamicGenerationSite {
461 site: node_range(node),
462 kind: GenerationKind::AccessorMacro,
463 });
464 }
465 }
466
467 fn visit_alias_method(
468 &mut self,
469 node: Node<'_>,
470 segments: &[String],
471 parent: Option<&CodeUnit>,
472 ) {
473 let Some(parent) = parent else {
474 return;
475 };
476 let Some(arguments) = node.child_by_field_name("arguments") else {
477 return;
478 };
479 let mut cursor = arguments.walk();
480 let Some(alias_arg) = arguments.named_children(&mut cursor).next() else {
481 return;
482 };
483 let Some(alias_name) = literal_symbol_or_string_name(alias_arg, self.source) else {
484 self.parsed
488 .record_materialization(MaterializationRecord::DynamicGenerationSite {
489 site: node_range(node),
490 kind: GenerationKind::AliasMacro,
491 });
492 return;
493 };
494 self.add_member_function(
495 node,
496 alias_arg,
497 segments,
498 parent,
499 &alias_name,
500 GenerationKind::AliasMacro,
501 );
502 }
503
504 fn add_member_function(
505 &mut self,
506 signature_node: Node<'_>,
507 range_node: Node<'_>,
508 segments: &[String],
509 parent: &CodeUnit,
510 name: &str,
511 generation: GenerationKind,
512 ) {
513 let code_unit = CodeUnit::new_fq(
514 self.file.clone(),
515 CodeUnitType::Function,
516 String::new(),
517 member_short_name(segments, name),
518 ruby_member_fq(segments, name),
519 );
520 self.parsed.replace_code_unit(
521 code_unit.clone(),
522 range_node,
523 self.source,
524 Some(parent.clone()),
525 None,
526 );
527 self.parsed
528 .record_materialization(MaterializationRecord::GeneratedDeclaration {
529 site: node_range(signature_node),
530 argument: node_range(range_node),
531 kind: generation,
532 unit: code_unit.clone(),
533 });
534 self.parsed.set_ruby_method_dispatch_mode(
535 code_unit.clone(),
536 ruby_method_dispatch_mode(signature_node, self.source),
537 );
538 self.parsed.add_signature(
539 code_unit,
540 ruby_node_text(signature_node, self.source)
541 .trim()
542 .to_string(),
543 );
544 }
545}
546
547fn member_short_name(segments: &[String], name: &str) -> String {
549 if segments.is_empty() {
550 name.to_string()
551 } else {
552 format!("{}.{}", segments.join("$"), name)
553 }
554}
555
556fn attr_field_member_name(node: Node<'_>, name: &str) -> String {
557 if method_is_singleton_context(node) {
558 format!("$singleton.@{name}")
559 } else {
560 format!("@{name}")
561 }
562}
563
564pub fn ruby_variable_field_name(node: Node<'_>, source: &str) -> Option<String> {
565 if !matches!(node.kind(), "instance_variable" | "class_variable") {
566 return None;
567 }
568 let name = ruby_node_text(node, source).trim();
569 (!name.is_empty()).then(|| name.to_string())
570}
571
572fn ruby_field_member_name(node: Node<'_>, source: &str, scope: RubyFieldScope) -> Option<String> {
577 let name = ruby_variable_field_name(node, source)?;
578 Some(match scope {
579 RubyFieldScope::Instance | RubyFieldScope::ClassVariable => name,
580 RubyFieldScope::SingletonClass => format!("$singleton.{name}"),
581 })
582}
583
584pub fn ruby_field_short_name(
585 segments: &[String],
586 node: Node<'_>,
587 source: &str,
588 scope: RubyFieldScope,
589) -> Option<String> {
590 if segments.is_empty() {
591 return None;
592 }
593 let member = ruby_field_member_name(node, source, scope)?;
594 Some(member_short_name(segments, &member))
595}
596
597fn ruby_field_fq(
599 segments: &[String],
600 node: Node<'_>,
601 source: &str,
602 scope: RubyFieldScope,
603) -> Option<FqName> {
604 if segments.is_empty() {
605 return None;
606 }
607 let name = ruby_variable_field_name(node, source)?;
608 Some(ruby_scoped_field_fq(
609 segments,
610 &name,
611 scope == RubyFieldScope::SingletonClass,
612 ))
613}
614
615fn ruby_scoped_field_fq(segments: &[String], name: &str, singleton: bool) -> FqName {
618 let mut fq = ruby_type_chain_fq(segments);
619 if singleton {
620 fq.push(ruby_segment("$singleton", SegmentKind::Package));
621 }
622 fq.push(ruby_segment(name, SegmentKind::Member));
623 fq
624}
625
626pub fn ruby_field_scope_for_assignment_left(
627 left: Node<'_>,
628 segments: &[String],
629 current_scope: Option<RubyFieldScope>,
630) -> Option<RubyFieldScope> {
631 if segments.is_empty() {
632 return None;
633 }
634 match left.kind() {
635 "class_variable" => Some(RubyFieldScope::ClassVariable),
636 "instance_variable" => Some(current_scope.unwrap_or(RubyFieldScope::SingletonClass)),
637 _ => None,
638 }
639}
640
641fn ruby_method_field_scope(node: Node<'_>) -> RubyFieldScope {
642 if method_is_singleton_context(node) {
643 RubyFieldScope::SingletonClass
644 } else {
645 RubyFieldScope::Instance
646 }
647}
648
649fn ruby_method_dispatch_mode(node: Node<'_>, source: &str) -> RubyMethodDispatchMode {
650 if module_function_applies_to_method(node, source) {
651 RubyMethodDispatchMode::ModuleFunction
652 } else if method_is_singleton_context(node) {
653 RubyMethodDispatchMode::Singleton
654 } else {
655 RubyMethodDispatchMode::Instance
656 }
657}
658
659fn method_is_singleton_context(node: Node<'_>) -> bool {
660 if node.kind() == "singleton_method" {
661 return true;
662 }
663 let mut parent = node.parent();
664 while let Some(current) = parent {
665 if current.kind() == "singleton_class" {
666 return true;
667 }
668 if matches!(current.kind(), "class" | "module") {
669 break;
670 }
671 parent = current.parent();
672 }
673 false
674}
675
676fn module_function_applies_to_method(node: Node<'_>, source: &str) -> bool {
677 if node.kind() != "method" {
678 return false;
679 }
680 let Some(name_node) = node.child_by_field_name("name") else {
681 return false;
682 };
683 let method_name = ruby_node_text(name_node, source).trim();
684 let Some(module) = enclosing_module_for_module_function(node) else {
685 return false;
686 };
687 let Some(body) = module.child_by_field_name("body") else {
688 return false;
689 };
690
691 let mut bare_module_function_active = false;
692 let mut stack = vec![body];
693 while let Some(current) = stack.pop() {
694 if current != body
695 && matches!(
696 current.kind(),
697 "class" | "module" | "method" | "singleton_method"
698 )
699 {
700 continue;
701 }
702 if current.kind() == "identifier"
703 && current.start_byte() < node.start_byte()
704 && ruby_node_text(current, source).trim() == "module_function"
705 {
706 bare_module_function_active = true;
707 continue;
708 }
709 if current.kind() == "call"
710 && let Some(method) = current.child_by_field_name("method")
711 && ruby_node_text(method, source).trim() == "module_function"
712 {
713 let mut names = module_function_names(current, source);
714 if names.next().is_none() {
715 if current.start_byte() < node.start_byte() {
716 bare_module_function_active = true;
717 }
718 } else if module_function_names(current, source).any(|name| name == method_name) {
719 return true;
720 }
721 continue;
722 }
723 for index in (0..current.named_child_count()).rev() {
724 if let Some(child) = current.named_child(index) {
725 stack.push(child);
726 }
727 }
728 }
729 bare_module_function_active
730}
731
732fn enclosing_module_for_module_function(node: Node<'_>) -> Option<Node<'_>> {
733 let mut parent = node.parent();
734 while let Some(current) = parent {
735 match current.kind() {
736 "module" => return Some(current),
737 "class" => return None,
738 _ => parent = current.parent(),
739 }
740 }
741 None
742}
743
744fn module_function_names<'a>(node: Node<'_>, source: &'a str) -> impl Iterator<Item = String> + 'a {
745 let mut names = Vec::new();
746 if let Some(arguments) = node.child_by_field_name("arguments") {
747 let mut cursor = arguments.walk();
748 for arg in arguments.named_children(&mut cursor) {
749 if let Some(name) = literal_symbol_or_string_name(arg, source) {
750 names.push(name);
751 }
752 }
753 }
754 names.into_iter()
755}
756
757fn assignment_constant_short_name(lexical_segments: &[String], name_path: &RubyNamePath) -> String {
758 let Some((name, owner_segments)) = name_path.segments.split_last() else {
759 return String::new();
760 };
761 if owner_segments.is_empty() {
762 return member_short_name(lexical_segments, name);
763 }
764 if name_path.absolute || owner_segments.len() > 1 || lexical_segments.is_empty() {
765 return member_short_name(owner_segments, name);
766 }
767
768 let mut resolved_owner = Vec::new();
769 resolved_owner.extend_from_slice(lexical_segments);
770 resolved_owner.extend_from_slice(owner_segments);
771 member_short_name(&resolved_owner, name)
772}
773
774fn assignment_constant_fq(lexical_segments: &[String], name_path: &RubyNamePath) -> FqName {
781 let Some((name, owner_segments)) = name_path.segments.split_last() else {
782 return FqName::new();
783 };
784 if owner_segments.is_empty() {
785 return ruby_member_fq(lexical_segments, name);
786 }
787 if name_path.absolute || owner_segments.len() > 1 || lexical_segments.is_empty() {
788 return ruby_member_fq(owner_segments, name);
789 }
790
791 let mut resolved_owner = Vec::new();
792 resolved_owner.extend_from_slice(lexical_segments);
793 resolved_owner.extend_from_slice(owner_segments);
794 ruby_member_fq(&resolved_owner, name)
795}
796
797pub struct RubyNamePath {
798 pub segments: Vec<String>,
799 pub absolute: bool,
800}
801
802pub fn extract_name_segments(name_node: Node<'_>, source: &str) -> Vec<String> {
807 extract_name_path(name_node, source).segments
808}
809
810pub fn extract_name_path(name_node: Node<'_>, source: &str) -> RubyNamePath {
811 match name_node.kind() {
812 "scope_resolution" => {
813 let mut path = name_node
814 .child_by_field_name("scope")
815 .map(|scope| extract_name_path(scope, source))
816 .unwrap_or_else(|| RubyNamePath {
817 segments: Vec::new(),
818 absolute: true,
819 });
820 if let Some(name) = name_node.child_by_field_name("name") {
821 path.segments.extend(extract_name_segments(name, source));
822 }
823 path
824 }
825 _ => {
826 let text = ruby_node_text(name_node, source).trim();
827 let segments = if text.is_empty() {
828 Vec::new()
829 } else {
830 vec![text.to_string()]
831 };
832 RubyNamePath {
833 segments,
834 absolute: false,
835 }
836 }
837 }
838}
839
840pub fn qualified_internal_name(node: Node<'_>, source: &str) -> Option<String> {
843 let segments = extract_name_segments(node, source);
844 (!segments.is_empty()).then(|| segments.join("$"))
845}
846
847fn extract_ruby_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
850 let mut supertypes = Vec::new();
851
852 if let Some(superclass) = node.child_by_field_name("superclass") {
853 let mut cursor = superclass.walk();
854 if let Some(expr) = superclass.named_children(&mut cursor).next()
855 && let Some(name) = qualified_internal_name(expr, source)
856 {
857 supertypes.push(name);
858 }
859 }
860
861 supertypes
862}
863
864fn literal_symbol_or_string_name(node: Node<'_>, source: &str) -> Option<String> {
867 if !matches!(node.kind(), "simple_symbol" | "string") {
868 return None;
869 }
870 let text = ruby_node_text(node, source).trim();
871 let stripped = text
872 .strip_prefix(':')
873 .unwrap_or(text)
874 .trim_matches(['"', '\'']);
875 (!stripped.is_empty()).then(|| stripped.to_string())
876}
877
878fn first_line(node: Node<'_>, source: &str) -> String {
880 ruby_node_text(node, source)
881 .lines()
882 .map(str::trim)
883 .find(|line| !line.is_empty())
884 .unwrap_or_default()
885 .to_string()
886}
887
888fn ruby_signature_metadata(signature: String, node: Node<'_>, source: &str) -> SignatureMetadata {
889 let Some(parameters_node) = node.child_by_field_name("parameters") else {
890 return SignatureMetadata::new(signature, Vec::new())
891 .with_dispatch_extensibility(DispatchExtensibility::Open);
892 };
893 let mut cursor = parameters_node.walk();
894 let labels = parameters_node
895 .named_children(&mut cursor)
896 .filter_map(|child| ruby_parameter_label_node(child))
897 .map(|label_node| ruby_node_text(label_node, source).trim().to_string())
898 .filter(|label| !label.is_empty())
899 .collect();
900 SignatureMetadata::with_parameter_labels(signature, labels)
901 .with_dispatch_extensibility(DispatchExtensibility::Open)
902}
903
904fn ruby_parameter_label_node(node: Node<'_>) -> Option<Node<'_>> {
905 match node.kind() {
906 "identifier" => Some(node),
907 "optional_parameter"
908 | "keyword_parameter"
909 | "splat_parameter"
910 | "hash_splat_parameter"
911 | "block_parameter" => node
912 .child_by_field_name("name")
913 .or_else(|| first_identifier_descendant(node)),
914 _ => None,
915 }
916}
917
918fn first_identifier_descendant(node: Node<'_>) -> Option<Node<'_>> {
919 let mut stack = vec![node];
920 while let Some(current) = stack.pop() {
921 if current.kind() == "identifier" {
922 return Some(current);
923 }
924 for index in (0..current.named_child_count()).rev() {
925 if let Some(child) = current.named_child(index) {
926 stack.push(child);
927 }
928 }
929 }
930 None
931}
932
933pub fn is_descendable_container(kind: &str) -> bool {
937 matches!(
938 kind,
939 "if" | "unless"
940 | "elsif"
941 | "else"
942 | "while"
943 | "until"
944 | "for"
945 | "case"
946 | "case_match"
947 | "when"
948 | "in_clause"
949 | "begin"
950 | "body_statement"
951 | "do"
952 | "do_block"
953 | "block"
954 | "then"
955 | "ensure"
956 | "rescue"
957 | "parenthesized_statements"
958 | "begin_block"
959 | "end_block"
960 )
961}
962
963pub fn collect_ruby_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
964 walk_named_tree_preorder(node, true, |node| {
965 if matches!(node.kind(), "identifier" | "constant") {
966 let text = ruby_node_text(node, source).trim();
967 if !text.is_empty() {
968 identifiers.insert(text.to_string());
969 }
970 }
971 WalkControl::Continue
972 });
973}