From a3fc233d959fbe0cc1a4f750c3f9a13349ccef55 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 21 Jan 2013 20:08:30 +0100 Subject: [PATCH] Reorganize classifiers * Classifiers are matcher subclass and delegate to lazy builded matcher interfaces * Solves chicken egg problem between target library load and classifier matcher instantiation --- lib/mutant.rb | 8 +- lib/mutant/cli.rb | 35 ++--- lib/mutant/cli/classifier.rb | 129 ++++++++++++++++++ .../classifier/method.rb} | 87 ++++-------- lib/mutant/cli/classifier/namespace.rb | 34 +++++ lib/mutant/matcher.rb | 42 +----- lib/mutant/matcher/method.rb | 12 -- .../matcher/{scope_methods.rb => methods.rb} | 4 +- .../matcher/{object_space.rb => namespace.rb} | 44 +++--- .../mutant/method_matching_spec.rb | 2 +- 10 files changed, 242 insertions(+), 155 deletions(-) create mode 100644 lib/mutant/cli/classifier.rb rename lib/mutant/{matcher/method/classifier.rb => cli/classifier/method.rb} (53%) create mode 100644 lib/mutant/cli/classifier/namespace.rb rename lib/mutant/matcher/{scope_methods.rb => methods.rb} (96%) rename lib/mutant/matcher/{object_space.rb => namespace.rb} (65%) diff --git a/lib/mutant.rb b/lib/mutant.rb index b13dbe72..8399cf94 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -89,12 +89,11 @@ require 'mutant/subject' require 'mutant/subject/method' require 'mutant/matcher' require 'mutant/matcher/chain' -require 'mutant/matcher/object_space' require 'mutant/matcher/method' require 'mutant/matcher/method/singleton' require 'mutant/matcher/method/instance' -require 'mutant/matcher/scope_methods' -require 'mutant/matcher/method/classifier' +require 'mutant/matcher/methods' +require 'mutant/matcher/namespace' require 'mutant/killer' require 'mutant/killer/static' require 'mutant/killer/rspec' @@ -109,6 +108,9 @@ require 'mutant/strategy/rspec/dm2/lookup' require 'mutant/strategy/rspec/dm2/lookup/method' require 'mutant/runner' require 'mutant/cli' +require 'mutant/cli/classifier' +require 'mutant/cli/classifier/namespace' +require 'mutant/cli/classifier/method' require 'mutant/color' require 'mutant/differ' require 'mutant/reporter' diff --git a/lib/mutant/cli.rb b/lib/mutant/cli.rb index ff232a3f..9b5dcdb2 100644 --- a/lib/mutant/cli.rb +++ b/lib/mutant/cli.rb @@ -9,6 +9,19 @@ module Mutant EXIT_FAILURE = 1 EXIT_SUCCESS = 0 + OPTIONS = { + '--code' => [:add_filter, Mutation::Filter::Code ], + '--debug' => [:set_debug ], + '-d' => [:set_debug ], + '--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ], + '--rspec-full' => [:set_strategy, Strategy::Rspec::Full ], + '--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ], + '--static-fail' => [:set_strategy, Strategy::Static::Fail ], + '--static-success' => [:set_strategy, Strategy::Static::Success ] + }.freeze + + OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze + # Run cli with arguments # # @param [Array] arguments @@ -98,19 +111,6 @@ module Mutant private - OPTIONS = { - '--code' => [:add_filter, Mutation::Filter::Code ], - '--debug' => [:set_debug ], - '-d' => [:set_debug ], - '--rspec-unit' => [:set_strategy, Strategy::Rspec::Unit ], - '--rspec-full' => [:set_strategy, Strategy::Rspec::Full ], - '--rspec-dm2' => [:set_strategy, Strategy::Rspec::DM2 ], - '--static-fail' => [:set_strategy, Strategy::Static::Fail ], - '--static-success' => [:set_strategy, Strategy::Static::Success ] - }.freeze - - OPTION_PATTERN = %r(\A-(?:-)?[a-zA-Z0-9\-]+\z).freeze - # Initialize CLI # # @param [Array] arguments @@ -204,15 +204,8 @@ module Mutant # def dispatch_matcher argument = current_argument - matcher = Mutant::Matcher.from_string(argument) - - unless matcher - raise Error, "Invalid matcher syntax: #{argument.inspect}" - end - - @matchers << matcher - consume(1) + @matchers << Classifier.build(argument) end # Process option argument diff --git a/lib/mutant/cli/classifier.rb b/lib/mutant/cli/classifier.rb new file mode 100644 index 00000000..fbaa4db0 --- /dev/null +++ b/lib/mutant/cli/classifier.rb @@ -0,0 +1,129 @@ +module Mutant + class CLI + # A classifier for input strings + class Classifier < Matcher + include AbstractType, Adamantium::Flat, Equalizer.new(:identification) + extend DescendantsTracker + + SCOPE_NAME_PATTERN = /[A-Za-z][A-Za-z_0-9]*/.freeze + METHOD_NAME_PATTERN = /[_A-Za-z][A-Za-z0-9_]*[!?=]?/.freeze + SCOPE_PATTERN = /(?:::)?#{SCOPE_NAME_PATTERN}(?:::#{SCOPE_NAME_PATTERN})*/.freeze + + SINGLETON_PATTERN = %r(\A(#{SCOPE_PATTERN})\z).freeze + + # Return constant + # + # @param [String] location + # + # @return [Class|Module] + # + # @api private + # + def self.constant_lookup(location) + location.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name| + parent.const_get(name) + end + end + + # Return matchers for input + # + # @param [String] input + # + # @return [Classifier] + # if a classifier handles the input + # + # @return [nil] + # otherwise + # + # @api private + # + def self.build(input) + classifiers = descendants.map do |descendant| + descendant.run(input) + end.compact + + raise if classifiers.length > 1 + + classifiers.first + end + + # Run classifier + # + # @return [Classifier] + # if input is handled by classifier + # + # @return [nil] + # otherwise + # + # @api private + # + def self.run(input) + match = self::REGEXP.match(input) + return unless match + + new(match) + end + + # No protected_class_method in ruby :( + class << self; protected :run; end + + # Enumerate subjects + # + # @return [self] + # if block given + # + # @return [Enumerator] + # otherwise + # + # @api private + # + def each(&block) + return to_enum unless block_given? + matcher.each(&block) + self + end + + # Return identification + # + # @return [String] + # + # @api private + # + def identification + match.to_s + end + memoize :identification + + private + + # Initialize object + # + # @param [MatchData] match + # + # @return [undefined] + # + # @api private + # + def initialize(match) + @match = match + end + + # Return match + # + # @return [MatchData] + # + # @api private + # + attr_reader :match + + # Return matcher + # + # @return [Matcher] + # + # @api private + # + abstract_method :matcher + + end + end +end diff --git a/lib/mutant/matcher/method/classifier.rb b/lib/mutant/cli/classifier/method.rb similarity index 53% rename from lib/mutant/matcher/method/classifier.rb rename to lib/mutant/cli/classifier/method.rb index c6e66b56..819ade59 100644 --- a/lib/mutant/matcher/method/classifier.rb +++ b/lib/mutant/cli/classifier/method.rb @@ -1,41 +1,20 @@ module Mutant - class Matcher - class Method < self - # A classifier for input strings - class Classifier - include Adamantium::Flat + class CLI + class Classifier + # Explicit method classifier + class Method < self TABLE = { - '.' => Matcher::ScopeMethods::Singleton, - '#' => Matcher::ScopeMethods::Instance + '.' => Matcher::Methods::Singleton, + '#' => Matcher::Methods::Instance }.freeze - SCOPE_FORMAT = /\A([^#.]+)(\.|#)(.+)\z/.freeze + REGEXP = %r(\A(#{SCOPE_PATTERN})([.#])(#{METHOD_NAME_PATTERN}\z)).freeze # Positions of captured regexp groups - SCOPE_NAME_POSITION = 1 - SCOPE_SYMBOL_POSITION = 2 - METHOD_NAME_POSITION = 3 - - private_class_method :new - - # Run classifier - # - # @param [String] input - # - # @return [Matcher::Method] - # returns matcher when input is in - # - # @return [nil] - # returns nil otherwise - # - # @api private - # - def self.run(input) - match = SCOPE_FORMAT.match(input) - return unless match - new(match).matcher - end + SCOPE_NAME_POSITION = 1 + SCOPE_SYMBOL_POSITION = 2 + METHOD_NAME_POSITION = 3 # Return method matcher # @@ -48,6 +27,8 @@ module Mutant end memoize :matcher + private + # Return method # # @return [Method, UnboundMethod] @@ -61,39 +42,6 @@ module Mutant end memoize :method, :freezer => :noop - # Return match - # - # @return [Matche] - # - # @api private - # - attr_reader :match - - private - - # Initialize matcher - # - # @param [MatchData] match - # - # @api private - # - def initialize(match) - @match = match - end - - # Return scope - # - # @return [Class|Module] - # - # @api private - # - def scope - scope_name.gsub(%r(\A::), '').split('::').inject(::Object) do |parent, name| - parent.const_get(name) - end - end - memoize :scope - # Return scope name # # @return [String] @@ -104,6 +52,16 @@ module Mutant match[SCOPE_NAME_POSITION] end + # Return scope + # + # @return [Class, Method] + # + # @api private + # + def scope + Classifier.constant_lookup(scope_name) + end + # Return method name # # @return [String] @@ -134,6 +92,7 @@ module Mutant TABLE.fetch(scope_symbol).new(scope) end memoize :scope_matcher + end end end diff --git a/lib/mutant/cli/classifier/namespace.rb b/lib/mutant/cli/classifier/namespace.rb new file mode 100644 index 00000000..0e73eac2 --- /dev/null +++ b/lib/mutant/cli/classifier/namespace.rb @@ -0,0 +1,34 @@ +module Mutant + class CLI + class Classifier + + # Namespace classifier + class Namespace < self + + REGEXP = %r(\A(#{SCOPE_PATTERN})\*\z).freeze + + private + + # Return matcher + # + # @return [Matcher] + # + # @api private + # + def matcher + Matcher::Namespace.new(namespace) + end + + # Return namespace + # + # @return [Class, Module] + # + # @api private + # + def namespace + Classifier.const_lookup(match.to_s) + end + end + end + end +end diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb index 271720e9..d6b654e1 100644 --- a/lib/mutant/matcher.rb +++ b/lib/mutant/matcher.rb @@ -1,5 +1,5 @@ module Mutant - # Abstract matcher to find ASTs to mutate + # Abstract matcher to find subjects to mutate class Matcher include Adamantium::Flat, Enumerable, AbstractType extend DescendantsTracker @@ -8,7 +8,11 @@ module Mutant # # @api private # - # @return [undefined] + # @return [self] + # if block given + # + # @return [Enumerabe] + # otherwise # abstract_method :each @@ -19,39 +23,5 @@ module Mutant # @api private # abstract_method :identification - - # Return matcher - # - # @param [String] input - # - # @return [nil] - # returns nil as default implementation - # - # @api private - # - def self.parse(input) - nil - end - - # Return match from string - # - # @param [String] input - # - # @return [Matcher] - # returns matcher input if successful - # - # @return [nil] - # returns nil otherwise - # - # @api private - # - def self.from_string(input) - descendants.each do |descendant| - matcher = descendant.parse(input) - return matcher if matcher - end - - nil - end end end diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index f3a92155..c3daa845 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -4,18 +4,6 @@ module Mutant class Method < self include Adamantium::Flat, Equalizer.new(:identification) - # Parse a method string into filter - # - # @param [String] input - # - # @return [Matcher::Method] - # - # @api private - # - def self.parse(input) - Classifier.run(input) - end - # Methods within rbx kernel directory are precompiled and their source # cannot be accessed via reading source location BLACKLIST = %r(\Akernel/).freeze diff --git a/lib/mutant/matcher/scope_methods.rb b/lib/mutant/matcher/methods.rb similarity index 96% rename from lib/mutant/matcher/scope_methods.rb rename to lib/mutant/matcher/methods.rb index 93e687f6..7d0c17c7 100644 --- a/lib/mutant/matcher/scope_methods.rb +++ b/lib/mutant/matcher/methods.rb @@ -1,7 +1,7 @@ module Mutant class Matcher - # Abstract base class for matcher that returns subjects extracted from scope methods - class ScopeMethods < self + # Abstract base class for matcher that returns method subjects extracted from scope + class Methods < self include AbstractType # Return scope diff --git a/lib/mutant/matcher/object_space.rb b/lib/mutant/matcher/namespace.rb similarity index 65% rename from lib/mutant/matcher/object_space.rb rename to lib/mutant/matcher/namespace.rb index f91586db..41134880 100644 --- a/lib/mutant/matcher/object_space.rb +++ b/lib/mutant/matcher/namespace.rb @@ -1,16 +1,16 @@ module Mutant class Matcher - # Matcher against object space - class ObjectSpace < self - include Equalizer.new(:scope_name_pattern) + # Matcher for specific namespace + class Namespace < self + include Equalizer.new(:pattern) # Enumerate subjects # - # @return [Enumerator] - # returns subject enumerator when no block given - # # @return [self] - # returns self otherwise + # if block given + # + # @return [Enumerator] + # otherwise # # @api private # @@ -24,29 +24,41 @@ module Mutant self end - # Return scope name pattern + # Return namespace # - # @return [Regexp] + # @return [Class::Module] # # @api private # - attr_reader :scope_name_pattern + attr_reader :namespace + + MATCHERS = [Matcher::Methods::Singleton, Matcher::Methods::Instance] private # Initialize object space matcher # - # @param [Regexp] scope_name_pattern - # @param [Enumerable<#each(scope)>] matchers + # @param [Class, Module] namespace # # @return [undefined] # # @api private # - def initialize(scope_name_pattern, matchers = [Matcher::ScopeMethods::Singleton, Matcher::ScopeMethods::Instance]) - @scope_name_pattern, @matchers = scope_name_pattern, @matchers = matchers #[Method::Singleton, Method::Instance] + def initialize(namespace) + @namespace = namespace end + # Return pattern + # + # @return [Regexp] + # + # @api private + # + def pattern + %r(\A#{Regexp.escape(namespace_name)}(?:::)?\z) + end + memoize :pattern + # Yield matchers for scope # # @param [::Class,::Module] scope @@ -56,7 +68,7 @@ module Mutant # @api private # def emit_scope_matches(scope, &block) - @matchers.each do |matcher| + MATCHERS.each do |matcher| matcher.new(scope).each(&block) end end @@ -84,7 +96,7 @@ module Mutant # @api private # def emit_scope(scope) - if [::Module, ::Class].include?(scope.class) and scope_name_pattern =~ scope.name + if [::Module, ::Class].include?(scope.class) and pattern =~ scope.name yield scope end end diff --git a/spec/integration/mutant/method_matching_spec.rb b/spec/integration/mutant/method_matching_spec.rb index d33be567..8e6eb03c 100644 --- a/spec/integration/mutant/method_matching_spec.rb +++ b/spec/integration/mutant/method_matching_spec.rb @@ -10,7 +10,7 @@ describe Mutant, 'method matching' do this_example = 'Mutant method matching' shared_examples_for this_example do - subject { Mutant::Matcher::Method.parse(pattern).to_a } + subject { Mutant::CLI::Classifier.build(pattern).to_a } let(:values) { defaults.merge(expectation) }