evilution 0.17.0 → 0.19.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +103 -33
  4. data/CHANGELOG.md +50 -0
  5. data/README.md +144 -50
  6. data/lib/evilution/ast/sorbet_sig_detector.rb +52 -0
  7. data/lib/evilution/baseline.rb +9 -1
  8. data/lib/evilution/cli.rb +398 -23
  9. data/lib/evilution/config.rb +10 -2
  10. data/lib/evilution/disable_comment.rb +90 -0
  11. data/lib/evilution/integration/rspec.rb +74 -5
  12. data/lib/evilution/isolation/fork.rb +10 -6
  13. data/lib/evilution/isolation/in_process.rb +14 -10
  14. data/lib/evilution/mcp/session_diff_tool.rb +5 -35
  15. data/lib/evilution/mutator/operator/collection_return.rb +33 -0
  16. data/lib/evilution/mutator/operator/defined_check.rb +16 -0
  17. data/lib/evilution/mutator/operator/keyword_argument.rb +91 -0
  18. data/lib/evilution/mutator/operator/multiple_assignment.rb +47 -0
  19. data/lib/evilution/mutator/operator/regex_capture.rb +43 -0
  20. data/lib/evilution/mutator/operator/scalar_return.rb +37 -0
  21. data/lib/evilution/mutator/operator/splat_operator.rb +46 -0
  22. data/lib/evilution/mutator/operator/yield_statement.rb +51 -0
  23. data/lib/evilution/mutator/registry.rb +9 -1
  24. data/lib/evilution/parallel/pool.rb +7 -53
  25. data/lib/evilution/parallel/work_queue.rb +265 -0
  26. data/lib/evilution/reporter/cli.rb +21 -1
  27. data/lib/evilution/reporter/html.rb +69 -3
  28. data/lib/evilution/reporter/json.rb +23 -2
  29. data/lib/evilution/reporter/suggestion.rb +29 -1
  30. data/lib/evilution/result/mutation_result.rb +5 -2
  31. data/lib/evilution/result/summary.rb +19 -2
  32. data/lib/evilution/runner.rb +123 -12
  33. data/lib/evilution/session/diff.rb +85 -0
  34. data/lib/evilution/spec_resolver.rb +13 -1
  35. data/lib/evilution/version.rb +1 -1
  36. data/lib/evilution.rb +11 -0
  37. data/script/memory_check +22 -0
  38. metadata +14 -2
@@ -12,6 +12,8 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
12
12
  def initialize(test_files: nil, hooks: nil)
13
13
  @test_files = test_files
14
14
  @rspec_loaded = false
15
+ @spec_resolver = Evilution::SpecResolver.new
16
+ @warned_files = Set.new
15
17
  super(hooks: hooks)
16
18
  end
17
19
 
@@ -98,9 +100,14 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
98
100
  # process exits after each run.
99
101
  #
100
102
  # This integration can also be invoked directly (e.g. in specs or alternative
101
- # runners) without fork isolation. RSpec.reset is called here as
102
- # defense-in-depth to clear RSpec state between mutation runs in those cases.
103
- ::RSpec.reset
103
+ # runners) without fork isolation. clear_examples reuses the existing World
104
+ # and Configuration (avoiding per-run instance growth) while clearing loaded
105
+ # example groups, constants, and configuration state.
106
+ if ::RSpec.respond_to?(:clear_examples)
107
+ ::RSpec.clear_examples
108
+ else
109
+ ::RSpec.reset
110
+ end
104
111
 
105
112
  out = StringIO.new
106
113
  err = StringIO.new
@@ -108,11 +115,60 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
108
115
  args = build_args(mutation)
109
116
  command = "rspec #{args.join(" ")}"
110
117
 
118
+ eg_before = snapshot_example_groups
111
119
  status = ::RSpec::Core::Runner.run(args, out, err)
112
120
 
113
121
  { passed: status.zero?, test_command: command }
114
122
  rescue StandardError => e
115
123
  { passed: false, error: e.message, test_command: command }
124
+ ensure
125
+ release_rspec_state(eg_before)
126
+ end
127
+
128
+ def snapshot_example_groups
129
+ groups = Set.new
130
+ ObjectSpace.each_object(Class) do |klass|
131
+ groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
132
+ rescue TypeError # rubocop:disable Lint/SuppressedException
133
+ end
134
+ groups
135
+ end
136
+
137
+ def release_rspec_state(eg_before)
138
+ release_example_groups(eg_before)
139
+ # Remove ExampleGroups constants so the named reference is dropped.
140
+ # We avoid a full RSpec.reset here because it creates new World and
141
+ # Configuration instances each call; the pre-run reset already handles
142
+ # that. Instead, clear the world's example_groups array (which holds
143
+ # direct class references) and the source cache.
144
+ ::RSpec::ExampleGroups.remove_all_constants if defined?(::RSpec::ExampleGroups)
145
+ release_world_example_groups
146
+ end
147
+
148
+ def release_example_groups(eg_before)
149
+ return unless eg_before
150
+
151
+ ObjectSpace.each_object(Class) do |klass|
152
+ next unless klass < ::RSpec::Core::ExampleGroup
153
+ next if eg_before.include?(klass.object_id)
154
+
155
+ # Remove nested module constants (LetDefinitions, NamedSubjectPreventSuper)
156
+ klass.constants(false).each do |const|
157
+ klass.send(:remove_const, const)
158
+ rescue NameError # rubocop:disable Lint/SuppressedException
159
+ end
160
+
161
+ klass.instance_variables.each do |ivar|
162
+ klass.remove_instance_variable(ivar)
163
+ end
164
+ rescue TypeError # rubocop:disable Lint/SuppressedException
165
+ end
166
+ end
167
+
168
+ def release_world_example_groups
169
+ world = ::RSpec.world
170
+ world.instance_variable_get(:@example_groups).clear if world.instance_variable_defined?(:@example_groups)
171
+ world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
116
172
  end
117
173
 
118
174
  def build_args(mutation)
@@ -123,7 +179,20 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
123
179
  def resolve_test_files(mutation)
124
180
  return test_files if test_files
125
181
 
126
- resolved = Evilution::SpecResolver.new.call(mutation.file_path)
127
- resolved ? [resolved] : ["spec"]
182
+ resolved = @spec_resolver.call(mutation.file_path)
183
+ if resolved
184
+ [resolved]
185
+ else
186
+ warn_unresolved_spec(mutation.file_path)
187
+ ["spec"]
188
+ end
189
+ end
190
+
191
+ def warn_unresolved_spec(file_path)
192
+ return if @warned_files.include?(file_path)
193
+
194
+ @warned_files << file_path
195
+ warn "[evilution] No matching spec found for #{file_path}, running full suite. " \
196
+ "Use --spec to specify the spec file."
128
197
  end
129
198
  end
@@ -16,6 +16,7 @@ class Evilution::Isolation::Fork
16
16
  def call(mutation:, test_command:, timeout:)
17
17
  sandbox_dir = Dir.mktmpdir("evilution-run")
18
18
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ parent_rss = Evilution::Memory.rss_kb
19
20
  read_io, write_io = IO.pipe
20
21
 
21
22
  pid = ::Process.fork do
@@ -33,7 +34,7 @@ class Evilution::Isolation::Fork
33
34
  result = wait_for_result(pid, read_io, timeout)
34
35
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
35
36
 
36
- build_mutation_result(mutation, result, duration)
37
+ build_mutation_result(mutation, result, duration, parent_rss)
37
38
  ensure
38
39
  read_io&.close
39
40
  write_io&.close
@@ -67,10 +68,12 @@ class Evilution::Isolation::Fork
67
68
  if read_io.wait_readable(timeout)
68
69
  data = read_io.read
69
70
  ::Process.wait(pid)
70
- return { timeout: false }.merge(Marshal.load(data)) unless data.empty? # rubocop:disable Security/MarshalLoad
71
71
 
72
- ::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
73
- { timeout: false, passed: false, error: "empty result from child" }
72
+ if data.empty?
73
+ { timeout: false, passed: false, error: "empty result from child" }
74
+ else
75
+ { timeout: false }.merge(Marshal.load(data)) # rubocop:disable Security/MarshalLoad
76
+ end
74
77
  else
75
78
  terminate_child(pid)
76
79
  { timeout: true }
@@ -90,7 +93,7 @@ class Evilution::Isolation::Fork
90
93
  ::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
91
94
  end
92
95
 
93
- def build_mutation_result(mutation, result, duration)
96
+ def build_mutation_result(mutation, result, duration, parent_rss_kb)
94
97
  status = if result[:timeout]
95
98
  :timeout
96
99
  elsif result[:error]
@@ -106,7 +109,8 @@ class Evilution::Isolation::Fork
106
109
  status: status,
107
110
  duration: duration,
108
111
  test_command: result[:test_command],
109
- child_rss_kb: result[:child_rss_kb]
112
+ child_rss_kb: result[:child_rss_kb],
113
+ parent_rss_kb: parent_rss_kb
110
114
  )
111
115
  end
112
116
  end
@@ -7,6 +7,13 @@ require_relative "../result/mutation_result"
7
7
  require_relative "../isolation"
8
8
 
9
9
  class Evilution::Isolation::InProcess
10
+ @null_out = File.open(File::NULL, "w")
11
+ @null_err = File.open(File::NULL, "w")
12
+
13
+ class << self
14
+ attr_reader :null_out, :null_err
15
+ end
16
+
10
17
  def call(mutation:, test_command:, timeout:)
11
18
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
19
  rss_before = Evilution::Memory.rss_kb
@@ -15,7 +22,7 @@ class Evilution::Isolation::InProcess
15
22
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
16
23
  delta = compute_memory_delta(rss_before, rss_after, result)
17
24
 
18
- build_mutation_result(mutation, result, duration, rss_after, delta)
25
+ build_mutation_result(mutation, result, duration, rss_before, rss_after, delta)
19
26
  end
20
27
 
21
28
  private
@@ -34,13 +41,9 @@ class Evilution::Isolation::InProcess
34
41
  def suppress_output
35
42
  saved_stdout = $stdout
36
43
  saved_stderr = $stderr
37
- File.open(File::NULL, "w") do |null_out|
38
- File.open(File::NULL, "w") do |null_err|
39
- $stdout = null_out
40
- $stderr = null_err
41
- yield
42
- end
43
- end
44
+ $stdout = self.class.null_out
45
+ $stderr = self.class.null_err
46
+ yield
44
47
  ensure
45
48
  $stdout = saved_stdout
46
49
  $stderr = saved_stderr
@@ -53,7 +56,7 @@ class Evilution::Isolation::InProcess
53
56
  rss_after - rss_before
54
57
  end
55
58
 
56
- def build_mutation_result(mutation, result, duration, rss_after, memory_delta_kb)
59
+ def build_mutation_result(mutation, result, duration, rss_before, rss_after, memory_delta_kb)
57
60
  status = if result[:timeout]
58
61
  :timeout
59
62
  elsif result[:error]
@@ -70,7 +73,8 @@ class Evilution::Isolation::InProcess
70
73
  duration: duration,
71
74
  test_command: result[:test_command],
72
75
  child_rss_kb: rss_after,
73
- memory_delta_kb: memory_delta_kb
76
+ memory_delta_kb: memory_delta_kb,
77
+ parent_rss_kb: rss_before
74
78
  )
75
79
  end
76
80
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "mcp"
5
5
  require_relative "../session/store"
6
+ require_relative "../session/diff"
6
7
 
7
8
  require_relative "../mcp"
8
9
 
@@ -33,7 +34,10 @@ class Evilution::MCP::SessionDiffTool < MCP::Tool
33
34
  base_data = store.load(base)
34
35
  head_data = store.load(head)
35
36
 
36
- ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(build_diff(base_data, head_data)) }])
37
+ diff = Evilution::Session::Diff.new
38
+ result = diff.call(base_data, head_data)
39
+
40
+ ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(result.to_h) }])
37
41
  rescue Evilution::Error => e
38
42
  error_response("not_found", e.message)
39
43
  rescue ::JSON::ParserError => e
@@ -45,40 +49,6 @@ class Evilution::MCP::SessionDiffTool < MCP::Tool
45
49
 
46
50
  private
47
51
 
48
- def build_diff(base_data, head_data)
49
- base_survivors = base_data["survived"] || []
50
- head_survivors = head_data["survived"] || []
51
-
52
- base_keys = base_survivors.to_set { |m| mutation_key(m) }
53
- head_keys = head_survivors.to_set { |m| mutation_key(m) }
54
-
55
- {
56
- "summary" => build_summary_diff(base_data, head_data),
57
- "fixed" => base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
58
- "new_survivors" => head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
59
- "persistent" => head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
60
- }
61
- end
62
-
63
- def build_summary_diff(base_data, head_data)
64
- base_summary = base_data["summary"] || {}
65
- head_summary = head_data["summary"] || {}
66
- base_score = base_summary["score"] || 0.0
67
- head_score = head_summary["score"] || 0.0
68
-
69
- {
70
- "base_score" => base_score,
71
- "head_score" => head_score,
72
- "score_delta" => (head_score - base_score).round(4),
73
- "base_survived" => base_summary["survived"] || 0,
74
- "head_survived" => head_summary["survived"] || 0
75
- }
76
- end
77
-
78
- def mutation_key(mutation)
79
- [mutation["operator"], mutation["file"], mutation["line"], mutation["subject"]]
80
- end
81
-
82
52
  def error_response(type, message)
83
53
  ::MCP::Tool::Response.new(
84
54
  [{ type: "text", text: ::JSON.generate({ error: { type: type, message: message } }) }],
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::CollectionReturn < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ body = node.body
8
+ if body.is_a?(Prism::StatementsNode) && body.body.length > 1
9
+ return_node = body.body.last
10
+ replacement = collection_replacement(return_node)
11
+
12
+ if replacement
13
+ add_mutation(
14
+ offset: body.location.start_offset,
15
+ length: body.location.length,
16
+ replacement: replacement,
17
+ node: node
18
+ )
19
+ end
20
+ end
21
+
22
+ super
23
+ end
24
+
25
+ def collection_replacement(node)
26
+ case node
27
+ when Prism::ArrayNode
28
+ "[]" if node.elements.any?
29
+ when Prism::HashNode
30
+ "{}" if node.elements.any?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::DefinedCheck < Evilution::Mutator::Base
6
+ def visit_defined_node(node)
7
+ add_mutation(
8
+ offset: node.location.start_offset,
9
+ length: node.location.length,
10
+ replacement: "true",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ params = node.parameters
8
+ if params
9
+ mutate_optional_keyword_defaults(params)
10
+ mutate_optional_keyword_removal(params)
11
+ mutate_keyword_rest_removal(params)
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def mutate_optional_keyword_defaults(params)
20
+ params.keywords.each do |kw|
21
+ next unless kw.is_a?(Prism::OptionalKeywordParameterNode)
22
+
23
+ name_loc = kw.name_loc
24
+ kw_loc = kw.location
25
+
26
+ add_mutation(
27
+ offset: kw_loc.start_offset,
28
+ length: kw_loc.length,
29
+ replacement: @file_source[name_loc.start_offset...name_loc.end_offset],
30
+ node: kw
31
+ )
32
+ end
33
+ end
34
+
35
+ def mutate_optional_keyword_removal(params)
36
+ all_params = collect_all_params(params)
37
+ return if all_params.length < 2
38
+
39
+ params.keywords.each do |kw|
40
+ next unless kw.is_a?(Prism::OptionalKeywordParameterNode)
41
+
42
+ remaining = all_params.reject { |p| p.equal?(kw) }
43
+ replacement = remaining.map(&:slice).join(", ")
44
+
45
+ add_mutation(
46
+ offset: params.location.start_offset,
47
+ length: params.location.length,
48
+ replacement: replacement,
49
+ node: kw
50
+ )
51
+ end
52
+ end
53
+
54
+ def mutate_keyword_rest_removal(params)
55
+ kr = params.keyword_rest
56
+ return unless kr.is_a?(Prism::KeywordRestParameterNode)
57
+
58
+ all_params = collect_all_params(params)
59
+
60
+ if all_params.length < 2
61
+ add_mutation(
62
+ offset: kr.location.start_offset,
63
+ length: kr.location.length,
64
+ replacement: "",
65
+ node: kr
66
+ )
67
+ else
68
+ remaining = all_params.reject { |p| p.equal?(kr) }
69
+ replacement = remaining.map(&:slice).join(", ")
70
+
71
+ add_mutation(
72
+ offset: params.location.start_offset,
73
+ length: params.location.length,
74
+ replacement: replacement,
75
+ node: kr
76
+ )
77
+ end
78
+ end
79
+
80
+ def collect_all_params(params)
81
+ result = []
82
+ result.concat(params.requireds)
83
+ result.concat(params.optionals)
84
+ result << params.rest if params.rest
85
+ result.concat(params.posts)
86
+ result.concat(params.keywords)
87
+ result << params.keyword_rest if params.keyword_rest
88
+ result << params.block if params.block
89
+ result
90
+ end
91
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::MultipleAssignment < Evilution::Mutator::Base
6
+ def visit_multi_write_node(node)
7
+ lefts = node.lefts
8
+ values = node.value.is_a?(Prism::ArrayNode) ? node.value.elements : nil
9
+
10
+ if values && lefts.length == values.length && lefts.length >= 2 && node.rest.nil?
11
+ mutate_target_removal(node, lefts, values)
12
+ mutate_swap(node, lefts, values) if lefts.length == 2
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ private
19
+
20
+ def mutate_target_removal(node, lefts, values)
21
+ lefts.each_index do |i|
22
+ remaining_lefts = lefts.each_with_index.filter_map { |l, j| l.slice if j != i }
23
+ remaining_values = values.each_with_index.filter_map { |v, j| v.slice if j != i }
24
+
25
+ replacement = "#{remaining_lefts.join(", ")} = #{remaining_values.join(", ")}"
26
+
27
+ add_mutation(
28
+ offset: node.location.start_offset,
29
+ length: node.location.length,
30
+ replacement: replacement,
31
+ node: node
32
+ )
33
+ end
34
+ end
35
+
36
+ def mutate_swap(node, lefts, values)
37
+ swapped_lefts = "#{lefts[1].slice}, #{lefts[0].slice}"
38
+ replacement = "#{swapped_lefts} = #{values.map(&:slice).join(", ")}"
39
+
40
+ add_mutation(
41
+ offset: node.location.start_offset,
42
+ length: node.location.length,
43
+ replacement: replacement,
44
+ node: node
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RegexCapture < Evilution::Mutator::Base
6
+ def visit_numbered_reference_read_node(node)
7
+ mutate_replace_with_nil(node)
8
+ mutate_swap_number(node)
9
+
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def mutate_replace_with_nil(node)
16
+ add_mutation(
17
+ offset: node.location.start_offset,
18
+ length: node.location.length,
19
+ replacement: "nil",
20
+ node: node
21
+ )
22
+ end
23
+
24
+ def mutate_swap_number(node)
25
+ number = node.number
26
+
27
+ if number > 1
28
+ add_mutation(
29
+ offset: node.location.start_offset,
30
+ length: node.location.length,
31
+ replacement: "$#{number - 1}",
32
+ node: node
33
+ )
34
+ end
35
+
36
+ add_mutation(
37
+ offset: node.location.start_offset,
38
+ length: node.location.length,
39
+ replacement: "$#{number + 1}",
40
+ node: node
41
+ )
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::ScalarReturn < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ body = node.body
8
+ if body.is_a?(Prism::StatementsNode) && body.body.length > 1
9
+ return_node = body.body.last
10
+ replacement = scalar_replacement(return_node)
11
+
12
+ if replacement
13
+ add_mutation(
14
+ offset: body.location.start_offset,
15
+ length: body.location.length,
16
+ replacement: replacement,
17
+ node: node
18
+ )
19
+ end
20
+ end
21
+
22
+ super
23
+ end
24
+
25
+ private
26
+
27
+ def scalar_replacement(node)
28
+ case node
29
+ when Prism::StringNode
30
+ '""' unless node.content.empty?
31
+ when Prism::IntegerNode
32
+ "0" unless node.value.zero?
33
+ when Prism::FloatNode
34
+ "0.0" unless node.value.zero?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
6
+ def visit_splat_node(node)
7
+ mutate_remove_splat(node) if node.expression
8
+
9
+ super
10
+ end
11
+
12
+ def visit_hash_node(node)
13
+ node.elements.each { |el| hash_elements.add(el) }
14
+ super
15
+ end
16
+
17
+ def visit_assoc_splat_node(node)
18
+ mutate_remove_double_splat(node) if node.value && !hash_elements.include?(node)
19
+
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ def hash_elements
26
+ @hash_elements ||= Set.new.compare_by_identity
27
+ end
28
+
29
+ def mutate_remove_splat(node)
30
+ add_mutation(
31
+ offset: node.location.start_offset,
32
+ length: node.location.length,
33
+ replacement: node.expression.slice,
34
+ node: node
35
+ )
36
+ end
37
+
38
+ def mutate_remove_double_splat(node)
39
+ add_mutation(
40
+ offset: node.location.start_offset,
41
+ length: node.location.length,
42
+ replacement: node.value.slice,
43
+ node: node
44
+ )
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::YieldStatement < Evilution::Mutator::Base
6
+ def visit_yield_node(node)
7
+ mutate_remove_yield(node)
8
+
9
+ if node.arguments
10
+ mutate_remove_arguments(node)
11
+ mutate_replace_value_with_nil(node)
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def mutate_remove_yield(node)
20
+ add_mutation(
21
+ offset: node.location.start_offset,
22
+ length: node.location.length,
23
+ replacement: "nil",
24
+ node: node
25
+ )
26
+ end
27
+
28
+ def mutate_remove_arguments(node)
29
+ add_mutation(
30
+ offset: node.location.start_offset,
31
+ length: node.location.length,
32
+ replacement: "yield",
33
+ node: node
34
+ )
35
+ end
36
+
37
+ def mutate_replace_value_with_nil(node)
38
+ replacement = if node.lparen_loc
39
+ "yield(nil)"
40
+ else
41
+ "yield nil"
42
+ end
43
+
44
+ add_mutation(
45
+ offset: node.location.start_offset,
46
+ length: node.location.length,
47
+ replacement: replacement,
48
+ node: node
49
+ )
50
+ end
51
+ end
@@ -57,7 +57,15 @@ class Evilution::Mutator::Registry
57
57
  Evilution::Mutator::Operator::IndexAssignmentRemoval,
58
58
  Evilution::Mutator::Operator::PatternMatchingGuard,
59
59
  Evilution::Mutator::Operator::PatternMatchingAlternative,
60
- Evilution::Mutator::Operator::PatternMatchingArray
60
+ Evilution::Mutator::Operator::PatternMatchingArray,
61
+ Evilution::Mutator::Operator::CollectionReturn,
62
+ Evilution::Mutator::Operator::ScalarReturn,
63
+ Evilution::Mutator::Operator::KeywordArgument,
64
+ Evilution::Mutator::Operator::MultipleAssignment,
65
+ Evilution::Mutator::Operator::YieldStatement,
66
+ Evilution::Mutator::Operator::SplatOperator,
67
+ Evilution::Mutator::Operator::DefinedCheck,
68
+ Evilution::Mutator::Operator::RegexCapture
61
69
  ].each { |op| registry.register(op) }
62
70
  registry
63
71
  end