mutant 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changelog.md +5 -0
- data/README.md +64 -3
- data/config/flay.yml +1 -1
- data/lib/mutant.rb +2 -0
- data/lib/mutant/cli.rb +1 -1
- data/lib/mutant/env/bootstrap.rb +36 -8
- data/lib/mutant/expression/method.rb +3 -5
- data/lib/mutant/expression/methods.rb +2 -4
- data/lib/mutant/expression/namespace.rb +4 -8
- data/lib/mutant/matcher.rb +3 -18
- data/lib/mutant/matcher/chain.rb +7 -13
- data/lib/mutant/matcher/compiler.rb +2 -13
- data/lib/mutant/matcher/filter.rb +6 -19
- data/lib/mutant/matcher/method.rb +124 -104
- data/lib/mutant/matcher/method/instance.rb +40 -34
- data/lib/mutant/matcher/method/singleton.rb +80 -61
- data/lib/mutant/matcher/methods.rb +19 -29
- data/lib/mutant/matcher/namespace.rb +22 -16
- data/lib/mutant/matcher/null.rb +4 -7
- data/lib/mutant/matcher/scope.rb +23 -13
- data/lib/mutant/matcher/static.rb +17 -0
- data/lib/mutant/mutation.rb +0 -5
- data/lib/mutant/reporter/cli/format.rb +2 -3
- data/lib/mutant/reporter/cli/printer/env_progress.rb +37 -11
- data/lib/mutant/reporter/cli/printer/status_progressive.rb +1 -1
- data/lib/mutant/scope.rb +6 -0
- data/lib/mutant/subject/method.rb +0 -7
- data/lib/mutant/subject/method/instance.rb +0 -10
- data/lib/mutant/subject/method/singleton.rb +0 -10
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/zombifier.rb +2 -1
- data/mutant-rspec.gemspec +1 -1
- data/spec/integration/mutant/rspec_spec.rb +1 -1
- data/spec/shared/method_matcher_behavior.rb +21 -14
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/mutant/env/boostrap_spec.rb +88 -26
- data/spec/unit/mutant/env_spec.rb +0 -1
- data/spec/unit/mutant/expression/method_spec.rb +3 -3
- data/spec/unit/mutant/expression/methods_spec.rb +3 -4
- data/spec/unit/mutant/expression/namespace/flat_spec.rb +2 -3
- data/spec/unit/mutant/expression/namespace/recursive_spec.rb +2 -4
- data/spec/unit/mutant/matcher/chain_spec.rb +21 -29
- data/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +16 -13
- data/spec/unit/mutant/matcher/compiler_spec.rb +49 -60
- data/spec/unit/mutant/matcher/filter_spec.rb +15 -31
- data/spec/unit/mutant/matcher/method/instance_spec.rb +84 -128
- data/spec/unit/mutant/matcher/method/singleton_spec.rb +48 -52
- data/spec/unit/mutant/matcher/methods/instance_spec.rb +21 -24
- data/spec/unit/mutant/matcher/methods/singleton_spec.rb +18 -21
- data/spec/unit/mutant/matcher/namespace_spec.rb +30 -38
- data/spec/unit/mutant/matcher/null_spec.rb +5 -20
- data/spec/unit/mutant/matcher/scope_spec.rb +33 -0
- data/spec/unit/mutant/matcher/static_spec.rb +11 -0
- data/spec/unit/mutant/mutation_spec.rb +30 -10
- data/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb +6 -0
- data/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb +2 -0
- data/spec/unit/mutant/reporter/cli/printer/status_spec.rb +10 -0
- data/spec/unit/mutant/reporter/cli_spec.rb +4 -0
- data/spec/unit/mutant/subject/method/instance_spec.rb +0 -28
- data/spec/unit/mutant/subject/method/singleton_spec.rb +0 -28
- data/test_app/Gemfile.rspec3.4 +7 -0
- data/test_app/lib/test_app.rb +16 -12
- data/test_app/lib/test_app/literal.rb +3 -0
- metadata +9 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: d83617d43f73b49c391d1b7e8f3317a666297c83
         | 
| 4 | 
            +
              data.tar.gz: 186b8ce58089718ba3cafa1c08f609b89941541e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 3d4ee1e94a96ba909cf327bde6afdca8ff5e50f80603ca8835115fdba2abbfa05ad89897366908f8b64bca2e9302605e7accddf1167260c187cf4e40417d0df5
         | 
| 7 | 
            +
              data.tar.gz: 7b599e22fa7ddd03e9f9ac91af9ce0e730b155c8df2b5ab1b5a67fc8cd62f8bf8972ca8df64b45fb5114a556e7416f3c86baf666ea62f5de5201e8b068406afc
         | 
    
        data/Changelog.md
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -73,6 +73,54 @@ To mutation test Rails models with rspec comment out ```require 'rspec/autorun'` | |
| 73 73 | 
             
            RAILS_ENV=test bundle exec mutant -r ./config/environment --use rspec User
         | 
| 74 74 | 
             
            ```
         | 
| 75 75 |  | 
| 76 | 
            +
            Limitations
         | 
| 77 | 
            +
            -----------
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            Mutant cannot emit mutations for...
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            * methods defined within a closure.  For example, methods defined using `module_eval`, `class_eval`,
         | 
| 82 | 
            +
              `define_method`, or `define_singleton_method`:
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                ```ruby
         | 
| 85 | 
            +
                class Example
         | 
| 86 | 
            +
                  class_eval do
         | 
| 87 | 
            +
                    def example1
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  module_eval do
         | 
| 92 | 
            +
                    def example2
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  define_method(:example3) do
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  define_singleton_method(:example4) do
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
                ```
         | 
| 103 | 
            +
             | 
| 104 | 
            +
            * singleton methods not defined on a constant or `self`
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                ```ruby
         | 
| 107 | 
            +
                class Foo
         | 
| 108 | 
            +
                  def self.bar; end   # ok
         | 
| 109 | 
            +
                  def Foo.baz; end    # ok
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  myself = self
         | 
| 112 | 
            +
                  def myself.qux; end # cannot mutate
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
                ```
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            * methods defined with eval:
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                ```ruby
         | 
| 119 | 
            +
                class Foo
         | 
| 120 | 
            +
                  class_eval('def bar; end') # cannot mutate
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
                ```
         | 
| 123 | 
            +
             | 
| 76 124 | 
             
            Mutation-Operators:
         | 
| 77 125 | 
             
            -------------------
         | 
| 78 126 |  | 
| @@ -182,11 +230,24 @@ There are some presentations about mutant in the wild: | |
| 182 230 | 
             
            * [eurucamp 2013](http://2013.eurucamp.org/) / FrOSCon-2013 http://slid.es/markusschirp/mutation-testing
         | 
| 183 231 | 
             
            * [Cologne.rb](http://www.colognerb.de/topics/mutation-testing-mit-mutant) / https://github.com/DonSchado/colognerb-on-mutant/blob/master/mutation_testing_slides.pdf
         | 
| 184 232 |  | 
| 185 | 
            -
            Blog | 
| 233 | 
            +
            Blog posts
         | 
| 186 234 | 
             
            ----------
         | 
| 187 235 |  | 
| 188 | 
            -
             | 
| 189 | 
            -
             | 
| 236 | 
            +
            Sorted by recency:
         | 
| 237 | 
            +
             | 
| 238 | 
            +
            * [How to write better code using mutation testing (November 2015)][blockscore]
         | 
| 239 | 
            +
            * [How good are your Ruby tests? Testing your tests with mutant (June 2015)][arkency1]
         | 
| 240 | 
            +
            * [Mutation testing and continuous integration (May 2015)][arkency2]
         | 
| 241 | 
            +
            * [Why I want to introduce mutation testing to the `rails_event_store` gem (April 2015)][arkency3]
         | 
| 242 | 
            +
            * [Mutation testing with mutant (April 2014)][sitepoint]
         | 
| 243 | 
            +
            * [Mutation testing with mutant (January 2013)][solnic]
         | 
| 244 | 
            +
             | 
| 245 | 
            +
            [blockscore]: https://blog.blockscore.com/how-to-write-better-code-using-mutation-testing/
         | 
| 246 | 
            +
            [sitepoint]: http://www.sitepoint.com/mutation-testing-mutant/
         | 
| 247 | 
            +
            [arkency1]: http://blog.arkency.com/2015/06/how-good-are-your-ruby-tests-testing-your-tests-with-mutant/
         | 
| 248 | 
            +
            [arkency2]: http://blog.arkency.com/2015/05/mutation-testing-and-continuous-integration/
         | 
| 249 | 
            +
            [arkency3]: http://blog.arkency.com/2015/04/why-i-want-to-introduce-mutation-testing-to-the-rails-event-store-gem/  
         | 
| 250 | 
            +
            [solnic]: http://solnic.eu/2013/01/23/mutation-testing-with-mutant.html
         | 
| 190 251 |  | 
| 191 252 | 
             
            The Crash / Stuck Problem (MRI)
         | 
| 192 253 | 
             
            -------------------------------
         | 
    
        data/config/flay.yml
    CHANGED
    
    
    
        data/lib/mutant.rb
    CHANGED
    
    | @@ -132,6 +132,7 @@ require 'mutant/mutator/node/match_current_line' | |
| 132 132 | 
             
            require 'mutant/loader'
         | 
| 133 133 | 
             
            require 'mutant/context'
         | 
| 134 134 | 
             
            require 'mutant/context/scope'
         | 
| 135 | 
            +
            require 'mutant/scope'
         | 
| 135 136 | 
             
            require 'mutant/subject'
         | 
| 136 137 | 
             
            require 'mutant/subject/method'
         | 
| 137 138 | 
             
            require 'mutant/subject/method/instance'
         | 
| @@ -148,6 +149,7 @@ require 'mutant/matcher/namespace' | |
| 148 149 | 
             
            require 'mutant/matcher/scope'
         | 
| 149 150 | 
             
            require 'mutant/matcher/filter'
         | 
| 150 151 | 
             
            require 'mutant/matcher/null'
         | 
| 152 | 
            +
            require 'mutant/matcher/static'
         | 
| 151 153 | 
             
            require 'mutant/expression'
         | 
| 152 154 | 
             
            require 'mutant/expression/parser'
         | 
| 153 155 | 
             
            require 'mutant/expression/method'
         | 
    
        data/lib/mutant/cli.rb
    CHANGED
    
    
    
        data/lib/mutant/env/bootstrap.rb
    CHANGED
    
    | @@ -4,10 +4,18 @@ module Mutant | |
| 4 4 | 
             
                class Bootstrap
         | 
| 5 5 | 
             
                  include Adamantium::Flat, Concord::Public.new(:config, :cache), Procto.call(:env)
         | 
| 6 6 |  | 
| 7 | 
            -
                   | 
| 8 | 
            -
                    "Fix your lib to follow normal ruby semantics!\n" \
         | 
| 7 | 
            +
                  SEMANTICS_MESSAGE_FORMAT =
         | 
| 8 | 
            +
                    "%<message>s. Fix your lib to follow normal ruby semantics!\n" \
         | 
| 9 9 | 
             
                    '{Module,Class}#name should return resolvable constant name as String or nil'.freeze
         | 
| 10 10 |  | 
| 11 | 
            +
                  CLASS_NAME_RAISED_EXCEPTION =
         | 
| 12 | 
            +
                    '%<scope_class>s#name from: %<scope>s raised an error: %<exception>s'.freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  CLASS_NAME_TYPE_MISMATCH_FORMAT =
         | 
| 15 | 
            +
                    '%<scope_class>s#name from: %<scope>s returned %<name>s'.freeze
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  private_constant(*constants(false))
         | 
| 18 | 
            +
             | 
| 11 19 | 
             
                  # Scopes that are eligible for matching
         | 
| 12 20 | 
             
                  #
         | 
| 13 21 | 
             
                  # @return [Enumerable<Matcher::Scope>]
         | 
| @@ -84,7 +92,12 @@ module Mutant | |
| 84 92 | 
             
                  def scope_name(scope)
         | 
| 85 93 | 
             
                    scope.name
         | 
| 86 94 | 
             
                  rescue => exception
         | 
| 87 | 
            -
                     | 
| 95 | 
            +
                    semantics_warning(
         | 
| 96 | 
            +
                      CLASS_NAME_RAISED_EXCEPTION,
         | 
| 97 | 
            +
                      scope_class: scope.class,
         | 
| 98 | 
            +
                      scope:       scope,
         | 
| 99 | 
            +
                      exception:   exception.inspect
         | 
| 100 | 
            +
                    )
         | 
| 88 101 | 
             
                    nil
         | 
| 89 102 | 
             
                  end
         | 
| 90 103 |  | 
| @@ -95,7 +108,7 @@ module Mutant | |
| 95 108 | 
             
                  # @api private
         | 
| 96 109 | 
             
                  def infect
         | 
| 97 110 | 
             
                    config.includes.each(&$LOAD_PATH.method(:<<))
         | 
| 98 | 
            -
                    config.requires.each(&method(:require))
         | 
| 111 | 
            +
                    config.requires.each(&Kernel.method(:require))
         | 
| 99 112 | 
             
                    @integration = config.integration.new(config).setup
         | 
| 100 113 | 
             
                  end
         | 
| 101 114 |  | 
| @@ -105,7 +118,7 @@ module Mutant | |
| 105 118 | 
             
                  #
         | 
| 106 119 | 
             
                  # @api private
         | 
| 107 120 | 
             
                  def matched_subjects
         | 
| 108 | 
            -
                    Matcher::Compiler.call( | 
| 121 | 
            +
                    Matcher::Compiler.call(config.matcher).call(self)
         | 
| 109 122 | 
             
                  end
         | 
| 110 123 |  | 
| 111 124 | 
             
                  # Initialize matchable scopes
         | 
| @@ -115,8 +128,8 @@ module Mutant | |
| 115 128 | 
             
                  # @api private
         | 
| 116 129 | 
             
                  def initialize_matchable_scopes
         | 
| 117 130 | 
             
                    scopes = ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
         | 
| 118 | 
            -
                      expression = expression(scope)
         | 
| 119 | 
            -
                      aggregate <<  | 
| 131 | 
            +
                      expression = expression(scope) || next
         | 
| 132 | 
            +
                      aggregate << Scope.new(scope, expression)
         | 
| 120 133 | 
             
                    end
         | 
| 121 134 |  | 
| 122 135 | 
             
                    @matchable_scopes = scopes.sort_by { |scope| scope.expression.syntax }
         | 
| @@ -137,12 +150,27 @@ module Mutant | |
| 137 150 | 
             
                    name = scope_name(scope) or return
         | 
| 138 151 |  | 
| 139 152 | 
             
                    unless name.instance_of?(String)
         | 
| 140 | 
            -
                       | 
| 153 | 
            +
                      semantics_warning(
         | 
| 154 | 
            +
                        CLASS_NAME_TYPE_MISMATCH_FORMAT,
         | 
| 155 | 
            +
                        scope_class: scope.class,
         | 
| 156 | 
            +
                        scope:       scope,
         | 
| 157 | 
            +
                        name:        name
         | 
| 158 | 
            +
                      )
         | 
| 141 159 | 
             
                      return
         | 
| 142 160 | 
             
                    end
         | 
| 143 161 |  | 
| 144 162 | 
             
                    config.expression_parser.try_parse(name)
         | 
| 145 163 | 
             
                  end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  # Write a semantics warning
         | 
| 166 | 
            +
                  #
         | 
| 167 | 
            +
                  # @return [undefined]
         | 
| 168 | 
            +
                  #
         | 
| 169 | 
            +
                  # @api private
         | 
| 170 | 
            +
                  def semantics_warning(format, options)
         | 
| 171 | 
            +
                    message = format % options
         | 
| 172 | 
            +
                    warn(SEMANTICS_MESSAGE_FORMAT % { message: message })
         | 
| 173 | 
            +
                  end
         | 
| 146 174 | 
             
                end # Boostrap
         | 
| 147 175 | 
             
              end # Env
         | 
| 148 176 | 
             
            end # Mutant
         | 
| @@ -32,15 +32,13 @@ module Mutant | |
| 32 32 |  | 
| 33 33 | 
             
                  # Matcher for expression
         | 
| 34 34 | 
             
                  #
         | 
| 35 | 
            -
                  # @param [Env] env
         | 
| 36 | 
            -
                  #
         | 
| 37 35 | 
             
                  # @return [Matcher]
         | 
| 38 36 | 
             
                  #
         | 
| 39 37 | 
             
                  # @api private
         | 
| 40 | 
            -
                  def matcher | 
| 41 | 
            -
                    methods_matcher = MATCHERS.fetch(scope_symbol).new( | 
| 38 | 
            +
                  def matcher
         | 
| 39 | 
            +
                    methods_matcher = MATCHERS.fetch(scope_symbol).new(scope)
         | 
| 42 40 |  | 
| 43 | 
            -
                    Matcher::Filter. | 
| 41 | 
            +
                    Matcher::Filter.new(methods_matcher, ->(subject) { subject.expression.eql?(self) })
         | 
| 44 42 | 
             
                  end
         | 
| 45 43 |  | 
| 46 44 | 
             
                private
         | 
| @@ -26,13 +26,11 @@ module Mutant | |
| 26 26 |  | 
| 27 27 | 
             
                  # Matcher on expression
         | 
| 28 28 | 
             
                  #
         | 
| 29 | 
            -
                  # @param [Env] env
         | 
| 30 | 
            -
                  #
         | 
| 31 29 | 
             
                  # @return [Matcher::Method]
         | 
| 32 30 | 
             
                  #
         | 
| 33 31 | 
             
                  # @api private
         | 
| 34 | 
            -
                  def matcher | 
| 35 | 
            -
                    MATCHERS.fetch(scope_symbol).new( | 
| 32 | 
            +
                  def matcher
         | 
| 33 | 
            +
                    MATCHERS.fetch(scope_symbol).new(scope)
         | 
| 36 34 | 
             
                  end
         | 
| 37 35 |  | 
| 38 36 | 
             
                  # Length of match with other expression
         | 
| @@ -35,13 +35,11 @@ module Mutant | |
| 35 35 |  | 
| 36 36 | 
             
                    # Matcher for expression
         | 
| 37 37 | 
             
                    #
         | 
| 38 | 
            -
                    # @param [Env::Bootstrap] env
         | 
| 39 | 
            -
                    #
         | 
| 40 38 | 
             
                    # @return [Matcher]
         | 
| 41 39 | 
             
                    #
         | 
| 42 40 | 
             
                    # @api private
         | 
| 43 | 
            -
                    def matcher | 
| 44 | 
            -
                      Matcher::Namespace.new( | 
| 41 | 
            +
                    def matcher
         | 
| 42 | 
            +
                      Matcher::Namespace.new(self)
         | 
| 45 43 | 
             
                    end
         | 
| 46 44 |  | 
| 47 45 | 
             
                    # Length of match with other expression
         | 
| @@ -71,13 +69,11 @@ module Mutant | |
| 71 69 |  | 
| 72 70 | 
             
                    # Matcher matcher on expression
         | 
| 73 71 | 
             
                    #
         | 
| 74 | 
            -
                    # @param [Env::Bootstrap] env
         | 
| 75 | 
            -
                    #
         | 
| 76 72 | 
             
                    # @return [Matcher]
         | 
| 77 73 | 
             
                    #
         | 
| 78 74 | 
             
                    # @api private
         | 
| 79 | 
            -
                    def matcher | 
| 80 | 
            -
                      Matcher::Scope.new( | 
| 75 | 
            +
                    def matcher
         | 
| 76 | 
            +
                      Matcher::Scope.new(Object.const_get(scope_name))
         | 
| 81 77 | 
             
                    end
         | 
| 82 78 |  | 
| 83 79 | 
             
                    # Syntax for expression
         | 
    
        data/lib/mutant/matcher.rb
    CHANGED
    
    | @@ -1,30 +1,15 @@ | |
| 1 1 | 
             
            module Mutant
         | 
| 2 2 | 
             
              # Abstract matcher to find subjects to mutate
         | 
| 3 3 | 
             
              class Matcher
         | 
| 4 | 
            -
                include Adamantium::Flat,  | 
| 4 | 
            +
                include Adamantium::Flat, AbstractType
         | 
| 5 5 |  | 
| 6 | 
            -
                #  | 
| 6 | 
            +
                # Call matcher
         | 
| 7 7 | 
             
                #
         | 
| 8 8 | 
             
                # @param [Env] env
         | 
| 9 | 
            -
                # @param [Object] input
         | 
| 10 | 
            -
                #
         | 
| 11 | 
            -
                # @return [undefined]
         | 
| 12 | 
            -
                #
         | 
| 13 | 
            -
                # @api private
         | 
| 14 | 
            -
                def self.build(*arguments)
         | 
| 15 | 
            -
                  new(*arguments)
         | 
| 16 | 
            -
                end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                # Enumerate subjects
         | 
| 19 | 
            -
                #
         | 
| 20 | 
            -
                # @return [self]
         | 
| 21 | 
            -
                #   if block given
         | 
| 22 9 | 
             
                #
         | 
| 23 10 | 
             
                # @return [Enumerable<Subject>]
         | 
| 24 | 
            -
                #   otherwise
         | 
| 25 11 | 
             
                #
         | 
| 26 | 
            -
                 | 
| 27 | 
            -
                abstract_method :each
         | 
| 12 | 
            +
                abstract_method :call
         | 
| 28 13 |  | 
| 29 14 | 
             
              end # Matcher
         | 
| 30 15 | 
             
            end # Mutant
         | 
    
        data/lib/mutant/matcher/chain.rb
    CHANGED
    
    | @@ -1,26 +1,20 @@ | |
| 1 1 | 
             
            module Mutant
         | 
| 2 2 | 
             
              class Matcher
         | 
| 3 | 
            -
                #  | 
| 3 | 
            +
                # Matcher chaining results of other matchers together
         | 
| 4 4 | 
             
                class Chain < self
         | 
| 5 5 | 
             
                  include Concord.new(:matchers)
         | 
| 6 6 |  | 
| 7 | 
            -
                  #  | 
| 7 | 
            +
                  # Call matcher
         | 
| 8 8 | 
             
                  #
         | 
| 9 | 
            -
                  # @ | 
| 10 | 
            -
                  #   if no block given
         | 
| 9 | 
            +
                  # @param [Env] env
         | 
| 11 10 | 
             
                  #
         | 
| 12 | 
            -
                  # @return [ | 
| 13 | 
            -
                  #   otherwise
         | 
| 11 | 
            +
                  # @return [Enumerable<Subject>]
         | 
| 14 12 | 
             
                  #
         | 
| 15 13 | 
             
                  # @api private
         | 
| 16 | 
            -
                  def  | 
| 17 | 
            -
                     | 
| 18 | 
            -
             | 
| 19 | 
            -
                    matchers.each do |matcher|
         | 
| 20 | 
            -
                      matcher.each(&block)
         | 
| 14 | 
            +
                  def call(env)
         | 
| 15 | 
            +
                    matchers.flat_map do |matcher|
         | 
| 16 | 
            +
                      matcher.call(env)
         | 
| 21 17 | 
             
                    end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                    self
         | 
| 24 18 | 
             
                  end
         | 
| 25 19 |  | 
| 26 20 | 
             
                end # Chain
         | 
| @@ -3,7 +3,7 @@ module Mutant | |
| 3 3 |  | 
| 4 4 | 
             
                # Compiler for complex matchers
         | 
| 5 5 | 
             
                class Compiler
         | 
| 6 | 
            -
                  include Concord.new(: | 
| 6 | 
            +
                  include Concord.new(:config), AST::Sexp, Procto.call(:result)
         | 
| 7 7 |  | 
| 8 8 | 
             
                  # Generated matcher
         | 
| 9 9 | 
             
                  #
         | 
| @@ -12,7 +12,7 @@ module Mutant | |
| 12 12 | 
             
                  # @api private
         | 
| 13 13 | 
             
                  def result
         | 
| 14 14 | 
             
                    Filter.new(
         | 
| 15 | 
            -
                      Chain. | 
| 15 | 
            +
                      Chain.new(config.match_expressions.map(&:matcher)),
         | 
| 16 16 | 
             
                      Morpher::Evaluator::Predicate::Boolean::And.new(
         | 
| 17 17 | 
             
                        [
         | 
| 18 18 | 
             
                          ignored_subjects,
         | 
| @@ -61,17 +61,6 @@ module Mutant | |
| 61 61 | 
             
                    Morpher::Evaluator::Predicate::Boolean::And.new(config.subject_filters)
         | 
| 62 62 | 
             
                  end
         | 
| 63 63 |  | 
| 64 | 
            -
                  # Matcher for expression on env
         | 
| 65 | 
            -
                  #
         | 
| 66 | 
            -
                  # @param [Mutant::Expression] expression
         | 
| 67 | 
            -
                  #
         | 
| 68 | 
            -
                  # @return [Matcher]
         | 
| 69 | 
            -
                  #
         | 
| 70 | 
            -
                  # @api private
         | 
| 71 | 
            -
                  def matcher(expression)
         | 
| 72 | 
            -
                    expression.matcher(env)
         | 
| 73 | 
            -
                  end
         | 
| 74 | 
            -
             | 
| 75 64 | 
             
                end # Compiler
         | 
| 76 65 | 
             
              end # Matcher
         | 
| 77 66 | 
             
            end # Mutant
         | 
| @@ -4,30 +4,17 @@ module Mutant | |
| 4 4 | 
             
                class Filter < self
         | 
| 5 5 | 
             
                  include Concord.new(:matcher, :predicate)
         | 
| 6 6 |  | 
| 7 | 
            -
                  # New matcher
         | 
| 8 | 
            -
                  #
         | 
| 9 | 
            -
                  # @param [Matcher] matcher
         | 
| 10 | 
            -
                  #
         | 
| 11 | 
            -
                  # @return [Matcher]
         | 
| 12 | 
            -
                  #
         | 
| 13 | 
            -
                  # @api private
         | 
| 14 | 
            -
                  def self.build(matcher, &predicate)
         | 
| 15 | 
            -
                    new(matcher, predicate)
         | 
| 16 | 
            -
                  end
         | 
| 17 | 
            -
             | 
| 18 7 | 
             
                  # Enumerate matches
         | 
| 19 8 | 
             
                  #
         | 
| 20 | 
            -
                  # @ | 
| 21 | 
            -
                  #   if block given
         | 
| 9 | 
            +
                  # @param [Env] env
         | 
| 22 10 | 
             
                  #
         | 
| 23 | 
            -
                  # @return [ | 
| 24 | 
            -
                  #   otherwise
         | 
| 11 | 
            +
                  # @return [Enumerable<Subject>]
         | 
| 25 12 | 
             
                  #
         | 
| 26 13 | 
             
                  # @api private
         | 
| 27 | 
            -
                  def  | 
| 28 | 
            -
                     | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 14 | 
            +
                  def call(env)
         | 
| 15 | 
            +
                    matcher
         | 
| 16 | 
            +
                      .call(env)
         | 
| 17 | 
            +
                      .select(&predicate.method(:call))
         | 
| 31 18 | 
             
                  end
         | 
| 32 19 |  | 
| 33 20 | 
             
                end # Filter
         | 
| @@ -1,133 +1,153 @@ | |
| 1 1 | 
             
            module Mutant
         | 
| 2 2 | 
             
              class Matcher
         | 
| 3 | 
            -
                #  | 
| 3 | 
            +
                # Abstract base class for method matchers
         | 
| 4 4 | 
             
                class Method < self
         | 
| 5 | 
            -
                  include  | 
| 6 | 
            -
             | 
| 5 | 
            +
                  include AbstractType,
         | 
| 6 | 
            +
                          Adamantium::Flat,
         | 
| 7 | 
            +
                          Concord::Public.new(:scope, :target_method, :evaluator)
         | 
| 7 8 |  | 
| 8 9 | 
             
                  # Methods within rbx kernel directory are precompiled and their source
         | 
| 9 10 | 
             
                  # cannot be accessed via reading source location. Same for methods created by eval.
         | 
| 10 11 | 
             
                  BLACKLIST = %r{\A(kernel/|\(eval\)\z)}.freeze
         | 
| 11 12 |  | 
| 12 | 
            -
                   | 
| 13 | 
            +
                  SOURCE_LOCATION_WARNING_FORMAT =
         | 
| 14 | 
            +
                    '%s does not have a valid source location, unable to emit subject'.freeze
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  CLOSURE_WARNING_FORMAT =
         | 
| 17 | 
            +
                    '%s is dynamically defined in a closure, unable to emit subject'.freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  # Matched subjects
         | 
| 13 20 | 
             
                  #
         | 
| 14 | 
            -
                  # @ | 
| 15 | 
            -
                  #   if no block given
         | 
| 21 | 
            +
                  # @param [Env] env
         | 
| 16 22 | 
             
                  #
         | 
| 17 | 
            -
                  # @return [ | 
| 18 | 
            -
                  #   otherwise
         | 
| 23 | 
            +
                  # @return [Enumerable<Subject>]
         | 
| 19 24 | 
             
                  #
         | 
| 20 25 | 
             
                  # @api private
         | 
| 21 | 
            -
                  def  | 
| 22 | 
            -
                     | 
| 26 | 
            +
                  def call(env)
         | 
| 27 | 
            +
                    evaluator.call(scope, target_method, env)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  # Abstract method match evaluator
         | 
| 31 | 
            +
                  #
         | 
| 32 | 
            +
                  # Present to avoid passing the env argument around in case the
         | 
| 33 | 
            +
                  # logic would be implemnented directly on the Matcher::Method
         | 
| 34 | 
            +
                  # instance
         | 
| 35 | 
            +
                  class Evaluator
         | 
| 36 | 
            +
                    include AbstractType,
         | 
| 37 | 
            +
                            Adamantium,
         | 
| 38 | 
            +
                            Concord.new(:scope, :target_method, :env),
         | 
| 39 | 
            +
                            Procto.call,
         | 
| 40 | 
            +
                            AST::NodePredicates
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    # Matched subjects
         | 
| 43 | 
            +
                    #
         | 
| 44 | 
            +
                    # @return [Enumerable<Subject>]
         | 
| 45 | 
            +
                    #
         | 
| 46 | 
            +
                    # @api private
         | 
| 47 | 
            +
                    def call
         | 
| 48 | 
            +
                      return EMPTY_ARRAY if skip?
         | 
| 23 49 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
                      yield subject
         | 
| 50 | 
            +
                      [subject].compact
         | 
| 26 51 | 
             
                    end
         | 
| 27 52 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
                  end
         | 
| 53 | 
            +
                  private
         | 
| 30 54 |  | 
| 31 | 
            -
             | 
| 55 | 
            +
                    # Test if method should be skipped
         | 
| 56 | 
            +
                    #
         | 
| 57 | 
            +
                    # @return [Truthy]
         | 
| 58 | 
            +
                    #
         | 
| 59 | 
            +
                    # @api private
         | 
| 60 | 
            +
                    def skip?
         | 
| 61 | 
            +
                      location = source_location
         | 
| 62 | 
            +
                      if location.nil? || BLACKLIST.match(location.first)
         | 
| 63 | 
            +
                        env.warn(SOURCE_LOCATION_WARNING_FORMAT % target_method)
         | 
| 64 | 
            +
                      elsif matched_node_path.any?(&method(:n_block?))
         | 
| 65 | 
            +
                        env.warn(CLOSURE_WARNING_FORMAT % target_method)
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
                    end
         | 
| 32 68 |  | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
                    if location.nil? || BLACKLIST.match(location.first)
         | 
| 41 | 
            -
                      env.warn(format('%s does not have valid source location unable to emit subject', target_method.inspect))
         | 
| 42 | 
            -
                      true
         | 
| 43 | 
            -
                    elsif matched_node_path.any?(&method(:n_block?))
         | 
| 44 | 
            -
                      env.warn(format('%s is defined from a 3rd party lib unable to emit subject', target_method.inspect))
         | 
| 45 | 
            -
                      true
         | 
| 46 | 
            -
                    else
         | 
| 47 | 
            -
                      false
         | 
| 69 | 
            +
                    # Target method name
         | 
| 70 | 
            +
                    #
         | 
| 71 | 
            +
                    # @return [String]
         | 
| 72 | 
            +
                    #
         | 
| 73 | 
            +
                    # @api private
         | 
| 74 | 
            +
                    def method_name
         | 
| 75 | 
            +
                      target_method.name
         | 
| 48 76 | 
             
                    end
         | 
| 49 | 
            -
                  end
         | 
| 50 77 |  | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 78 | 
            +
                    # Target context
         | 
| 79 | 
            +
                    #
         | 
| 80 | 
            +
                    # @return [Context::Scope]
         | 
| 81 | 
            +
                    #
         | 
| 82 | 
            +
                    # @api private
         | 
| 83 | 
            +
                    def context
         | 
| 84 | 
            +
                      Context::Scope.new(scope, source_path)
         | 
| 85 | 
            +
                    end
         | 
| 59 86 |  | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 87 | 
            +
                    # Root source node
         | 
| 88 | 
            +
                    #
         | 
| 89 | 
            +
                    # @return [Parser::AST::Node]
         | 
| 90 | 
            +
                    #
         | 
| 91 | 
            +
                    # @api private
         | 
| 92 | 
            +
                    def ast
         | 
| 93 | 
            +
                      env.cache.parse(source_path)
         | 
| 94 | 
            +
                    end
         | 
| 68 95 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 96 | 
            +
                    # Path to source
         | 
| 97 | 
            +
                    #
         | 
| 98 | 
            +
                    # @return [Pathname]
         | 
| 99 | 
            +
                    #
         | 
| 100 | 
            +
                    # @api private
         | 
| 101 | 
            +
                    def source_path
         | 
| 102 | 
            +
                      Pathname.new(source_location.first)
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                    memoize :source_path
         | 
| 77 105 |  | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
                  memoize :source_path
         | 
| 106 | 
            +
                    # Source file line
         | 
| 107 | 
            +
                    #
         | 
| 108 | 
            +
                    # @return [Fixnum]
         | 
| 109 | 
            +
                    #
         | 
| 110 | 
            +
                    # @api private
         | 
| 111 | 
            +
                    def source_line
         | 
| 112 | 
            +
                      source_location.last
         | 
| 113 | 
            +
                    end
         | 
| 87 114 |  | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 115 | 
            +
                    # Full source location
         | 
| 116 | 
            +
                    #
         | 
| 117 | 
            +
                    # @return [Array{String,Fixnum}]
         | 
| 118 | 
            +
                    #
         | 
| 119 | 
            +
                    # @api private
         | 
| 120 | 
            +
                    def source_location
         | 
| 121 | 
            +
                      target_method.source_location
         | 
| 122 | 
            +
                    end
         | 
| 96 123 |  | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                     | 
| 104 | 
            -
             | 
| 124 | 
            +
                    # Matched subject
         | 
| 125 | 
            +
                    #
         | 
| 126 | 
            +
                    # @return [Subject]
         | 
| 127 | 
            +
                    #   if there is a matched node
         | 
| 128 | 
            +
                    #
         | 
| 129 | 
            +
                    # @return [nil]
         | 
| 130 | 
            +
                    #   otherwise
         | 
| 131 | 
            +
                    #
         | 
| 132 | 
            +
                    # @api private
         | 
| 133 | 
            +
                    def subject
         | 
| 134 | 
            +
                      node = matched_node_path.last || return
         | 
| 135 | 
            +
                      self.class::SUBJECT_CLASS.new(context, node)
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                    memoize :subject
         | 
| 105 138 |  | 
| 106 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
                   | 
| 116 | 
            -
                    node = matched_node_path.last
         | 
| 117 | 
            -
                    return unless node
         | 
| 118 | 
            -
                    self.class::SUBJECT_CLASS.new(context, node)
         | 
| 119 | 
            -
                  end
         | 
| 120 | 
            -
                  memoize :subject
         | 
| 139 | 
            +
                    # Matched node path
         | 
| 140 | 
            +
                    #
         | 
| 141 | 
            +
                    # @return [Array<Parser::AST::Node>]
         | 
| 142 | 
            +
                    #
         | 
| 143 | 
            +
                    # @api private
         | 
| 144 | 
            +
                    def matched_node_path
         | 
| 145 | 
            +
                      AST.find_last_path(ast, &method(:match?))
         | 
| 146 | 
            +
                    end
         | 
| 147 | 
            +
                    memoize :matched_node_path
         | 
| 148 | 
            +
                  end # Evaluator
         | 
| 121 149 |  | 
| 122 | 
            -
                   | 
| 123 | 
            -
                  #
         | 
| 124 | 
            -
                  # @return [Array<Parser::AST::Node>]
         | 
| 125 | 
            -
                  #
         | 
| 126 | 
            -
                  # @api private
         | 
| 127 | 
            -
                  def matched_node_path
         | 
| 128 | 
            -
                    AST.find_last_path(ast, &method(:match?))
         | 
| 129 | 
            -
                  end
         | 
| 130 | 
            -
                  memoize :matched_node_path
         | 
| 150 | 
            +
                  private_constant(*constants(false))
         | 
| 131 151 |  | 
| 132 152 | 
             
                end # Method
         | 
| 133 153 | 
             
              end # Matcher
         |