module RipperPlus::Transformer

Transforms a 1.9.2 Ripper AST into a RipperPlus AST. The only change as a result of this transformation is that the nodes for local variable references and vcalls (bareword calls to self) have different node types:

def foo(x)
  y
  y = x
end

becomes

[:def,

[:@ident, "foo", [1, 4]],
[:paren, [:params, [[:@ident, "x", [1, 8]]], nil, nil, nil, nil]],
[:bodystmt,
 [[:vcall, [:@ident, "y", [2, 2]]],
  [:assign,
   [:var_field, [:@ident, "y", [3, 2]]],
   [:var_ref, [:@ident, "x", [3, 6]]]]],
 nil, nil, nil]]

while:

def foo(x)
  y = x
  y
end

becomes

[:def,

[:@ident, "foo", [1, 4]],
[:paren, [:params, [[:@ident, "x", [1, 8]]], nil, nil, nil, nil]],
[:bodystmt,
 [[:assign,
   [:var_field, [:@ident, "y", [2, 2]]],
   [:var_ref, [:@ident, "x", [2, 6]]]],
  [:var_ref, [:@ident, "y", [3, 2]]]],
 nil, nil, nil]]

Public Instance Methods

add_locals_from_regexp(regexp, scope_stack) click to toggle source
# File lib/ripper-plus/transformer.rb, line 194
def add_locals_from_regexp(regexp, scope_stack)
  regexp_parts = regexp[1]
  if regexp_parts.one? && regexp_parts[0][0] == :@tstring_content
    regexp_text = regexp_parts[0][1]
    captures = Regexp.new(regexp_text).names
    captures.each { |var_name| scope_stack.add_variable(var_name) }
  end
end
add_variable_list(list, scope_stack, allow_duplicates=true) click to toggle source
# File lib/ripper-plus/transformer.rb, line 203
def add_variable_list(list, scope_stack, allow_duplicates=true)
  list.each { |var| add_variables_from_node(var, scope_stack, allow_duplicates) }
end
add_variables_from_node(lhs, scope_stack, allow_duplicates=true) click to toggle source

Adds variables to the given scope stack from the given node. Allows nodes from parameter lists, left-hand-sides, block argument lists, and so on.

# File lib/ripper-plus/transformer.rb, line 210
def add_variables_from_node(lhs, scope_stack, allow_duplicates=true)
  case lhs[0]
  when :@ident
    scope_stack.add_variable(lhs[1], allow_duplicates)
  when :const_path_field, :@const, :top_const_field
    if scope_stack.in_method?
      raise DynamicConstantError.new
    end
  when Array
    add_variable_list(lhs, scope_stack, allow_duplicates)
  when :mlhs_paren, :var_field, :rest_param, :blockarg
    add_variables_from_node(lhs[1], scope_stack, allow_duplicates)
  when :mlhs_add_star
    pre_star, star, post_star = lhs[1..3]
    add_variable_list(pre_star, scope_stack, allow_duplicates)
    if star
      add_variables_from_node(star, scope_stack, allow_duplicates)
    end
    add_variable_list(post_star, scope_stack, allow_duplicates) if post_star
  when :param_error
    raise InvalidArgumentError.new
  when :assign_error
    raise LHSError.new
  end
end
transform(root, opts={}) click to toggle source

Transforms the given AST into a RipperPlus AST.

# File lib/ripper-plus/transformer.rb, line 52
def transform(root, opts={})
  new_copy = opts[:in_place] ? root : clone_sexp(root)
  scope_stack = ScopeStack.new
  transform_tree(new_copy, scope_stack)
  new_copy
end
transform_in_order(tree, scope_stack) click to toggle source

If this node’s subtrees are ordered as they are lexically, as most are, transform each subtree in order.

# File lib/ripper-plus/transformer.rb, line 238
def transform_in_order(tree, scope_stack)
  # some nodes have no type: include the first element in this case
  range = Symbol === tree[0] ? 1..-1 : 0..-1
  tree[range].each do |subtree|
    # obviously don't transform literals or token locations
    if Array === subtree && !(Fixnum === subtree[0])
      transform_tree(subtree, scope_stack)
    end
  end
end
transform_params(param_node, scope_stack) click to toggle source

Transforms a parameter list, and adds the new variables to current scope. Used by both block args and method args.

# File lib/ripper-plus/transformer.rb, line 251
def transform_params(param_node, scope_stack)
  param_node = param_node[1] if param_node[0] == :paren
  if param_node
    positional_1, optional, rest, positional_2, block = param_node[1..5]
    add_variable_list(positional_1, scope_stack, false) if positional_1
    if optional
      optional.each do |var, value|
        # MUST walk value first. (def foo(y=y); end) == (def foo(y=y()); end)
        transform_tree(value, scope_stack)
        add_variables_from_node(var, scope_stack, false)
      end
    end
    if rest && rest[1]
      add_variables_from_node(rest, scope_stack, false)
    end
    add_variable_list(positional_2, scope_stack, false) if positional_2
    add_variables_from_node(block, scope_stack, false) if block
  end
end
transform_params_then_body(tree, params, body, scope_stack) click to toggle source
# File lib/ripper-plus/transformer.rb, line 186
def transform_params_then_body(tree, params, body, scope_stack)
  transform_params(params, scope_stack)
rescue SyntaxError
  wrap_node_with_error(tree)
else
  transform_tree(body, scope_stack)
end
transform_tree(tree, scope_stack) click to toggle source

Transforms the given tree into a RipperPlus AST, using a scope stack. This will be recursively called through each level of the tree.

# File lib/ripper-plus/transformer.rb, line 61
def transform_tree(tree, scope_stack)
  if Symbol === tree[0]
    case tree[0]
    when :assign, :massign
      lhs, rhs = tree[1..2]
      begin
        add_variables_from_node(lhs, scope_stack)
      rescue SyntaxError => err
        wrap_node_with_error(tree)
      else
        transform_tree(rhs, scope_stack)
      end
    when :for
      vars, iterated, body = tree[1..3]
      add_variables_from_node(vars, scope_stack)
      transform_tree(iterated, scope_stack)
      transform_tree(body, scope_stack)
    when :var_ref
      # When we reach a :var_ref, we should know everything we need to know
      # in order to tell if it should be transformed into a :vcall.
      if tree[1][0] == :@ident && !scope_stack.has_variable?(tree[1][1])
        tree[0] = :vcall
      end
    when :class
      name, superclass, body = tree[1..3]
      if name[1][0] == :class_name_error || scope_stack.in_method?
        wrap_node_with_error(tree)
      else
        transform_tree(superclass, scope_stack) if superclass  # superclass node
        scope_stack.with_closed_scope do
          transform_tree(body, scope_stack)
        end
      end
    when :module
      name, body = tree[1..2]
      if name[1][0] == :class_name_error || scope_stack.in_method?
        wrap_node_with_error(tree)
      else
        scope_stack.with_closed_scope do
          transform_tree(body, scope_stack)  # body
        end
      end
    when :sclass
      singleton, body = tree[1..2]
      transform_tree(singleton, scope_stack)
      scope_stack.with_closed_scope do
        transform_tree(body, scope_stack)
      end
    when :def
      scope_stack.with_closed_scope(true) do
        param_node = tree[2]
        body = tree[3]
        transform_params_then_body(tree, param_node, body, scope_stack)
      end
    when :defs
      transform_tree(tree[1], scope_stack)  # singleton could be a method call!
      scope_stack.with_closed_scope(true) do
        param_node = tree[4]
        body = tree[5]
        transform_params_then_body(tree, param_node, body, scope_stack)
      end
    when :lambda
      param_node, body = tree[1..2]
      scope_stack.with_open_scope do
        transform_params_then_body(tree, param_node, body, scope_stack)
      end
    when :rescue
      list, name, body = tree[1..3]
      transform_tree(list, scope_stack) if list
      # Don't forget the rescue argument!
      if name
        add_variables_from_node(name, scope_stack)
      end
      transform_tree(body, scope_stack)
    when :method_add_block
      call, block = tree[1..2]
      # first transform the call
      transform_tree(call, scope_stack)
      # then transform the block
      param_node, body = block[1..2]
      scope_stack.with_open_scope do
        begin
          if param_node
            transform_params(param_node[1], scope_stack)
            if param_node[2]
              add_variable_list(param_node[2], scope_stack, false)
            end
          end
        rescue SyntaxError
          wrap_node_with_error(tree)
        else
          transform_tree(body, scope_stack)
        end
      end
    when :binary
      # must check for named groups in a literal match. wowzerz.
      lhs, op, rhs = tree[1..3]
      if op == :=~
        if lhs[0] == :regexp_literal
          add_locals_from_regexp(lhs, scope_stack)
          transform_tree(rhs, scope_stack)
        elsif lhs[0] == :paren && !lhs[1].empty? && lhs[1] != [[:void_stmt]] && lhs[1].last[0] == :regexp_literal
          lhs[1][0..-2].each { |node| transform_tree(node, scope_stack) }
          add_locals_from_regexp(lhs[1].last, scope_stack)
          transform_tree(rhs, scope_stack)
        else
          transform_in_order(tree, scope_stack)
        end
      else
        transform_in_order(tree, scope_stack)
      end
    when :if_mod, :unless_mod, :while_mod, :until_mod, :rescue_mod
      # The AST is the reverse of the parse order for these nodes.
      transform_tree(tree[2], scope_stack)
      transform_tree(tree[1], scope_stack)
    when :alias_error, :assign_error  # error already top-level! wrap it again.
      wrap_node_with_error(tree)
    else
      transform_in_order(tree, scope_stack)
    end
  else
    transform_in_order(tree, scope_stack)
  end
end
wrap_node_with_error(tree) click to toggle source

Wraps the given node as an error node with minimal space overhead.

# File lib/ripper-plus/transformer.rb, line 272
def wrap_node_with_error(tree)
  new_tree = [:error, tree.dup]
  tree.replace(new_tree)
end

Private Instance Methods

clone_sexp(node) click to toggle source

Deep-copies the sexp. I wish Array#clone did deep copies…

# File lib/ripper-plus/transformer.rb, line 278
def clone_sexp(node)
  node.map do |part|
    # guess: arrays most common, then un-dupables, then dup-ables (strings only)
    if Array === part
      clone_sexp(part)
    elsif [Symbol, Fixnum, TrueClass, FalseClass, NilClass].any? { |k| k === part }
      part
    else
      part.dup
    end
  end
end