Current section

Files

Jump to
metastatic lib metastatic adapters ruby to_meta.ex
Raw

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
# 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