Skip to main content

brokk_bifrost_ruby/
structural.rs

1//! Ruby structural spec for `query_code`.
2
3use crate::syntax::single_static_string_content_node;
4use brokk_bifrost_core::analyzer::Language;
5use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
6    attach_argument_role_with_derived_name, attach_role_with_derived_name, attach_terminal_callee,
7    first_named_child,
8};
9use brokk_bifrost_core::analyzer::structural::edges::{
10    INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
11};
12use brokk_bifrost_core::analyzer::structural::facts::Span;
13use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
14use brokk_bifrost_core::analyzer::structural::materialization::{
15    DeclarationMaterializationSupport, RUBY_MATERIALIZATION_SUPPORT,
16};
17use brokk_bifrost_core::analyzer::structural::occurrences::{
18    NO_OCCURRENCE_ROLE_SUPPORT, OccurrenceRoleSupport,
19};
20use brokk_bifrost_core::analyzer::structural::resolution::{
21    LexicalEnvironmentSupport, NO_LEXICAL_ENVIRONMENT_SUPPORT,
22};
23use brokk_bifrost_core::analyzer::structural::routes::{
24    IdentityRouteSupport, NO_IDENTITY_ROUTE_SUPPORT,
25};
26use brokk_bifrost_core::analyzer::structural::spec::{RoleSink, StructuralSpec};
27use tree_sitter::Node;
28
29#[derive(Debug, Default)]
30pub struct RubyStructuralSpec;
31
32pub static RUBY_STRUCTURAL_SPEC: RubyStructuralSpec = RubyStructuralSpec;
33
34pub const RUBY_KIND_TABLE: &[(&str, NormalizedKind)] = &[
35    ("call", NormalizedKind::Call),
36    ("method", NormalizedKind::Function),
37    ("singleton_method", NormalizedKind::Method),
38    ("block", NormalizedKind::Lambda),
39    ("do_block", NormalizedKind::Lambda),
40    ("lambda", NormalizedKind::Lambda),
41    ("class", NormalizedKind::Class),
42    ("module", NormalizedKind::Class),
43    ("assignment", NormalizedKind::Assignment),
44    ("operator_assignment", NormalizedKind::Assignment),
45    ("scope_resolution", NormalizedKind::FieldAccess),
46    ("unary", NormalizedKind::NumericLiteral),
47    ("identifier", NormalizedKind::Identifier),
48    ("constant", NormalizedKind::Identifier),
49    ("instance_variable", NormalizedKind::Identifier),
50    ("class_variable", NormalizedKind::Identifier),
51    ("global_variable", NormalizedKind::Identifier),
52    ("self", NormalizedKind::Identifier),
53    ("simple_symbol", NormalizedKind::Identifier),
54    ("delimited_symbol", NormalizedKind::Identifier),
55    ("hash_key_symbol", NormalizedKind::Identifier),
56    ("string", NormalizedKind::StringLiteral),
57    ("integer", NormalizedKind::NumericLiteral),
58    ("float", NormalizedKind::NumericLiteral),
59    ("true", NormalizedKind::BooleanLiteral),
60    ("false", NormalizedKind::BooleanLiteral),
61    ("nil", NormalizedKind::NullLiteral),
62    ("return", NormalizedKind::Return),
63    ("rescue", NormalizedKind::Catch),
64    ("if", NormalizedKind::If),
65    ("unless", NormalizedKind::If),
66    ("while", NormalizedKind::WhileLoop),
67    ("until", NormalizedKind::WhileLoop),
68    ("for", NormalizedKind::ForLoop),
69];
70
71fn expression_target_node(mut node: Node<'_>) -> Node<'_> {
72    while matches!(node.kind(), "parenthesized_statements") {
73        let Some(child) = first_named_child(node) else {
74            break;
75        };
76        node = child;
77    }
78    node
79}
80
81fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
82    let mut current = expression_target_node(expression);
83    loop {
84        match current.kind() {
85            "identifier" | "constant" | "instance_variable" | "class_variable"
86            | "global_variable" | "self" | "hash_key_symbol" => return Some(current),
87            "simple_symbol" | "delimited_symbol" => return symbol_name_node(current),
88            "scope_resolution" => current = current.child_by_field_name("name")?,
89            "call" => current = current.child_by_field_name("method")?,
90            "pair" => current = current.child_by_field_name("key")?,
91            _ => return None,
92        }
93    }
94}
95
96fn symbol_name_node(node: Node<'_>) -> Option<Node<'_>> {
97    first_named_child_of_kind(node, "string_content").or(Some(node))
98}
99
100fn first_named_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
101    (0..node.named_child_count())
102        .filter_map(|index| node.named_child(index))
103        .find(|child| child.kind() == kind)
104}
105
106fn is_numeric_literal_node(node: Node<'_>) -> bool {
107    matches!(node.kind(), "integer" | "float")
108}
109
110fn is_signed_numeric_unary(node: Node<'_>) -> bool {
111    node.kind() == "unary"
112        && node
113            .child_by_field_name("operator")
114            .is_some_and(|operator| matches!(operator.kind(), "+" | "-"))
115        && node
116            .child_by_field_name("operand")
117            .map(expression_target_node)
118            .is_some_and(is_numeric_literal_node)
119}
120
121fn is_inside_signed_numeric_wrapper(node: Node<'_>) -> bool {
122    let Some(parent) = node.parent() else {
123        return false;
124    };
125    is_signed_numeric_unary(parent)
126}
127
128fn call_method_node(node: Node<'_>) -> Option<Node<'_>> {
129    node.child_by_field_name("method")
130}
131
132fn is_bare_call_identifier(node: Node<'_>) -> bool {
133    node.kind() == "identifier"
134        && node
135            .parent()
136            .is_some_and(|parent| parent.kind() == "body_statement")
137}
138
139fn attach_argument_roles(sink: &mut RoleSink<'_>, arguments: Node<'_>) {
140    for index in 0..arguments.named_child_count() {
141        if !sink.should_continue() {
142            break;
143        }
144        let Some(argument) = arguments.named_child(index) else {
145            continue;
146        };
147        if argument.kind() == "pair" {
148            if let Some(key) = argument.child_by_field_name("key")
149                && let Some(value) = argument
150                    .child_by_field_name("value")
151                    .map(expression_target_node)
152            {
153                sink.kwarg(expression_name_node(key).unwrap_or(key), value);
154            }
155        } else {
156            attach_argument_role_with_derived_name(sink, argument, expression_name_node);
157        }
158    }
159}
160
161fn node_text<'source>(node: Node<'_>, source: &'source str) -> &'source str {
162    node.utf8_text(source.as_bytes()).unwrap_or("")
163}
164
165fn module_argument_node(node: Node<'_>) -> Option<Node<'_>> {
166    let arguments = node.child_by_field_name("arguments")?;
167    (0..arguments.named_child_count())
168        .filter_map(|index| arguments.named_child(index))
169        .find(|argument| argument.kind() == "string")
170}
171
172fn is_import_call(node: Node<'_>, source: &str) -> bool {
173    if node.child_by_field_name("receiver").is_some() {
174        return false;
175    }
176
177    let Some(method) = call_method_node(node) else {
178        return false;
179    };
180    matches!(
181        node_text(method, source).trim(),
182        "require" | "require_relative" | "load" | "autoload"
183    ) && module_argument_node(node).is_some()
184}
185
186fn static_string_content_span(node: Node<'_>) -> Option<Span> {
187    if node.kind() != "string" {
188        return None;
189    }
190    let content = single_static_string_content_node(node)?;
191    Some(Span {
192        start_byte: content.start_byte(),
193        end_byte: content.end_byte(),
194    })
195}
196
197impl StructuralSpec for RubyStructuralSpec {
198    fn language(&self) -> Language {
199        Language::Ruby
200    }
201
202    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
203        RUBY_KIND_TABLE
204    }
205
206    fn refine_kind(
207        &self,
208        node: Node<'_>,
209        kind: NormalizedKind,
210        enclosing: Option<NormalizedKind>,
211        source: &str,
212    ) -> NormalizedKind {
213        if is_bare_call_identifier(node) {
214            NormalizedKind::Call
215        } else if node.kind() == "call" && is_import_call(node, source) {
216            NormalizedKind::Import
217        } else if node.kind() == "method"
218            && kind == NormalizedKind::Function
219            && enclosing == Some(NormalizedKind::Class)
220        {
221            NormalizedKind::Method
222        } else {
223            kind
224        }
225    }
226
227    fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
228        if kind == NormalizedKind::Lambda
229            && matches!(node.kind(), "block" | "do_block")
230            && node
231                .parent()
232                .is_some_and(|parent| parent.kind() == "lambda")
233        {
234            return false;
235        }
236
237        if kind == NormalizedKind::NumericLiteral {
238            if node.kind() == "unary" {
239                return is_signed_numeric_unary(node);
240            }
241            if is_numeric_literal_node(node) && is_inside_signed_numeric_wrapper(node) {
242                return false;
243            }
244        }
245
246        true
247    }
248
249    fn supports_kind(&self, kind: NormalizedKind) -> bool {
250        kind == NormalizedKind::Import
251            || self
252                .kind_table()
253                .iter()
254                .any(|(_, fact_kind)| fact_kind.satisfies(kind))
255    }
256
257    fn supports_role(&self, role: Role) -> bool {
258        role != Role::Decorator
259    }
260
261    /// Ruby has not learned occurrence-role classification yet (#1473).
262    /// The empty table is the honest answer: queries and assertions that ask
263    /// for an occurrence role here report incomplete rather than clean-empty.
264    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
265        &NO_OCCURRENCE_ROLE_SUPPORT
266    }
267
268    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
269        &NO_LEXICAL_ENVIRONMENT_SUPPORT
270    }
271
272    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
273        &RUBY_MATERIALIZATION_SUPPORT
274    }
275
276    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
277        &INVERSE_REFERENCE_EDGE_SUPPORT
278    }
279
280    fn identity_route_support(&self) -> &IdentityRouteSupport {
281        &NO_IDENTITY_ROUTE_SUPPORT
282    }
283
284    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
285        match kind {
286            NormalizedKind::Call => {
287                if is_bare_call_identifier(node) {
288                    attach_terminal_callee(sink, node, Some(node));
289                } else if let Some(method) = call_method_node(node) {
290                    attach_terminal_callee(sink, method, expression_name_node(method));
291                }
292                if let Some(receiver) = node.child_by_field_name("receiver") {
293                    attach_role_with_derived_name(
294                        sink,
295                        Role::Receiver,
296                        receiver,
297                        expression_name_node,
298                    );
299                }
300                if let Some(arguments) = node.child_by_field_name("arguments") {
301                    attach_argument_roles(sink, arguments);
302                }
303                if let Some(block) = node.child_by_field_name("block") {
304                    attach_role_with_derived_name(sink, Role::Arg, block, expression_name_node);
305                }
306            }
307            NormalizedKind::FieldAccess => {
308                if let Some(field) = node.child_by_field_name("name") {
309                    attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
310                    if let Some(name) = expression_name_node(field) {
311                        sink.set_name(name);
312                    }
313                }
314                if let Some(object) = node.child_by_field_name("scope") {
315                    attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
316                }
317            }
318            NormalizedKind::Function
319            | NormalizedKind::Method
320            | NormalizedKind::Class
321            | NormalizedKind::Declaration => {
322                if let Some(name) = node.child_by_field_name("name") {
323                    sink.set_name(expression_name_node(name).unwrap_or(name));
324                }
325            }
326            NormalizedKind::Assignment => {
327                if let Some(left) = node.child_by_field_name("left") {
328                    let left = expression_target_node(left);
329                    attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
330                    if let Some(name) = expression_name_node(left) {
331                        sink.set_name(name);
332                    }
333                }
334                if let Some(right) = node.child_by_field_name("right") {
335                    let right = expression_target_node(right);
336                    attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
337                }
338            }
339            NormalizedKind::Import => {
340                if let Some(module) = module_argument_node(node)
341                    && let Some(name) = static_string_content_span(module)
342                {
343                    sink.role_named_span(Role::Module, module, name);
344                }
345            }
346            NormalizedKind::Identifier => match expression_name_node(node) {
347                Some(name) => sink.set_name(name),
348                None => sink.set_name(node),
349            },
350            _ => {}
351        }
352    }
353}