Change to use unparser release with modern AST

This commit is contained in:
Markus Schirp 2018-11-29 22:07:24 +00:00
parent d4f0a77ea6
commit c75db83bd5
27 changed files with 416 additions and 269 deletions

View file

@ -15,7 +15,7 @@ PATH
parser (~> 2.5.1)
procto (~> 0.0.2)
regexp_parser (~> 1.2)
unparser (~> 0.3.0)
unparser (~> 0.4.0)
mutant-rspec (0.8.20)
mutant (~> 0.8.20)
rspec-core (>= 3.4.0, < 4.0.0)
@ -141,7 +141,7 @@ GEM
simplecov-html (0.10.2)
thread_safe (0.3.6)
unicode-display_width (1.4.0)
unparser (0.3.0)
unparser (0.4.0)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)

View file

@ -135,7 +135,6 @@ require 'mutant/mutator/node/send'
require 'mutant/mutator/node/send/binary'
require 'mutant/mutator/node/send/conditional'
require 'mutant/mutator/node/send/attribute_assignment'
require 'mutant/mutator/node/send/index'
require 'mutant/mutator/node/when'
require 'mutant/mutator/node/class'
require 'mutant/mutator/node/define'
@ -151,6 +150,8 @@ require 'mutant/mutator/node/regopt'
require 'mutant/mutator/node/resbody'
require 'mutant/mutator/node/rescue'
require 'mutant/mutator/node/match_current_line'
require 'mutant/mutator/node/index'
require 'mutant/mutator/node/procarg_zero'
require 'mutant/loader'
require 'mutant/context'
require 'mutant/scope'

View file

@ -30,14 +30,7 @@ module Mutant
naked_proc? || proc_new?
end
# Test if AST node is a valid assignment target
#
# @return [Boolean]
def assignment?
index_assignment? || attribute_assignment?
end
# Test if AST node is an attribute assignment?
# Test if AST node is a valid attribute assignment
#
# @return [Boolean]
def attribute_assignment?
@ -45,13 +38,6 @@ module Mutant
selector.to_s.end_with?(ATTRIBUTE_ASSIGNMENT_SELECTOR_SUFFIX)
end
# Test if AST node is an index assign
#
# @return [Boolean]
def index_assignment?
selector.equal?(INDEX_ASSIGNMENT_SELECTOR)
end
# Test for binary operator implemented as method
#
# @return [Boolean]

View file

@ -182,10 +182,12 @@ module Mutant
# Nodes that are NOT generated by parser but used by mutant / unparser.
GENERATED = Set.new(%i[empty]).freeze
EXTRA = Set.new(GENERATED + REGEXP)
# Nodes missing from parser metadata
MISSING = Set.new(%i[index indexasgn lambda procarg0]).freeze
# All node types mutant handles
ALL = Set.new((Parser::Meta::NODE_TYPES + EXTRA) - BLACKLIST).freeze
ALL = Set.new(
(Parser::Meta::NODE_TYPES + GENERATED + REGEXP + MISSING) - BLACKLIST
).freeze
end # Types
end # AST
end # Mutant

View file

@ -99,7 +99,7 @@ module Mutant
def node(input)
case input
when String
Unparser::Preprocessor.run(::Parser::CurrentRuby.parse(input))
Unparser::Preprocessor.run(Unparser.parse(input))
when ::Parser::AST::Node
input
else

View file

@ -96,6 +96,13 @@ module Mutant
emit(::Parser::AST::Node.new(node.type, children))
end
# Emit propagation if node can stand alone
#
# @return [undefined]
def emit_propagation(node)
emit(node) unless AST::Types::NOT_STANDALONE.include?(node.type)
end
# Emit singleton literals
#
# @return [undefined]

View file

@ -67,7 +67,6 @@ module Mutant
end
end # Optional
end # Argument
end # Node
end # Mutator

View file

@ -24,11 +24,23 @@ module Mutant
# @return [undefined]
def emit_argument_presence
emit_type
Util::Array::Presence.call(children).each do |children|
emit_type(*children)
if children.one? && n_mlhs?(Mutant::Util.one(children))
emit_procarg(Mutant::Util.one(children))
else
emit_type(*children)
end
end
end
# Emit procarg form
#
# @return [undefined]
def emit_procarg(arg)
emit_type(s(:procarg0, *arg))
end
# Emit argument mutations
#
# @return [undefined]

View file

@ -17,7 +17,7 @@ module Mutant
# @return [undefined]
def dispatch
emit_singletons
emit(send)
emit(send) unless n_lambda?(send)
emit_send_mutations(&method(:n_send?))
emit_arguments_mutations

View file

@ -0,0 +1,129 @@
# frozen_string_literal: true
module Mutant
class Mutator
class Node
# Base mutator for index operations
class Index < self
NO_VALUE_RANGE = (1..-1).freeze
SEND_REPLACEMENTS = %i[at fetch key?].freeze
private_constant(*constants(false))
children :receiver
private
# Emit mutations
#
# @return [undefined]
def dispatch
emit_singletons
emit_receiver_mutations { |node| !n_nil?(node) }
emit(receiver)
emit_send_forms
emit_drop_mutation
mutate_indices
end
# Emit send forms
#
# @return [undefined]
def emit_send_forms
return if asgn_left?
SEND_REPLACEMENTS.each do |selector|
emit(s(:send, receiver, selector, *indices))
end
end
# Emit mutation `foo[n..-1]` -> `foo.drop(n)`
#
# @return [undefined]
def emit_drop_mutation
return unless indices.one? && n_irange?(Mutant::Util.one(indices))
start, ending = *indices.first
return unless ending.eql?(s(:int, -1))
emit(s(:send, receiver, :drop, start))
end
# Mutate indices
#
# @return [undefined]
def mutate_indices
children_indices(index_range).each do |index|
emit_propagation(children.fetch(index)) unless asgn_left?
delete_child(index)
mutate_child(index)
end
end
# The index nodes
#
# @return [Enumerable<Parser::AST::Node>]
def indices
children[index_range]
end
class Read < self
handle :index
private
# The range index children can be found
#
# @return [Range]
def index_range
NO_VALUE_RANGE
end
end
# Mutator for index assignments
class Assign < self
REGULAR_RANGE = (1..-2).freeze
private_constant(*constants(false))
handle :indexasgn
private
# Emit mutations
#
# @return [undefined]
def dispatch
super()
return if asgn_left?
emit_index_read
emit(children.last)
mutate_child(children.length.pred)
end
# Emit index read
#
# @return [undefined]
def emit_index_read
emit(s(:index, receiver, *children[index_range]))
end
# Index indices
#
# @return [Range<Integer>]
def index_range
if asgn_left?
NO_VALUE_RANGE
else
REGULAR_RANGE
end
end
end # Assign
end # Index
end # Node
end # Mutator
end # Mutant

View file

@ -7,7 +7,7 @@ module Mutant
# Mutation emitter to handle noop nodes
class Noop < self
handle(:__ENCODING__, :block_pass, :cbase)
handle(:__ENCODING__, :block_pass, :cbase, :lambda)
private

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Mutant
class Mutator
class Node
class ProcargZero < self
MAP = {
::Parser::AST::Node => :emit_argument_node_mutations,
Symbol => :emit_argument_symbol_mutations
}.freeze
private_constant(*constants(false))
handle :procarg0
children :argument
private
# Emit mutations
#
# @return [undefined]
def dispatch
__send__(MAP.fetch(argument.class))
end
# Emit argument symbol mutations
#
# @return [undefined]
def emit_argument_symbol_mutations
emit_type(:"_#{argument}") unless argument.to_s.start_with?('_')
end
# Emit argument node mutations
#
# @return [undefined]
def emit_argument_node_mutations
emit_argument_mutations
first = Mutant::Util.one(argument.children)
emit_type(first)
end
end # ProcargZero
end # Node
end # Mutator
end # Mutant

View file

@ -40,7 +40,6 @@ module Mutant
values_at: %i[fetch_values],
match: %i[match?],
'=~': %i[match?],
:[] => %i[at fetch key?],
:== => %i[eql? equal?],
:>= => %i[> == eql? equal?],
:<= => %i[< == eql? equal?],
@ -61,17 +60,7 @@ module Mutant
# @return [undefined]
def dispatch
emit_singletons
if meta.index_assignment?
run(Index::Assign)
else
non_index_dispatch
end
end
# Perform non index dispatch
#
# @return [undefined]
def non_index_dispatch
if meta.binary_method_operator?
run(Binary)
elsif meta.attribute_assignment?
@ -117,7 +106,6 @@ module Mutant
emit_dig_mutation
emit_double_negation_mutation
emit_lambda_mutation
emit_drop_mutation
end
# Emit selector mutations specific to top level constants
@ -167,19 +155,6 @@ module Mutant
emit(s(:send, fetch_mutation, :dig, *tail))
end
# Emit mutation `foo[n..-1]` -> `foo.drop(n)`
#
# @return [undefined]
def emit_drop_mutation
return if !selector.equal?(:[]) || !arguments.one? || !n_irange?(arguments.first)
start, ending = *arguments.first
return unless ending.eql?(s(:int, -1))
emit(s(:send, receiver, :drop, start))
end
# Emit mutation from `to_i` to `Integer(...)`
#
# @return [undefined]
@ -227,8 +202,7 @@ module Mutant
#
# @return [undefined]
def emit_argument_propagation
node = arguments.first
emit(node) if arguments.one? && !NOT_STANDALONE.include?(node.type)
emit_propagation(Mutant::Util.one(arguments)) if arguments.one?
end
# Emit receiver mutations

View file

@ -1,52 +0,0 @@
# frozen_string_literal: true
module Mutant
class Mutator
class Node
class Send
# Base mutator for index operations
class Index < self
# Mutator for index assignments
class Assign < self
define_named_child(:value, -1)
INDEX_RANGE = (2..-2).freeze
private
# Emit mutations
#
# @return [undefined]
def dispatch
emit_naked_receiver
emit_value_mutations
emit_index_read
emit(value)
mutate_indices
end
# Mutate indices
#
# @return [undefined]
def mutate_indices
children_indices(INDEX_RANGE).each do |index|
delete_child(index)
mutate_child(index)
end
end
# Emit index read
#
# @return [undefined]
def emit_index_read
emit_type(receiver, :[], *children[INDEX_RANGE])
end
end # Assign
end # Index
end # Send
end # Node
end # Mutator
end # Mutant

View file

@ -18,7 +18,7 @@ module Mutant
#
# @return [AST::Node]
def call(path)
@cache[path] ||= ::Parser::CurrentRuby.parse(path.read)
@cache[path] ||= Unparser.parse(path.read)
end
end # Parser

View file

@ -115,7 +115,7 @@ module Mutant
#
# @return [Parser::AST::Node]
def namespaced_node(source_path)
s(:module, s(:const, nil, namespace), ::Parser::CurrentRuby.parse(source_path.read))
s(:module, s(:const, nil, namespace), Unparser.parse(source_path.read))
end
end # Zombifier

View file

@ -24,8 +24,8 @@ Mutant::Meta::Example.add :block do
mutation 'foo { |a, b| raise }'
mutation 'foo { |a, _b| }'
mutation 'foo { |_a, b| }'
mutation 'foo { |a| }'
mutation 'foo { |b| }'
mutation 'foo { |a, | }'
mutation 'foo { |b, | }'
mutation 'foo { || }'
end
@ -39,7 +39,7 @@ Mutant::Meta::Example.add :block do
mutation 'foo { |(a), c| }'
mutation 'foo { |(b), c| }'
mutation 'foo { |(a, b)| }'
mutation 'foo { |c| }'
mutation 'foo { |c, | }'
mutation 'foo { |(_a, b), c| }'
mutation 'foo { |(a, _b), c| }'
mutation 'foo { |(a, b), _c| }'
@ -61,6 +61,25 @@ Mutant::Meta::Example.add :block do
mutation 'foo {}'
end
Mutant::Meta::Example.add :block do
source 'foo { |_a| }'
singleton_mutations
mutation 'foo { || }'
mutation 'foo { |_a| raise }'
mutation 'foo'
end
Mutant::Meta::Example.add :block do
source 'foo { |a| }'
singleton_mutations
mutation 'foo { || }'
mutation 'foo { |a| raise }'
mutation 'foo { |_a| }'
mutation 'foo'
end
Mutant::Meta::Example.add :block do
source 'foo { |(a)| }'

133
meta/index.rb Normal file
View file

@ -0,0 +1,133 @@
# frozen_string_literal: true
Mutant::Meta::Example.add :index do
source 'self.foo[]'
singleton_mutations
mutation 'self.foo'
mutation 'self.foo.at()'
mutation 'self.foo.fetch()'
mutation 'self.foo.key?()'
mutation 'self[]'
mutation 'foo[]'
end
Mutant::Meta::Example.add :index do
source 'foo[1]'
singleton_mutations
mutation '1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(1)'
mutation 'foo.fetch(1)'
mutation 'foo.key?(1)'
mutation 'self[1]'
mutation 'foo[0]'
mutation 'foo[2]'
mutation 'foo[-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
end
Mutant::Meta::Example.add :index do
source 'foo[n..-2]'
singleton_mutations
mutation 'n..-2'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n..-2)'
mutation 'foo.fetch(n..-2)'
mutation 'foo.key?(n..-2)'
mutation 'self[n..-2]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n..nil]'
mutation 'foo[n..self]'
mutation 'foo[n..-1]'
mutation 'foo[n..2]'
mutation 'foo[n..0]'
mutation 'foo[n..1]'
mutation 'foo[n..-3]'
mutation 'foo[n...-2]'
mutation 'foo[nil..-2]'
mutation 'foo[self..-2]'
end
Mutant::Meta::Example.add :index do
source 'foo[n...-1]'
singleton_mutations
mutation 'n...-1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n...-1)'
mutation 'foo.fetch(n...-1)'
mutation 'foo.key?(n...-1)'
mutation 'self[n...-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n...nil]'
mutation 'foo[n...self]'
mutation 'foo[n..-1]'
mutation 'foo[n...0]'
mutation 'foo[n...1]'
mutation 'foo[n...-2]'
mutation 'foo[nil...-1]'
mutation 'foo[self...-1]'
end
Mutant::Meta::Example.add :index do
source 'foo[n..-1]'
singleton_mutations
mutation 'n..-1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n..-1)'
mutation 'foo.fetch(n..-1)'
mutation 'foo.key?(n..-1)'
mutation 'self[n..-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n..nil]'
mutation 'foo[n..self]'
mutation 'foo[n..0]'
mutation 'foo[n..1]'
mutation 'foo[n..-2]'
mutation 'foo[n...-1]'
mutation 'foo[nil..-1]'
mutation 'foo[self..-1]'
mutation 'foo.drop(n)'
end
Mutant::Meta::Example.add :index do
source 'self[foo]'
singleton_mutations
mutation 'self[self]'
mutation 'self[nil]'
mutation 'self[]'
mutation 'self.at(foo)'
mutation 'self.fetch(foo)'
mutation 'self.key?(foo)'
mutation 'foo'
end
Mutant::Meta::Example.add :index do
source 'foo[*bar]'
singleton_mutations
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(*bar)'
mutation 'foo.fetch(*bar)'
mutation 'foo.key?(*bar)'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[bar]'
mutation 'foo[*self]'
mutation 'foo[*nil]'
mutation 'self[*bar]'
end

31
meta/indexasgn.rb Normal file
View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
Mutant::Meta::Example.add :indexasgn do
source 'foo[bar] = baz'
singleton_mutations
mutation 'self[bar] = baz'
mutation 'foo'
mutation 'foo[bar]'
mutation 'foo.at(bar)'
mutation 'foo.fetch(bar)'
mutation 'foo.key?(bar)'
mutation 'foo[bar] = self'
mutation 'foo[bar] = nil'
mutation 'foo[nil] = baz'
mutation 'foo[self] = baz'
mutation 'foo[] = baz'
mutation 'baz'
mutation 'bar'
end
Mutant::Meta::Example.add :indexasgn, :op_asgn do
source 'self[foo] += bar'
singleton_mutations
mutation 'self[] += bar'
mutation 'self[nil] += bar'
mutation 'self[self] += bar'
mutation 'self[foo] += nil'
mutation 'self[foo] += self'
end

9
meta/lambda.rb Normal file
View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
Mutant::Meta::Example.add :block, :lambda do
source '->() {}'
singleton_mutations
mutation '->() { raise }'
end

View file

@ -353,20 +353,6 @@ Mutant::Meta::Example.add :send do
mutation 'baz'
end
Mutant::Meta::Example.add :send do
source 'foo[bar] = baz'
singleton_mutations
mutation 'foo'
mutation 'foo[bar]'
mutation 'foo[bar] = self'
mutation 'foo[bar] = nil'
mutation 'foo[nil] = baz'
mutation 'foo[self] = baz'
mutation 'foo[] = baz'
mutation 'baz'
end
Mutant::Meta::Example.add :send do
source 'foo(*bar)'
@ -484,36 +470,6 @@ Mutant::Meta::Example.add :send do
mutation 'self / foo'
end
Mutant::Meta::Example.add :send do
source 'foo[1]'
singleton_mutations
mutation '1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(1)'
mutation 'foo.fetch(1)'
mutation 'foo.key?(1)'
mutation 'self[1]'
mutation 'foo[0]'
mutation 'foo[2]'
mutation 'foo[-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
end
Mutant::Meta::Example.add :send do
source 'self.foo[]'
singleton_mutations
mutation 'self.foo'
mutation 'self.foo.at()'
mutation 'self.foo.fetch()'
mutation 'self.foo.key?()'
mutation 'self[]'
mutation 'foo[]'
end
Mutant::Meta::Example.add :send do
source 'foo(n..-1)'
@ -532,108 +488,6 @@ Mutant::Meta::Example.add :send do
mutation 'foo(n..-2)'
end
Mutant::Meta::Example.add :send do
source 'foo[n..-2]'
singleton_mutations
mutation 'n..-2'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n..-2)'
mutation 'foo.fetch(n..-2)'
mutation 'foo.key?(n..-2)'
mutation 'self[n..-2]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n..nil]'
mutation 'foo[n..self]'
mutation 'foo[n..-1]'
mutation 'foo[n..2]'
mutation 'foo[n..0]'
mutation 'foo[n..1]'
mutation 'foo[n..-3]'
mutation 'foo[n...-2]'
mutation 'foo[nil..-2]'
mutation 'foo[self..-2]'
end
Mutant::Meta::Example.add :send do
source 'foo[n...-1]'
singleton_mutations
mutation 'n...-1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n...-1)'
mutation 'foo.fetch(n...-1)'
mutation 'foo.key?(n...-1)'
mutation 'self[n...-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n...nil]'
mutation 'foo[n...self]'
mutation 'foo[n..-1]'
mutation 'foo[n...0]'
mutation 'foo[n...1]'
mutation 'foo[n...-2]'
mutation 'foo[nil...-1]'
mutation 'foo[self...-1]'
end
Mutant::Meta::Example.add :send do
source 'foo[n..-1]'
singleton_mutations
mutation 'n..-1'
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(n..-1)'
mutation 'foo.fetch(n..-1)'
mutation 'foo.key?(n..-1)'
mutation 'self[n..-1]'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[n..nil]'
mutation 'foo[n..self]'
mutation 'foo[n..0]'
mutation 'foo[n..1]'
mutation 'foo[n..-2]'
mutation 'foo[n...-1]'
mutation 'foo[nil..-1]'
mutation 'foo[self..-1]'
mutation 'foo.drop(n)'
end
Mutant::Meta::Example.add :send do
source 'self[foo]'
singleton_mutations
mutation 'self[self]'
mutation 'self[nil]'
mutation 'self[]'
mutation 'self.at(foo)'
mutation 'self.fetch(foo)'
mutation 'self.key?(foo)'
mutation 'foo'
end
Mutant::Meta::Example.add :send do
source 'foo[*bar]'
singleton_mutations
mutation 'foo'
mutation 'foo[]'
mutation 'foo.at(*bar)'
mutation 'foo.fetch(*bar)'
mutation 'foo.key?(*bar)'
mutation 'foo[nil]'
mutation 'foo[self]'
mutation 'foo[bar]'
mutation 'foo[*self]'
mutation 'foo[*nil]'
mutation 'self[*bar]'
end
(Mutant::AST::Types::BINARY_METHOD_OPERATORS - %i[=~ <= >= < > == != eql?]).each do |operator|
Mutant::Meta::Example.add :send do
source "true #{operator} false"

View file

@ -36,7 +36,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency('parser', '~> 2.5.1')
gem.add_runtime_dependency('procto', '~> 0.0.2')
gem.add_runtime_dependency('regexp_parser', '~> 1.2')
gem.add_runtime_dependency('unparser', '~> 0.3.0')
gem.add_runtime_dependency('unparser', '~> 0.4.0')
gem.add_development_dependency('devtools', '~> 0.1.22')
gem.add_development_dependency('parallel', '~> 1.3')

View file

@ -44,7 +44,7 @@ module ParserHelper
end
def parse(string)
Unparser::Preprocessor.run(Parser::CurrentRuby.parse(string))
Unparser::Preprocessor.run(Unparser.parse(string))
end
def parse_expression(string)

View file

@ -4,7 +4,7 @@ RSpec.describe Mutant::AST::Meta::Send, '#proc?' do
subject { described_class.new(node).proc? }
shared_context 'proc send' do |source|
let(:node) { Parser::CurrentRuby.parse(source).children.first }
let(:node) { Unparser.parse(source).children.first }
end
shared_examples 'proc definition' do |*args|

View file

@ -4,7 +4,7 @@ RSpec.describe Mutant::AST::Meta::Send, '#receiver_possible_top_level_const?' do
subject { described_class.new(node).receiver_possible_top_level_const? }
def parse(source)
Parser::CurrentRuby.parse(source)
Unparser.parse(source)
end
context 'when implicit top level const' do

View file

@ -4,24 +4,22 @@ RSpec.describe Mutant::AST::Meta::Send do
let(:object) { described_class.new(node) }
def parse(source)
Parser::CurrentRuby.parse(source)
Unparser.parse(source)
end
let(:method_call) { parse('foo.bar(baz)') }
let(:attribute_read) { parse('foo.bar') }
let(:index_assignment) { parse('foo[0] = bar') }
let(:attribute_assignment) { parse('foo.bar = baz') }
let(:binary_method_operator) { parse('foo == bar') }
class Expectation
include Adamantium, Anima.new(:name, :assignment, :attribute_assignment, :index_assignment, :binary_method_operator)
include Adamantium, Anima.new(:name, :attribute_assignment, :binary_method_operator)
ALL = [
[:method_call, false, false, false, false],
[:attribute_read, false, false, false, false],
[:index_assignment, true, false, true, false],
[:attribute_assignment, true, true, false, false],
[:binary_method_operator, false, false, false, true]
[:method_call, false, false],
[:attribute_read, false, false],
[:attribute_assignment, true, false],
[:binary_method_operator, false, true]
].map do |values|
new(Hash[anima.attribute_names.zip(values)])
end.freeze

View file

@ -19,7 +19,7 @@ RSpec.describe Mutant::Subject do
let(:object) { class_under_test.new(context, node) }
let(:node) do
Parser::CurrentRuby.parse(<<-RUBY)
Unparser.parse(<<-RUBY)
def foo
end
RUBY