Port method extraction to parser based ast

This commit is contained in:
Markus Schirp 2013-06-04 19:22:33 +02:00
parent 6747e636cd
commit fca5b8a168
16 changed files with 120 additions and 53 deletions

View file

@ -4,7 +4,7 @@ guard :bundler do
watch('Gemfile') watch('Gemfile')
end end
guard :rspec, :all_on_start => false, :all_after_pass => false do guard :rspec, :cli => '--fail-fast', :all_on_start => false, :all_after_pass => false do
# run all specs if the spec_helper or supporting files files are modified # run all specs if the spec_helper or supporting files files are modified
watch('spec/spec_helper.rb') { 'spec/unit' } watch('spec/spec_helper.rb') { 'spec/unit' }
watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec/unit' } watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec/unit' }

View file

@ -8,6 +8,7 @@ require 'equalizer'
require 'digest/sha1' require 'digest/sha1'
require 'inflecto' require 'inflecto'
require 'parser' require 'parser'
require 'parser/current'
require 'unparser' require 'unparser'
require 'ice_nine' require 'ice_nine'
require 'diff/lcs' require 'diff/lcs'
@ -20,6 +21,7 @@ require 'concord'
module Mutant module Mutant
end end
require 'mutant/node_helpers'
require 'mutant/singleton_methods' require 'mutant/singleton_methods'
require 'mutant/constants' require 'mutant/constants'
require 'mutant/support/method_object' require 'mutant/support/method_object'

View file

@ -3,6 +3,7 @@ module Mutant
# Scope context for mutation (Class or Module) # Scope context for mutation (Class or Module)
class Scope < self class Scope < self
include Adamantium::Flat, Equalizer.new(:scope, :source_path) include Adamantium::Flat, Equalizer.new(:scope, :source_path)
extend NodeHelpers
# Return AST wrapping mutated node # Return AST wrapping mutated node
# #
@ -40,12 +41,12 @@ module Mutant
# @api private # @api private
# #
def self.wrap(scope, node) def self.wrap(scope, node)
name = scope.name.split('::').last.to_sym name = s(:const, nil, scope.name.split('::').last.to_sym)
case scope case scope
when ::Class when ::Class
::Rubinius::AST::Class.new(0, name, nil, node) s(:class, name, nil, node)
when ::Module when ::Module
::Rubinius::AST::Module.new(0, name, node) s(:module, name, node)
else else
raise "Cannot wrap scope: #{scope.inspect}" raise "Cannot wrap scope: #{scope.inspect}"
end end

View file

@ -50,7 +50,7 @@ module Mutant
# @api private # @api private
# #
def source def source
ToSource.to_source(@root) Unparser.unparse(@root)
end end
end # Eval end # Eval

View file

@ -78,7 +78,7 @@ module Mutant
# @api private # @api private
# #
def ast def ast
File.read(source_path).to_ast Parser::CurrentRuby.parse(File.read(source_path))
end end
# Return path to source # Return path to source
@ -128,19 +128,47 @@ module Mutant
end end
memoize :subject memoize :subject
class Finder
def self.run(root, &predicate)
new(root, predicate).match
end
private_class_method :new
attr_reader :match
private
def initialize(root, predicate)
@root, @predicate = root, predicate
test(root)
end
def test(node)
if @predicate.call(node)
@match = node
end
node.children.each do |child|
test(child) if child.kind_of?(Parser::AST::Node)
end
end
end
# Return matched node # Return matched node
# #
# @return [Rubinus::AST::Node] # @return [Parser::AST::Node]
# if node could be found
#
# @return [nil]
# otherwise
# #
# @api private # @api private
# #
def matched_node def matched_node
last_match = nil Finder.run(ast) do |node|
ast.walk do |predicate, node| match?(node)
last_match = node if match?(node)
predicate
end end
last_match
end end
end # Method end # Method

View file

@ -16,6 +16,8 @@ module Mutant
end end
memoize :identification memoize :identification
NAME_INDEX = 0
private private
# Check if node is matched # Check if node is matched
@ -31,9 +33,11 @@ module Mutant
# @api private # @api private
# #
def match?(node) def match?(node)
node.line == source_line && location = node.location || return
node.class == Rubinius::AST::Define && expression = location.expression || return
node.name == method_name expression.line == source_line &&
node.type == :def &&
node.children[NAME_INDEX] == method_name
end end
end # Instance end # Instance

View file

@ -16,6 +16,10 @@ module Mutant
end end
memoize :identification memoize :identification
RECEIVER_INDEX = 0
NAME_INDEX = 1
CONST_NAME_INDEX = 1
private private
# Test for node match # Test for node match
@ -31,7 +35,6 @@ module Mutant
# @api private # @api private
# #
def match?(node) def match?(node)
node.class == Rubinius::AST::DefineSingleton &&
line?(node) && line?(node) &&
name?(node) && name?(node) &&
receiver?(node) receiver?(node)
@ -50,7 +53,8 @@ module Mutant
# @api private # @api private
# #
def line?(node) def line?(node)
node.line == source_line expression = node.location.expression || return
expression.line == source_line
end end
# Test for name match # Test for name match
@ -66,7 +70,7 @@ module Mutant
# @api private # @api private
# #
def name?(node) def name?(node)
node.body.name == method_name node.children[NAME_INDEX] == method_name
end end
# Test for receiver match # Test for receiver match
@ -82,11 +86,11 @@ module Mutant
# @api private # @api private
# #
def receiver?(node) def receiver?(node)
receiver = node.receiver receiver = node.children[RECEIVER_INDEX]
case receiver case receiver.type
when Rubinius::AST::Self when :self
true true
when Rubinius::AST::ConstantAccess when :const
receiver_name?(receiver) receiver_name?(receiver)
else else
$stderr.puts "Unable to find singleton method definition can only match receiver on Rubinius::AST::Self or Rubinius::AST::ConstantAccess, got #{receiver.class}" $stderr.puts "Unable to find singleton method definition can only match receiver on Rubinius::AST::Self or Rubinius::AST::ConstantAccess, got #{receiver.class}"
@ -107,7 +111,8 @@ module Mutant
# @api private # @api private
# #
def receiver_name?(node) def receiver_name?(node)
node.name.to_s == context.unqualified_name name = node.children[CONST_NAME_INDEX]
name.to_s == context.unqualified_name
end end
end # Singleton end # Singleton

View file

@ -0,0 +1,18 @@
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
end # NodeHelpers
end # Mutant

View file

@ -18,18 +18,18 @@ shared_examples_for 'a method matcher' do
end end
it 'should have correct line number' do it 'should have correct line number' do
(node.line - base).should eql(method_line) (node.location.expression.line - base).should eql(method_line)
end end
it 'should have correct arity' do it 'should have correct arity' do
arguments.required.length.should eql(method_arity) arguments.children.length.should eql(method_arity)
end end
it 'should have correct scope in context' do it 'should have correct scope in context' do
context.send(:scope).should eql(scope) context.send(:scope).should eql(scope)
end end
it 'should have the correct node class' do it 'should have the correct node type' do
node.should be_a(node_class) node.type.should be(type)
end end
end end

View file

@ -5,7 +5,7 @@ shared_examples_for 'a mutator' do
let(:object) { described_class } let(:object) { described_class }
unless instance_methods.map(&:to_s).include?('node') unless instance_methods.map(&:to_s).include?('node')
let(:node) { source.to_ast } let(:node) { parse(source) }
end end
it_should_behave_like 'a command method' it_should_behave_like 'a command method'

View file

@ -8,6 +8,17 @@ $: << File.join(TestApp.root,'lib')
require 'test_app' require 'test_app'
require 'mutant' require 'mutant'
module ParserHelper
def generate(node)
Unparser.unparse(node)
end
def parse(string)
Parser::CurrentRuby.parse(string)
end
end
RSpec.configure do |config| RSpec.configure do |config|
config.include(CompressHelper) config.include(CompressHelper)
config.include(ParserHelper)
end end

View file

@ -5,13 +5,13 @@ describe Mutant::Context::Scope, '#root' do
let(:object) { described_class.new(TestApp::Literal, path) } let(:object) { described_class.new(TestApp::Literal, path) }
let(:path) { mock('Path') } let(:path) { mock('Path') }
let(:node) { ':node'.to_ast } let(:node) { parse(':node') }
let(:scope) { subject.body } let(:scope) { subject.body }
let(:scope_body) { scope.body } let(:scope_body) { scope.body }
let(:expected_source) do let(:expected_source) do
ToSource.to_source(<<-RUBY.to_ast) generate(parse(<<-RUBY))
module TestApp module TestApp
class Literal class Literal
:node :node
@ -21,11 +21,11 @@ describe Mutant::Context::Scope, '#root' do
end end
let(:generated_source) do let(:generated_source) do
ToSource.to_source(subject) Unparser.unparse(subject)
end end
let(:round_tripped_source) do let(:round_tripped_source) do
ToSource.to_source(expected_source.to_ast) Unparser.unparse(parse(expected_source))
end end
it 'should create correct source' do it 'should create correct source' do

View file

@ -26,7 +26,7 @@ describe Mutant::Loader::Eval, '.run' do
end end
let(:node) do let(:node) do
source.to_ast parse(source)
end end
it 'should load nodes into vm' do it 'should load nodes into vm' do
@ -36,6 +36,6 @@ describe Mutant::Loader::Eval, '.run' do
it 'should set file and line correctly' do it 'should set file and line correctly' do
subject subject
::SomeNamespace::Bar.instance_method(:some_method).source_location.should eql(['test.rb', 3]) ::SomeNamespace::Bar.instance_method(:some_method).source_location.should eql(['test.rb', 4])
end end
end end

View file

@ -14,16 +14,16 @@ describe Mutant::Matcher::Method::Instance, '#each' do
subject { object.each { |subject| yields << subject } } subject { object.each { |subject| yields << subject } }
let(:node_class) { Rubinius::AST::Define } let(:type) { :def }
let(:method_name) { :bar } let(:method_name) { :bar }
let(:method_arity) { 0 } let(:method_arity) { 0 }
def name def name
node.name node.children[0]
end end
def arguments def arguments
node.arguments node.children[1]
end end
context 'when method is defined once' do context 'when method is defined once' do

View file

@ -14,15 +14,15 @@ describe Mutant::Matcher::Method::Singleton, '#each' do
subject { object.each { |subject| yields << subject } } subject { object.each { |subject| yields << subject } }
let(:node_class) { Rubinius::AST::DefineSingleton } let(:type) { :defs }
let(:method_arity) { 0 } let(:method_arity) { 0 }
def name def name
node.body.name node.children[1]
end end
def arguments def arguments
node.body.arguments node.children[2]
end end
context 'on singleton methods' do context 'on singleton methods' do

View file

@ -9,22 +9,20 @@ describe Mutant::Mutator, 'block' do
mutations = [] mutations = []
# Mutation of each statement in block # Mutation of each statement in block
mutations << "foo\nself.bar".to_ast mutations << "foo\nself.bar"
mutations << "self.foo\nbar".to_ast mutations << "self.foo\nbar"
# Remove statement in block # Remove statement in block
mutations << Rubinius::AST::Block.new(1, ['self.foo'.to_ast]) mutations << 'self.foo'
mutations << Rubinius::AST::Block.new(1, ['self.bar'.to_ast]) mutations << 'self.bar'
mutations << Rubinius::AST::Block.new(1, ['nil'.to_ast]) mutations << 'nil'
end end
it_should_behave_like 'a mutator' it_should_behave_like 'a mutator'
end end
context 'with one statement' do context 'with one statement' do
let(:node) { Rubinius::AST::Block.new(1, ['self.foo'.to_ast]) } let(:node) { 'self.foo' }
let(:mutations) do let(:mutations) do
mutations = [] mutations = []