mutant 0.11.11 → 0.11.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant/ast/find_metaclass_containing.rb +6 -6
  3. data/lib/mutant/ast/meta/const.rb +1 -1
  4. data/lib/mutant/ast/meta/optarg.rb +1 -1
  5. data/lib/mutant/ast/meta/resbody.rb +1 -1
  6. data/lib/mutant/ast/meta/send.rb +1 -1
  7. data/lib/mutant/ast/meta/symbol.rb +1 -1
  8. data/lib/mutant/ast/meta.rb +1 -1
  9. data/lib/mutant/ast/named_children.rb +1 -1
  10. data/lib/mutant/ast/node_predicates.rb +1 -1
  11. data/lib/mutant/ast/nodes.rb +1 -1
  12. data/lib/mutant/ast/pattern/lexer.rb +1 -1
  13. data/lib/mutant/ast/pattern/parser.rb +1 -1
  14. data/lib/mutant/ast/pattern/source.rb +1 -1
  15. data/lib/mutant/ast/pattern/token.rb +1 -1
  16. data/lib/mutant/ast/pattern.rb +1 -1
  17. data/lib/mutant/ast/regexp/transformer/direct.rb +1 -1
  18. data/lib/mutant/ast/regexp/transformer/named_group.rb +1 -1
  19. data/lib/mutant/ast/regexp/transformer/options_group.rb +1 -1
  20. data/lib/mutant/ast/regexp/transformer/quantifier.rb +1 -1
  21. data/lib/mutant/ast/regexp/transformer/recursive.rb +1 -1
  22. data/lib/mutant/ast/regexp/transformer/root.rb +1 -1
  23. data/lib/mutant/ast/regexp/transformer/text.rb +1 -1
  24. data/lib/mutant/ast/regexp/transformer.rb +1 -1
  25. data/lib/mutant/ast/regexp.rb +1 -1
  26. data/lib/mutant/ast/sexp.rb +1 -1
  27. data/lib/mutant/ast/structure.rb +8 -3
  28. data/lib/mutant/ast/types.rb +1 -1
  29. data/lib/mutant/ast.rb +31 -29
  30. data/lib/mutant/bootstrap.rb +8 -7
  31. data/lib/mutant/cli/command/environment.rb +5 -3
  32. data/lib/mutant/cli/command/util.rb +17 -2
  33. data/lib/mutant/config.rb +76 -40
  34. data/lib/mutant/env.rb +1 -1
  35. data/lib/mutant/license/subscription/opensource.rb +13 -3
  36. data/lib/mutant/matcher/method/instance.rb +3 -2
  37. data/lib/mutant/matcher/method/metaclass.rb +4 -3
  38. data/lib/mutant/matcher/method/singleton.rb +3 -2
  39. data/lib/mutant/matcher/method.rb +36 -20
  40. data/lib/mutant/meta/example.rb +4 -1
  41. data/lib/mutant/mutation/config.rb +36 -0
  42. data/lib/mutant/mutator/node/arguments.rb +1 -1
  43. data/lib/mutant/mutator/node/begin.rb +2 -1
  44. data/lib/mutant/mutator/node/block.rb +5 -3
  45. data/lib/mutant/mutator/node/define.rb +5 -2
  46. data/lib/mutant/mutator/node/kwargs.rb +2 -2
  47. data/lib/mutant/mutator/node/literal/regex.rb +1 -1
  48. data/lib/mutant/mutator/node/literal/symbol.rb +2 -2
  49. data/lib/mutant/mutator/node/named_value/constant_assignment.rb +1 -1
  50. data/lib/mutant/mutator/node/named_value/variable_assignment.rb +2 -2
  51. data/lib/mutant/mutator/node/resbody.rb +0 -10
  52. data/lib/mutant/mutator/node.rb +47 -1
  53. data/lib/mutant/mutator/util/array.rb +0 -17
  54. data/lib/mutant/mutator.rb +2 -26
  55. data/lib/mutant/parser.rb +0 -7
  56. data/lib/mutant/reporter/cli/printer/config.rb +1 -1
  57. data/lib/mutant/subject/config.rb +5 -4
  58. data/lib/mutant/subject.rb +4 -1
  59. data/lib/mutant/transform.rb +14 -0
  60. data/lib/mutant/version.rb +1 -1
  61. data/lib/mutant.rb +2 -1
  62. metadata +3 -2
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 major 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)
@@ -39,7 +39,17 @@ module Mutant
39
39
  new(match[:host], match[:path].downcase)
40
40
  end
41
41
  private_class_method :parse_url
42
- end
42
+
43
+ def allow?(other)
44
+ other.host.eql?(host) && path_match?(other.path)
45
+ end
46
+
47
+ private
48
+
49
+ def path_match?(other_path)
50
+ path.eql?(other_path) || (path.end_with?('/*') && other_path.start_with?(path[..-2]))
51
+ end
52
+ end # Opensource
43
53
 
44
54
  def self.from_json(value)
45
55
  new(
@@ -60,7 +70,7 @@ module Mutant
60
70
  private
61
71
 
62
72
  def check_subscription(actual)
63
- if (licensed & actual).any?
73
+ if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } }
64
74
  success
65
75
  else
66
76
  failure(licensed, actual)
@@ -30,8 +30,9 @@ module Mutant
30
30
 
31
31
  # Instance method specific evaluator
32
32
  class Evaluator < Evaluator
33
- SUBJECT_CLASS = Subject::Method::Instance
34
- NAME_INDEX = 0
33
+ MATCH_NODE_TYPE = :def
34
+ NAME_INDEX = 0
35
+ SUBJECT_CLASS = Subject::Method::Instance
35
36
 
36
37
  private
37
38
 
@@ -22,10 +22,11 @@ module Mutant
22
22
  # Metaclass method evaluator
23
23
  class Evaluator < Evaluator
24
24
  # Terminology note: the "receiver" is the `self` in `class << self`
25
- SUBJECT_CLASS = Subject::Method::Metaclass
26
- NAME_INDEX = 0
27
25
  CONST_NAME_INDEX = 1
26
+ MATCH_NODE_TYPE = :def
27
+ NAME_INDEX = 0
28
28
  SCLASS_RECEIVER_INDEX = 0
29
+ SUBJECT_CLASS = Subject::Method::Metaclass
29
30
  RECEIVER_WARNING = 'Can only match :def inside :sclass on ' \
30
31
  ':self or :const, got :sclass on %p ' \
31
32
  'unable to match'
@@ -45,7 +46,7 @@ module Mutant
45
46
  end
46
47
 
47
48
  def metaclass_containing(node)
48
- AST::FindMetaclassContaining.call(ast.node, node)
49
+ AST::FindMetaclassContaining.call(ast, node)
49
50
  end
50
51
 
51
52
  def line?(node)
@@ -18,10 +18,11 @@ module Mutant
18
18
 
19
19
  # Singleton method evaluator
20
20
  class Evaluator < Evaluator
21
- SUBJECT_CLASS = Subject::Method::Singleton
22
- RECEIVER_INDEX = 0
21
+ MATCH_NODE_TYPE = :defs
23
22
  NAME_INDEX = 1
23
+ RECEIVER_INDEX = 0
24
24
  RECEIVER_WARNING = 'Can only match :defs on :self or :const got %p unable to match'
25
+ SUBJECT_CLASS = Subject::Method::Singleton
25
26
 
26
27
  private
27
28
 
@@ -41,23 +41,38 @@ module Mutant
41
41
  #
42
42
  # @return [Enumerable<Subject>]
43
43
  def call
44
- return EMPTY_ARRAY if skip?
44
+ location = source_location
45
+
46
+ if location.nil? || !location.first.end_with?('.rb')
47
+ env.warn(SOURCE_LOCATION_WARNING_FORMAT % target_method)
45
48
 
46
- [subject].compact
49
+ return EMPTY_ARRAY
50
+ end
51
+
52
+ match_view
47
53
  end
48
54
 
49
55
  private
50
56
 
51
- def skip?
52
- location = source_location
53
-
54
- file = location&.first
57
+ def match_view
58
+ return EMPTY_ARRAY if matched_view.nil?
55
59
 
56
- if location.nil? || !file.end_with?('.rb')
57
- env.warn(SOURCE_LOCATION_WARNING_FORMAT % target_method)
58
- elsif matched_node_path.any?(&method(:n_block?))
60
+ if matched_view.path.any?(&:block.public_method(:equal?))
59
61
  env.warn(CLOSURE_WARNING_FORMAT % target_method)
62
+
63
+ return EMPTY_ARRAY
60
64
  end
65
+
66
+ [subject]
67
+ end
68
+
69
+ def subject
70
+ self.class::SUBJECT_CLASS.new(
71
+ config: subject_config(matched_view.node),
72
+ context: context,
73
+ node: matched_view.node,
74
+ visibility: visibility
75
+ )
61
76
  end
62
77
 
63
78
  def method_name
@@ -95,21 +110,22 @@ module Mutant
95
110
  T::Private::Methods.signature_for_method(target_method)
96
111
  end
97
112
 
98
- def subject
99
- node = matched_node_path.last || return
100
-
101
- self.class::SUBJECT_CLASS.new(
102
- config: Subject::Config.parse(ast.comment_associations.fetch(node, [])),
103
- context: context,
104
- node: node,
105
- visibility: visibility
113
+ def subject_config(node)
114
+ Subject::Config.parse(
115
+ comments: ast.comment_associations.fetch(node, []),
116
+ mutation: env.config.mutation
106
117
  )
107
118
  end
108
119
 
109
- def matched_node_path
110
- AST.find_last_path(ast.node, &method(:match?))
120
+ def matched_view
121
+ return if source_location.nil?
122
+
123
+ ast
124
+ .on_line(source_line)
125
+ .select { |view| view.node.type.eql?(self.class::MATCH_NODE_TYPE) && match?(view.node) }
126
+ .last
111
127
  end
112
- memoize :matched_node_path
128
+ memoize :matched_view
113
129
 
114
130
  def visibility
115
131
  # This can be cleaned up once we are on >ruby-3.0
@@ -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
@@ -38,9 +38,11 @@ module Mutant
38
38
  end
39
39
 
40
40
  def body_has_control?
41
- AST.find_last_path(body) do |node|
42
- n_break?(node) || n_next?(node)
43
- end.any?
41
+ AST::Structure.for(body.type).each_node(body) do |node|
42
+ return true if n_break?(node) || n_next?(node)
43
+ end
44
+
45
+ false
44
46
  end
45
47
 
46
48
  def mutate_body_receiver
@@ -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
@@ -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