mutant 0.11.9 → 0.11.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant/ast/pattern/lexer.rb +171 -0
  3. data/lib/mutant/ast/pattern/parser.rb +194 -0
  4. data/lib/mutant/ast/pattern/source.rb +39 -0
  5. data/lib/mutant/ast/pattern/token.rb +15 -0
  6. data/lib/mutant/ast/pattern.rb +125 -0
  7. data/lib/mutant/ast/structure.rb +890 -0
  8. data/lib/mutant/bootstrap.rb +8 -7
  9. data/lib/mutant/cli/command/environment.rb +5 -3
  10. data/lib/mutant/cli/command/util.rb +17 -2
  11. data/lib/mutant/config.rb +76 -40
  12. data/lib/mutant/env.rb +1 -1
  13. data/lib/mutant/license/subscription/opensource.rb +1 -1
  14. data/lib/mutant/matcher/method.rb +8 -1
  15. data/lib/mutant/meta/example.rb +4 -1
  16. data/lib/mutant/mutation/config.rb +36 -0
  17. data/lib/mutant/mutator/node/arguments.rb +1 -1
  18. data/lib/mutant/mutator/node/begin.rb +2 -1
  19. data/lib/mutant/mutator/node/define.rb +5 -2
  20. data/lib/mutant/mutator/node/kwargs.rb +2 -2
  21. data/lib/mutant/mutator/node/literal/regex.rb +1 -1
  22. data/lib/mutant/mutator/node/literal/symbol.rb +2 -2
  23. data/lib/mutant/mutator/node/named_value/constant_assignment.rb +1 -1
  24. data/lib/mutant/mutator/node/named_value/variable_assignment.rb +2 -2
  25. data/lib/mutant/mutator/node/regexp/capture_group.rb +3 -5
  26. data/lib/mutant/mutator/node/regexp/named_group.rb +5 -5
  27. data/lib/mutant/mutator/node/resbody.rb +0 -10
  28. data/lib/mutant/mutator/node.rb +47 -1
  29. data/lib/mutant/mutator/util/array.rb +0 -17
  30. data/lib/mutant/mutator.rb +2 -26
  31. data/lib/mutant/parallel/driver.rb +18 -2
  32. data/lib/mutant/reporter/cli/printer/config.rb +1 -1
  33. data/lib/mutant/runner.rb +4 -0
  34. data/lib/mutant/subject/config.rb +5 -4
  35. data/lib/mutant/subject.rb +4 -1
  36. data/lib/mutant/transform.rb +14 -0
  37. data/lib/mutant/version.rb +1 -1
  38. data/lib/mutant/world.rb +1 -0
  39. data/lib/mutant.rb +9 -1
  40. metadata +9 -2
@@ -24,16 +24,15 @@ module Mutant
24
24
 
25
25
  # Run Bootstrap
26
26
  #
27
- # @param [World] world
28
- # @param [Config] config
27
+ # @param [Env] env
29
28
  #
30
29
  # @return [Either<String, Env>]
31
30
  #
32
31
  # rubocop:disable Metrics/MethodLength
33
- def self.call(world, config)
34
- env = load_hooks(Env.empty(world, config))
32
+ def self.call(env)
33
+ env = load_hooks(env)
35
34
  .tap(&method(:infect))
36
- .with(matchable_scopes: matchable_scopes(world, config))
35
+ .with(matchable_scopes: matchable_scopes(env))
37
36
 
38
37
  subjects = start_subject(env, Matcher.from_config(env.config.matcher).call(env))
39
38
 
@@ -82,8 +81,10 @@ module Mutant
82
81
  end
83
82
  private_class_method :infect
84
83
 
85
- def self.matchable_scopes(world, config)
86
- scopes = world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate|
84
+ def self.matchable_scopes(env)
85
+ config = env.config
86
+
87
+ scopes = env.world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate|
87
88
  expression = expression(config.reporter, config.expression_parser, scope) || next
88
89
  aggregate << Scope.new(scope, expression)
89
90
  end
@@ -23,9 +23,11 @@ module Mutant
23
23
  end
24
24
 
25
25
  def bootstrap
26
- Config.load_config_file(world)
26
+ env = Env.empty(world, @config)
27
+
28
+ Config.load_config_file(env)
27
29
  .fmap(&method(:expand))
28
- .bind { Bootstrap.call(world, @config) }
30
+ .bind { Bootstrap.call(env.with(config: @config)) }
29
31
  end
30
32
 
31
33
  def expand(file_config)
@@ -121,7 +123,7 @@ module Mutant
121
123
  set(jobs: Integer(number))
122
124
  end
123
125
  parser.on('-t', '--mutation-timeout NUMBER', 'Per mutation analysis timeout') do |number|
124
- set(mutation_timeout: Float(number))
126
+ set(mutation: @config.mutation.with(timeout: Float(number)))
125
127
  end
126
128
  end
127
129
  end # Run
@@ -14,6 +14,12 @@ module Mutant
14
14
  OPTIONS = %i[add_target_options].freeze
15
15
 
16
16
  def action
17
+ @ignore_patterns.map! do |syntax|
18
+ AST::Pattern.parse(syntax).from_right do |message|
19
+ return Either::Left.new(message)
20
+ end
21
+ end
22
+
17
23
  @targets.each(&method(:print_mutations))
18
24
  Either::Right.new(nil)
19
25
  end
@@ -50,18 +56,27 @@ module Mutant
50
56
  def initialize(_arguments)
51
57
  super
52
58
 
53
- @targets = []
59
+ @targets = []
60
+ @ignore_patterns = []
54
61
  end
55
62
 
56
63
  def add_target_options(parser)
57
64
  parser.on('-e', '--evaluate SOURCE') do |source|
58
65
  @targets << Target::Source.new(source)
59
66
  end
67
+
68
+ parser.on('-i', '--ignore-pattern AST_PATTERN') do |pattern|
69
+ @ignore_patterns << pattern
70
+ end
60
71
  end
61
72
 
62
73
  def print_mutations(target)
63
74
  world.stdout.puts(target.identification)
64
- Mutator.mutate(target.node).each do |mutation|
75
+
76
+ Mutator::Node.mutate(
77
+ config: Mutant::Mutation::Config::DEFAULT.with(ignore_patterns: @ignore_patterns),
78
+ node: target.node
79
+ ).each do |mutation|
65
80
  Reporter::CLI::Printer::Mutation.call(
66
81
  world.stdout,
67
82
  Mutant::Mutation::Evil.new(target, mutation)
data/lib/mutant/config.rb CHANGED
@@ -19,7 +19,7 @@ module Mutant
19
19
  :isolation,
20
20
  :jobs,
21
21
  :matcher,
22
- :mutation_timeout,
22
+ :mutation,
23
23
  :reporter,
24
24
  :requires,
25
25
  :zombie
@@ -39,6 +39,20 @@ module Mutant
39
39
  mutant.yml
40
40
  ].freeze
41
41
 
42
+ MUTATION_TIMEOUT_DEPRECATION = <<~'MESSAGE'
43
+ Deprecated configuration toplevel key `mutation_timeout` found.
44
+
45
+ This key will be removed in the next mayor version.
46
+ Instead place your mutation timeout configuration under the `mutation` key
47
+ like this:
48
+
49
+ ```
50
+ # mutant.yml
51
+ mutation:
52
+ timeout: 10.0 # float here.
53
+ ```
54
+ MESSAGE
55
+
42
56
  private_constant(*constants(false))
43
57
 
44
58
  # Merge with other config
@@ -59,7 +73,7 @@ module Mutant
59
73
  integration: other.integration || integration,
60
74
  jobs: other.jobs || jobs,
61
75
  matcher: matcher.merge(other.matcher),
62
- mutation_timeout: other.mutation_timeout || mutation_timeout,
76
+ mutation: mutation.merge(other.mutation),
63
77
  requires: requires + other.requires,
64
78
  zombie: zombie || other.zombie
65
79
  )
@@ -69,17 +83,16 @@ module Mutant
69
83
 
70
84
  # Load config file
71
85
  #
72
- # @param [World] world
73
- # @param [Config] config
86
+ # @param [Env] env
74
87
  #
75
88
  # @return [Either<String,Config>]
76
- def self.load_config_file(world)
89
+ def self.load_config_file(env)
77
90
  files = CANDIDATES
78
- .map(&world.pathname.public_method(:new))
91
+ .map(&env.world.pathname.public_method(:new))
79
92
  .select(&:readable?)
80
93
 
81
94
  if files.one?
82
- load_contents(files.first).fmap(&DEFAULT.public_method(:with))
95
+ load_contents(env, files.first).fmap(&DEFAULT.public_method(:with))
83
96
  elsif files.empty?
84
97
  Either::Right.new(DEFAULT)
85
98
  else
@@ -97,14 +110,30 @@ module Mutant
97
110
  )
98
111
  end
99
112
 
100
- def self.load_contents(path)
113
+ def self.load_contents(env, path)
101
114
  Transform::Named
102
- .new(path.to_s, TRANSFORM)
115
+ .new(
116
+ path.to_s,
117
+ sequence(env.config.reporter)
118
+ )
103
119
  .call(path)
104
120
  .lmap(&:compact_message)
105
121
  end
106
122
  private_class_method :load_contents
107
123
 
124
+ def self.sequence(reporter)
125
+ Transform::Sequence.new(
126
+ [
127
+ Transform::Exception.new(SystemCallError, :read.to_proc),
128
+ Transform::Exception.new(YAML::SyntaxError, YAML.public_method(:safe_load)),
129
+ Transform::Primitive.new(Hash),
130
+ Transform::Success.new(->(hash) { deprecations(reporter, hash) }),
131
+ *TRANSFORMS
132
+ ]
133
+ )
134
+ end
135
+ private_class_method :sequence
136
+
108
137
  # The configuration from the environment
109
138
  #
110
139
  # @return [Config]
@@ -139,38 +168,45 @@ module Mutant
139
168
 
140
169
  Either::Right.new(hash)
141
170
  end
142
- TRANSFORM = Transform::Sequence.new(
143
- [
144
- Transform::Exception.new(SystemCallError, :read.to_proc),
145
- Transform::Exception.new(YAML::SyntaxError, YAML.public_method(:safe_load)),
146
- Transform::Hash.new(
147
- optional: [
148
- Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
149
- Transform::Hash::Key.new(
150
- 'environment_variables',
151
- Transform::Sequence.new(
152
- [
153
- Transform::Primitive.new(Hash),
154
- Transform::Block.capture(:environment_variables, &method(:parse_environment_variables))
155
- ]
156
- )
157
- ),
158
- Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
159
- Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
160
- Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
161
- Transform::Hash::Key.new('integration', Transform::STRING),
162
- Transform::Hash::Key.new('jobs', Transform::INTEGER),
163
- Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
164
- Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
165
- Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
166
- ],
167
- required: []
168
- ),
169
- Transform::Hash::Symbolize.new
170
- ]
171
- )
172
171
 
173
- private_constant(:TRANSFORM)
172
+ def self.deprecations(reporter, hash)
173
+ if hash.key?('mutation_timeout')
174
+ reporter.warn(MUTATION_TIMEOUT_DEPRECATION)
175
+
176
+ (hash['mutation'] ||= {})['timeout'] ||= hash.delete('mutation_timeout')
177
+ end
178
+
179
+ hash
180
+ end
181
+
182
+ TRANSFORMS = [
183
+ Transform::Hash.new(
184
+ optional: [
185
+ Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
186
+ Transform::Hash::Key.new(
187
+ 'environment_variables',
188
+ Transform::Sequence.new(
189
+ [
190
+ Transform::Primitive.new(Hash),
191
+ Transform::Block.capture(:environment_variables, &method(:parse_environment_variables))
192
+ ]
193
+ )
194
+ ),
195
+ Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
196
+ Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
197
+ Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
198
+ Transform::Hash::Key.new('integration', Transform::STRING),
199
+ Transform::Hash::Key.new('jobs', Transform::INTEGER),
200
+ Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
201
+ Transform::Hash::Key.new('mutation', Mutation::Config::TRANSFORM),
202
+ Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
203
+ ],
204
+ required: []
205
+ ),
206
+ Transform::Hash::Symbolize.new
207
+ ].freeze
208
+
209
+ private_constant(:TRANSFORMS)
174
210
  end # Config
175
211
  # rubocop:enable Metrics/ClassLength
176
212
  end # Mutant
data/lib/mutant/env.rb CHANGED
@@ -137,7 +137,7 @@ module Mutant
137
137
  private
138
138
 
139
139
  def run_mutation_tests(mutation, tests)
140
- config.isolation.call(config.mutation_timeout) do
140
+ config.isolation.call(config.mutation.timeout) do
141
141
  hooks.run(:mutation_insert_pre, mutation)
142
142
  result = mutation.insert(world.kernel)
143
143
  hooks.run(:mutation_insert_post, mutation)
@@ -18,7 +18,7 @@ module Mutant
18
18
  end
19
19
 
20
20
  def self.parse(input)
21
- new(*input.split('/', 2))
21
+ new(*input.split('/', 2).map(&:downcase))
22
22
  end
23
23
 
24
24
  def self.parse_remote(input)
@@ -99,13 +99,20 @@ module Mutant
99
99
  node = matched_node_path.last || return
100
100
 
101
101
  self.class::SUBJECT_CLASS.new(
102
- config: Subject::Config.parse(ast.comment_associations.fetch(node, [])),
102
+ config: subject_config(node),
103
103
  context: context,
104
104
  node: node,
105
105
  visibility: visibility
106
106
  )
107
107
  end
108
108
 
109
+ def subject_config(node)
110
+ Subject::Config.parse(
111
+ comments: ast.comment_associations.fetch(node, []),
112
+ mutation: env.config.mutation
113
+ )
114
+ end
115
+
109
116
  def matched_node_path
110
117
  AST.find_last_path(ast.node, &method(:match?))
111
118
  end
@@ -55,7 +55,10 @@ module Mutant
55
55
  #
56
56
  # @return [Enumerable<Mutant::Mutation>]
57
57
  def generated
58
- Mutator.mutate(node).map do |node|
58
+ Mutator::Node.mutate(
59
+ config: Mutation::Config::DEFAULT,
60
+ node: node
61
+ ).map do |node|
59
62
  Mutation::Evil.new(self, node)
60
63
  end
61
64
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutation
5
+ class Config
6
+ include Anima.new(:ignore_patterns, :timeout)
7
+
8
+ DEFAULT = new(
9
+ timeout: nil,
10
+ ignore_patterns: []
11
+ )
12
+
13
+ ignore_pattern = Transform::Block.capture('ignore pattern', &AST::Pattern.method(:parse))
14
+
15
+ TRANSFORM = Transform::Sequence.new(
16
+ [
17
+ Transform::Hash.new(
18
+ optional: [
19
+ Transform::Hash::Key.new('ignore_patterns', Transform::Array.new(ignore_pattern)),
20
+ Transform::Hash::Key.new('timeout', Transform::FLOAT)
21
+ ],
22
+ required: []
23
+ ),
24
+ Transform::Hash::Symbolize.new,
25
+ Transform::Success.new(DEFAULT.method(:with))
26
+ ]
27
+ )
28
+
29
+ def merge(other)
30
+ with(
31
+ timeout: other.timeout || timeout
32
+ )
33
+ end
34
+ end # Config
35
+ end # Mutation
36
+ end # Mutant
@@ -50,7 +50,7 @@ module Mutant
50
50
 
51
51
  def emit_argument_mutations
52
52
  children.each_with_index do |child, index|
53
- Mutator.mutate(child).each do |mutant|
53
+ mutate(node: child).each do |mutant|
54
54
  next if invalid_argument_replacement?(mutant, index)
55
55
  emit_child_update(index, mutant)
56
56
  end
@@ -12,8 +12,9 @@ module Mutant
12
12
  private
13
13
 
14
14
  def dispatch
15
+ ignore_single = children.any?(&method(:ignore?))
15
16
  mutate_single_child do |child|
16
- emit(child)
17
+ emit(child) unless ignore_single
17
18
  end
18
19
  end
19
20
  end # Begin
@@ -13,8 +13,11 @@ module Mutant
13
13
  emit_optarg_body_assignments
14
14
  emit_body(N_RAISE)
15
15
  emit_body(N_ZSUPER)
16
- emit_body(nil)
17
- emit_body_mutations if body
16
+
17
+ return unless body && !ignore?(body)
18
+
19
+ emit_body(nil) unless body.children.any?(&method(:ignore?))
20
+ emit_body_mutations
18
21
  end
19
22
 
20
23
  def emit_optarg_body_assignments
@@ -20,14 +20,14 @@ module Mutant
20
20
  end
21
21
 
22
22
  def emit_argument_presence
23
- Util::Array::Presence.call(children).each do |children|
23
+ Util::Array::Presence.call(input: children, parent: nil).each do |children|
24
24
  emit_type(*children) unless children.empty?
25
25
  end
26
26
  end
27
27
 
28
28
  def emit_argument_mutations
29
29
  children.each_with_index do |child, index|
30
- Mutator.mutate(child).each do |mutant|
30
+ mutate(node: child).each do |mutant|
31
31
  unless forbid_argument?(mutant)
32
32
  emit_child_update(index, mutant)
33
33
  end
@@ -33,7 +33,7 @@ module Mutant
33
33
  # Regular expressions with interpolation are skipped.
34
34
  return unless (body_ast = AST::Regexp.expand_regexp_ast(input))
35
35
 
36
- Mutator.mutate(body_ast).each do |mutation|
36
+ mutate(node: body_ast).each do |mutation|
37
37
  source = AST::Regexp.to_expression(mutation).to_s
38
38
  emit_type(s(:str, source), options)
39
39
  end
@@ -15,9 +15,9 @@ module Mutant
15
15
 
16
16
  def dispatch
17
17
  emit_singletons
18
- Util::Symbol.call(value).each(&method(:emit_type))
19
- end
20
18
 
19
+ Util::Symbol.call(input: value, parent: nil).each(&method(:emit_type))
20
+ end
21
21
  end # Symbol
22
22
  end # Literal
23
23
  end # Node
@@ -26,7 +26,7 @@ module Mutant
26
26
  end
27
27
 
28
28
  def mutate_name
29
- Util::Symbol.call(name).each do |name|
29
+ Util::Symbol.call(input: name, parent: nil).each do |name|
30
30
  emit_name(name.upcase)
31
31
  end
32
32
  end
@@ -34,11 +34,11 @@ module Mutant
34
34
  def mutate_name
35
35
  prefix, regexp = MAP.fetch(node.type)
36
36
  stripped = name.to_s.sub(regexp, EMPTY_STRING)
37
- Util::Symbol.call(stripped).each do |name|
37
+
38
+ Util::Symbol.call(input: stripped, parent: nil).each do |name|
38
39
  emit_name(:"#{prefix}#{name}")
39
40
  end
40
41
  end
41
-
42
42
  end # VariableAssignment
43
43
  end # NamedValue
44
44
  end # Node
@@ -8,15 +8,13 @@ module Mutant
8
8
  class CaptureGroup < Node
9
9
  handle(:regexp_capture_group)
10
10
 
11
- children :group
12
-
13
11
  private
14
12
 
15
13
  def dispatch
16
- return unless group
14
+ return if children.empty?
17
15
 
18
- emit(s(:regexp_passive_group, group))
19
- emit_group_mutations
16
+ emit(s(:regexp_passive_group, *children))
17
+ children.each_index(&method(:mutate_child))
20
18
  end
21
19
  end # EndOfLineAnchor
22
20
  end # Regexp
@@ -8,25 +8,25 @@ module Mutant
8
8
  class NamedGroup < Node
9
9
  handle(:regexp_named_group)
10
10
 
11
- children :name, :group
11
+ children :name
12
12
 
13
13
  private
14
14
 
15
15
  def dispatch
16
- return unless group
16
+ return if remaining_children.empty?
17
17
 
18
- emit_group_mutations
18
+ remaining_children_indices.each(&method(:mutate_child))
19
19
 
20
20
  # Allows unused captures to be kept and named if they are explicitly prefixed with an
21
21
  # underscore, like we allow with unused local variables.
22
22
  return if name_underscored?
23
23
 
24
- emit(s(:regexp_passive_group, group))
24
+ emit(s(:regexp_passive_group, *remaining_children))
25
25
  emit_name_underscore_mutation
26
26
  end
27
27
 
28
28
  def emit_name_underscore_mutation
29
- emit_type("_#{name}", group)
29
+ emit_type("_#{name}", *remaining_children)
30
30
  end
31
31
 
32
32
  def name_underscored?
@@ -15,17 +15,7 @@ module Mutant
15
15
  def dispatch
16
16
  emit_assignment(nil)
17
17
  emit_body_mutations if body
18
- mutate_captures
19
18
  end
20
-
21
- def mutate_captures
22
- return unless captures
23
- Util::Array::Element.call(captures.children).each do |matchers|
24
- next if matchers.any?(&method(:n_nil?))
25
- emit_captures(s(:array, *matchers))
26
- end
27
- end
28
-
29
19
  end # Resbody
30
20
  end # Node
31
21
  end # Mutator
@@ -10,8 +10,37 @@ module Mutant
10
10
  include AbstractType, Unparser::Constants
11
11
  include AST::NamedChildren, AST::NodePredicates, AST::Sexp, AST::Nodes
12
12
 
13
+ include anima.add(:config)
14
+
13
15
  TAUTOLOGY = ->(_input) { true }
14
16
 
17
+ REGISTRY = Registry.new(->(_) { Node::Generic })
18
+
19
+ # Lookup and invoke dedicated AST mutator
20
+ #
21
+ # @param input [Parser::AST::Node]
22
+ # @param parent [nil,Mutant::Mutator::Node]
23
+ #
24
+ # @return [Set<Parser::AST::Node>]
25
+ def self.mutate(config:, node:, parent: nil)
26
+ config.ignore_patterns.each do |pattern|
27
+ return Set.new if pattern.match?(node)
28
+ end
29
+
30
+ self::REGISTRY.lookup(node.type).call(
31
+ config: config,
32
+ input: node,
33
+ parent: parent
34
+ )
35
+ end
36
+
37
+ def self.handle(*types)
38
+ types.each do |type|
39
+ self::REGISTRY.register(type, self)
40
+ end
41
+ end
42
+ private_class_method :handle
43
+
15
44
  # Helper to define a named child
16
45
  #
17
46
  # @param [Parser::AST::Node] node
@@ -37,9 +66,13 @@ module Mutant
37
66
  alias_method :node, :input
38
67
  alias_method :dup_node, :dup_input
39
68
 
69
+ def mutate(node:, parent: nil)
70
+ self.class.mutate(config: config, node: node, parent: parent)
71
+ end
72
+
40
73
  def mutate_child(index, &block)
41
74
  block ||= TAUTOLOGY
42
- Mutator.mutate(children.fetch(index), self).each do |mutation|
75
+ mutate(node: children.fetch(index), parent: self).each do |mutation|
43
76
  next unless block.call(mutation)
44
77
  emit_child_update(index, mutation)
45
78
  end
@@ -96,6 +129,19 @@ module Mutant
96
129
  end
97
130
  end
98
131
 
132
+ def run(mutator)
133
+ mutator.call(
134
+ config: config,
135
+ input: input,
136
+ parent: nil
137
+ ).each(&method(:emit))
138
+ end
139
+
140
+ def ignore?(node)
141
+ config.ignore_patterns.any? do |pattern|
142
+ pattern.match?(node)
143
+ end
144
+ end
99
145
  end # Node
100
146
  end # Mutator
101
147
  end # Mutant
@@ -21,23 +21,6 @@ module Mutant
21
21
  end
22
22
 
23
23
  end # Presence
24
-
25
- # Array element mutator
26
- class Element < Util
27
-
28
- private
29
-
30
- def dispatch
31
- input.each_with_index do |element, index|
32
- Mutator.mutate(element).each do |mutation|
33
- dup = dup_input
34
- dup[index] = mutation
35
- emit(dup)
36
- end
37
- end
38
- end
39
-
40
- end # Element
41
24
  end # Array
42
25
  end # Util
43
26
  end # Mutator