Progress on method matching
* Adjust metrics * Add initial integration spec on method matching * Yard and Heckle coverage is at 100% (heckle cov is disputable) * Rcov does not really make sense as MRI 1.8 cannot reach all code paths.
This commit is contained in:
parent
d74481b8fb
commit
dc893bfd7d
21 changed files with 446 additions and 105 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/.rbx
|
||||
/Gemfile.lock
|
||||
/tmp
|
||||
/coverage
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
---
|
||||
threshold: 9
|
||||
total_score: 41
|
||||
threshold: 8
|
||||
total_score: 50
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
---
|
||||
threshold: 12.4
|
||||
threshold: 13.7
|
||||
|
|
|
@ -11,7 +11,7 @@ ClassVariableCheck: {}
|
|||
CyclomaticComplexityBlockCheck:
|
||||
complexity: 2
|
||||
CyclomaticComplexityMethodCheck:
|
||||
complexity: 2
|
||||
complexity: 3
|
||||
EmptyRescueBodyCheck: {}
|
||||
ForLoopCheck: {}
|
||||
MethodLineCountCheck:
|
||||
|
|
|
@ -8,10 +8,10 @@ UncommunicativeParameterName:
|
|||
- !ruby/regexp /[0-9]$/
|
||||
- !ruby/regexp /[A-Z]/
|
||||
LargeClass:
|
||||
max_methods: 10
|
||||
max_methods: 11
|
||||
exclude: []
|
||||
enabled: true
|
||||
max_instance_variables: 3
|
||||
max_instance_variables: 2
|
||||
UncommunicativeMethodName:
|
||||
accept: []
|
||||
exclude: []
|
||||
|
|
|
@ -23,11 +23,30 @@ module Mutant
|
|||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def self.not_implemented(object)
|
||||
method = caller(1).first[/`(.*)'/,1].to_sym
|
||||
delimiter = object.kind_of?(Module) ? '.' : '#'
|
||||
raise NotImplementedError,"#{object.class}#{delimiter}#{method} is not implemented"
|
||||
constant_name,delimiter = not_implemented_info(object)
|
||||
raise NotImplementedError,"#{constant_name}#{delimiter}#{method} is not implemented"
|
||||
end
|
||||
|
||||
# Return name and delimiter
|
||||
#
|
||||
# @param [Object] object
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def self.not_implemented_info(object)
|
||||
if object.kind_of?(Module)
|
||||
[object.name,'.']
|
||||
else
|
||||
[object.class.name,'#']
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method :not_implemented_info
|
||||
end
|
||||
|
||||
require 'mutant/matcher'
|
||||
|
|
|
@ -7,6 +7,8 @@ module Mutant
|
|||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
def each
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
|
|
@ -2,35 +2,6 @@ module Mutant
|
|||
class Matcher
|
||||
# A filter for methods
|
||||
class Method < Matcher
|
||||
# Return constant name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :constant_name
|
||||
|
||||
# Return method name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :method_name
|
||||
|
||||
# Initialize method filter
|
||||
#
|
||||
# @param [String] constant_name
|
||||
# @param [String] method_name
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(constant_name, method_name)
|
||||
@constant_name, @method_name = constant_name, method_name
|
||||
end
|
||||
|
||||
# Parse a method string into filter
|
||||
#
|
||||
# @param [String] input
|
||||
|
@ -53,15 +24,69 @@ module Mutant
|
|||
#
|
||||
# @api private
|
||||
#
|
||||
def each
|
||||
def each(&block)
|
||||
return to_enum(__method__) unless block_given?
|
||||
yield root_node
|
||||
node = root_node
|
||||
yield node if node
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return constant name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :constant_name
|
||||
private :constant_name
|
||||
|
||||
# Return method name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
attr_reader :method_name
|
||||
private :method_name
|
||||
|
||||
# Initialize method filter
|
||||
#
|
||||
# @param [String] constant_name
|
||||
# @param [Symbol] method_name
|
||||
#
|
||||
# @return [undefined]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(constant_name, method_name)
|
||||
@constant_name, @method_name = constant_name, method_name
|
||||
end
|
||||
|
||||
# Return method
|
||||
#
|
||||
# @return [UnboundMethod]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
# Return node classes this matcher matches
|
||||
#
|
||||
# @return [Rubinius::AST::Node]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def node_class
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
# Check if node is matched
|
||||
#
|
||||
# @param [Rubinius::AST::Node]
|
||||
# @param [Rubinius::AST::Node] node
|
||||
#
|
||||
# @return [true]
|
||||
# returns true if node matches method
|
||||
|
@ -72,44 +97,9 @@ module Mutant
|
|||
# @api private
|
||||
#
|
||||
def match?(node)
|
||||
node.line == source_file_line && node_class == node.class && node.name.to_s == method_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return method
|
||||
#
|
||||
# @return [UnboundMethod]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method
|
||||
Mutant.not_implemente(self)
|
||||
end
|
||||
|
||||
# Return node classes this matcher matches
|
||||
#
|
||||
# @return [Enumerable]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def node_classes
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
# Return root node
|
||||
#
|
||||
# @return [Rubinus::AST::Node]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def root_node
|
||||
root_node = nil
|
||||
ast.walk do |_, node|
|
||||
root_node = node if match?(node)
|
||||
true
|
||||
end
|
||||
root_node
|
||||
node.line == source_file_line &&
|
||||
node.class == node_class &&
|
||||
node.name == method_name
|
||||
end
|
||||
|
||||
# Return full ast
|
||||
|
@ -152,6 +142,21 @@ module Mutant
|
|||
method.source_location
|
||||
end
|
||||
|
||||
# Return root node
|
||||
#
|
||||
# @return [Rubinus::AST]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def root_node
|
||||
root_node = nil
|
||||
ast.walk do |predicate, node|
|
||||
root_node = node if match?(node)
|
||||
true
|
||||
end
|
||||
root_node
|
||||
end
|
||||
|
||||
# Return constant
|
||||
#
|
||||
# @return [Class|Module]
|
||||
|
|
|
@ -3,6 +3,8 @@ module Mutant
|
|||
class Method < Matcher
|
||||
# A classifier for input strings
|
||||
class Classifier
|
||||
extend Veritas::Immutable
|
||||
|
||||
TABLE = {
|
||||
'.' => Matcher::Method::Singleton,
|
||||
'#' => Matcher::Method::Instance
|
||||
|
@ -12,35 +14,81 @@ module Mutant
|
|||
|
||||
private_class_method :new
|
||||
|
||||
# Run classifier
|
||||
#
|
||||
# @param [String] input
|
||||
#
|
||||
# @return [Matcher::Method]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def self.run(input)
|
||||
match = SCOPE_FORMAT.match(input)
|
||||
raise ArgumentError, "Cannot determine subject from #{input.inspect}" unless match
|
||||
new(match).matcher
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
# Return method matcher
|
||||
#
|
||||
# @return [Matcher::Method]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def matcher
|
||||
scope.new(constant_name, method_name)
|
||||
matcher_class.new(constant_name, method_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Initialize matcher
|
||||
#
|
||||
# @param [MatchData] match
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def initialize(match)
|
||||
@match = match
|
||||
end
|
||||
|
||||
# Return constant name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def constant_name
|
||||
@match[1]
|
||||
end
|
||||
|
||||
# Return method name
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method_name
|
||||
@match[3]
|
||||
@match[3].to_sym
|
||||
end
|
||||
|
||||
# Return scope symbol
|
||||
#
|
||||
# @return [Symbol]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def scope_symbol
|
||||
@match[2]
|
||||
end
|
||||
|
||||
def scope
|
||||
# Return matcher class
|
||||
#
|
||||
# @return [Class<Matcher>]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def matcher_class
|
||||
TABLE.fetch(scope_symbol)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,22 @@ module Mutant
|
|||
# A instance method filter
|
||||
class Instance < Method
|
||||
private
|
||||
# Return method instance
|
||||
#
|
||||
# @return [UnboundMethod]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method
|
||||
constant.instance_method(method_name)
|
||||
end
|
||||
|
||||
# Return matched node class
|
||||
#
|
||||
# @return [Rubinius::AST::Define]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def node_class
|
||||
Rubinius::AST::Define
|
||||
end
|
||||
|
|
|
@ -4,12 +4,24 @@ module Mutant
|
|||
# A singleton method filter
|
||||
class Singleton < Method
|
||||
private
|
||||
# Return method instance
|
||||
#
|
||||
# @return [UnboundMethod]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def method
|
||||
constant.method(method_name)
|
||||
end
|
||||
|
||||
# Return matched node class
|
||||
#
|
||||
# @return [Rubinius::AST::Define]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def node_class
|
||||
Rubinius::AST::DefineSingleton
|
||||
Rubinius::AST::DefineSingletonScope
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
82
spec/integration/method_matching_spec.rb
Normal file
82
spec/integration/method_matching_spec.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
require 'spec_helper'
|
||||
if defined?(Rubinius)
|
||||
class Toplevel
|
||||
def simple
|
||||
end
|
||||
|
||||
def self.simple
|
||||
end
|
||||
|
||||
def multiple; end;
|
||||
def multiple(foo); end
|
||||
|
||||
def self.complex; end; def complex(foo); end
|
||||
|
||||
class Nested
|
||||
def foo
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :foo
|
||||
|
||||
def multiline(
|
||||
foo
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe Mutant,'method matching' do
|
||||
def match(input)
|
||||
Mutant::Matcher::Method.parse(input).to_a.first
|
||||
end
|
||||
|
||||
it 'allows to match simple instance methods' do
|
||||
match = match('Toplevel#simple')
|
||||
match.name.should be(:simple)
|
||||
match.line.should be(4)
|
||||
match.arguments.required.should be_empty
|
||||
end
|
||||
|
||||
it 'allows to match simple singleton methods' do
|
||||
match = match('Toplevel.simple')
|
||||
match.name.should be(:simple)
|
||||
match.line.should be(7)
|
||||
match.arguments.required.should be_empty
|
||||
end
|
||||
|
||||
it 'returns last method definition' do
|
||||
match = match('Toplevel#multiple')
|
||||
match.name.should be(:multiple)
|
||||
match.line.should be(11)
|
||||
match.arguments.required.length.should be(1)
|
||||
end
|
||||
|
||||
it 'does not fail on multiple definitions of differend scope per row' do
|
||||
match = match('Toplevel.complex')
|
||||
match.name.should be(:complex)
|
||||
match.line.should be(13)
|
||||
match.arguments.required.length.should be(0)
|
||||
end
|
||||
|
||||
it 'allows matching on nested methods' do
|
||||
match = match('Toplevel::Nested#foo')
|
||||
match.name.should be(:foo)
|
||||
match.line.should be(16)
|
||||
match.arguments.required.length.should be(0)
|
||||
end
|
||||
|
||||
# pending 'allows matching on attr_readers' do
|
||||
# match = match('Toplevel#foo')
|
||||
# match.name.should be(:foo)
|
||||
# match.line.should be(19)
|
||||
# match.arguments.required.length.should be(0)
|
||||
# end
|
||||
|
||||
it 'does not fail on multi line defs' do
|
||||
match = match('Toplevel#multiline')
|
||||
match.name.should be(:multiline)
|
||||
match.line.should be(23)
|
||||
match.arguments.required.length.should be(1)
|
||||
end
|
||||
end
|
||||
end
|
35
spec/unit/mutant/class_methods/not_implemented_spec.rb
Normal file
35
spec/unit/mutant/class_methods/not_implemented_spec.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mutant,'.not_implemented' do
|
||||
let(:object) { described_class.new }
|
||||
|
||||
let(:described_class) do
|
||||
Class.new do
|
||||
def foo
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
def self.foo
|
||||
Mutant.not_implemented(self)
|
||||
end
|
||||
|
||||
def self.name
|
||||
'Test'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on instance method' do
|
||||
subject { object.foo }
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(NotImplementedError,'Test#foo is not implemented')
|
||||
end
|
||||
end
|
||||
|
||||
context 'on singleton method' do
|
||||
subject { described_class.foo }
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(NotImplementedError,'Test.foo is not implemented')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,34 +1,21 @@
|
|||
require 'spec_helper'
|
||||
|
||||
shared_examples_for 'a method filter parse result' do
|
||||
it { should be(response) }
|
||||
|
||||
it 'should initialize method filter with correct arguments' do
|
||||
expected_class.should_receive(:new).with('Foo', 'bar').and_return(response)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
describe Mutant::Matcher::Method, '.parse' do
|
||||
subject { described_class.parse(input) }
|
||||
|
||||
before do
|
||||
expected_class.stub(:new => response)
|
||||
end
|
||||
|
||||
let(:response) { mock('Response') }
|
||||
let(:input) { mock('Input') }
|
||||
|
||||
context 'when input is in instance method format' do
|
||||
let(:input) { 'Foo#bar' }
|
||||
let(:expected_class) { described_class::Instance }
|
||||
let(:classifier) { described_class::Classifier }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
before do
|
||||
classifier.stub(:run => response)
|
||||
end
|
||||
|
||||
context 'when input is in singleton method format' do
|
||||
let(:input) { 'Foo.bar' }
|
||||
let(:expected_class) { described_class::Singleton }
|
||||
it { should be(response) }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
it 'should call classifier' do
|
||||
classifier.should_receive(:run).with(input).and_return(response)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
require 'spec_helper'
|
||||
|
||||
shared_examples_for 'a method filter parse result' do
|
||||
it { should be(response) }
|
||||
|
||||
it 'should initialize method filter with correct arguments' do
|
||||
expected_class.should_receive(:new).with('Foo', :bar).and_return(response)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
describe Mutant::Matcher::Method::Classifier, '.run' do
|
||||
subject { described_class.run(input) }
|
||||
|
||||
context 'with format' do
|
||||
before do
|
||||
expected_class.stub(:new => response)
|
||||
end
|
||||
|
||||
let(:response) { mock('Response') }
|
||||
|
||||
context 'in instance method notation' do
|
||||
let(:input) { 'Foo#bar' }
|
||||
let(:expected_class) { Mutant::Matcher::Method::Instance }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
end
|
||||
|
||||
context 'when input is in singleton method notation' do
|
||||
let(:input) { 'Foo.bar' }
|
||||
let(:expected_class) { Mutant::Matcher::Method::Singleton }
|
||||
|
||||
it_should_behave_like 'a method filter parse result'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when input is not in a valid format' do
|
||||
let(:input) { 'Foo' }
|
||||
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(ArgumentError,"Cannot determine subject from #{input.inspect}")
|
||||
end
|
||||
end
|
||||
end
|
29
spec/unit/mutant/matcher/method/classifier/matcher_spec.rb
Normal file
29
spec/unit/mutant/matcher/method/classifier/matcher_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# This method cannot be called directly, spec only exists for heckle demands
|
||||
describe Mutant::Matcher::Method::Classifier,'#matcher' do
|
||||
subject { object.matcher }
|
||||
|
||||
let(:object) { described_class.send(:new,match) }
|
||||
|
||||
let(:match) { [mock,constant_name,scope_symbol,method_name] }
|
||||
|
||||
let(:constant_name) { mock('Constant Name') }
|
||||
let(:method_name) { 'foo' }
|
||||
|
||||
context 'with "#" as scope symbol' do
|
||||
let(:scope_symbol) { '#' }
|
||||
|
||||
it { should be_a(Mutant::Matcher::Method::Instance) }
|
||||
its(:method_name) { should be(method_name.to_sym) }
|
||||
its(:constant_name) { should be(constant_name) }
|
||||
end
|
||||
|
||||
context 'with "." as scope symbol' do
|
||||
let(:scope_symbol) { '.' }
|
||||
|
||||
it { should be_a(Mutant::Matcher::Method::Singleton) }
|
||||
its(:method_name) { should be(method_name.to_sym) }
|
||||
its(:constant_name) { should be(constant_name) }
|
||||
end
|
||||
end
|
39
spec/unit/mutant/matcher/method/each_spec.rb
Normal file
39
spec/unit/mutant/matcher/method/each_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# This method implementation cannot be called from the outside, but heckle needs to be happy.
|
||||
|
||||
describe Mutant::Matcher::Method,'#each' do
|
||||
let(:class_under_test) do
|
||||
node = self.root_node
|
||||
Class.new(described_class) do
|
||||
define_method(:root_node) do
|
||||
node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
subject { object.each { |item| yields << item } }
|
||||
|
||||
let(:object) { class_under_test.allocate }
|
||||
let(:yields) { [] }
|
||||
|
||||
it_should_behave_like 'an #each method'
|
||||
|
||||
let(:root_node) { mock('Root Node') }
|
||||
|
||||
context 'with match' do
|
||||
it 'should yield root node' do
|
||||
expect { subject }.to change { yields.dup }.from([]).to([root_node])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without match' do
|
||||
let(:root_node) { nil }
|
||||
|
||||
it 'should yield nothing' do
|
||||
subject
|
||||
yields.should eql([])
|
||||
end
|
||||
end
|
||||
end
|
11
spec/unit/mutant/matcher/method/method_spec.rb
Normal file
11
spec/unit/mutant/matcher/method/method_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mutant::Matcher::Method,'#method' do
|
||||
subject { object.send(:method) }
|
||||
|
||||
let(:object) { described_class.allocate }
|
||||
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(NotImplementedError,'Mutant::Matcher::Method#method is not implemented')
|
||||
end
|
||||
end
|
12
spec/unit/mutant/matcher/method/node_class_spec.rb
Normal file
12
spec/unit/mutant/matcher/method/node_class_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mutant::Matcher::Method,'#node_class' do
|
||||
subject { object.send(:node_class) }
|
||||
|
||||
let(:object) { described_class.allocate }
|
||||
|
||||
it 'should raise error' do
|
||||
expect { subject }.to raise_error(NotImplementedError,'Mutant::Matcher::Method#node_class is not implemented')
|
||||
end
|
||||
end
|
||||
|
|
@ -29,12 +29,12 @@ begin
|
|||
end
|
||||
|
||||
desc 'Heckle each module and class'
|
||||
task :heckle => :rcov do
|
||||
task :heckle do
|
||||
unless Ruby2Ruby::VERSION == '1.2.2'
|
||||
raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
|
||||
end
|
||||
|
||||
require 'veritas-mongo-adapter'
|
||||
require 'mutant'
|
||||
|
||||
root_module_regexp = Regexp.union('Mutant')
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
begin
|
||||
# Require veritas before metric foo pulls AS
|
||||
require 'veritas'
|
||||
require 'metric_fu'
|
||||
require 'json'
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue