free_mutant/lib/mutant/mutator.rb

307 lines
6.1 KiB
Ruby

module Mutant
# Generator for mutations
class Mutator
include Adamantium::Flat, AbstractClass
# Enumerate mutations on node
#
# @param [Rubinius::AST::Node] node
# @param [#call] block
#
# @return [self]
#
# @api private
#
def self.each(node, &block)
return to_enum(__method__, node) unless block_given?
Registry.lookup(node.class).new(node, block)
self
end
# Register node class handler
#
# @param [Class:Rubinius::AST::Node] node_class
#
# @return [undefined]
#
# @api private
#
def self.handle(node_class)
Registry.register(node_class,self)
end
private_class_method :handle
# Return wrapped node
#
# @return [Rubius::AST::Node]
#
# @api private
#
attr_reader :node
private
# Initialize generator
#
# @param [Rubinius::AST::Node] node
# @param [#call(node)] block
#
# @return [undefined]
#
# @api private
#
def initialize(node, block)
@node, @block = Helper.deep_clone(node), block
IceNine.deep_freeze(@node)
dispatch
end
# Dispatch node generations
#
# @return [undefined]
#
# @api private
#
abstract_method :dispatch
# Append node to generated mutations if node does not equal orignal node
#
# @param [Rubinius::AST::Node] node
#
# @return [self]
#
# @api private
#
def emit(node)
return self unless new?(node)
emit!(node)
end
# Maximum amount of tries to generate a new node
MAX_TRIES = 3
# Call block until it generates a new AST node
#
# The primary use of this method is to give the random generated AST nodes
# a nice interface for retring generation when generation accidentally generated the
# same AST that is about to be mutated.
#
# @yield
# Execute block until AST node that does not equal wrapped node is generated by block
#
# @return [self]
#
# @raise [RuntimeError]
# raises RuntimeError in case no new ast node can be generated after MAX_TRIES.
#
# @api private
#
def emit_new
MAX_TRIES.times do
node = yield
if new?(node)
emit!(node)
return
end
end
raise "New AST could not be generated after #{MAX_TRIES} attempts"
end
# Call block with node
#
# @param [Rubinius::AST::Node] node
#
# @return [self]
#
# @api private
#
def emit!(node)
@block.call(node)
self
end
# Check if node does not equal original node
#
# @param [Rubinius::AST::Node] node
#
# @return [true]
# returns true when node is differend from the node to be mutated
#
# @return [false]
# returns false otherwise
#
# @api private
#
def new?(node)
sexp != node.to_sexp
end
# Return s-expressions for node
#
# @return [Array]
#
# @api private
#
def sexp
node.to_sexp
end
# Emit a new AST node
#
# @param [Rubinis::AST::Node:Class] node_class
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def emit_node(node_class, *arguments)
emit(new(node_class, *arguments))
end
# Create a new AST node with same class as wrapped node
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def new_self(*arguments)
new(node.class, *arguments)
end
# Create a new AST node with Rubnius::AST::NilLiteral class
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def new_nil
new(Rubinius::AST::NilLiteral)
end
# Create a new AST node with the same line as wrapped node
#
# @param [Class:Rubinius::AST::Node] node_class
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def new(node_class, *arguments)
node_class.new(node.line, *arguments)
end
# Emit a new AST node with same class as wrapped node
#
# @return [undefined]
#
# @api private
#
def emit_self(*arguments)
emit(new_self(*arguments))
end
# Emit a new node with wrapping class for each entry in values
#
# @param [Array] values
#
# @return [undefined]
#
# @api private
#
def emit_values(values)
values.each do |value|
emit_self(value)
end
end
# Emit element presence mutations
#
# @param [Array] elements
#
# @return [undefined]
#
# @api private
#
def emit_element_presence(elements)
elements.each_index do |index|
dup_elements = elements.dup
dup_elements.delete_at(index)
emit_self(dup_elements)
end
end
# Emit body mutations
#
# @return [undefined]
#
# @api private
#
def emit_body_mutations
Mutator.each(node.body) do |mutation|
node = dup_node
node.body = mutation
emit(node)
end
end
# Emit element mutations
#
# @param [Array] elements
#
# @return [undefined]
#
# @api private
#
def emit_elements(elements)
elements.each_with_index do |element, index|
dup_elements = elements.dup
Mutator.each(element).each do |mutation|
dup_elements[index]=mutation
emit_self(dup_elements)
end
end
end
# Emit a new AST node with NilLiteral class
#
# @return [Rubinius::AST::NilLiteral]
#
# @api private
#
def emit_nil
emit(new_nil)
end
# Return AST representing send
#
# @param [Rubinius::AST::Node] receiver
# @param [Symbol] name
# @param [Rubinius::AST::Node] arguments
#
# @return [Rubnius::AST::SendWithArguments]
#
# @api private
#
def new_send(receiver, name, arguments=nil)
if arguments
new(Rubinius::AST::SendWithArguments, receiver, name, arguments)
else
new(Rubinius::AST::Send, receiver, name)
end
end
# Return duplicated (unfrozen) node each call
#
# @return [Rubinius::AST::Node]
#
# @api private
#
def dup_node
node.dup
end
memoize :sexp
end
end