Skip to main content

brokk_bifrost_ruby/
syntax.rs

1//! Pure Ruby node helpers shared by the declaration walk, the structural spec,
2//! the import binder and both usage-graph scans.
3//!
4//! These were free functions at the top of `analyzer/ruby/mod.rs`, above the
5//! `RubyAnalyzer` struct: nothing here needs an analyzer handle, only a
6//! `tree_sitter::Node` and the source text it points into.
7
8use brokk_bifrost_core::analyzer::model::Range;
9use tree_sitter::Node;
10
11pub fn single_static_string_content_node(node: Node<'_>) -> Option<Node<'_>> {
12    if node.named_child_count() != 1 {
13        return None;
14    }
15    let content = node.named_child(0)?;
16    (content.kind() == "string_content").then_some(content)
17}
18
19pub fn ruby_call_arguments(node: Node<'_>) -> Vec<Node<'_>> {
20    let Some(arguments) = ruby_call_arguments_node(node) else {
21        return Vec::new();
22    };
23    let mut cursor = arguments.walk();
24    arguments
25        .named_children(&mut cursor)
26        .filter(|child| is_runtime_node(child.kind()))
27        .collect()
28}
29
30pub fn ruby_first_call_argument(node: Node<'_>) -> Option<Node<'_>> {
31    let arguments = ruby_call_arguments_node(node)?;
32    let mut cursor = arguments.walk();
33    arguments
34        .named_children(&mut cursor)
35        .find(|child| is_runtime_node(child.kind()))
36}
37
38fn ruby_call_arguments_node(node: Node<'_>) -> Option<Node<'_>> {
39    node.child_by_field_name("arguments")
40}
41
42/// Whether an argument-list child is a runtime value rather than a parameter,
43/// comment or symbol-key slot. Also read by the parked Ruby value-semantics
44/// lowerer, which enumerates call arguments the same way.
45pub fn is_runtime_node(kind: &str) -> bool {
46    !matches!(
47        kind,
48        "comment"
49            | "method_parameters"
50            | "lambda_parameters"
51            | "block_parameters"
52            | "block_parameter"
53            | "optional_parameter"
54            | "keyword_parameter"
55            | "splat_parameter"
56            | "hash_splat_parameter"
57            | "forward_parameter"
58            | "destructured_parameter"
59            | "exception_variable"
60            | "hash_key_symbol"
61            | "bare_symbol"
62    )
63}
64
65/// Returns the source range of the semantic identifier carried by a Ruby symbol.
66///
67/// Tree-sitter represents an unquoted symbol such as `:audit` as one leaf
68/// `simple_symbol` node, so its parser range includes the leading colon. Static
69/// quoted symbols have a structured `string_content` child that excludes both
70/// the colon and quote delimiters. Other nodes keep their parser range.
71pub fn ruby_semantic_identifier_range(node: Node<'_>, source: &str) -> Range {
72    let node_range = || Range {
73        start_byte: node.start_byte(),
74        end_byte: node.end_byte(),
75        start_line: node.start_position().row,
76        end_line: node.end_position().row,
77    };
78
79    match node.kind() {
80        "simple_symbol" => {
81            let text = source.get(node.start_byte()..node.end_byte()).unwrap_or("");
82            if text.strip_prefix(':').is_none_or(str::is_empty) {
83                return node_range();
84            }
85            Range {
86                start_byte: node.start_byte() + ':'.len_utf8(),
87                end_byte: node.end_byte(),
88                start_line: node.start_position().row,
89                end_line: node.end_position().row,
90            }
91        }
92        "delimited_symbol" => {
93            let Some(content) = single_static_string_content_node(node) else {
94                return node_range();
95            };
96            Range {
97                start_byte: content.start_byte(),
98                end_byte: content.end_byte(),
99                start_line: content.start_position().row,
100                end_line: content.end_position().row,
101            }
102        }
103        _ => node_range(),
104    }
105}