Packages
metastatic
0.23.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/adapters/ruby/to_meta.ex
defmodule Metastatic.Adapters.Ruby.ToMeta do
@moduledoc """
Transform Ruby AST (M1) to MetaAST (M2).
This module implements the abstraction function α_Ruby that lifts
Ruby-specific AST structures to the meta-level representation.
## Transformation Strategy
The transformation follows a pattern-matching approach, handling each
Ruby AST construct (from parser gem) and mapping it to the appropriate MetaAST node type.
### M2.1 (Core Layer)
- Literals: integers, floats, strings, booleans, nil, symbols
- Variables: local, instance, class, global
- Binary operators: arithmetic, comparison, boolean
- Unary operators: negation, logical not
- Method calls
- Conditionals: if/elsif/else, unless, ternary
- Blocks: sequential expressions
- Assignment: local variable assignment
### M2.2 (Extended Layer)
- Loops: while, until, for
- Iterators: each, map, select, reduce
- Blocks & Procs: blocks, lambdas, procs
- Pattern matching: case/when (classic), case/in (Ruby 3+)
- Exception handling: begin/rescue/ensure
### M2.3 (Native Layer)
- Class definitions
- Module definitions
- Method definitions
- String interpolation
- Regular expressions
- Metaprogramming constructs
"""
@doc """
Transform Ruby AST to MetaAST.
Returns `{:ok, meta_ast, metadata}` on success or `{:error, reason}` on failure.
## New 3-Tuple Format
All MetaAST nodes are uniform 3-element tuples:
{type_atom, keyword_meta, children_or_value}
## Examples
iex> transform(%{"type" => "int", "children" => [42]})
{:ok, {:literal, [subtype: :integer], 42}, %{}}
iex> transform(%{"type" => "lvar", "children" => ["x"]})
{:ok, {:variable, [], "x"}, %{}}
"""
@spec transform(term()) :: {:ok, term(), map()} | {:error, String.t()}
# Literals - M2.1 Core Layer
# Integer literal
def transform(%{"type" => "int", "children" => [value]} = ast) when is_integer(value) do
{:ok, add_location({:literal, [subtype: :integer], value}, ast), %{}}
end
# Float literal
def transform(%{"type" => "float", "children" => [value]} = ast) when is_float(value) do
{:ok, add_location({:literal, [subtype: :float], value}, ast), %{}}
end
# String literal
def transform(%{"type" => "str", "children" => [value]} = ast) when is_binary(value) do
{:ok, add_location({:literal, [subtype: :string], value}, ast), %{}}
end
# Symbol literal
def transform(%{"type" => "sym", "children" => [value]} = ast)
when is_atom(value) or is_binary(value) do
symbol = if is_binary(value), do: String.to_atom(value), else: value
{:ok, add_location({:literal, [subtype: :symbol], symbol}, ast), %{}}
end
# Boolean literals
def transform(%{"type" => "true", "children" => []} = ast) do
{:ok, add_location({:literal, [subtype: :boolean], true}, ast), %{}}
end
def transform(%{"type" => "false", "children" => []} = ast) do
{:ok, add_location({:literal, [subtype: :boolean], false}, ast), %{}}
end
# Nil literal
def transform(%{"type" => "nil", "children" => []} = ast) do
{:ok, add_location({:literal, [subtype: :null], nil}, ast), %{}}
end
# Self keyword
def transform(%{"type" => "self", "children" => []} = ast) do
{:ok, add_location({:variable, [scope: :special], "self"}, ast), %{scope: :special}}
end
# Handle bare nil (used in unary operators)
def transform(nil) do
{:ok, nil, %{}}
end
# Constant base (root namespace ::)
# Used in absolute constant paths like ::Array, ::SomeModule::Class
def transform(%{"type" => "cbase", "children" => []} = ast) do
{:ok, add_location({:literal, [subtype: :constant], ""}, ast), %{namespace: :root}}
end
# Array literal
def transform(%{"type" => "array", "children" => elements} = ast) do
with {:ok, elements_meta} <- transform_list(elements) do
{:ok, add_location({:list, [], elements_meta}, ast), %{collection_type: :array}}
end
end
# Constant (e.g., StandardError, Array, Hash)
def transform(%{"type" => "const", "children" => [namespace, name]} = ast) do
const_name = if is_atom(name), do: Atom.to_string(name), else: name
qualified_name =
if is_nil(namespace) do
const_name
else
with {:ok, namespace_meta, _} <- transform(namespace) do
"#{format_const(namespace_meta)}::#{const_name}"
end
end
case qualified_name do
{:ok, _, _} = result ->
result
name when is_binary(name) ->
{:ok, add_location({:literal, [subtype: :constant], name}, ast), %{}}
end
end
# Hash literal
def transform(%{"type" => "hash", "children" => pairs} = ast) do
with {:ok, pairs_meta} <- transform_hash_pairs(pairs) do
{:ok, add_location({:map, [], pairs_meta}, ast), %{collection_type: :hash}}
end
end
# Variables - M2.1 Core Layer
# Local variable
def transform(%{"type" => "lvar", "children" => [name]} = ast)
when is_binary(name) or is_atom(name) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :local], var_name}, ast), %{scope: :local}}
end
# Instance variable (@var)
def transform(%{"type" => "ivar", "children" => [name]} = ast)
when is_binary(name) or is_atom(name) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :instance], var_name}, ast), %{scope: :instance}}
end
# Class variable (@@var)
def transform(%{"type" => "cvar", "children" => [name]} = ast)
when is_binary(name) or is_atom(name) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :class], var_name}, ast), %{scope: :class}}
end
# Global variable ($var)
def transform(%{"type" => "gvar", "children" => [name]} = ast)
when is_binary(name) or is_atom(name) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :global], var_name}, ast), %{scope: :global}}
end
# Binary Operations - M2.1 Core Layer
# In Ruby, most binary operators are implemented as method calls (send nodes)
# Arithmetic operators: +, -, *, /, %, **
# Operators can be either atoms or strings from JSON
# Note: We check that left is not nil to avoid catching local method calls
def transform(%{"type" => "send", "children" => [left, op, right]} = ast)
when not is_nil(left) and not is_nil(right) do
cond do
is_bitwise_op?(op) ->
op_atom = normalize_op(op)
with {:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{:ok,
add_location(
{:binary_op, [category: :bitwise, operator: op_atom], [left_meta, right_meta]},
ast
), %{}}
end
is_arithmetic_op?(op) ->
op_atom = normalize_op(op)
with {:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{:ok,
add_location(
{:binary_op, [category: :arithmetic, operator: op_atom], [left_meta, right_meta]},
ast
), %{}}
end
is_comparison_op?(op) ->
op_atom = normalize_op(op)
with {:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{:ok,
add_location(
{:binary_op, [category: :comparison, operator: op_atom], [left_meta, right_meta]},
ast
), %{}}
end
true ->
# Regular method call with receiver
transform_method_call_with_receiver(left, op, right, ast)
end
end
# Boolean operators: &&, ||, and, or
def transform(%{"type" => "and", "children" => [left, right]} = ast) do
with {:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{:ok,
add_location(
{:binary_op, [category: :boolean, operator: :and], [left_meta, right_meta]},
ast
), %{}}
end
end
def transform(%{"type" => "or", "children" => [left, right]} = ast) do
with {:ok, left_meta, _} <- transform(left),
{:ok, right_meta, _} <- transform(right) do
{:ok,
add_location(
{:binary_op, [category: :boolean, operator: :or], [left_meta, right_meta]},
ast
), %{}}
end
end
# Unary Operations - M2.1 Core Layer
# Unary operators - 3 children with nil as third: [operand, op, nil]
def transform(%{"type" => "send", "children" => [operand, op, nil]} = ast) do
op_atom = normalize_op(op)
cond do
op_atom in [:-, :+] ->
with {:ok, operand_meta, _} <- transform(operand) do
{:ok,
add_location(
{:unary_op, [category: :arithmetic, operator: op_atom], [operand_meta]},
ast
), %{}}
end
op_atom == :! ->
with {:ok, operand_meta, _} <- transform(operand) do
{:ok,
add_location(
{:unary_op, [category: :boolean, operator: :not], [operand_meta]},
ast
), %{}}
end
true ->
# Not actually a unary operator
{:error, "Invalid unary operator: #{op}"}
end
end
# Method call without arguments: 2 children [receiver, method]
def transform(%{"type" => "send", "children" => [receiver, method]} = ast) do
method_str = if is_atom(method), do: Atom.to_string(method), else: method
if is_nil(receiver) do
# Local method call without args: hello
{:ok, add_location({:function_call, [name: method_str], []}, ast), %{call_type: :local}}
else
# Check if this looks like attribute access (not a method call)
# In Ruby, obj.attr without parentheses could be either
# For simplicity, treat as attribute_access if receiver is a variable
with {:ok, receiver_meta, _} <- transform(receiver) do
case receiver_meta do
{:variable, _, _} ->
# Likely attribute access: obj.field
{:ok,
add_location({:attribute_access, [attribute: method_str], [receiver_meta]}, ast),
%{kind: :instance_var}}
_ ->
# Method call with receiver but no arguments: obj.method
qualified_name = "#{format_receiver(receiver_meta)}.#{method_str}"
{:ok, add_location({:function_call, [name: qualified_name], []}, ast),
%{call_type: :instance}}
end
end
end
end
# Module mixins - M2.2s Structural Layer
# Ruby `include`, `prepend`, and `extend` are semantically import directives
# that bring in module behaviour, not regular method calls.
def transform(
%{"type" => "send", "children" => [nil, mixin_type, %{"type" => "const"} = const_node]} =
ast
)
when mixin_type in [:include, :prepend, :extend, "include", "prepend", "extend"] do
with {:ok, const_meta, _} <- transform(const_node) do
source_name = extract_constant_name(const_meta)
import_type =
case normalize_op(mixin_type) do
:include -> :include
:prepend -> :prepend
:extend -> :extend
end
{:ok,
add_location(
{:import, [source: source_name, import_type: import_type, language: :ruby], []},
ast
), %{mixin_type: import_type}}
end
end
# Method Calls - M2.1 Core Layer
# Method call without receiver (local method call)
def transform(%{"type" => "send", "children" => [nil, method_name | args]} = ast) do
method_str = if is_atom(method_name), do: Atom.to_string(method_name), else: method_name
with {:ok, args_meta} <- transform_list(args) do
{:ok, add_location({:function_call, [name: method_str], args_meta}, ast),
%{call_type: :local}}
end
end
# Method call with receiver (method name as atom)
def transform(%{"type" => "send", "children" => [receiver, method_name | args]} = ast)
when not is_nil(receiver) and is_atom(method_name) do
with {:ok, receiver_meta, _} <- transform(receiver),
{:ok, args_meta} <- transform_list(args) do
# For now, represent as "receiver.method" format
# In future, might want to preserve receiver as separate field
method_str = "#{format_receiver(receiver_meta)}.#{method_name}"
{:ok, add_location({:function_call, [name: method_str], args_meta}, ast),
%{call_type: :instance}}
end
end
# Method call with receiver (method name as string)
def transform(%{"type" => "send", "children" => [receiver, method_name | args]} = ast)
when not is_nil(receiver) and is_binary(method_name) do
with {:ok, receiver_meta, _} <- transform(receiver),
{:ok, args_meta} <- transform_list(args) do
method_str = "#{format_receiver(receiver_meta)}.#{method_name}"
{:ok, add_location({:function_call, [name: method_str], args_meta}, ast),
%{call_type: :instance}}
end
end
# Conditionals - M2.1 Core Layer
# if/elsif/else
def transform(%{"type" => "if", "children" => [condition, then_branch, else_branch]} = ast) do
with {:ok, cond_meta, _} <- transform(condition),
{:ok, then_meta, _} <- transform_or_nil(then_branch),
{:ok, else_meta, _} <- transform_or_nil(else_branch) do
{:ok, add_location({:conditional, [], [cond_meta, then_meta, else_meta]}, ast), %{}}
end
end
# Ternary operator (also parsed as if)
# Note: Ruby's parser treats ternary the same as if
# Assignment - M2.1 Core Layer
# Local variable assignment
def transform(%{"type" => "lvasgn", "children" => [name, value]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:assignment, [scope: :local], [{:variable, [], var_name}, value_meta]},
ast
), %{scope: :local}}
end
end
# Local variable binding (without value, e.g., in rescue clauses)
def transform(%{"type" => "lvasgn", "children" => [name]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :local], var_name}, ast),
%{scope: :local, binding: true}}
end
# Instance variable binding (without value, e.g., in or_asgn targets)
def transform(%{"type" => "ivasgn", "children" => [name]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :instance], var_name}, ast),
%{scope: :instance, binding: true}}
end
# Instance variable assignment
def transform(%{"type" => "ivasgn", "children" => [name, value]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:assignment, [scope: :instance], [{:variable, [], var_name}, value_meta]},
ast
), %{scope: :instance}}
end
end
# Class variable binding (without value, e.g., in or_asgn targets)
def transform(%{"type" => "cvasgn", "children" => [name]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :class], var_name}, ast),
%{scope: :class, binding: true}}
end
# Class variable assignment
def transform(%{"type" => "cvasgn", "children" => [name, value]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:assignment, [scope: :class], [{:variable, [], var_name}, value_meta]},
ast
), %{scope: :class}}
end
end
# Global variable binding (without value, e.g., in or_asgn targets)
def transform(%{"type" => "gvasgn", "children" => [name]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, add_location({:variable, [scope: :global], var_name}, ast),
%{scope: :global, binding: true}}
end
# Global variable assignment
def transform(%{"type" => "gvasgn", "children" => [name, value]} = ast) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:assignment, [scope: :global], [{:variable, [], var_name}, value_meta]},
ast
), %{scope: :global}}
end
end
# Conditional assignment operators - M2.2s Structural Layer
# ||= (or-assignment) - idiomatic Ruby memoization: @x ||= compute
def transform(%{"type" => "or_asgn", "children" => [target, value]} = ast) do
with {:ok, target_meta, _} <- transform(target),
{:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:augmented_assignment, [category: :boolean, operator: :"||="],
[target_meta, value_meta]},
ast
), %{original_type: :or_asgn}}
end
end
# &&= (and-assignment)
def transform(%{"type" => "and_asgn", "children" => [target, value]} = ast) do
with {:ok, target_meta, _} <- transform(target),
{:ok, value_meta, _} <- transform(value) do
{:ok,
add_location(
{:augmented_assignment, [category: :boolean, operator: :"&&="],
[target_meta, value_meta]},
ast
), %{original_type: :and_asgn}}
end
end
# M2.2s Structural Layer - Augmented assignment
# Ruby represents += as op_asgn (operational assignment)
def transform(%{"type" => "op_asgn", "children" => [target, op, value]} = ast) do
op_atom = normalize_op(op)
with {:ok, target_meta, _} <- transform(target),
{:ok, value_meta, _} <- transform(value) do
# Determine operation category
category =
cond do
is_arithmetic_op?(op) -> :arithmetic
is_comparison_op?(op) -> :comparison
true -> :other
end
{:ok,
add_location(
{:augmented_assignment, [category: category, operator: op_atom],
[target_meta, value_meta]},
ast
), %{}}
end
end
# Safe navigation operator (&.) - M2.1 Core Layer
# csend is identical to send but uses &. instead of .
# Ruby: obj&.method (returns nil if obj is nil instead of raising NoMethodError)
# csend without arguments: 2 children [receiver, method]
def transform(%{"type" => "csend", "children" => [receiver, method]} = ast) do
method_str = if is_atom(method), do: Atom.to_string(method), else: method
with {:ok, receiver_meta, _} <- transform(receiver) do
case receiver_meta do
{:variable, _, _} ->
{:ok,
add_location(
{:attribute_access, [attribute: method_str, null_safe: true], [receiver_meta]},
ast
), %{kind: :instance_var, null_safe: true}}
_ ->
qualified_name = "#{format_receiver(receiver_meta)}.#{method_str}"
{:ok,
add_location(
{:function_call, [name: qualified_name, null_safe: true], []},
ast
), %{call_type: :instance, null_safe: true}}
end
end
end
# csend with arguments (including operators): 3+ children [receiver, method, args...]
def transform(%{"type" => "csend", "children" => [receiver, method | args]} = ast)
when not is_nil(receiver) do
method_str = if is_atom(method), do: Atom.to_string(method), else: method
with {:ok, receiver_meta, _} <- transform(receiver),
{:ok, args_meta} <- transform_list(args) do
qualified_name = "#{format_receiver(receiver_meta)}.#{method_str}"
{:ok,
add_location(
{:function_call, [name: qualified_name, null_safe: true], args_meta},
ast
), %{call_type: :instance, null_safe: true}}
end
end
# Blocks - M2.1 Core Layer
# Begin block (sequential statements)
def transform(%{"type" => "begin", "children" => statements} = ast) when is_list(statements) do
with {:ok, statements_meta} <- transform_list(statements) do
{:ok, add_location({:block, [], statements_meta}, ast), %{}}
end
end
# Loops - M2.2 Extended Layer
# While loop: while condition do body end
def transform(%{"type" => "while", "children" => [condition, body]} = ast) do
with {:ok, cond_meta, _} <- transform(condition),
{:ok, body_meta, _} <- transform_or_nil(body) do
{:ok, add_location({:loop, [loop_type: :while], [cond_meta, body_meta]}, ast), %{}}
end
end
# Until loop: until condition do body end
def transform(%{"type" => "until", "children" => [condition, body]} = ast) do
with {:ok, cond_meta, _} <- transform(condition),
{:ok, body_meta, _} <- transform_or_nil(body) do
# Until is equivalent to "while not condition"
negated_cond = {:unary_op, [category: :boolean, operator: :not], [cond_meta]}
{:ok, add_location({:loop, [loop_type: :while], [negated_cond, body_meta]}, ast),
%{original_type: :until}}
end
end
# For loop: for var in collection do body end
def transform(%{"type" => "for", "children" => [var_asgn, collection, body]} = ast) do
with {:ok, var_meta, _} <- transform_iterator_variable(var_asgn),
{:ok, collection_meta, _} <- transform(collection),
{:ok, body_meta, _} <- transform_or_nil(body) do
{:ok,
add_location({:loop, [loop_type: :for_each], [var_meta, collection_meta, body_meta]}, ast),
%{}}
end
end
# Blocks with parameters and iterators - M2.2 Extended Layer
# Block with receiver (iterator methods: each, map, select, reduce, etc.)
# Structure: block [send [collection, method, ...args], args [param...], body]
def transform(%{"type" => "block", "children" => [send_node, args_node, body]} = ast) do
case send_node do
# Lambda: lambda { |x| body } or ->(x) { body }
%{"type" => "send", "children" => [nil, "lambda"]} ->
transform_lambda(args_node, body, ast)
# Iterator methods: [1,2,3].each { |x| ... }
%{"type" => "send", "children" => [collection, method | method_args]} ->
transform_iterator_method(collection, method, method_args, args_node, body, ast)
_ ->
{:error, "Unsupported block construct: #{inspect(send_node)}"}
end
end
# Pattern Matching - M2.2 Extended Layer
# Case/when statement
def transform(%{"type" => "case", "children" => children} = ast) do
[scrutinee | branches] = children
{else_branch, when_branches} = extract_else_branch(branches)
with {:ok, scrutinee_meta, _} <- transform(scrutinee),
{:ok, branches_meta} <- transform_when_branches(when_branches),
{:ok, else_meta, _} <- transform_or_nil(else_branch) do
{:ok, add_location({:pattern_match, [], [scrutinee_meta, branches_meta, else_meta]}, ast),
%{}}
end
end
# Exception Handling - M2.2 Extended Layer
# Begin/rescue/ensure block (single child delegates to inner node)
def transform(%{"type" => "kwbegin", "children" => [ensure_or_rescue]})
when is_map(ensure_or_rescue) do
case ensure_or_rescue do
%{"type" => type} when type in ["rescue", "ensure"] ->
transform(ensure_or_rescue)
_ ->
# Single-statement kwbegin, treat as transparent wrapper
transform(ensure_or_rescue)
end
end
# kwbegin with multiple statements (explicit begin...end block)
def transform(%{"type" => "kwbegin", "children" => statements} = ast)
when is_list(statements) and length(statements) > 1 do
with {:ok, statements_meta} <- transform_list(statements) do
{:ok, add_location({:block, [], statements_meta}, ast), %{}}
end
end
# Ensure block (with or without rescue)
def transform(%{"type" => "ensure", "children" => [try_body, ensure_body]} = ast) do
case try_body do
%{"type" => "rescue"} ->
# Has both rescue and ensure
with {:ok, rescue_meta, rescue_metadata} <- transform(try_body),
{:ok, ensure_meta, _} <- transform(ensure_body) do
# Merge rescue handlers with ensure
metadata = Map.put(rescue_metadata, :ensure, ensure_meta)
{:ok, rescue_meta, metadata}
end
_ ->
# Only ensure, no rescue
with {:ok, try_meta, _} <- transform(try_body),
{:ok, ensure_meta, _} <- transform(ensure_body) do
{:ok, add_location({:exception_handling, [], [try_meta, [], nil]}, ast),
%{ensure: ensure_meta}}
end
end
end
# Rescue block
def transform(%{"type" => "rescue", "children" => children} = ast) do
[try_body | rescue_bodies] = children
rescue_handlers = Enum.filter(rescue_bodies, &match?(%{"type" => "resbody"}, &1))
else_body = Enum.find(rescue_bodies, fn node -> not match?(%{"type" => "resbody"}, node) end)
with {:ok, try_meta, _} <- transform(try_body),
{:ok, handlers_meta} <- transform_rescue_handlers(rescue_handlers),
{:ok, else_meta, _} <- transform_or_nil(else_body) do
{:ok, add_location({:exception_handling, [], [try_meta, handlers_meta, else_meta]}, ast),
%{}}
end
end
# M2.2s Structural Layer - Container support
# Class definition - maps to container
def transform(%{"type" => "class", "children" => [name, superclass, body]} = ast) do
with {:ok, name_meta, _} <- transform(name),
{:ok, superclass_meta, _} <- transform_or_nil(superclass),
{:ok, body_meta, _} <- transform_or_nil(body) do
class_name = extract_constant_name(name_meta)
parent_name = extract_parent_name(superclass_meta)
# Add class context to the container node itself
class_context = %{
language: :ruby,
module: class_name
}
# Create container: {:container, [container_type: :class, name: name, ...], [body]}
container =
{:container, [container_type: :class, name: class_name, parent: parent_name], [body_meta]}
{:ok, add_location_with_context(container, ast, class_context),
%{ruby_ast: ast, superclass: superclass_meta}}
end
end
# Module definition - maps to container
def transform(%{"type" => "module", "children" => [name, body]} = ast) do
with {:ok, name_meta, _} <- transform(name),
{:ok, body_meta, _} <- transform_or_nil(body) do
module_name = extract_constant_name(name_meta)
# Add module context to the container node itself
module_context = %{
language: :ruby,
module: module_name
}
# Create container: {:container, [container_type: :module, name: name], [body]}
container = {:container, [container_type: :module, name: module_name], [body_meta]}
{:ok, add_location_with_context(container, ast, module_context), %{ruby_ast: ast}}
end
end
# M2.2s Structural Layer - Function definition support
# Method definition (def) - maps to function_def
def transform(%{"type" => "def", "children" => [name, args, body]} = ast) do
method_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, params} <- extract_method_params(args),
{:ok, body_meta, _} <- transform_or_nil(body) do
arity = length(params)
# Add function context to the function_def node itself
func_context = %{
language: :ruby,
function: method_name,
arity: arity,
visibility: :public
}
# Create function_def: {:function_def, [name: name, params: params, ...], [body]}
function_def =
{:function_def, [name: method_name, params: params, visibility: :public, arity: arity],
[body_meta]}
{:ok, add_location_with_context(function_def, ast, func_context), %{ruby_ast: ast}}
end
end
# Class method definition (def self.method) or singleton method (def obj.method)
# Ruby parser represents this as "defs" type
def transform(%{"type" => "defs", "children" => [receiver, name, args, body]} = ast) do
method_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, receiver_meta, _} <- transform(receiver),
{:ok, params} <- extract_method_params(args),
{:ok, body_meta, _} <- transform_or_nil(body) do
# For class methods (def self.method), qualify the name
qualified_name =
case receiver_meta do
{:variable, _, "self"} -> "self.#{method_name}"
_ -> "#{format_receiver(receiver_meta)}.#{method_name}"
end
arity = length(params)
# Add function context
func_context = %{
language: :ruby,
function: qualified_name,
arity: arity,
visibility: :public
}
# Create function_def with qualified name
function_def =
{:function_def, [name: qualified_name, params: params, visibility: :public, arity: arity],
[body_meta]}
{:ok, add_location_with_context(function_def, ast, func_context),
%{ruby_ast: ast, is_class_method: true}}
end
end
# Private/protected method definition - uses access control keywords
# Note: Ruby's private/protected are modifier keywords, not part of def itself
# They're typically handled at a higher level in the AST
# Constant assignment (e.g., BAR = 42)
def transform(%{"type" => "casgn", "children" => [namespace, name, value]} = ast) do
const_name = if is_atom(name), do: Atom.to_string(name), else: name
with {:ok, value_meta, _} <- transform(value) do
metadata = %{
name: const_name,
namespace: namespace,
value: value_meta
}
{:ok, {:language_specific, [language: :ruby, hint: :constant_assignment], ast}, metadata}
end
end
# Yield with arguments
def transform(%{"type" => "yield", "children" => args} = ast) do
with {:ok, args_meta} <- transform_list(args) do
metadata = %{args: args_meta}
{:ok, {:language_specific, [language: :ruby, hint: :yield], ast}, metadata}
end
end
# Alias (alias new_name old_name)
def transform(%{"type" => "alias", "children" => [new_name, old_name]} = ast) do
new_sym = extract_symbol_name(new_name)
old_sym = extract_symbol_name(old_name)
metadata = %{new_name: new_sym, old_name: old_sym}
{:ok, {:language_specific, [language: :ruby, hint: :alias], ast}, metadata}
end
# String interpolation (dstr) - M2.1 Core Layer
def transform(%{"type" => "dstr", "children" => parts} = ast) do
transformed_parts =
Enum.map(parts, fn
%{"type" => "str", "children" => [str]} ->
{:ok, {:literal, [subtype: :string], str}, %{}}
%{"type" => "begin", "children" => [expr]} ->
transform(expr)
other ->
transform(other)
end)
if Enum.all?(transformed_parts, &match?({:ok, _, _}, &1)) do
meta_parts = Enum.map(transformed_parts, fn {:ok, m, _} -> m end)
{:ok, add_location({:string_interpolation, [], meta_parts}, ast), %{}}
else
first_error = Enum.find(transformed_parts, &match?({:error, _}, &1))
first_error || {:error, "Failed to transform string interpolation parts"}
end
end
# Regular expression - M2.1 Core Layer (literal :regex)
def transform(%{"type" => "regexp", "children" => [pattern | options]} = ast) do
pattern_str =
case pattern do
%{"type" => "str", "children" => [str]} -> str
%{"type" => "dstr", "children" => _} -> inspect(pattern)
_ -> inspect(pattern)
end
flags = extract_regex_flags(options)
{:ok, add_location({:literal, [subtype: :regex, flags: flags], pattern_str}, ast), %{}}
end
# Singleton class (class << self)
def transform(%{"type" => "sclass", "children" => [object, body]} = ast) do
with {:ok, object_meta, _} <- transform(object),
{:ok, body_meta, _} <- transform_or_nil(body) do
metadata = %{object: object_meta, body: body_meta}
{:ok, {:language_specific, [language: :ruby, hint: :singleton_class], ast}, metadata}
end
end
# Super with arguments
def transform(%{"type" => "super", "children" => args} = ast) do
with {:ok, args_meta} <- transform_list(args) do
metadata = %{args: args_meta}
{:ok, {:language_specific, [language: :ruby, hint: :super], ast}, metadata}
end
end
# Zsuper (super with no arguments, passes all parent arguments)
def transform(%{"type" => "zsuper", "children" => []} = ast) do
{:ok, {:language_specific, [language: :ruby, hint: :zsuper], ast}, %{}}
end
# Return statement - M2.1 Core Layer (early_return)
def transform(%{"type" => "return", "children" => children} = ast) do
with {:ok, values_meta} <- transform_list(children) do
value =
case values_meta do
[] -> nil
[single] -> single
multiple -> {:tuple, [], multiple}
end
{:ok, add_location({:early_return, [], [value]}, ast), %{}}
end
end
# Break statement - M2.1 Core Layer (early_return)
def transform(%{"type" => "break", "children" => children} = ast) do
with {:ok, values_meta} <- transform_list(children) do
value = if values_meta == [], do: nil, else: List.first(values_meta)
{:ok, add_location({:early_return, [kind: :break], [value]}, ast), %{}}
end
end
# Next statement - M2.1 Core Layer (early_return, like continue)
def transform(%{"type" => "next", "children" => children} = ast) do
with {:ok, values_meta} <- transform_list(children) do
value = if values_meta == [], do: nil, else: List.first(values_meta)
{:ok, add_location({:early_return, [kind: :continue], [value]}, ast), %{}}
end
end
# Redo statement - control flow
def transform(%{"type" => "redo", "children" => []} = ast) do
{:ok, {:language_specific, [language: :ruby, hint: :redo], ast}, %{}}
end
# Retry statement - control flow (in rescue blocks)
def transform(%{"type" => "retry", "children" => []} = ast) do
{:ok, {:language_specific, [language: :ruby, hint: :retry], ast}, %{}}
end
# Range literals - inclusive (1..10) - M2.1 Core Layer
def transform(%{"type" => "irange", "children" => [start_val, end_val]} = ast) do
with {:ok, start_meta, _} <- transform(start_val),
{:ok, end_meta, _} <- transform(end_val) do
{:ok,
add_location(
{:range, [inclusive: true], [start_meta, end_meta]},
ast
), %{range_type: :inclusive}}
end
end
# Range literals - exclusive (1...10) - M2.1 Core Layer
def transform(%{"type" => "erange", "children" => [start_val, end_val]} = ast) do
with {:ok, start_meta, _} <- transform(start_val),
{:ok, end_meta, _} <- transform(end_val) do
{:ok,
add_location(
{:range, [inclusive: false], [start_meta, end_meta]},
ast
), %{range_type: :exclusive}}
end
end
# Splat operator (*args)
def transform(%{"type" => "splat", "children" => [value]} = ast) do
with {:ok, value_meta, _} <- transform(value) do
{:ok, {:language_specific, [language: :ruby, hint: :splat], ast}, %{value: value_meta}}
end
end
# Block pass operator (&block)
def transform(%{"type" => "block_pass", "children" => [value]} = ast) do
with {:ok, value_meta, _} <- transform(value) do
{:ok, {:language_specific, [language: :ruby, hint: :block_pass], ast}, %{value: value_meta}}
end
end
# Keyword splat operator (**kw)
def transform(%{"type" => "kwsplat", "children" => [value]} = ast) do
with {:ok, value_meta, _} <- transform(value) do
{:ok, {:language_specific, [language: :ruby, hint: :kwsplat], ast}, %{value: value_meta}}
end
end
# Multiple assignment (parallel assignment): a, b = values
def transform(%{"type" => "masgn", "children" => [lhs, rhs]} = ast) do
with {:ok, lhs_meta, _} <- transform(lhs),
{:ok, rhs_meta, _} <- transform(rhs) do
{:ok, {:language_specific, [language: :ruby, hint: :multiple_assignment], ast},
%{left: lhs_meta, right: rhs_meta}}
end
end
# Multiple left-hand side (targets in multiple assignment): a, b, c
def transform(%{"type" => "mlhs", "children" => targets}) do
with {:ok, targets_meta} <- transform_list(targets) do
{:ok, {:language_specific, [language: :ruby, hint: :mlhs], targets_meta}, %{}}
end
end
# Defined? operator
def transform(%{"type" => "defined?", "children" => [expr]} = ast) do
with {:ok, expr_meta, _} <- transform(expr) do
{:ok, {:language_specific, [language: :ruby, hint: :defined], ast}, %{expr: expr_meta}}
end
end
# BEGIN block (executed before program starts)
def transform(%{"type" => "preexe", "children" => [body]} = ast) do
with {:ok, body_meta, _} <- transform(body) do
{:ok, {:language_specific, [language: :ruby, hint: :begin_block], ast}, %{body: body_meta}}
end
end
# END block (executed after program ends)
def transform(%{"type" => "postexe", "children" => [body]} = ast) do
with {:ok, body_meta, _} <- transform(body) do
{:ok, {:language_specific, [language: :ruby, hint: :end_block], ast}, %{body: body_meta}}
end
end
# Proc creation - treat as lambda with :proc hint
# Handle proc blocks that come through as block + send[nil, "proc"]
# This is handled in transform_iterator_method, but we need a fallback
# Catch-all for unsupported constructs
def transform(unsupported) do
{:error, "Unsupported Ruby AST construct: #{inspect(unsupported)}"}
end
# Helper Functions
defp transform_list(items) when is_list(items) do
items
|> Enum.reject(&is_nil/1)
|> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
case transform(item) do
{:ok, meta, _} -> {:cont, {:ok, [meta | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, items} -> {:ok, Enum.reverse(items)}
error -> error
end
end
defp transform_or_nil(nil), do: {:ok, nil, %{}}
defp transform_or_nil(value), do: transform(value)
defp transform_hash_pairs(pairs) when is_list(pairs) do
pairs
|> Enum.reduce_while({:ok, []}, fn pair, {:ok, acc} ->
case transform_hash_pair(pair) do
{:ok, pair_meta} -> {:cont, {:ok, [pair_meta | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, pairs} -> {:ok, Enum.reverse(pairs)}
error -> error
end
end
defp transform_hash_pair(%{"type" => "pair", "children" => [key, value]} = ast) do
with {:ok, key_meta, _} <- transform(key),
{:ok, value_meta, _} <- transform(value) do
{:ok, add_location({:pair, [], [key_meta, value_meta]}, ast)}
end
end
# Keyword splat in hash context (e.g., {**other_hash})
defp transform_hash_pair(%{"type" => "kwsplat", "children" => [value]}) do
with {:ok, value_meta, _} <- transform(value) do
{:ok, {:language_specific, [language: :ruby, hint: :kwsplat], value_meta}}
end
end
defp transform_hash_pair(other) do
{:error, "Invalid hash pair: #{inspect(other)}"}
end
defp format_receiver({:variable, _, name}), do: name
defp format_receiver(_), do: "obj"
defp format_const({:literal, [subtype: :constant], name}), do: name
defp format_const({:literal, _, name}) when is_binary(name), do: name
defp format_const(_), do: "Unknown"
# Operator normalization helpers
defp normalize_op(op) when is_atom(op), do: op
defp normalize_op(op) when is_binary(op), do: String.to_atom(op)
@ruby_arithmetic_ops [:+, :-, :*, :/, :%, :**]
@ruby_bitwise_ops [:"<<", :">>"]
defp is_arithmetic_op?(op) when is_atom(op), do: op in @ruby_arithmetic_ops
defp is_arithmetic_op?(op) when is_binary(op), do: op in ["+", "-", "*", "/", "%", "**"]
defp is_bitwise_op?(op) when is_atom(op), do: op in @ruby_bitwise_ops
defp is_bitwise_op?(op) when is_binary(op), do: op in ["<<", ">>"]
defp is_comparison_op?(op) when is_atom(op) do
op in [:==, :!=, :<, :>, :<=, :>=, :===, :eql?, :"<=>"]
end
defp is_comparison_op?(op) when is_binary(op) do
op in ["==", "!=", "<", ">", "<=", ">=", "===", "eql?", "<=>"]
end
defp transform_method_call_with_receiver(receiver, method_name, arg, ast) do
method_str = if is_atom(method_name), do: Atom.to_string(method_name), else: method_name
with {:ok, receiver_meta, _} <- transform(receiver),
{:ok, arg_meta, _} <- transform(arg) do
qualified_name = "#{format_receiver(receiver_meta)}.#{method_str}"
{:ok, add_location({:function_call, [name: qualified_name], [arg_meta]}, ast),
%{call_type: :instance}}
end
end
# M2.2 Extended Layer Helper Functions
# Transform iterator variable (from lvasgn in for loops)
defp transform_iterator_variable(%{"type" => "lvasgn", "children" => [name]}) do
var_name = if is_atom(name), do: Atom.to_string(name), else: name
{:ok, var_name, %{}}
end
defp transform_iterator_variable(other) do
{:error, "Invalid iterator variable: #{inspect(other)}"}
end
# Transform lambda
defp transform_lambda(args_node, body, block_ast) do
with {:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
{:ok, add_location({:lambda, [params: params, captures: []], [body_meta]}, block_ast), %{}}
end
end
# Extract lambda parameters from args node (reuses extract_single_param)
defp extract_lambda_params(%{"type" => "args", "children" => args}) do
params =
Enum.map(args, &extract_single_param/1)
|> Enum.reject(&is_nil/1)
{:ok, params}
end
defp extract_lambda_params(_), do: {:ok, []}
# Transform iterator methods (map, each, select, reduce, proc, etc.)
defp transform_iterator_method(collection, method, method_args, args_node, body, block_ast) do
method_atom = normalize_op(method)
case method_atom do
m when m in [:each, :map, :select, :filter, :reject] ->
transform_map_like_iterator(collection, m, args_node, body, block_ast)
:reduce ->
transform_reduce_iterator(collection, method_args, args_node, body, block_ast)
:proc ->
# proc { |x| body } - treat as lambda with :proc hint
transform_proc(args_node, body, block_ast)
:tap ->
# Object#tap - yields self to block, returns self
transform_map_like_iterator(collection, :tap, args_node, body, block_ast)
:times ->
# Integer#times - iterate n times
transform_times_iterator(collection, args_node, body, block_ast)
:upto ->
# Integer#upto - iterate from n upto m
transform_upto_iterator(collection, method_args, args_node, body, block_ast)
:downto ->
# Integer#downto - iterate from n down to m
transform_downto_iterator(collection, method_args, args_node, body, block_ast)
:each_with_index ->
transform_map_like_iterator(collection, :each_with_index, args_node, body, block_ast)
:each_with_object ->
transform_reduce_like_iterator(
collection,
:each_with_object,
method_args,
args_node,
body,
block_ast
)
:inject ->
# inject is an alias for reduce
transform_reduce_iterator(collection, method_args, args_node, body, block_ast)
:find ->
transform_map_like_iterator(collection, :find, args_node, body, block_ast)
:detect ->
# detect is alias for find
transform_map_like_iterator(collection, :find, args_node, body, block_ast)
:sort_by ->
transform_map_like_iterator(collection, :sort_by, args_node, body, block_ast)
:group_by ->
transform_map_like_iterator(collection, :group_by, args_node, body, block_ast)
:partition ->
transform_map_like_iterator(collection, :partition, args_node, body, block_ast)
:flat_map ->
transform_map_like_iterator(collection, :flat_map, args_node, body, block_ast)
:take_while ->
transform_map_like_iterator(collection, :take_while, args_node, body, block_ast)
:drop_while ->
transform_map_like_iterator(collection, :drop_while, args_node, body, block_ast)
:all? ->
transform_map_like_iterator(collection, :all?, args_node, body, block_ast)
:any? ->
transform_map_like_iterator(collection, :any?, args_node, body, block_ast)
:none? ->
transform_map_like_iterator(collection, :none?, args_node, body, block_ast)
:one? ->
transform_map_like_iterator(collection, :one?, args_node, body, block_ast)
:count ->
transform_map_like_iterator(collection, :count, args_node, body, block_ast)
:sum ->
transform_map_like_iterator(collection, :sum, args_node, body, block_ast)
:min_by ->
transform_map_like_iterator(collection, :min_by, args_node, body, block_ast)
:max_by ->
transform_map_like_iterator(collection, :max_by, args_node, body, block_ast)
_ ->
# Generic block call - still transform it as a collection operation with custom op_type
transform_generic_block(collection, method_atom, method_args, args_node, body, block_ast)
end
end
# Transform proc { |x| body }
defp transform_proc(args_node, body, block_ast) do
with {:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
{:ok,
add_location(
{:lambda, [params: params, captures: [], kind: :proc], [body_meta]},
block_ast
), %{kind: :proc}}
end
end
# Transform times iterator: 5.times { |i| ... }
defp transform_times_iterator(count_node, args_node, body, block_ast) do
with {:ok, count_meta, _} <- transform(count_node),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
{:ok, add_location({:collection_op, [op_type: :times], [lambda, count_meta]}, block_ast),
%{}}
end
end
# Transform upto iterator: 1.upto(10) { |i| ... }
defp transform_upto_iterator(start_node, [end_node], args_node, body, block_ast) do
with {:ok, start_meta, _} <- transform(start_node),
{:ok, end_meta, _} <- transform(end_node),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
range = {:range, [inclusive: true], [start_meta, end_meta]}
{:ok, add_location({:collection_op, [op_type: :each], [lambda, range]}, block_ast),
%{original: :upto}}
end
end
defp transform_upto_iterator(_start_node, _args, _args_node, _body, _block_ast) do
{:error, "Invalid upto iterator: expected exactly one argument"}
end
# Transform downto iterator: 10.downto(1) { |i| ... }
defp transform_downto_iterator(start_node, [end_node], args_node, body, block_ast) do
with {:ok, start_meta, _} <- transform(start_node),
{:ok, end_meta, _} <- transform(end_node),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
# Downto is a reverse range
{:ok,
add_location(
{:collection_op, [op_type: :downto], [lambda, start_meta, end_meta]},
block_ast
), %{}}
end
end
defp transform_downto_iterator(_start_node, _args, _args_node, _body, _block_ast) do
{:error, "Invalid downto iterator: expected exactly one argument"}
end
# Transform reduce-like iterators with initial value
defp transform_reduce_like_iterator(collection, method, method_args, args_node, body, block_ast) do
with {:ok, collection_meta, _} <- transform(collection),
{:ok, initial_meta} <- extract_reduce_initial(method_args),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
{:ok,
add_location(
{:collection_op, [op_type: method], [lambda, collection_meta, initial_meta]},
block_ast
), %{}}
end
end
# Transform generic block calls - for any method we don't specifically handle
defp transform_generic_block(collection, method, method_args, args_node, body, block_ast) do
with {:ok, collection_meta, _} <- transform_or_nil(collection),
{:ok, method_args_meta} <- transform_list(method_args),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
{:ok,
add_location(
{:collection_op, [op_type: method], [lambda, collection_meta | method_args_meta]},
block_ast
), %{generic: true}}
end
end
# Transform map-like iterators (each, map, select, filter)
defp transform_map_like_iterator(collection, method, args_node, body, block_ast) do
with {:ok, collection_meta, _} <- transform(collection),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
{:ok,
add_location({:collection_op, [op_type: method], [lambda, collection_meta]}, block_ast),
%{}}
end
end
# Transform reduce iterator
defp transform_reduce_iterator(collection, method_args, args_node, body, block_ast) do
with {:ok, collection_meta, _} <- transform(collection),
{:ok, initial_meta} <- extract_reduce_initial(method_args),
{:ok, params} <- extract_lambda_params(args_node),
{:ok, body_meta, _} <- transform(body) do
lambda = {:lambda, [params: params, captures: []], [body_meta]}
{:ok,
add_location(
{:collection_op, [op_type: :reduce], [lambda, collection_meta, initial_meta]},
block_ast
), %{}}
end
end
# Extract initial value from reduce method args
defp extract_reduce_initial([initial | _]) do
case transform(initial) do
{:ok, meta, _} -> {:ok, meta}
error -> error
end
end
defp extract_reduce_initial([]), do: {:ok, nil}
# Pattern matching helpers
defp extract_else_branch(branches) do
else_branch = Enum.find(branches, fn node -> not match?(%{"type" => "when"}, node) end)
when_branches = Enum.filter(branches, &match?(%{"type" => "when"}, &1))
{else_branch, when_branches}
end
defp transform_when_branches(when_branches) do
when_branches
|> Enum.reduce_while({:ok, []}, fn when_node, {:ok, acc} ->
case transform_when_branch(when_node) do
{:ok, branch_meta} -> {:cont, {:ok, [branch_meta | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, branches} -> {:ok, Enum.reverse(branches)}
error -> error
end
end
defp transform_when_branch(%{"type" => "when", "children" => [pattern | rest]}) do
# Rest is either [body] or [] if body is nil
body = List.first(rest)
with {:ok, pattern_meta, _} <- transform(pattern),
{:ok, body_meta, _} <- transform_or_nil(body) do
{:ok, {pattern_meta, body_meta}}
end
end
# Exception handling helpers
defp transform_rescue_handlers(handlers) do
handlers
|> Enum.reduce_while({:ok, []}, fn handler, {:ok, acc} ->
case transform_rescue_handler(handler) do
{:ok, handler_meta} -> {:cont, {:ok, [handler_meta | acc]}}
{:error, _} = err -> {:halt, err}
end
end)
|> case do
{:ok, handlers} -> {:ok, Enum.reverse(handlers)}
error -> error
end
end
defp transform_rescue_handler(%{
"type" => "resbody",
"children" => [exception_types, var_binding, body]
}) do
with {:ok, types_meta} <- transform_exception_types(exception_types),
{:ok, var_meta, _} <- transform_or_nil(var_binding),
{:ok, body_meta, _} <- transform(body) do
{:ok, {types_meta, var_meta, body_meta}}
end
end
defp transform_exception_types(%{"type" => "array", "children" => types}) do
transform_list(types)
end
defp transform_exception_types(nil), do: {:ok, []}
# M2.2s Structural Layer Helpers
defp extract_constant_name({:literal, [subtype: :constant], name}), do: name
defp extract_constant_name({:literal, _, name}) when is_binary(name), do: name
defp extract_constant_name(_), do: "Unknown"
defp extract_parent_name({:literal, [subtype: :constant], name}), do: name
defp extract_parent_name({:literal, _, name}) when is_binary(name), do: name
defp extract_parent_name(nil), do: nil
defp extract_parent_name(_), do: nil
defp extract_method_params(%{"type" => "args", "children" => args}) do
params =
Enum.map(args, &extract_single_param/1)
|> Enum.reject(&is_nil/1)
{:ok, params}
end
defp extract_method_params(_), do: {:ok, []}
# Extract a single parameter node into {:param, meta, name} format
# Required positional parameter: def foo(x)
defp extract_single_param(%{"type" => "arg", "children" => [name]}) do
{:param, [], normalize_param_name(name)}
end
# Optional positional parameter with default: def foo(x = 1)
defp extract_single_param(%{"type" => "optarg", "children" => [name, default_value]}) do
default_meta =
case transform(default_value) do
{:ok, meta, _} -> meta
_ -> nil
end
{:param, [default: default_meta], normalize_param_name(name)}
end
# Required keyword parameter: def foo(name:)
defp extract_single_param(%{"type" => "kwarg", "children" => [name]}) do
{:param, [keyword: true], normalize_param_name(name)}
end
# Optional keyword parameter with default: def foo(name: "default")
defp extract_single_param(%{"type" => "kwoptarg", "children" => [name, default_value]}) do
default_meta =
case transform(default_value) do
{:ok, meta, _} -> meta
_ -> nil
end
{:param, [keyword: true, default: default_meta], normalize_param_name(name)}
end
# Rest parameter (splat): def foo(*args)
defp extract_single_param(%{"type" => "restarg", "children" => [name]}) do
{:param, [rest: true], normalize_param_name(name)}
end
# Anonymous rest parameter: def foo(*)
defp extract_single_param(%{"type" => "restarg", "children" => []}) do
{:param, [rest: true], "*"}
end
# Keyword rest parameter (double splat): def foo(**opts)
defp extract_single_param(%{"type" => "kwrestarg", "children" => [name]}) do
{:param, [keyword_rest: true], normalize_param_name(name)}
end
# Anonymous keyword rest parameter: def foo(**)
defp extract_single_param(%{"type" => "kwrestarg", "children" => []}) do
{:param, [keyword_rest: true], "**"}
end
# Block parameter: def foo(&block)
defp extract_single_param(%{"type" => "blockarg", "children" => [name]}) do
{:param, [block: true], normalize_param_name(name)}
end
# Forward arguments (...) Ruby 2.7+
defp extract_single_param(%{"type" => "forward_arg", "children" => []}) do
{:param, [forward: true], "..."}
end
# Catch-all for unrecognized parameter types
defp extract_single_param(_), do: nil
defp normalize_param_name(name) when is_atom(name), do: Atom.to_string(name)
defp normalize_param_name(name) when is_binary(name), do: name
defp extract_symbol_name(%{"type" => "sym", "children" => [name]}) when is_atom(name) do
Atom.to_string(name)
end
defp extract_symbol_name(%{"type" => "sym", "children" => [name]}) when is_binary(name),
do: name
defp extract_symbol_name(%{"type" => "dsym", "children" => parts}), do: {:dynamic, parts}
defp extract_symbol_name(_), do: "unknown"
# Extract regex flags from regopt node
defp extract_regex_flags([%{"type" => "regopt", "children" => flags}]) when is_list(flags) do
Enum.map(flags, fn
flag when is_atom(flag) -> Atom.to_string(flag)
flag when is_binary(flag) -> flag
end)
end
defp extract_regex_flags(_), do: []
# Location extraction helpers
# For 3-tuple format: {type, keyword_meta, value_or_children}
# Location info is merged into the keyword_meta (2nd element)
@doc false
@spec add_location(tuple(), map()) :: tuple()
defp add_location({type, meta, value_or_children}, %{"location" => loc}) when is_map(loc) do
location_fields =
[]
|> maybe_prepend(:line, loc["begin_line"])
|> maybe_prepend(:col, loc["begin_column"])
|> maybe_prepend(:end_line, loc["end_line"] || loc["begin_line"])
|> maybe_prepend(:end_col, loc["end_column"])
{type, meta ++ location_fields, value_or_children}
end
defp add_location(meta_ast, _ast_node), do: meta_ast
# Add location and context metadata (for M1 metadata preservation)
# Merges both location and context into keyword_meta
@doc false
defp add_location_with_context({type, meta, value_or_children}, %{"location" => loc}, context)
when is_map(loc) and is_map(context) do
location_fields =
[]
|> maybe_prepend(:line, loc["begin_line"])
|> maybe_prepend(:col, loc["begin_column"])
|> maybe_prepend(:end_line, loc["end_line"])
|> maybe_prepend(:end_col, loc["end_column"])
context_fields = Enum.map(context, fn {k, v} -> {k, v} end)
{type, meta ++ location_fields ++ context_fields, value_or_children}
end
defp add_location_with_context({type, meta, value_or_children}, _ast, context)
when is_map(context) do
context_fields = Enum.map(context, fn {k, v} -> {k, v} end)
{type, meta ++ context_fields, value_or_children}
end
defp maybe_prepend(list, _key, nil), do: list
defp maybe_prepend(list, key, value), do: [{key, value} | list]
end