Introduce AST::Meta to externalize semantic analysis

* Add Mutant::AST namespace to hold all AST related data / helpers.
* Mutant::AST will be externalized into an ast-meta gem that can be
  shared with unparser for deduplication.
* Over the time the mutators itself will not need to deal with semantic
  analysis of the AST anymore by themselves.
* Move AST analysis for send nodes to AST::Meta
* Fix #209
This commit is contained in:
Markus Schirp 2014-06-29 21:25:17 +00:00
parent 774c7aa50e
commit 13cd04d9be
31 changed files with 418 additions and 225 deletions

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 960
total_score: 977

View file

@ -113,7 +113,7 @@ UncommunicativeMethodName:
enabled: true
exclude:
- Mutant::Mutation#sha1
- Mutant::NodeHelpers#s
- Mutant::AST::Sexp#s
reject:
- !ruby/regexp /^[a-z]$/
- !ruby/regexp /[0-9]$/
@ -159,5 +159,5 @@ UtilityFunction:
- Mutant::Meta::Example::Verification#format_mutation
- Mutant::Mutation::Evil#success?
- Mutant::Mutation::Neutral#success?
- Mutant::NodeHelpers#s
- Mutant::AST::Sexp#s
max_helper_calls: 0

View file

@ -25,45 +25,8 @@ module Mutant
# The frozen empty array used within mutant
EMPTY_ARRAY = [].freeze
symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze }
SCOPE_OPERATOR = '::'.freeze
# Set of nodes that cannot be on the LHS of an assignment
NOT_ASSIGNABLE = symbolset.(%w[int float str dstr class module self nil])
# Set of op-assign types
OP_ASSIGN = symbolset.call(%w[or_asgn and_asgn op_asgn])
# Set of node types that are not valid when emitted standalone
NOT_STANDALONE = symbolset.(%w[splat restarg block_pass])
INDEX_OPERATORS = symbolset.(%w[[] []=])
UNARY_METHOD_OPERATORS = symbolset.(%w[~@ +@ -@ !])
# Operators ruby implementeds as methods
METHOD_OPERATORS = symbolset.(%w[
<=> === []= [] <= >= == !~ != =~ <<
>> ** * % / | ^ & < > + - ~@ +@ -@ !
])
BINARY_METHOD_OPERATORS = (
METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS)
).to_set.freeze
OPERATOR_METHODS = (
METHOD_OPERATORS + INDEX_OPERATORS + UNARY_METHOD_OPERATORS
).to_set.freeze
# Nodes that are NOT handled by mutant.
#
# not - 1.8 only, mutant does not support 1.8
#
NODE_BLACKLIST = symbolset.(%w[not])
# Nodes that are NOT generated by parser but used by mutant / unparser.
NODE_EXTRA = symbolset.(%w[empty])
# All node types mutant handles
NODE_TYPES = ((Parser::Meta::NODE_TYPES + NODE_EXTRA) - NODE_BLACKLIST).to_set.freeze
# Lookup constant for location
#
# @param [String] location
@ -116,9 +79,15 @@ module Mutant
end # Mutant
require 'mutant/version'
require 'mutant/ast'
require 'mutant/ast/sexp'
require 'mutant/ast/types'
require 'mutant/ast/nodes'
require 'mutant/ast/named_children'
require 'mutant/ast/node_predicates'
require 'mutant/ast/meta'
require 'mutant/cache'
require 'mutant/delegator'
require 'mutant/node_helpers'
require 'mutant/warning_filter'
require 'mutant/warning_expectation'
require 'mutant/walker'

5
lib/mutant/ast.rb Normal file
View file

@ -0,0 +1,5 @@
module Mutant
# AST helpers
module AST
end # AST
end # Mutant

131
lib/mutant/ast/meta.rb Normal file
View file

@ -0,0 +1,131 @@
module Mutant
module AST
# Node meta information mixin
module Meta
REGISTRY = {}
# Return meta for node
#
# @param [Parser::AST::Node] node
#
# @return [Meta]
#
# @api private
#
def self.for(node)
REGISTRY.fetch(node.type, Generic).new(node)
end
# Generic metadata for send nodes
class Send
include Concord.new(:node), NamedChildren
children :receiver, :selector
REGISTRY[:send] = self
INDEX_ASSIGNMENT_SELECTOR = :[]=
ATTRIBUTE_ASSIGNMENT_SELECTOR_SUFFIX = '='.freeze
# Return arguments
#
# @return [Enumerable<Parser::AST::Node>]
#
# @api private
#
alias_method :arguments, :remaining_children
# Test if AST node is a valid assignment target
#
# @return [Boolean]
#
# @api private
#
def assignment?
index_assignment? || attribute_assignment?
end
# Test if AST node is an attribute assignment?
#
# @return [Boolean]
#
# @api private
#
def attribute_assignment?
arguments.one? && attribute_assignment_selector?
end
# Test if AST node is an index assign
#
# @return [Boolean]
#
# @api private
#
def index_assignment?
arguments.length.equal?(2) && index_assignment_selector?
end
# Test for binary operator implemented as method
#
# @return [Boolean]
#
# @api private
#
def binary_method_operator?
arguments.one? && Types::BINARY_METHOD_OPERATORS.include?(selector)
end
# Test if node is part of an mlhs
#
# @return [Boolean]
#
# @api private
#
def mlhs?
(index_assignment_selector? && arguments.one?) || (arguments.empty? && attribute_assignment_selector?)
end
private
# Test for index assignment operator
#
# @return [Boolean]
#
# @api private
#
def index_assignment_selector?
selector.equal?(INDEX_ASSIGNMENT_SELECTOR)
end
# Test for attribute assignment selector
#
# @return [Boolean]
#
# @api private
#
def attribute_assignment_selector?
!Types::METHOD_OPERATORS.include?(selector) && selector.to_s.end_with?(ATTRIBUTE_ASSIGNMENT_SELECTOR_SUFFIX)
end
end # Send
# Generic node metatada
class Generic
include Adamantium, Concord.new(:node)
# Test if AST node is a valid assign target
#
# @return [Boolean]
#
# @api private
#
def assignment?
Types::ASSIGNABLE_VARIABLES.include?(node.type)
end
end # Generic
end #
end # AST
end # Mutant

View file

@ -0,0 +1,98 @@
module Mutant
module AST
# Helper methods to define named children
module NamedChildren
# Hook called when module gets included
#
# @param [Class, Module] host
#
# @return [undefined]
#
# @api private
#
def self.included(host)
super
host.class_eval do
include InstanceMethods
extend ClassMethods
end
end
# Methods mixed int ot instance level
module InstanceMethods
private
# Return children
#
# @return [Array<Parser::AST::Node]
#
# @api private
#
def children
node.children
end
end # InstanceMethods
# Methods mixed in at class level
module ClassMethods
private
# Define named child
#
# @param [Symbol] name
# @param [Fixnum] index
#
# @return [undefined]
#
# @api private
#
def define_named_child(name, index)
define_method(name) do
children.at(index)
end
end
# Define remaining children
#
# @param [Array<Symbol>] names
#
# @return [undefined]
#
# @api private
#
def define_remaining_children(names)
define_method(:remaining_children_with_index) do
children.each_with_index.drop(names.length)
end
define_method(:remaining_children_indices) do
children.each_index.drop(names.length)
end
define_method(:remaining_children) do
children.drop(names.length)
end
end
# Create name helpers
#
# @return [undefined]
#
# @api private
#
def children(*names)
names.each_with_index do |name, index|
define_named_child(name, index)
end
define_remaining_children(names)
end
end # ClassMethods
end # NamedChildren
end # AST
end # Mutant

View file

@ -0,0 +1,19 @@
module Mutant
module AST
# Module for node predicates
module NodePredicates
Types::ALL.each do |type|
fail "method: #{type} is already defined" if instance_methods(true).include?(type)
name = "n_#{type.to_s.sub(/\??\z/, '?')}"
define_method(name) do |node|
node.type.equal?(type)
end
private name
end
end # NodePredicates
end # AST
end # Mutant

21
lib/mutant/ast/nodes.rb Normal file
View file

@ -0,0 +1,21 @@
module Mutant
module AST
# Singleton nodes
module Nodes
extend Sexp
N_NAN = s(:send, s(:float, 0.0), :/, s(:float, 0.0))
N_INFINITY = s(:send, s(:float, 1.0), :/, s(:float, 0.0))
N_NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:float, 0.0))
N_RAISE = s(:send, nil, :raise)
N_TRUE = s(:true)
N_FALSE = s(:false)
N_NIL = s(:nil)
N_EMPTY = s(:empty)
N_SELF = s(:self)
N_ZSUPER = s(:zsuper)
N_EMPTY_SUPER = s(:super)
end # Node
end # AST
end # Mutant

34
lib/mutant/ast/sexp.rb Normal file
View file

@ -0,0 +1,34 @@
module Mutant
module AST
# Mixin for node sexp syntax
module Sexp
private
# Build node
#
# @param [Symbol] type
#
# @return [Parser::AST::Node]
#
# @api private
#
def s(type, *children)
Parser::AST::Node.new(type, children)
end
# Build a negated boolean node
#
# @param [Parser::AST::Node] node
#
# @return [Parser::AST::Node]
#
# @api private
#
def n_not(node)
s(:send, node, :!)
end
end # Sexp
end # AST
end # Mutant

48
lib/mutant/ast/types.rb Normal file
View file

@ -0,0 +1,48 @@
module Mutant
module AST
# Groups of node types
module Types
symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze }
ASSIGNABLE_VARIABLES = symbolset.(%w[ivasgn lvasgn cvasgn gvasgn])
INDEX_ASSIGN_OPERATOR = :[]=
# Set of nodes that cannot be on the LHS of an assignment
NOT_ASSIGNABLE = symbolset.(%w[int float str dstr class module self nil])
# Set of op-assign types
OP_ASSIGN = symbolset.(%w[or_asgn and_asgn op_asgn])
# Set of node types that are not valid when emitted standalone
NOT_STANDALONE = symbolset.(%w[splat restarg block_pass])
INDEX_OPERATORS = symbolset.(%w[[] []=])
UNARY_METHOD_OPERATORS = symbolset.(%w[~@ +@ -@ !])
# Operators ruby implementeds as methods
METHOD_OPERATORS = symbolset.(%w[
<=> === []= [] <= >= == !~ != =~ <<
>> ** * % / | ^ & < > + - ~@ +@ -@ !
])
BINARY_METHOD_OPERATORS = (
METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS)
).to_set.freeze
OPERATOR_METHODS = (
METHOD_OPERATORS + INDEX_OPERATORS + UNARY_METHOD_OPERATORS
).to_set.freeze
# Nodes that are NOT handled by mutant.
#
# not - 1.8 only, mutant does not support 1.8
#
BLACKLIST = symbolset.(%w[not])
# Nodes that are NOT generated by parser but used by mutant / unparser.
EXTRA = symbolset.(%w[empty])
# All node types mutant handles
ALL = ((Parser::Meta::NODE_TYPES + EXTRA) - BLACKLIST).to_set.freeze
end # Types
end # AST
end # Mutant

View file

@ -4,7 +4,7 @@ module Mutant
# Comandline parser
class CLI
include Adamantium::Flat, Equalizer.new(:config), NodeHelpers
include Adamantium::Flat, Equalizer.new(:config)
# Error raised when CLI argv is invalid
Error = Class.new(RuntimeError)
@ -186,7 +186,7 @@ module Mutant
@builder.add_subject_ignore(Expression.parse(pattern))
end
opts.on('--code CODE', 'Scope execution to subjects with CODE') do |code|
@builder.add_subject_selector(Morpher.compile(s(:eql, s(:attribute, :code), s(:static, code))))
@builder.add_subject_selector(:code, code)
end
end

View file

@ -3,7 +3,7 @@ module Mutant
# Scope context for mutation (Class or Module)
class Scope < self
include Adamantium::Flat, Concord::Public.new(:scope, :source_path)
extend NodeHelpers
extend AST::Sexp
NAMESPACE_DELIMITER = '::'.freeze

View file

@ -10,7 +10,7 @@ module Mutant
METHOD_NAME_PATTERN = Regexp.union(
/[A-Za-z_][A-Za-z\d_]*[!?=]?/,
*OPERATOR_METHODS.map(&:to_s)
*AST::Types::OPERATOR_METHODS.map(&:to_s)
).freeze
SCOPE_PATTERN = /#{SCOPE_NAME_PATTERN}(?:#{SCOPE_OPERATOR}#{SCOPE_NAME_PATTERN})*/.freeze

View file

@ -2,7 +2,7 @@ module Mutant
class Matcher
# Builder for complex matchers
class Builder
include NodeHelpers, Concord.new(:cache)
include Concord.new(:cache), AST::Sexp
# Initalize object
#
@ -40,8 +40,8 @@ module Mutant
#
# @api private
#
def add_subject_selector(selector)
@subject_selectors << selector
def add_subject_selector(attribute, value)
@subject_selectors << Morpher.compile(s(:eql, s(:attribute, attribute), s(:static, value)))
self
end

View file

@ -1,7 +1,6 @@
module Mutant
# Namespace for mutant metadata
module Meta
require 'mutant/meta/example'
require 'mutant/meta/example/dsl'

View file

@ -4,7 +4,7 @@ module Mutant
# Example DSL
class DSL
include NodeHelpers
include AST::Sexp
# Run DSL on block
#

View file

@ -5,11 +5,13 @@ module Mutant
# Abstract base class for node mutators
class Node < self
include AbstractType, NodeHelpers, Unparser::Constants
include AbstractType, Unparser::Constants
include AST::NamedChildren, AST::NodePredicates, AST::Sexp, AST::Nodes
# Define named child
# Helper to define a named child
#
# @param [Parser::AST::Node] node
#
# @param [Symbol] name
# @param [Fixnum] index
#
# @return [undefined]
@ -17,9 +19,7 @@ module Mutant
# @api private
#
def self.define_named_child(name, index)
define_method(name) do
children.at(index)
end
super
define_method("emit_#{name}_mutations") do |&block|
mutate_child(index, &block)
@ -29,43 +29,6 @@ module Mutant
emit_child_update(index, node)
end
end
private_class_method :define_named_child
# Define remaining children
#
# @param [Array<Symbol>] names
#
# @return [undefined]
#
# @api private
#
def self.define_remaining_children(names)
define_method(:remaining_children_with_index) do
children.each_with_index.drop(names.length)
end
define_method(:remaining_children_indices) do
children.each_index.drop(names.length)
end
define_method(:remaining_children) do
children.drop(names.length)
end
end
private_class_method :define_remaining_children
# Create name helpers
#
# @return [undefined]
#
# @api private
#
def self.children(*names)
names.each_with_index do |name, index|
define_named_child(name, index)
end
define_remaining_children(names)
end
private_class_method :children
private
@ -98,6 +61,15 @@ module Mutant
end
end
# Return ast meta description
#
# @return [AST::Meta]
#
def meta
AST::Meta.for(node)
end
memoize :meta
# Return children
#
# @return [Array<Parser::AST::Node>]
@ -246,7 +218,7 @@ module Mutant
# @api private
#
def asgn_left?
OP_ASSIGN.include?(parent_type) && parent.node.children.first.equal?(node)
AST::Types::OP_ASSIGN.include?(parent_type) && parent.node.children.first.equal?(node)
end
end # Node

View file

@ -22,7 +22,7 @@ module Mutant
emit_right_mutations
return if n_ivasgn?(left)
emit_left_mutations do |node|
!n_self?(node)
AST::Meta.for(node).assignment?
end
end

View file

@ -4,6 +4,7 @@ module Mutant
# Namespace for send mutators
class Send < self
include AST::Types
handle(:send)
@ -19,12 +20,6 @@ module Mutant
:== => [:eql?, :equal?]
)
INDEX_REFERENCE = :[]
INDEX_ASSIGN = :[]=
VARIABLE_ASSIGN = :'='
ASSIGNMENT_OPERATORS = [INDEX_ASSIGN, VARIABLE_ASSIGN].to_set.freeze
ATTRIBUTE_ASSIGNMENT = /\A[a-z\d_]+=\z/.freeze
private
# Perform dispatch
@ -35,7 +30,7 @@ module Mutant
#
def dispatch
emit_singletons
if selector.equal?(INDEX_ASSIGN)
if meta.index_assignment?
run(Index::Assign)
else
non_index_dispatch
@ -50,9 +45,9 @@ module Mutant
#
def non_index_dispatch
case
when binary_operator?
when meta.binary_method_operator?
run(Binary)
when attribute_assignment?
when meta.attribute_assignment?
run(AttributeAssignment)
else
normal_dispatch
@ -103,26 +98,6 @@ module Mutant
emit(receiver) if receiver && !NOT_ASSIGNABLE.include?(receiver.type)
end
# Test for binary operator
#
# @return [Boolean]
#
# @api private
#
def binary_operator?
arguments.one? && BINARY_METHOD_OPERATORS.include?(selector)
end
# Test for attribute assignment
#
# @return [Boolean]
#
# @api private
#
def attribute_assignment?
arguments.one? && ATTRIBUTE_ASSIGNMENT =~ selector
end
# Mutate arguments
#
# @return [undefined]
@ -173,40 +148,10 @@ module Mutant
KEYWORDS.include?(selector) ||
METHOD_OPERATORS.include?(selector) ||
OP_ASSIGN.include?(parent_type) ||
attribute_assignment?
meta.attribute_assignment?
)
end
# Test for assignment
#
# @return [Boolean]
#
# @api private
#
def assignment?
arguments.one? && (ASSIGNMENT_OPERATORS.include?(selector) || attribute_assignment?)
end
# Test if node is part of an mlhs
#
# @return [Boolean]
#
# @api private
#
def mlhs?
assignment? && !arguments?
end
# Test for empty arguments
#
# @return [Boolean]
#
# @api private
#
def arguments?
arguments.any?
end
end # Send
end # Node
end # Mutator

View file

@ -7,9 +7,6 @@ module Mutant
handle(:super)
Z_SUPER = NodeHelpers.s(:zsuper)
EMPTY_SUPER = NodeHelpers.s(:super)
private
# Emit mutations
@ -20,8 +17,8 @@ module Mutant
#
def dispatch
emit_singletons
emit(Z_SUPER)
emit(EMPTY_SUPER)
emit(N_ZSUPER)
emit(N_EMPTY_SUPER)
children.each_index do |index|
mutate_child(index)
delete_child(index)

View file

@ -64,7 +64,7 @@ module Mutant
# @api private
#
def self.assert_valid_type(type)
unless NODE_TYPES.include?(type) || type.kind_of?(Class)
unless AST::Types::ALL.include?(type) || type.kind_of?(Class)
raise InvalidTypeError, "invalid type registration: #{type}"
end
end

View file

@ -1,52 +0,0 @@
module Mutant
# Mixin for node helpers
module NodeHelpers
# Build node
#
# @param [Symbol] type
#
# @return [Parser::AST::Node]
#
# @api private
#
def s(type, *children)
Parser::AST::Node.new(type, children)
end
module_function :s
N_NAN = s(:send, s(:float, 0.0), :/, s(:float, 0.0))
N_INFINITY = s(:send, s(:float, 1.0), :/, s(:float, 0.0))
N_NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:float, 0.0))
N_RAISE = s(:send, nil, :raise)
N_TRUE = s(:true)
N_FALSE = s(:false)
N_NIL = s(:nil)
N_EMPTY = s(:empty)
N_SELF = s(:self)
# Build a negated boolean node
#
# @param [Parser::AST::Node] node
#
# @return [Parser::AST::Node]
#
# @api private
#
def n_not(node)
s(:send, node, :!)
end
NODE_TYPES.each do |type|
fail "method: #{type} is already defined" if instance_methods(true).include?(type)
name = "n_#{type.to_s.sub(/\??\z/, '?')}"
define_method(name) do |node|
node.type.equal?(type)
end
private name
end
end # NodeHelpers
end # Mutant

View file

@ -39,7 +39,7 @@ module Mutant
# Mutator for memoized instance methods
class Memoized < self
include NodeHelpers
include AST::Sexp
# Return source
#

View file

@ -2,7 +2,7 @@ module Mutant
class Zombifier
# File containing source beeing zombified
class File
include NodeHelpers, Adamantium::Flat, Concord::Public.new(:path)
include Adamantium::Flat, Concord::Public.new(:path), AST::Sexp
# Zombify contents of file
#

View file

@ -22,3 +22,14 @@ Mutant::Meta::Example.add do
mutation '@a ||= -1'
mutation '@a ||= 2'
end
Mutant::Meta::Example.add do
source 'foo[:bar] ||= 1'
singleton_mutations
mutation 'foo[:bar] ||= nil'
mutation 'foo[:bar] ||= self'
mutation 'foo[:bar] ||= 0'
mutation 'foo[:bar] ||= -1'
mutation 'foo[:bar] ||= 2'
end

View file

@ -259,7 +259,7 @@ Mutant::Meta::Example.add do
mutation 'self[*bar]'
end
(Mutant::BINARY_METHOD_OPERATORS - [:==, :eql?]).each do |operator|
(Mutant::AST::Types::BINARY_METHOD_OPERATORS - [:==, :eql?]).each do |operator|
Mutant::Meta::Example.add do
source "true #{operator} false"

View file

@ -3,8 +3,8 @@ require 'spec_helper'
describe do
specify 'mutant should not crash for any node parser can generate' do
Mutant::NODE_TYPES.each do |type|
Mutant::Mutator::Registry.lookup(Mutant::NodeHelpers.s(type))
Mutant::AST::Types::ALL.each do |type|
Mutant::Mutator::Registry.lookup(s(type))
end
end
end

View file

@ -46,7 +46,8 @@ end
RSpec.configure do |config|
config.include(CompressHelper)
config.include(ParserHelper)
config.include(Mutant::NodeHelpers)
config.include(Mutant::AST::Sexp)
config.expect_with :rspec do |rspec|
rspec.syntax = :expect
end

View file

@ -6,7 +6,7 @@ describe Mutant::Mutation do
SYMBOL = 'test'.freeze
end
let(:object) { TestMutation.new(mutation_subject, Mutant::NodeHelpers::N_NIL) }
let(:object) { TestMutation.new(mutation_subject, Mutant::AST::Nodes::N_NIL) }
let(:mutation_subject) { double('Subject', identification: 'subject', source: 'original') }
let(:node) { double('Node') }

View file

@ -3,8 +3,6 @@
require 'spec_helper'
describe Mutant::Subject::Method::Instance do
include Mutant::NodeHelpers
let(:object) { described_class.new(context, node) }
let(:context) { double }
@ -73,8 +71,6 @@ describe Mutant::Subject::Method::Instance do
end
describe Mutant::Subject::Method::Instance::Memoized do
include Mutant::NodeHelpers
let(:object) { described_class.new(context, node) }
let(:context) { double }

View file

@ -3,7 +3,6 @@
require 'spec_helper'
describe Mutant::Subject::Method::Singleton do
include Mutant::NodeHelpers
let(:object) { described_class.new(context, node) }
let(:context) { double }