1use 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 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}