1use crate::local_bindings::{
4 LocalBindingTimeline, UnboundedLocalBindingBudget, collect_local_bindings,
5};
6use crate::syntax::single_static_string_content_node;
7use brokk_bifrost_core::analyzer::Language;
8use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
9 attach_argument_role_with_derived_name, attach_role_with_derived_name, attach_terminal_callee,
10 first_named_child,
11};
12use brokk_bifrost_core::analyzer::structural::callable::CallSiteContext;
13use brokk_bifrost_core::analyzer::structural::edges::{
14 INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
15};
16use brokk_bifrost_core::analyzer::structural::facts::Span;
17use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
18use brokk_bifrost_core::analyzer::structural::materialization::{
19 DeclarationMaterializationSupport, RUBY_MATERIALIZATION_SUPPORT,
20};
21use brokk_bifrost_core::analyzer::structural::occurrences::{
22 NO_OCCURRENCE_ROLE_SUPPORT, OccurrenceRoleSupport,
23};
24use brokk_bifrost_core::analyzer::structural::resolution::{
25 LexicalEnvironmentSupport, NO_LEXICAL_ENVIRONMENT_SUPPORT,
26};
27use brokk_bifrost_core::analyzer::structural::routes::{
28 IdentityRouteSupport, NO_IDENTITY_ROUTE_SUPPORT,
29};
30use brokk_bifrost_core::analyzer::structural::spec::{RoleSink, StructuralSpec};
31use brokk_bifrost_core::hash::HashSet;
32use tree_sitter::Node;
33
34#[derive(Debug, Default)]
35pub struct RubyStructuralSpec;
36
37pub static RUBY_STRUCTURAL_SPEC: RubyStructuralSpec = RubyStructuralSpec;
38
39pub const RUBY_KIND_TABLE: &[(&str, NormalizedKind)] = &[
40 ("call", NormalizedKind::Call),
41 ("method", NormalizedKind::Function),
42 ("singleton_method", NormalizedKind::Method),
43 ("block", NormalizedKind::Lambda),
44 ("do_block", NormalizedKind::Lambda),
45 ("lambda", NormalizedKind::Lambda),
46 ("class", NormalizedKind::Class),
47 ("module", NormalizedKind::Class),
48 ("assignment", NormalizedKind::Assignment),
49 ("operator_assignment", NormalizedKind::Assignment),
50 ("scope_resolution", NormalizedKind::FieldAccess),
51 ("unary", NormalizedKind::NumericLiteral),
52 ("identifier", NormalizedKind::Identifier),
53 ("constant", NormalizedKind::Identifier),
54 ("instance_variable", NormalizedKind::Identifier),
55 ("class_variable", NormalizedKind::Identifier),
56 ("global_variable", NormalizedKind::Identifier),
57 ("self", NormalizedKind::Identifier),
58 ("simple_symbol", NormalizedKind::Identifier),
59 ("delimited_symbol", NormalizedKind::Identifier),
60 ("hash_key_symbol", NormalizedKind::Identifier),
61 ("string", NormalizedKind::StringLiteral),
62 ("integer", NormalizedKind::NumericLiteral),
63 ("float", NormalizedKind::NumericLiteral),
64 ("true", NormalizedKind::BooleanLiteral),
65 ("false", NormalizedKind::BooleanLiteral),
66 ("nil", NormalizedKind::NullLiteral),
67 ("return", NormalizedKind::Return),
68 ("rescue", NormalizedKind::Catch),
69 ("if", NormalizedKind::If),
70 ("unless", NormalizedKind::If),
71 ("while", NormalizedKind::WhileLoop),
72 ("until", NormalizedKind::WhileLoop),
73 ("for", NormalizedKind::ForLoop),
74];
75
76fn expression_target_node(mut node: Node<'_>) -> Node<'_> {
77 while matches!(node.kind(), "parenthesized_statements") {
78 let Some(child) = first_named_child(node) else {
79 break;
80 };
81 node = child;
82 }
83 node
84}
85
86fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
87 let mut current = expression_target_node(expression);
88 loop {
89 match current.kind() {
90 "identifier" | "constant" | "instance_variable" | "class_variable"
91 | "global_variable" | "self" | "hash_key_symbol" => return Some(current),
92 "simple_symbol" | "delimited_symbol" => return symbol_name_node(current),
93 "scope_resolution" => current = current.child_by_field_name("name")?,
94 "call" => current = current.child_by_field_name("method")?,
95 "pair" => current = current.child_by_field_name("key")?,
96 _ => return None,
97 }
98 }
99}
100
101fn symbol_name_node(node: Node<'_>) -> Option<Node<'_>> {
102 first_named_child_of_kind(node, "string_content").or(Some(node))
103}
104
105fn first_named_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
106 (0..node.named_child_count())
107 .filter_map(|index| node.named_child(index))
108 .find(|child| child.kind() == kind)
109}
110
111fn is_numeric_literal_node(node: Node<'_>) -> bool {
112 matches!(node.kind(), "integer" | "float")
113}
114
115fn is_signed_numeric_unary(node: Node<'_>) -> bool {
116 node.kind() == "unary"
117 && node
118 .child_by_field_name("operator")
119 .is_some_and(|operator| matches!(operator.kind(), "+" | "-"))
120 && node
121 .child_by_field_name("operand")
122 .map(expression_target_node)
123 .is_some_and(is_numeric_literal_node)
124}
125
126fn is_inside_signed_numeric_wrapper(node: Node<'_>) -> bool {
127 let Some(parent) = node.parent() else {
128 return false;
129 };
130 is_signed_numeric_unary(parent)
131}
132
133fn call_method_node(node: Node<'_>) -> Option<Node<'_>> {
134 node.child_by_field_name("method")
135}
136
137fn is_nested_scope_root(node: Node<'_>) -> bool {
142 match node.kind() {
143 "method" | "singleton_method" | "class" | "module" | "singleton_class" | "lambda" => true,
144 "block" | "do_block" => node.parent().is_none_or(|parent| parent.kind() != "lambda"),
145 _ => false,
146 }
147}
148
149fn is_value_read_position(node: Node<'_>) -> bool {
157 let Some(parent) = node.parent() else {
158 return false;
159 };
160 let is_field = |field: &str| {
161 parent
162 .child_by_field_name(field)
163 .is_some_and(|child| child.id() == node.id())
164 };
165 match parent.kind() {
166 "program" | "body_statement" | "then" | "else" | "do" | "block_body" | "begin"
168 | "interpolation" => true,
169 "parenthesized_statements" => {
174 let mut ancestor = parent;
175 while ancestor.kind() == "parenthesized_statements" {
176 match ancestor.parent() {
177 Some(next) => ancestor = next,
178 None => return true,
179 }
180 }
181 !(ancestor.kind() == "unary"
182 && ancestor
183 .child_by_field_name("operator")
184 .is_some_and(|operator| operator.kind() == "defined?"))
185 }
186 "argument_list" | "splat_argument" | "hash_splat_argument" | "block_argument" => true,
188 "binary" | "range" | "array" | "element_reference" => true,
190 "unary" => {
193 is_field("operand")
194 && parent
195 .child_by_field_name("operator")
196 .is_none_or(|operator| operator.kind() != "defined?")
197 }
198 "if" | "unless" | "elsif" | "while" | "until" | "conditional" | "case" | "case_match"
200 | "when" | "rescue_modifier" => true,
201 "pair" => is_field("value"),
202 "assignment" | "operator_assignment" => is_field("right"),
203 "call" => is_field("receiver"),
204 _ => false,
205 }
206}
207
208fn bare_call_identifier_starts(root: Node<'_>, source: &str) -> HashSet<usize> {
215 let mut starts = HashSet::default();
216 let mut timelines: Vec<LocalBindingTimeline> = Vec::new();
217 let mut scopes: Vec<(Node<'_>, Option<usize>)> = vec![(root, None)];
218 while let Some((scope, inherited)) = scopes.pop() {
219 let body = scope.child_by_field_name("body").unwrap_or(scope);
220 let inherited_bindings = matches!(scope.kind(), "lambda" | "block" | "do_block")
221 .then(|| inherited.map(|index| (&timelines[index], scope.start_byte())))
222 .flatten();
223 let collection = collect_local_bindings(
224 source,
225 scope,
226 body,
227 inherited_bindings,
228 &mut UnboundedLocalBindingBudget,
229 )
230 .unwrap_or_else(|impossible| match impossible {});
231 let timeline_index = timelines.len();
232 timelines.push(collection.timeline);
233 let timeline = &timelines[timeline_index];
234
235 let mut walk = vec![scope];
236 while let Some(node) = walk.pop() {
237 for index in (0..node.named_child_count()).rev() {
238 let Some(child) = node.named_child(index) else {
239 continue;
240 };
241 if is_nested_scope_root(child) {
242 scopes.push((child, Some(timeline_index)));
243 } else {
244 walk.push(child);
245 }
246 }
247 if node.kind() == "identifier"
248 && is_value_read_position(node)
249 && !timeline.is_active_at(node_text(node, source), node.start_byte())
250 {
251 starts.insert(node.start_byte());
252 }
253 }
254 }
255 starts
256}
257
258fn attach_argument_roles(sink: &mut RoleSink<'_>, arguments: Node<'_>) {
259 for index in 0..arguments.named_child_count() {
260 if !sink.should_continue() {
261 break;
262 }
263 let Some(argument) = arguments.named_child(index) else {
264 continue;
265 };
266 if argument.kind() == "pair" {
267 if let Some(key) = argument.child_by_field_name("key")
268 && let Some(value) = argument
269 .child_by_field_name("value")
270 .map(expression_target_node)
271 {
272 sink.kwarg(expression_name_node(key).unwrap_or(key), value);
273 }
274 } else {
275 attach_argument_role_with_derived_name(sink, argument, expression_name_node);
276 }
277 }
278}
279
280fn node_text<'source>(node: Node<'_>, source: &'source str) -> &'source str {
281 node.utf8_text(source.as_bytes()).unwrap_or("")
282}
283
284fn module_argument_node(node: Node<'_>) -> Option<Node<'_>> {
285 let arguments = node.child_by_field_name("arguments")?;
286 (0..arguments.named_child_count())
287 .filter_map(|index| arguments.named_child(index))
288 .find(|argument| argument.kind() == "string")
289}
290
291fn is_import_call(node: Node<'_>, source: &str) -> bool {
292 if node.child_by_field_name("receiver").is_some() {
293 return false;
294 }
295
296 let Some(method) = call_method_node(node) else {
297 return false;
298 };
299 matches!(
300 node_text(method, source).trim(),
301 "require" | "require_relative" | "load" | "autoload"
302 ) && module_argument_node(node).is_some()
303}
304
305fn static_string_content_span(node: Node<'_>) -> Option<Span> {
306 if node.kind() != "string" {
307 return None;
308 }
309 let content = single_static_string_content_node(node)?;
310 Some(Span {
311 start_byte: content.start_byte(),
312 end_byte: content.end_byte(),
313 })
314}
315
316impl StructuralSpec for RubyStructuralSpec {
317 fn language(&self) -> Language {
318 Language::Ruby
319 }
320
321 fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
322 RUBY_KIND_TABLE
323 }
324
325 fn call_site_context(&self, root: Node<'_>, source: &str) -> CallSiteContext {
330 CallSiteContext::with_identifier_call_starts(bare_call_identifier_starts(root, source))
331 }
332
333 fn refine_kind(
334 &self,
335 node: Node<'_>,
336 kind: NormalizedKind,
337 enclosing: Option<NormalizedKind>,
338 source: &str,
339 context: &CallSiteContext,
340 ) -> NormalizedKind {
341 if node.kind() == "identifier" && context.is_identifier_call_at(node.start_byte()) {
342 NormalizedKind::Call
343 } else if node.kind() == "call" && is_import_call(node, source) {
344 NormalizedKind::Import
345 } else if node.kind() == "method"
346 && kind == NormalizedKind::Function
347 && enclosing == Some(NormalizedKind::Class)
348 {
349 NormalizedKind::Method
350 } else {
351 kind
352 }
353 }
354
355 fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
356 if kind == NormalizedKind::Lambda
357 && matches!(node.kind(), "block" | "do_block")
358 && node
359 .parent()
360 .is_some_and(|parent| parent.kind() == "lambda")
361 {
362 return false;
363 }
364
365 if kind == NormalizedKind::NumericLiteral {
366 if node.kind() == "unary" {
367 return is_signed_numeric_unary(node);
368 }
369 if is_numeric_literal_node(node) && is_inside_signed_numeric_wrapper(node) {
370 return false;
371 }
372 }
373
374 true
375 }
376
377 fn supports_kind(&self, kind: NormalizedKind) -> bool {
378 kind == NormalizedKind::Import
379 || self
380 .kind_table()
381 .iter()
382 .any(|(_, fact_kind)| fact_kind.satisfies(kind))
383 }
384
385 fn supports_role(&self, role: Role) -> bool {
386 role != Role::Decorator
387 }
388
389 fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
393 &NO_OCCURRENCE_ROLE_SUPPORT
394 }
395
396 fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
397 &NO_LEXICAL_ENVIRONMENT_SUPPORT
398 }
399
400 fn materialization_support(&self) -> &DeclarationMaterializationSupport {
401 &RUBY_MATERIALIZATION_SUPPORT
402 }
403
404 fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
405 &INVERSE_REFERENCE_EDGE_SUPPORT
406 }
407
408 fn identity_route_support(&self) -> &IdentityRouteSupport {
409 &NO_IDENTITY_ROUTE_SUPPORT
410 }
411
412 fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
413 match kind {
414 NormalizedKind::Call => {
415 if node.kind() == "identifier" {
418 attach_terminal_callee(sink, node, Some(node));
419 } else if let Some(method) = call_method_node(node) {
420 attach_terminal_callee(sink, method, expression_name_node(method));
421 }
422 if let Some(receiver) = node.child_by_field_name("receiver") {
423 attach_role_with_derived_name(
424 sink,
425 Role::Receiver,
426 receiver,
427 expression_name_node,
428 );
429 }
430 if let Some(arguments) = node.child_by_field_name("arguments") {
431 attach_argument_roles(sink, arguments);
432 }
433 if let Some(block) = node.child_by_field_name("block") {
434 attach_role_with_derived_name(sink, Role::Arg, block, expression_name_node);
435 }
436 }
437 NormalizedKind::FieldAccess => {
438 if let Some(field) = node.child_by_field_name("name") {
439 attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
440 if let Some(name) = expression_name_node(field) {
441 sink.set_name(name);
442 }
443 }
444 if let Some(object) = node.child_by_field_name("scope") {
445 attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
446 }
447 }
448 NormalizedKind::Function
449 | NormalizedKind::Method
450 | NormalizedKind::Class
451 | NormalizedKind::Declaration => {
452 if let Some(name) = node.child_by_field_name("name") {
453 sink.set_name(expression_name_node(name).unwrap_or(name));
454 }
455 }
456 NormalizedKind::Assignment => {
457 if let Some(left) = node.child_by_field_name("left") {
458 let left = expression_target_node(left);
459 attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
460 if let Some(name) = expression_name_node(left) {
461 sink.set_name(name);
462 }
463 }
464 if let Some(right) = node.child_by_field_name("right") {
465 let right = expression_target_node(right);
466 attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
467 }
468 }
469 NormalizedKind::Import => {
470 if let Some(module) = module_argument_node(node)
471 && let Some(name) = static_string_content_span(module)
472 {
473 sink.role_named_span(Role::Module, module, name);
474 }
475 }
476 NormalizedKind::Identifier => match expression_name_node(node) {
477 Some(name) => sink.set_name(name),
478 None => sink.set_name(node),
479 },
480 _ => {}
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 fn parse(source: &str) -> tree_sitter::Tree {
490 let mut parser = tree_sitter::Parser::new();
491 parser
492 .set_language(&tree_sitter_ruby::LANGUAGE.into())
493 .expect("Ruby grammar is valid");
494 parser.parse(source, None).expect("source parses")
495 }
496
497 fn identifier_start(source: &str, needle: &str, occurrence: usize) -> usize {
500 let mut found = 0;
501 let mut from = 0;
502 loop {
503 let start = from
504 + source[from..]
505 .find(needle)
506 .unwrap_or_else(|| panic!("needle {needle:?} occurrence {occurrence}"));
507 let boundary = |byte: Option<u8>| {
508 byte.is_none_or(|byte| !(byte.is_ascii_alphanumeric() || byte == b'_'))
509 };
510 if boundary(source.as_bytes().get(start.wrapping_sub(1)).copied())
511 && boundary(source.as_bytes().get(start + needle.len()).copied())
512 {
513 if found == occurrence {
514 return start;
515 }
516 found += 1;
517 }
518 from = start + needle.len();
519 }
520 }
521
522 fn bare_call_starts(source: &str) -> HashSet<usize> {
523 let tree = parse(source);
524 bare_call_identifier_starts(tree.root_node(), source)
525 }
526
527 fn assert_bare_call(source: &str, needle: &str, occurrence: usize, expected: bool) {
528 let starts = bare_call_starts(source);
529 let start = identifier_start(source, needle, occurrence);
530 assert_eq!(
531 starts.contains(&start),
532 expected,
533 "{needle:?} occurrence {occurrence} at byte {start} in {source:?}; classified starts: {starts:?}"
534 );
535 }
536
537 #[test]
541 fn argument_position_bare_call_is_classified() {
542 let source = "def dfb_source\n \"tainted\"\nend\n\ndef dfb_sink(value)\nend\n\ndef run\n dfb_sink(dfb_source)\nend\n";
543 assert_bare_call(source, "dfb_source", 1, true);
544 assert_bare_call(source, "dfb_sink", 1, false);
545 }
546
547 #[test]
550 fn assignment_value_bare_call_is_classified_and_the_local_is_not() {
551 let source = "def dfb_source\n \"tainted\"\nend\n\ndef dfb_sink(value)\nend\n\ndef run\n value = dfb_source\n dfb_sink(value)\nend\n";
552 assert_bare_call(source, "dfb_source", 1, true);
553 assert_bare_call(source, "value", 1, false);
554 assert_bare_call(source, "value", 2, false);
555 }
556
557 #[test]
561 fn statement_position_and_parenthesized_forms_are_unchanged() {
562 let source =
563 "def dfb_source\n \"tainted\"\nend\n\ndef run\n dfb_source\n dfb_source()\nend\n";
564 assert_bare_call(source, "dfb_source", 1, true);
565 assert_bare_call(source, "dfb_source", 2, false);
566 }
567
568 #[test]
572 fn assigned_local_shadows_the_same_named_bare_call() {
573 let source = "def run\n dfb_source = compute\n dfb_sink(dfb_source)\nend\n";
574 assert_bare_call(source, "compute", 0, true);
575 assert_bare_call(source, "dfb_source", 0, false);
576 assert_bare_call(source, "dfb_source", 1, false);
577 }
578
579 #[test]
582 fn parameter_shadows_the_same_named_bare_call() {
583 let source = "def run(dfb_source)\n dfb_sink(dfb_source)\nend\n";
584 assert_bare_call(source, "dfb_source", 1, false);
585 }
586
587 #[test]
591 fn receiver_calls_classify_only_the_unbound_receiver() {
592 let source =
593 "def run\n helper = Helper.new\n helper.dfb_source\n unbound.dfb_source\nend\n";
594 assert_bare_call(source, "dfb_source", 0, false);
595 assert_bare_call(source, "dfb_source", 1, false);
596 assert_bare_call(source, "helper", 1, false);
597 assert_bare_call(source, "unbound", 0, true);
598 }
599
600 #[test]
602 fn nested_argument_positions_are_classified() {
603 let source = "def run\n dfb_sink(wrap(dfb_source))\nend\n";
604 assert_bare_call(source, "dfb_source", 0, true);
605 assert_bare_call(source, "wrap", 0, false);
606 }
607
608 #[test]
612 fn blocks_inherit_active_bindings() {
613 let source = "def run\n captured = 1\n items.each do |x|\n dfb_sink(captured)\n dfb_sink(free_name)\n dfb_sink(x)\n end\nend\n";
614 assert_bare_call(source, "captured", 1, false);
615 assert_bare_call(source, "free_name", 0, true);
616 assert_bare_call(source, "x", 1, false);
617 assert_bare_call(source, "items", 0, true);
618 }
619
620 #[test]
623 fn reads_before_the_activating_assignment_are_bare_calls() {
624 let source = "def run\n dfb_sink(v)\n v = 1\n dfb_sink(v)\nend\n";
625 assert_bare_call(source, "v", 0, true);
626 assert_bare_call(source, "v", 1, false);
627 assert_bare_call(source, "v", 2, false);
628 }
629
630 #[test]
634 fn trailing_local_reads_in_statement_position_stay_identifiers() {
635 let source = "def run(x)\n compute\n x\nend\n";
636 assert_bare_call(source, "compute", 0, true);
637 assert_bare_call(source, "x", 1, false);
638 }
639
640 #[test]
643 fn defined_operands_are_not_classified() {
644 let source = "def run\n defined?(maybe_missing)\n defined? bare_operand\nend\n";
645 assert_bare_call(source, "maybe_missing", 0, false);
646 assert_bare_call(source, "bare_operand", 0, false);
647 }
648
649 #[test]
651 fn top_level_reads_follow_the_program_scope_timeline() {
652 let source = "x = 1\nx\nfree_top_level\n";
653 assert_bare_call(source, "x", 1, false);
654 assert_bare_call(source, "free_top_level", 0, true);
655 }
656
657 #[test]
661 fn condition_and_operand_positions_are_classified() {
662 let source = "def run(bound)\n if cond_call\n bound + operand_call\n end\n cond_call ? bound : other_call\n \"#{interp_call}\"\nend\n";
663 assert_bare_call(source, "cond_call", 0, true);
664 assert_bare_call(source, "operand_call", 0, true);
665 assert_bare_call(source, "other_call", 0, true);
666 assert_bare_call(source, "interp_call", 0, true);
667 assert_bare_call(source, "bound", 1, false);
668 assert_bare_call(source, "bound", 2, false);
669 }
670
671 #[test]
674 fn pattern_binders_are_preserved_as_identifiers() {
675 let source =
676 "def run\n case subject_call\n in [first, second]\n dfb_sink(first)\n end\nend\n";
677 assert_bare_call(source, "subject_call", 0, true);
678 assert_bare_call(source, "first", 0, false);
679 assert_bare_call(source, "first", 1, false);
680 assert_bare_call(source, "second", 0, false);
681 }
682
683 #[test]
686 fn methods_and_classes_do_not_inherit_locals() {
687 let source = "outer = 1\nouter\ndef run\n outer\nend\nclass Widget\n outer\nend\n";
688 assert_bare_call(source, "outer", 1, false);
689 assert_bare_call(source, "outer", 2, true);
690 assert_bare_call(source, "outer", 3, true);
691 }
692}