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