use crate::syntax::single_static_string_content_node;
use brokk_bifrost_core::analyzer::Language;
use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
attach_argument_role_with_derived_name, attach_role_with_derived_name, attach_terminal_callee,
first_named_child,
};
use brokk_bifrost_core::analyzer::structural::edges::{
INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
};
use brokk_bifrost_core::analyzer::structural::facts::Span;
use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
use brokk_bifrost_core::analyzer::structural::materialization::{
DeclarationMaterializationSupport, RUBY_MATERIALIZATION_SUPPORT,
};
use brokk_bifrost_core::analyzer::structural::occurrences::{
NO_OCCURRENCE_ROLE_SUPPORT, OccurrenceRoleSupport,
};
use brokk_bifrost_core::analyzer::structural::resolution::{
LexicalEnvironmentSupport, NO_LEXICAL_ENVIRONMENT_SUPPORT,
};
use brokk_bifrost_core::analyzer::structural::routes::{
IdentityRouteSupport, NO_IDENTITY_ROUTE_SUPPORT,
};
use brokk_bifrost_core::analyzer::structural::spec::{RoleSink, StructuralSpec};
use tree_sitter::Node;
#[derive(Debug, Default)]
pub struct RubyStructuralSpec;
pub static RUBY_STRUCTURAL_SPEC: RubyStructuralSpec = RubyStructuralSpec;
pub const RUBY_KIND_TABLE: &[(&str, NormalizedKind)] = &[
("call", NormalizedKind::Call),
("method", NormalizedKind::Function),
("singleton_method", NormalizedKind::Method),
("block", NormalizedKind::Lambda),
("do_block", NormalizedKind::Lambda),
("lambda", NormalizedKind::Lambda),
("class", NormalizedKind::Class),
("module", NormalizedKind::Class),
("assignment", NormalizedKind::Assignment),
("operator_assignment", NormalizedKind::Assignment),
("scope_resolution", NormalizedKind::FieldAccess),
("unary", NormalizedKind::NumericLiteral),
("identifier", NormalizedKind::Identifier),
("constant", NormalizedKind::Identifier),
("instance_variable", NormalizedKind::Identifier),
("class_variable", NormalizedKind::Identifier),
("global_variable", NormalizedKind::Identifier),
("self", NormalizedKind::Identifier),
("simple_symbol", NormalizedKind::Identifier),
("delimited_symbol", NormalizedKind::Identifier),
("hash_key_symbol", NormalizedKind::Identifier),
("string", NormalizedKind::StringLiteral),
("integer", NormalizedKind::NumericLiteral),
("float", NormalizedKind::NumericLiteral),
("true", NormalizedKind::BooleanLiteral),
("false", NormalizedKind::BooleanLiteral),
("nil", NormalizedKind::NullLiteral),
("return", NormalizedKind::Return),
("rescue", NormalizedKind::Catch),
("if", NormalizedKind::If),
("unless", NormalizedKind::If),
("while", NormalizedKind::WhileLoop),
("until", NormalizedKind::WhileLoop),
("for", NormalizedKind::ForLoop),
];
fn expression_target_node(mut node: Node<'_>) -> Node<'_> {
while matches!(node.kind(), "parenthesized_statements") {
let Some(child) = first_named_child(node) else {
break;
};
node = child;
}
node
}
fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
let mut current = expression_target_node(expression);
loop {
match current.kind() {
"identifier" | "constant" | "instance_variable" | "class_variable"
| "global_variable" | "self" | "hash_key_symbol" => return Some(current),
"simple_symbol" | "delimited_symbol" => return symbol_name_node(current),
"scope_resolution" => current = current.child_by_field_name("name")?,
"call" => current = current.child_by_field_name("method")?,
"pair" => current = current.child_by_field_name("key")?,
_ => return None,
}
}
}
fn symbol_name_node(node: Node<'_>) -> Option<Node<'_>> {
first_named_child_of_kind(node, "string_content").or(Some(node))
}
fn first_named_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
(0..node.named_child_count())
.filter_map(|index| node.named_child(index))
.find(|child| child.kind() == kind)
}
fn is_numeric_literal_node(node: Node<'_>) -> bool {
matches!(node.kind(), "integer" | "float")
}
fn is_signed_numeric_unary(node: Node<'_>) -> bool {
node.kind() == "unary"
&& node
.child_by_field_name("operator")
.is_some_and(|operator| matches!(operator.kind(), "+" | "-"))
&& node
.child_by_field_name("operand")
.map(expression_target_node)
.is_some_and(is_numeric_literal_node)
}
fn is_inside_signed_numeric_wrapper(node: Node<'_>) -> bool {
let Some(parent) = node.parent() else {
return false;
};
is_signed_numeric_unary(parent)
}
fn call_method_node(node: Node<'_>) -> Option<Node<'_>> {
node.child_by_field_name("method")
}
fn is_bare_call_identifier(node: Node<'_>) -> bool {
node.kind() == "identifier"
&& node
.parent()
.is_some_and(|parent| parent.kind() == "body_statement")
}
fn attach_argument_roles(sink: &mut RoleSink<'_>, arguments: Node<'_>) {
for index in 0..arguments.named_child_count() {
if !sink.should_continue() {
break;
}
let Some(argument) = arguments.named_child(index) else {
continue;
};
if argument.kind() == "pair" {
if let Some(key) = argument.child_by_field_name("key")
&& let Some(value) = argument
.child_by_field_name("value")
.map(expression_target_node)
{
sink.kwarg(expression_name_node(key).unwrap_or(key), value);
}
} else {
attach_argument_role_with_derived_name(sink, argument, expression_name_node);
}
}
}
fn node_text<'source>(node: Node<'_>, source: &'source str) -> &'source str {
node.utf8_text(source.as_bytes()).unwrap_or("")
}
fn module_argument_node(node: Node<'_>) -> Option<Node<'_>> {
let arguments = node.child_by_field_name("arguments")?;
(0..arguments.named_child_count())
.filter_map(|index| arguments.named_child(index))
.find(|argument| argument.kind() == "string")
}
fn is_import_call(node: Node<'_>, source: &str) -> bool {
if node.child_by_field_name("receiver").is_some() {
return false;
}
let Some(method) = call_method_node(node) else {
return false;
};
matches!(
node_text(method, source).trim(),
"require" | "require_relative" | "load" | "autoload"
) && module_argument_node(node).is_some()
}
fn static_string_content_span(node: Node<'_>) -> Option<Span> {
if node.kind() != "string" {
return None;
}
let content = single_static_string_content_node(node)?;
Some(Span {
start_byte: content.start_byte(),
end_byte: content.end_byte(),
})
}
impl StructuralSpec for RubyStructuralSpec {
fn language(&self) -> Language {
Language::Ruby
}
fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
RUBY_KIND_TABLE
}
fn refine_kind(
&self,
node: Node<'_>,
kind: NormalizedKind,
enclosing: Option<NormalizedKind>,
source: &str,
) -> NormalizedKind {
if is_bare_call_identifier(node) {
NormalizedKind::Call
} else if node.kind() == "call" && is_import_call(node, source) {
NormalizedKind::Import
} else if node.kind() == "method"
&& kind == NormalizedKind::Function
&& enclosing == Some(NormalizedKind::Class)
{
NormalizedKind::Method
} else {
kind
}
}
fn should_extract(&self, node: Node<'_>, kind: NormalizedKind) -> bool {
if kind == NormalizedKind::Lambda
&& matches!(node.kind(), "block" | "do_block")
&& node
.parent()
.is_some_and(|parent| parent.kind() == "lambda")
{
return false;
}
if kind == NormalizedKind::NumericLiteral {
if node.kind() == "unary" {
return is_signed_numeric_unary(node);
}
if is_numeric_literal_node(node) && is_inside_signed_numeric_wrapper(node) {
return false;
}
}
true
}
fn supports_kind(&self, kind: NormalizedKind) -> bool {
kind == NormalizedKind::Import
|| self
.kind_table()
.iter()
.any(|(_, fact_kind)| fact_kind.satisfies(kind))
}
fn supports_role(&self, role: Role) -> bool {
role != Role::Decorator
}
fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
&NO_OCCURRENCE_ROLE_SUPPORT
}
fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
&NO_LEXICAL_ENVIRONMENT_SUPPORT
}
fn materialization_support(&self) -> &DeclarationMaterializationSupport {
&RUBY_MATERIALIZATION_SUPPORT
}
fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
&INVERSE_REFERENCE_EDGE_SUPPORT
}
fn identity_route_support(&self) -> &IdentityRouteSupport {
&NO_IDENTITY_ROUTE_SUPPORT
}
fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
match kind {
NormalizedKind::Call => {
if is_bare_call_identifier(node) {
attach_terminal_callee(sink, node, Some(node));
} else if let Some(method) = call_method_node(node) {
attach_terminal_callee(sink, method, expression_name_node(method));
}
if let Some(receiver) = node.child_by_field_name("receiver") {
attach_role_with_derived_name(
sink,
Role::Receiver,
receiver,
expression_name_node,
);
}
if let Some(arguments) = node.child_by_field_name("arguments") {
attach_argument_roles(sink, arguments);
}
if let Some(block) = node.child_by_field_name("block") {
attach_role_with_derived_name(sink, Role::Arg, block, expression_name_node);
}
}
NormalizedKind::FieldAccess => {
if let Some(field) = node.child_by_field_name("name") {
attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
if let Some(name) = expression_name_node(field) {
sink.set_name(name);
}
}
if let Some(object) = node.child_by_field_name("scope") {
attach_role_with_derived_name(sink, Role::Object, object, expression_name_node);
}
}
NormalizedKind::Function
| NormalizedKind::Method
| NormalizedKind::Class
| NormalizedKind::Declaration => {
if let Some(name) = node.child_by_field_name("name") {
sink.set_name(expression_name_node(name).unwrap_or(name));
}
}
NormalizedKind::Assignment => {
if let Some(left) = node.child_by_field_name("left") {
let left = expression_target_node(left);
attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
if let Some(name) = expression_name_node(left) {
sink.set_name(name);
}
}
if let Some(right) = node.child_by_field_name("right") {
let right = expression_target_node(right);
attach_role_with_derived_name(sink, Role::Right, right, expression_name_node);
}
}
NormalizedKind::Import => {
if let Some(module) = module_argument_node(node)
&& let Some(name) = static_string_content_span(module)
{
sink.role_named_span(Role::Module, module, name);
}
}
NormalizedKind::Identifier => match expression_name_node(node) {
Some(name) => sink.set_name(name),
None => sink.set_name(node),
},
_ => {}
}
}
}