mutant 0.10.20 → 0.10.25
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/lib/mutant.rb +23 -4
- data/lib/mutant/ast/meta/send.rb +0 -1
- data/lib/mutant/ast/regexp.rb +37 -0
- data/lib/mutant/ast/regexp/transformer.rb +150 -0
- data/lib/mutant/ast/regexp/transformer/direct.rb +121 -0
- data/lib/mutant/ast/regexp/transformer/named_group.rb +50 -0
- data/lib/mutant/ast/regexp/transformer/options_group.rb +68 -0
- data/lib/mutant/ast/regexp/transformer/quantifier.rb +90 -0
- data/lib/mutant/ast/regexp/transformer/recursive.rb +56 -0
- data/lib/mutant/ast/regexp/transformer/root.rb +28 -0
- data/lib/mutant/ast/regexp/transformer/text.rb +58 -0
- data/lib/mutant/ast/types.rb +115 -11
- data/lib/mutant/cli/command/environment.rb +9 -3
- data/lib/mutant/config.rb +8 -54
- data/lib/mutant/config/coverage_criteria.rb +61 -0
- data/lib/mutant/env.rb +8 -6
- data/lib/mutant/expression.rb +0 -12
- data/lib/mutant/expression/namespace.rb +1 -1
- data/lib/mutant/isolation/fork.rb +1 -1
- data/lib/mutant/loader.rb +1 -1
- data/lib/mutant/matcher.rb +2 -2
- data/lib/mutant/matcher/config.rb +27 -6
- data/lib/mutant/matcher/method.rb +2 -3
- data/lib/mutant/matcher/method/metaclass.rb +1 -1
- data/lib/mutant/meta/example/dsl.rb +6 -1
- data/lib/mutant/mutator/node/arguments.rb +0 -2
- data/lib/mutant/mutator/node/block.rb +5 -1
- data/lib/mutant/mutator/node/dynamic_literal.rb +1 -1
- data/lib/mutant/mutator/node/kwargs.rb +44 -0
- data/lib/mutant/mutator/node/literal/regex.rb +26 -0
- data/lib/mutant/mutator/node/literal/symbol.rb +0 -2
- data/lib/mutant/mutator/node/regexp.rb +20 -0
- data/lib/mutant/mutator/node/regexp/alternation_meta.rb +20 -0
- data/lib/mutant/mutator/node/regexp/beginning_of_line_anchor.rb +20 -0
- data/lib/mutant/mutator/node/regexp/capture_group.rb +25 -0
- data/lib/mutant/mutator/node/regexp/character_type.rb +31 -0
- data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +20 -0
- data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +20 -0
- data/lib/mutant/mutator/node/regexp/zero_or_more.rb +34 -0
- data/lib/mutant/mutator/node/sclass.rb +1 -1
- data/lib/mutant/mutator/node/send.rb +36 -19
- data/lib/mutant/parallel.rb +43 -28
- data/lib/mutant/parallel/driver.rb +9 -3
- data/lib/mutant/parallel/worker.rb +60 -2
- data/lib/mutant/pipe.rb +94 -0
- data/lib/mutant/reporter/cli/printer/env.rb +1 -1
- data/lib/mutant/reporter/cli/printer/isolation_result.rb +1 -6
- data/lib/mutant/result.rb +8 -0
- data/lib/mutant/runner.rb +7 -10
- data/lib/mutant/runner/sink.rb +12 -2
- data/lib/mutant/subject.rb +1 -3
- data/lib/mutant/subject/method/instance.rb +2 -4
- data/lib/mutant/timer.rb +2 -4
- data/lib/mutant/transform.rb +25 -2
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +1 -2
- metadata +48 -9
- data/lib/mutant/warnings.rb +0 -106
| @@ -29,13 +29,19 @@ module Mutant | |
| 29 29 | 
             
                    end
         | 
| 30 30 |  | 
| 31 31 | 
             
                    def expand(file_config)
         | 
| 32 | 
            +
                      if @config.matcher.subjects.any?
         | 
| 33 | 
            +
                        file_config = file_config.with(
         | 
| 34 | 
            +
                          matcher: file_config.matcher.with(subjects: [])
         | 
| 35 | 
            +
                        )
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
             | 
| 32 38 | 
             
                      @config = Config.env.merge(file_config).merge(@config).expand_defaults
         | 
| 33 39 | 
             
                    end
         | 
| 34 40 |  | 
| 35 41 | 
             
                    def parse_remaining_arguments(arguments)
         | 
| 36 42 | 
             
                      Mutant.traverse(@config.expression_parser, arguments)
         | 
| 37 | 
            -
                        .fmap do | | 
| 38 | 
            -
                          matcher( | 
| 43 | 
            +
                        .fmap do |expressions|
         | 
| 44 | 
            +
                          matcher(subjects: expressions)
         | 
| 39 45 | 
             
                          self
         | 
| 40 46 | 
             
                        end
         | 
| 41 47 | 
             
                    end
         | 
| @@ -82,7 +88,7 @@ module Mutant | |
| 82 88 | 
             
                      parser.separator('Matcher:')
         | 
| 83 89 |  | 
| 84 90 | 
             
                      parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
         | 
| 85 | 
            -
                        add_matcher(: | 
| 91 | 
            +
                        add_matcher(:ignore, @config.expression_parser.call(pattern).from_right)
         | 
| 86 92 | 
             
                      end
         | 
| 87 93 | 
             
                      parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
         | 
| 88 94 | 
             
                        add_matcher(:start_expressions, @config.expression_parser.call(pattern).from_right)
         | 
    
        data/lib/mutant/config.rb
    CHANGED
    
    | @@ -37,54 +37,6 @@ module Mutant | |
| 37 37 |  | 
| 38 38 | 
             
                private_constant(*constants(false))
         | 
| 39 39 |  | 
| 40 | 
            -
                class CoverageCriteria
         | 
| 41 | 
            -
                  include Anima.new(:process_abort, :test_result, :timeout)
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                  EMPTY = new(
         | 
| 44 | 
            -
                    process_abort: nil,
         | 
| 45 | 
            -
                    test_result:   nil,
         | 
| 46 | 
            -
                    timeout:       nil
         | 
| 47 | 
            -
                  )
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                  DEFAULT = new(
         | 
| 50 | 
            -
                    process_abort: false,
         | 
| 51 | 
            -
                    test_result:   true,
         | 
| 52 | 
            -
                    timeout:       false
         | 
| 53 | 
            -
                  )
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                  TRANSFORM =
         | 
| 56 | 
            -
                    Transform::Sequence.new(
         | 
| 57 | 
            -
                      [
         | 
| 58 | 
            -
                        Transform::Hash.new(
         | 
| 59 | 
            -
                          optional: [
         | 
| 60 | 
            -
                            Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
         | 
| 61 | 
            -
                            Transform::Hash::Key.new('test_result',   Transform::BOOLEAN),
         | 
| 62 | 
            -
                            Transform::Hash::Key.new('timeout',       Transform::BOOLEAN)
         | 
| 63 | 
            -
                          ],
         | 
| 64 | 
            -
                          required: []
         | 
| 65 | 
            -
                        ),
         | 
| 66 | 
            -
                        Transform::Hash::Symbolize.new,
         | 
| 67 | 
            -
                        ->(value) { Either::Right.new(DEFAULT.with(**value)) }
         | 
| 68 | 
            -
                      ]
         | 
| 69 | 
            -
                    )
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                  def merge(other)
         | 
| 72 | 
            -
                    self.class.new(
         | 
| 73 | 
            -
                      process_abort: overwrite(other, :process_abort),
         | 
| 74 | 
            -
                      test_result:   overwrite(other, :test_result),
         | 
| 75 | 
            -
                      timeout:       overwrite(other, :timeout)
         | 
| 76 | 
            -
                    )
         | 
| 77 | 
            -
                  end
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                private
         | 
| 80 | 
            -
             | 
| 81 | 
            -
                  def overwrite(other, attribute_name)
         | 
| 82 | 
            -
                    other_value = other.public_send(attribute_name)
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                    other_value.nil? ? public_send(attribute_name) : other_value
         | 
| 85 | 
            -
                  end
         | 
| 86 | 
            -
                end # CoverageCriteria
         | 
| 87 | 
            -
             | 
| 88 40 | 
             
                # Merge with other config
         | 
| 89 41 | 
             
                #
         | 
| 90 42 | 
             
                # @param [Config] other
         | 
| @@ -116,13 +68,14 @@ module Mutant | |
| 116 68 | 
             
                #
         | 
| 117 69 | 
             
                # @return [Either<String,Config>]
         | 
| 118 70 | 
             
                def self.load_config_file(world)
         | 
| 119 | 
            -
                   | 
| 120 | 
            -
             | 
| 71 | 
            +
                  files = CANDIDATES
         | 
| 72 | 
            +
                    .map(&world.pathname.public_method(:new))
         | 
| 73 | 
            +
                    .select(&:readable?)
         | 
| 121 74 |  | 
| 122 75 | 
             
                  if files.one?
         | 
| 123 | 
            -
                    load_contents(files.first).fmap(& | 
| 76 | 
            +
                    load_contents(files.first).fmap(&DEFAULT.public_method(:with))
         | 
| 124 77 | 
             
                  elsif files.empty?
         | 
| 125 | 
            -
                    Either::Right.new( | 
| 78 | 
            +
                    Either::Right.new(DEFAULT)
         | 
| 126 79 | 
             
                  else
         | 
| 127 80 | 
             
                    Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
         | 
| 128 81 | 
             
                  end
         | 
| @@ -159,13 +112,14 @@ module Mutant | |
| 159 112 | 
             
                    Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
         | 
| 160 113 | 
             
                    Transform::Hash.new(
         | 
| 161 114 | 
             
                      optional: [
         | 
| 162 | 
            -
                        Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
         | 
| 115 | 
            +
                        Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
         | 
| 163 116 | 
             
                        Transform::Hash::Key.new('fail_fast',         Transform::BOOLEAN),
         | 
| 164 117 | 
             
                        Transform::Hash::Key.new('includes',          Transform::STRING_ARRAY),
         | 
| 165 118 | 
             
                        Transform::Hash::Key.new('integration',       Transform::STRING),
         | 
| 166 119 | 
             
                        Transform::Hash::Key.new('jobs',              Transform::INTEGER),
         | 
| 167 120 | 
             
                        Transform::Hash::Key.new('mutation_timeout',  Transform::FLOAT),
         | 
| 168 | 
            -
                        Transform::Hash::Key.new('requires',          Transform::STRING_ARRAY)
         | 
| 121 | 
            +
                        Transform::Hash::Key.new('requires',          Transform::STRING_ARRAY),
         | 
| 122 | 
            +
                        Transform::Hash::Key.new('matcher',           Matcher::Config::LOADER)
         | 
| 169 123 | 
             
                      ],
         | 
| 170 124 | 
             
                      required: []
         | 
| 171 125 | 
             
                    ),
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Mutant
         | 
| 4 | 
            +
              class Config
         | 
| 5 | 
            +
                # Configuration of coverge conditions
         | 
| 6 | 
            +
                class CoverageCriteria
         | 
| 7 | 
            +
                  include Anima.new(:process_abort, :test_result, :timeout)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  EMPTY = new(
         | 
| 10 | 
            +
                    process_abort: nil,
         | 
| 11 | 
            +
                    test_result:   nil,
         | 
| 12 | 
            +
                    timeout:       nil
         | 
| 13 | 
            +
                  )
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  DEFAULT = new(
         | 
| 16 | 
            +
                    process_abort: false,
         | 
| 17 | 
            +
                    test_result:   true,
         | 
| 18 | 
            +
                    timeout:       false
         | 
| 19 | 
            +
                  )
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  TRANSFORM =
         | 
| 22 | 
            +
                    Transform::Sequence.new(
         | 
| 23 | 
            +
                      [
         | 
| 24 | 
            +
                        Transform::Hash.new(
         | 
| 25 | 
            +
                          optional: [
         | 
| 26 | 
            +
                            Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
         | 
| 27 | 
            +
                            Transform::Hash::Key.new('test_result',   Transform::BOOLEAN),
         | 
| 28 | 
            +
                            Transform::Hash::Key.new('timeout',       Transform::BOOLEAN)
         | 
| 29 | 
            +
                          ],
         | 
| 30 | 
            +
                          required: []
         | 
| 31 | 
            +
                        ),
         | 
| 32 | 
            +
                        Transform::Hash::Symbolize.new,
         | 
| 33 | 
            +
                        ->(value) { Either::Right.new(DEFAULT.with(**value)) }
         | 
| 34 | 
            +
                      ]
         | 
| 35 | 
            +
                    )
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Merge coverage criteria with other instance
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # Values from the other instance have precedence.
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  # @param [CoverageCriteria] other
         | 
| 42 | 
            +
                  #
         | 
| 43 | 
            +
                  # @return [CoverageCriteria]
         | 
| 44 | 
            +
                  def merge(other)
         | 
| 45 | 
            +
                    self.class.new(
         | 
| 46 | 
            +
                      process_abort: overwrite(other, :process_abort),
         | 
| 47 | 
            +
                      test_result:   overwrite(other, :test_result),
         | 
| 48 | 
            +
                      timeout:       overwrite(other, :timeout)
         | 
| 49 | 
            +
                    )
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                private
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def overwrite(other, attribute_name)
         | 
| 55 | 
            +
                    other_value = other.public_send(attribute_name)
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    other_value.nil? ? public_send(attribute_name) : other_value
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end # CoverageCriteria
         | 
| 60 | 
            +
              end # Config
         | 
| 61 | 
            +
            end # Mutant
         | 
    
        data/lib/mutant/env.rb
    CHANGED
    
    | @@ -43,19 +43,21 @@ module Mutant | |
| 43 43 | 
             
                end
         | 
| 44 44 | 
             
                # rubocop:enable Metrics/MethodLength
         | 
| 45 45 |  | 
| 46 | 
            -
                #  | 
| 46 | 
            +
                # Cover mutation with specific index
         | 
| 47 47 | 
             
                #
         | 
| 48 | 
            -
                # @param [ | 
| 48 | 
            +
                # @param [Integer] mutation_index
         | 
| 49 49 | 
             
                #
         | 
| 50 | 
            -
                # @return [Result:: | 
| 51 | 
            -
                def  | 
| 50 | 
            +
                # @return [Result::MutationIndex]
         | 
| 51 | 
            +
                def cover_index(mutation_index)
         | 
| 52 | 
            +
                  mutation = mutations.fetch(mutation_index)
         | 
| 53 | 
            +
             | 
| 52 54 | 
             
                  start = timer.now
         | 
| 53 55 |  | 
| 54 56 | 
             
                  tests = selections.fetch(mutation.subject)
         | 
| 55 57 |  | 
| 56 | 
            -
                  Result:: | 
| 58 | 
            +
                  Result::MutationIndex.new(
         | 
| 57 59 | 
             
                    isolation_result: run_mutation_tests(mutation, tests),
         | 
| 58 | 
            -
                     | 
| 60 | 
            +
                    mutation_index:   mutation_index,
         | 
| 59 61 | 
             
                    runtime:          timer.now - start
         | 
| 60 62 | 
             
                  )
         | 
| 61 63 | 
             
                end
         | 
    
        data/lib/mutant/expression.rb
    CHANGED
    
    | @@ -4,8 +4,6 @@ module Mutant | |
| 4 4 |  | 
| 5 5 | 
             
              # Abstract base class for match expression
         | 
| 6 6 | 
             
              class Expression
         | 
| 7 | 
            -
                include AbstractType
         | 
| 8 | 
            -
             | 
| 9 7 | 
             
                fragment             = /[A-Za-z][A-Za-z\d_]*/.freeze
         | 
| 10 8 | 
             
                SCOPE_NAME_PATTERN   = /(?<scope_name>#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze
         | 
| 11 9 | 
             
                SCOPE_SYMBOL_PATTERN = '(?<scope_symbol>[.#])'
         | 
| @@ -16,16 +14,6 @@ module Mutant | |
| 16 14 | 
             
                  super.freeze
         | 
| 17 15 | 
             
                end
         | 
| 18 16 |  | 
| 19 | 
            -
                # Syntax of expression
         | 
| 20 | 
            -
                #
         | 
| 21 | 
            -
                # @return [Matcher]
         | 
| 22 | 
            -
                abstract_method :matcher
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                # Syntax of expression
         | 
| 25 | 
            -
                #
         | 
| 26 | 
            -
                # @return [String]
         | 
| 27 | 
            -
                abstract_method :syntax
         | 
| 28 | 
            -
             | 
| 29 17 | 
             
                # Match length with other expression
         | 
| 30 18 | 
             
                #
         | 
| 31 19 | 
             
                # @param [Expression] other
         | 
    
        data/lib/mutant/loader.rb
    CHANGED
    
    
    
        data/lib/mutant/matcher.rb
    CHANGED
    
    | @@ -20,7 +20,7 @@ module Mutant | |
| 20 20 | 
             
                # @return [Matcher]
         | 
| 21 21 | 
             
                def self.from_config(config)
         | 
| 22 22 | 
             
                  Filter.new(
         | 
| 23 | 
            -
                    Chain.new(config. | 
| 23 | 
            +
                    Chain.new(config.subjects.map(&:matcher)),
         | 
| 24 24 | 
             
                    method(:allowed_subject?).curry.call(config)
         | 
| 25 25 | 
             
                  )
         | 
| 26 26 | 
             
                end
         | 
| @@ -42,7 +42,7 @@ module Mutant | |
| 42 42 | 
             
                #
         | 
| 43 43 | 
             
                # @return [Boolean]
         | 
| 44 44 | 
             
                def self.ignore_subject?(config, subject)
         | 
| 45 | 
            -
                  config. | 
| 45 | 
            +
                  config.ignore.any? do |expression|
         | 
| 46 46 | 
             
                    expression.prefix?(subject.expression)
         | 
| 47 47 | 
             
                  end
         | 
| 48 48 | 
             
                end
         | 
| @@ -5,8 +5,8 @@ module Mutant | |
| 5 5 | 
             
                # Subject matcher configuration
         | 
| 6 6 | 
             
                class Config
         | 
| 7 7 | 
             
                  include Adamantium, Anima.new(
         | 
| 8 | 
            -
                    : | 
| 9 | 
            -
                    : | 
| 8 | 
            +
                    :ignore,
         | 
| 9 | 
            +
                    :subjects,
         | 
| 10 10 | 
             
                    :start_expressions,
         | 
| 11 11 | 
             
                    :subject_filters
         | 
| 12 12 | 
             
                  )
         | 
| @@ -17,15 +17,36 @@ module Mutant | |
| 17 17 | 
             
                  ENUM_DELIMITER      = ','
         | 
| 18 18 | 
             
                  EMPTY_ATTRIBUTES    = 'empty'
         | 
| 19 19 | 
             
                  PRESENTATIONS       = IceNine.deep_freeze(
         | 
| 20 | 
            -
                     | 
| 21 | 
            -
                     | 
| 22 | 
            -
                     | 
| 23 | 
            -
                     | 
| 20 | 
            +
                    ignore:            :syntax,
         | 
| 21 | 
            +
                    start_expressions: :syntax,
         | 
| 22 | 
            +
                    subject_filters:   :inspect,
         | 
| 23 | 
            +
                    subjects:          :syntax
         | 
| 24 24 | 
             
                  )
         | 
| 25 25 | 
             
                  private_constant(*constants(false))
         | 
| 26 26 |  | 
| 27 27 | 
             
                  DEFAULT = new(Hash[anima.attribute_names.map { |name| [name, []] }])
         | 
| 28 28 |  | 
| 29 | 
            +
                  expression = Transform::Block.capture(:expression) do |input|
         | 
| 30 | 
            +
                    Mutant::Config::DEFAULT.expression_parser.call(input)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  expression_array = Transform::Array.new(expression)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  LOADER =
         | 
| 36 | 
            +
                    Transform::Sequence.new(
         | 
| 37 | 
            +
                      [
         | 
| 38 | 
            +
                        Transform::Hash.new(
         | 
| 39 | 
            +
                          optional: [
         | 
| 40 | 
            +
                            Transform::Hash::Key.new('subjects', expression_array),
         | 
| 41 | 
            +
                            Transform::Hash::Key.new('ignore', expression_array)
         | 
| 42 | 
            +
                          ],
         | 
| 43 | 
            +
                          required: []
         | 
| 44 | 
            +
                        ),
         | 
| 45 | 
            +
                        Transform::Hash::Symbolize.new,
         | 
| 46 | 
            +
                        ->(attributes) { Either::Right.new(DEFAULT.with(attributes)) }
         | 
| 47 | 
            +
                      ]
         | 
| 48 | 
            +
                    )
         | 
| 49 | 
            +
             | 
| 29 50 | 
             
                  # Inspection string
         | 
| 30 51 | 
             
                  #
         | 
| 31 52 | 
             
                  # @return [String]
         | 
| @@ -37,7 +37,7 @@ module Mutant | |
| 37 37 | 
             
                    # @return [Example]
         | 
| 38 38 | 
             
                    #
         | 
| 39 39 | 
             
                    # @raise [RuntimeError]
         | 
| 40 | 
            -
                    #   in case example cannot be  | 
| 40 | 
            +
                    #   in case the example cannot be built
         | 
| 41 41 | 
             
                    def example
         | 
| 42 42 | 
             
                      fail 'source not defined' unless @source
         | 
| 43 43 |  | 
| @@ -82,6 +82,11 @@ module Mutant | |
| 82 82 | 
             
                      mutation('self')
         | 
| 83 83 | 
             
                    end
         | 
| 84 84 |  | 
| 85 | 
            +
                    def regexp_mutations
         | 
| 86 | 
            +
                      mutation('//')
         | 
| 87 | 
            +
                      mutation('/nomatch\A/')
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
             | 
| 85 90 | 
             
                    def node(input)
         | 
| 86 91 | 
             
                      case input
         | 
| 87 92 | 
             
                      when String
         | 
| @@ -21,7 +21,7 @@ module Mutant | |
| 21 21 | 
             
                    end
         | 
| 22 22 |  | 
| 23 23 | 
             
                    def mutate_body
         | 
| 24 | 
            -
                      emit_body(nil)
         | 
| 24 | 
            +
                      emit_body(nil) unless unconditional_loop?
         | 
| 25 25 | 
             
                      emit_body(N_RAISE)
         | 
| 26 26 |  | 
| 27 27 | 
             
                      return unless body
         | 
| @@ -31,6 +31,10 @@ module Mutant | |
| 31 31 | 
             
                      mutate_body_receiver
         | 
| 32 32 | 
             
                    end
         | 
| 33 33 |  | 
| 34 | 
            +
                    def unconditional_loop?
         | 
| 35 | 
            +
                      send.eql?(s(:send, nil, :loop))
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 34 38 | 
             
                    def body_has_control?
         | 
| 35 39 | 
             
                      AST.find_last_path(body) do |node|
         | 
| 36 40 | 
             
                        n_break?(node) || n_next?(node)
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Mutant
         | 
| 4 | 
            +
              class Mutator
         | 
| 5 | 
            +
                class Node
         | 
| 6 | 
            +
                  # Mutator for kwargs node
         | 
| 7 | 
            +
                  class Kwargs < self
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    DISALLOW = %i[nil self].freeze
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    private_constant(*constants(false))
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    handle(:kwargs)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  private
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def dispatch
         | 
| 18 | 
            +
                      emit_argument_presence
         | 
| 19 | 
            +
                      emit_argument_mutations
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def emit_argument_presence
         | 
| 23 | 
            +
                      Util::Array::Presence.call(children).each do |children|
         | 
| 24 | 
            +
                        emit_type(*children) unless children.empty?
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def emit_argument_mutations
         | 
| 29 | 
            +
                      children.each_with_index do |child, index|
         | 
| 30 | 
            +
                        Mutator.mutate(child).each do |mutant|
         | 
| 31 | 
            +
                          unless forbid_argument?(mutant)
         | 
| 32 | 
            +
                            emit_child_update(index, mutant)
         | 
| 33 | 
            +
                          end
         | 
| 34 | 
            +
                        end
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    def forbid_argument?(node)
         | 
| 39 | 
            +
                      n_pair?(node) && DISALLOW.include?(node.children.first.type)
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end # Kwargs
         | 
| 42 | 
            +
                end # Node
         | 
| 43 | 
            +
              end # Mutator
         | 
| 44 | 
            +
            end # Mutant
         |