evilution 0.19.0 → 0.21.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +35 -35
  4. data/CHANGELOG.md +36 -0
  5. data/README.md +25 -4
  6. data/lib/evilution/cli.rb +11 -2
  7. data/lib/evilution/config.rb +12 -2
  8. data/lib/evilution/equivalent/detector.rb +3 -1
  9. data/lib/evilution/equivalent/heuristic/alias_swap.rb +2 -1
  10. data/lib/evilution/equivalent/heuristic/void_context.rb +77 -0
  11. data/lib/evilution/integration/crash_detector.rb +55 -0
  12. data/lib/evilution/integration/rspec.rb +64 -29
  13. data/lib/evilution/isolation/fork.rb +3 -6
  14. data/lib/evilution/mutator/base.rb +1 -1
  15. data/lib/evilution/mutator/operator/begin_unwrap.rb +21 -0
  16. data/lib/evilution/mutator/operator/block_param_removal.rb +57 -0
  17. data/lib/evilution/mutator/operator/case_when.rb +55 -0
  18. data/lib/evilution/mutator/operator/equality_to_identity.rb +22 -0
  19. data/lib/evilution/mutator/operator/index_to_dig.rb +1 -1
  20. data/lib/evilution/mutator/operator/lambda_body.rb +18 -0
  21. data/lib/evilution/mutator/operator/loop_flip.rb +27 -0
  22. data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -6
  23. data/lib/evilution/mutator/operator/predicate_replacement.rb +27 -0
  24. data/lib/evilution/mutator/operator/retry_removal.rb +16 -0
  25. data/lib/evilution/mutator/operator/send_mutation.rb +8 -1
  26. data/lib/evilution/mutator/operator/string_interpolation.rb +32 -0
  27. data/lib/evilution/mutator/operator/string_literal.rb +18 -0
  28. data/lib/evilution/mutator/registry.rb +19 -3
  29. data/lib/evilution/related_spec_heuristic.rb +63 -0
  30. data/lib/evilution/reporter/cli.rb +14 -8
  31. data/lib/evilution/reporter/html.rb +32 -2
  32. data/lib/evilution/reporter/json.rb +14 -0
  33. data/lib/evilution/result/coverage_gap.rb +35 -0
  34. data/lib/evilution/result/coverage_gap_grouper.rb +22 -0
  35. data/lib/evilution/result/summary.rb +5 -0
  36. data/lib/evilution/runner.rb +28 -1
  37. data/lib/evilution/session/store.rb +13 -0
  38. data/lib/evilution/temp_dir_tracker.rb +39 -0
  39. data/lib/evilution/version.rb +1 -1
  40. data/lib/evilution.rb +9 -0
  41. metadata +17 -2
@@ -25,14 +25,16 @@ class Evilution::Config
25
25
  spec_files: [],
26
26
  ignore_patterns: [],
27
27
  show_disabled: false,
28
- baseline_session: nil
28
+ baseline_session: nil,
29
+ skip_heredoc_literals: false
29
30
  }.freeze
30
31
 
31
32
  attr_reader :target_files, :timeout, :format,
32
33
  :target, :min_score, :integration, :verbose, :quiet,
33
34
  :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
34
35
  :progress, :save_session, :line_ranges, :spec_files, :hooks,
35
- :ignore_patterns, :show_disabled, :baseline_session
36
+ :ignore_patterns, :show_disabled, :baseline_session,
37
+ :skip_heredoc_literals
36
38
 
37
39
  def initialize(**options)
38
40
  file_options = options.delete(:skip_config_file) ? {} : load_config_file
@@ -89,6 +91,10 @@ class Evilution::Config
89
91
  show_disabled
90
92
  end
91
93
 
94
+ def skip_heredoc_literals?
95
+ skip_heredoc_literals
96
+ end
97
+
92
98
  def self.file_options
93
99
  CONFIG_FILES.each do |path|
94
100
  next unless File.exist?(path)
@@ -131,6 +137,9 @@ class Evilution::Config
131
137
  # Generate concrete RSpec test code in suggestions (default: false)
132
138
  # suggest_tests: false
133
139
 
140
+ # Skip all string literal mutations inside heredocs (default: false)
141
+ # skip_heredoc_literals: false
142
+
134
143
  # Hooks: Ruby files returning a Proc, keyed by lifecycle event
135
144
  # hooks:
136
145
  # worker_process_start: config/evilution_hooks/worker_start.rb
@@ -179,6 +188,7 @@ class Evilution::Config
179
188
  @ignore_patterns = validate_ignore_patterns(merged[:ignore_patterns])
180
189
  @show_disabled = merged[:show_disabled]
181
190
  @baseline_session = merged[:baseline_session]
191
+ @skip_heredoc_literals = merged[:skip_heredoc_literals]
182
192
  @hooks = validate_hooks(merged[:hooks])
183
193
  end
184
194
 
@@ -6,6 +6,7 @@ require_relative "heuristic/alias_swap"
6
6
  require_relative "heuristic/dead_code"
7
7
  require_relative "heuristic/arithmetic_identity"
8
8
  require_relative "heuristic/comment_marking"
9
+ require_relative "heuristic/void_context"
9
10
 
10
11
  require_relative "../equivalent"
11
12
 
@@ -38,7 +39,8 @@ class Evilution::Equivalent::Detector
38
39
  Evilution::Equivalent::Heuristic::AliasSwap.new,
39
40
  Evilution::Equivalent::Heuristic::DeadCode.new,
40
41
  Evilution::Equivalent::Heuristic::ArithmeticIdentity.new,
41
- Evilution::Equivalent::Heuristic::CommentMarking.new
42
+ Evilution::Equivalent::Heuristic::CommentMarking.new,
43
+ Evilution::Equivalent::Heuristic::VoidContext.new
42
44
  ]
43
45
  end
44
46
  end
@@ -7,7 +7,8 @@ class Evilution::Equivalent::Heuristic::AliasSwap
7
7
  Set[:detect, :find],
8
8
  Set[:length, :size],
9
9
  Set[:collect, :map],
10
- Set[:count, :length]
10
+ Set[:count, :length],
11
+ Set[:count, :size]
11
12
  ].freeze
12
13
 
13
14
  MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::VoidContext
6
+ # Method pairs where the only difference is the return value.
7
+ # In void context (return value unused), these are equivalent.
8
+ VOID_EQUIVALENT_PAIRS = Set[
9
+ Set[:each, :map],
10
+ Set[:each, :reverse_each]
11
+ ].freeze
12
+
13
+ MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
14
+
15
+ def match?(mutation)
16
+ return false unless MATCHING_OPERATORS.include?(mutation.operator_name)
17
+
18
+ pair = extract_method_pair(mutation.diff)
19
+ return false unless pair
20
+ return false unless VOID_EQUIVALENT_PAIRS.include?(pair)
21
+
22
+ void_context?(mutation)
23
+ end
24
+
25
+ private
26
+
27
+ def extract_method_pair(diff)
28
+ removed = extract_method(diff, "- ")
29
+ added = extract_method(diff, "+ ")
30
+ return nil unless removed && added
31
+
32
+ Set[removed.to_sym, added.to_sym]
33
+ end
34
+
35
+ def extract_method(diff, prefix)
36
+ line = diff.split("\n").find { |l| l.start_with?(prefix) }
37
+ return nil unless line
38
+
39
+ match = line.match(/\.(\w+)(?:[\s({]|$)/)
40
+ match && match[1]
41
+ end
42
+
43
+ def void_context?(mutation)
44
+ node = mutation.subject.node
45
+ return false unless node
46
+
47
+ body = node.body
48
+ return false unless body.is_a?(Prism::StatementsNode)
49
+
50
+ statements = body.body
51
+ call_node = find_call_at_line(statements, mutation.line)
52
+ return false unless call_node
53
+
54
+ # The call is in void context if:
55
+ # 1. It's a direct statement (not wrapped in assignment)
56
+ # 2. It's not the last statement in the method body
57
+ statement_index = statements.index { |s| contains_line?(s, mutation.line) && direct_call?(s) }
58
+ return false unless statement_index
59
+
60
+ statement_index < statements.length - 1
61
+ end
62
+
63
+ def find_call_at_line(statements, line)
64
+ statements.each do |stmt|
65
+ return stmt if stmt.is_a?(Prism::CallNode) && stmt.location.start_line == line
66
+ end
67
+ nil
68
+ end
69
+
70
+ def direct_call?(statement)
71
+ statement.is_a?(Prism::CallNode)
72
+ end
73
+
74
+ def contains_line?(node, line)
75
+ line.between?(node.location.start_line, node.location.end_line)
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration"
4
+
5
+ class Evilution::Integration::CrashDetector
6
+ def self.register_with_rspec
7
+ ::RSpec::Core::Formatters.register self, :example_failed
8
+ end
9
+
10
+ def initialize(_output)
11
+ reset
12
+ end
13
+
14
+ def reset
15
+ @assertion_failures = 0
16
+ @crashes = []
17
+ end
18
+
19
+ def example_failed(notification)
20
+ exception = notification.example.exception
21
+
22
+ if assertion_exception?(exception)
23
+ @assertion_failures += 1
24
+ else
25
+ @crashes << exception
26
+ end
27
+ end
28
+
29
+ def has_assertion_failure? # rubocop:disable Naming/PredicatePrefix
30
+ @assertion_failures.positive?
31
+ end
32
+
33
+ def has_crash? # rubocop:disable Naming/PredicatePrefix
34
+ @crashes.any?
35
+ end
36
+
37
+ def only_crashes?
38
+ @crashes.any? && @assertion_failures.zero?
39
+ end
40
+
41
+ def crash_summary
42
+ return nil if @crashes.empty?
43
+
44
+ types = @crashes.map { |e| e.class.name }.uniq
45
+ "#{types.join(", ")} (#{@crashes.length} crash#{"es" unless @crashes.length == 1})"
46
+ end
47
+
48
+ private
49
+
50
+ def assertion_exception?(exception)
51
+ exception.is_a?(::RSpec::Expectations::ExpectationNotMetError) ||
52
+ (defined?(::RSpec::Mocks::MockExpectationError) &&
53
+ exception.is_a?(::RSpec::Mocks::MockExpectationError))
54
+ end
55
+ end
@@ -4,7 +4,10 @@ require "fileutils"
4
4
  require "stringio"
5
5
  require "tmpdir"
6
6
  require_relative "base"
7
+ require_relative "crash_detector"
7
8
  require_relative "../spec_resolver"
9
+ require_relative "../related_spec_heuristic"
10
+ require_relative "../temp_dir_tracker"
8
11
 
9
12
  require_relative "../integration"
10
13
 
@@ -13,14 +16,14 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
13
16
  @test_files = test_files
14
17
  @rspec_loaded = false
15
18
  @spec_resolver = Evilution::SpecResolver.new
19
+ @related_spec_heuristic = Evilution::RelatedSpecHeuristic.new
20
+ @crash_detector = nil
16
21
  @warned_files = Set.new
17
22
  super(hooks: hooks)
18
23
  end
19
24
 
20
25
  def call(mutation)
21
- @original_content = nil
22
26
  @temp_dir = nil
23
- @lock_file = nil
24
27
  ensure_rspec_loaded
25
28
  @hooks.fire(:mutation_insert_pre, mutation: mutation, file_path: mutation.file_path) if @hooks
26
29
  apply_mutation(mutation)
@@ -39,6 +42,7 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
39
42
 
40
43
  @hooks.fire(:setup_integration_pre, integration: :rspec) if @hooks
41
44
  require "rspec/core"
45
+ Evilution::Integration::CrashDetector.register_with_rspec
42
46
  @rspec_loaded = true
43
47
  @hooks.fire(:setup_integration_post, integration: :rspec) if @hooks
44
48
  rescue LoadError => e
@@ -46,51 +50,60 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
46
50
  end
47
51
 
48
52
  def apply_mutation(mutation)
53
+ @temp_dir = Dir.mktmpdir("evilution")
54
+ Evilution::TempDirTracker.register(@temp_dir)
55
+ @displaced_feature = nil
49
56
  subpath = resolve_require_subpath(mutation.file_path)
50
57
 
51
58
  if subpath
52
- @temp_dir = Dir.mktmpdir("evilution")
53
59
  dest = File.join(@temp_dir, subpath)
54
60
  FileUtils.mkdir_p(File.dirname(dest))
55
61
  File.write(dest, mutation.mutated_source)
56
62
  $LOAD_PATH.unshift(@temp_dir)
63
+ displace_loaded_feature(mutation.file_path)
57
64
  else
58
- # Fallback: direct write when file isn't under any $LOAD_PATH entry.
59
- # Acquire an exclusive lock to prevent concurrent workers from corrupting the file.
60
- lock_path = File.join(Dir.tmpdir, "evilution-#{File.expand_path(mutation.file_path).hash.abs}.lock")
61
- @lock_file = File.open(lock_path, File::CREAT | File::RDWR)
62
- @lock_file.flock(File::LOCK_EX)
63
- @original_content = File.read(mutation.file_path)
64
- File.write(mutation.file_path, mutation.mutated_source)
65
+ absolute = File.expand_path(mutation.file_path)
66
+ dest = File.join(@temp_dir, absolute)
67
+ FileUtils.mkdir_p(File.dirname(dest))
68
+ File.write(dest, mutation.mutated_source)
69
+ load(dest)
65
70
  end
66
71
  end
67
72
 
68
- def restore_original(mutation)
69
- if @temp_dir
70
- $LOAD_PATH.delete(@temp_dir)
71
- $LOADED_FEATURES.reject! { |f| f.start_with?(@temp_dir) }
72
- FileUtils.rm_rf(@temp_dir)
73
- @temp_dir = nil
74
- elsif @original_content
75
- File.write(mutation.file_path, @original_content)
76
- @lock_file&.flock(File::LOCK_UN)
77
- @lock_file&.close
78
- @lock_file = nil
79
- end
73
+ def restore_original(mutation) # rubocop:disable Lint/UnusedMethodArgument
74
+ return unless @temp_dir
75
+
76
+ $LOAD_PATH.delete(@temp_dir)
77
+ $LOADED_FEATURES.reject! { |f| f.start_with?(@temp_dir) }
78
+ $LOADED_FEATURES << @displaced_feature if @displaced_feature && !$LOADED_FEATURES.include?(@displaced_feature)
79
+ @displaced_feature = nil
80
+ FileUtils.rm_rf(@temp_dir)
81
+ Evilution::TempDirTracker.unregister(@temp_dir)
82
+ @temp_dir = nil
80
83
  end
81
84
 
82
85
  def resolve_require_subpath(file_path)
83
86
  absolute = File.expand_path(file_path)
87
+ best_subpath = nil
84
88
 
85
89
  $LOAD_PATH.each do |entry|
86
90
  dir = File.expand_path(entry)
87
91
  prefix = dir.end_with?("/") ? dir : "#{dir}/"
88
92
  next unless absolute.start_with?(prefix)
89
93
 
90
- return absolute.delete_prefix(prefix)
94
+ candidate = absolute.delete_prefix(prefix)
95
+ best_subpath = candidate if best_subpath.nil? || candidate.length < best_subpath.length
91
96
  end
92
97
 
93
- nil
98
+ best_subpath
99
+ end
100
+
101
+ def displace_loaded_feature(file_path)
102
+ absolute = File.expand_path(file_path)
103
+ return unless $LOADED_FEATURES.include?(absolute)
104
+
105
+ @displaced_feature = absolute
106
+ $LOADED_FEATURES.delete(absolute)
94
107
  end
95
108
 
96
109
  def run_rspec(mutation)
@@ -115,10 +128,11 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
115
128
  args = build_args(mutation)
116
129
  command = "rspec #{args.join(" ")}"
117
130
 
131
+ detector = reset_crash_detector
118
132
  eg_before = snapshot_example_groups
119
133
  status = ::RSpec::Core::Runner.run(args, out, err)
120
134
 
121
- { passed: status.zero?, test_command: command }
135
+ build_rspec_result(status, command, detector)
122
136
  rescue StandardError => e
123
137
  { passed: false, error: e.message, test_command: command }
124
138
  ensure
@@ -171,6 +185,26 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
171
185
  world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
172
186
  end
173
187
 
188
+ def reset_crash_detector
189
+ if @crash_detector
190
+ @crash_detector.reset
191
+ else
192
+ @crash_detector = Evilution::Integration::CrashDetector.new(StringIO.new)
193
+ ::RSpec.configuration.add_formatter(@crash_detector)
194
+ end
195
+ @crash_detector
196
+ end
197
+
198
+ def build_rspec_result(status, command, detector)
199
+ if status.zero?
200
+ { passed: true, test_command: command }
201
+ elsif detector.only_crashes?
202
+ { passed: false, error: "test crashes: #{detector.crash_summary}", test_command: command }
203
+ else
204
+ { passed: false, test_command: command }
205
+ end
206
+ end
207
+
174
208
  def build_args(mutation)
175
209
  files = resolve_test_files(mutation)
176
210
  ["--format", "progress", "--no-color", "--order", "defined", *files]
@@ -180,12 +214,13 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
180
214
  return test_files if test_files
181
215
 
182
216
  resolved = @spec_resolver.call(mutation.file_path)
183
- if resolved
184
- [resolved]
185
- else
217
+ unless resolved
186
218
  warn_unresolved_spec(mutation.file_path)
187
- ["spec"]
219
+ return ["spec"]
188
220
  end
221
+
222
+ related = @related_spec_heuristic.call(mutation)
223
+ ([resolved] + related).uniq
189
224
  end
190
225
 
191
226
  def warn_unresolved_spec(file_path)
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "tmpdir"
5
5
  require_relative "../memory"
6
+ require_relative "../temp_dir_tracker"
6
7
 
7
8
  require_relative "../isolation"
8
9
 
@@ -44,12 +45,8 @@ class Evilution::Isolation::Fork
44
45
 
45
46
  private
46
47
 
47
- def restore_original_source(mutation)
48
- return if File.read(mutation.file_path) == mutation.original_source
49
-
50
- File.write(mutation.file_path, mutation.original_source)
51
- rescue StandardError => e
52
- warn("Warning: failed to restore #{mutation.file_path}: #{e.message}")
48
+ def restore_original_source(mutation) # rubocop:disable Lint/UnusedMethodArgument
49
+ Evilution::TempDirTracker.cleanup_all
53
50
  end
54
51
 
55
52
  def suppress_child_output
@@ -7,7 +7,7 @@ require_relative "../mutator"
7
7
  class Evilution::Mutator::Base < Prism::Visitor
8
8
  attr_reader :mutations
9
9
 
10
- def initialize
10
+ def initialize(**_options)
11
11
  @mutations = []
12
12
  @subject = nil
13
13
  @file_source = nil
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::BeginUnwrap < Evilution::Mutator::Base
6
+ def visit_begin_node(node)
7
+ return super if node.rescue_clause || node.else_clause || node.ensure_clause
8
+ return super if node.statements.nil?
9
+ return super if node.begin_keyword_loc.nil?
10
+
11
+ body_text = @file_source.byteslice(node.statements.location.start_offset, node.statements.location.length)
12
+ add_mutation(
13
+ offset: node.location.start_offset,
14
+ length: node.location.length,
15
+ replacement: body_text,
16
+ node: node
17
+ )
18
+
19
+ super
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ return super unless node.parameters
8
+ return super unless node.parameters.block
9
+
10
+ if only_block_param?(node.parameters)
11
+ remove_entire_params(node)
12
+ else
13
+ remove_block_param(node)
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ private
20
+
21
+ def only_block_param?(params)
22
+ params.requireds.empty? &&
23
+ params.optionals.empty? &&
24
+ params.keywords.empty? &&
25
+ params.rest.nil? &&
26
+ params.keyword_rest.nil?
27
+ end
28
+
29
+ def remove_entire_params(node)
30
+ start_offset = node.lparen_loc.start_offset
31
+ end_offset = node.rparen_loc.start_offset + node.rparen_loc.length
32
+ add_mutation(
33
+ offset: start_offset,
34
+ length: end_offset - start_offset,
35
+ replacement: "",
36
+ node: node
37
+ )
38
+ end
39
+
40
+ def remove_block_param(node)
41
+ block_loc = node.parameters.block.location
42
+ params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
43
+ block_rel = block_loc.start_offset - node.parameters.location.start_offset
44
+
45
+ # Find the comma before the block param and remove ", &block"
46
+ comma_pos = params_text.rindex(",", block_rel - 1)
47
+ remove_start = node.parameters.location.start_offset + comma_pos
48
+ remove_end = block_loc.start_offset + block_loc.length
49
+
50
+ add_mutation(
51
+ offset: remove_start,
52
+ length: remove_end - remove_start,
53
+ replacement: "",
54
+ node: node
55
+ )
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
6
+ def visit_case_node(node)
7
+ remove_when_branches(node)
8
+ replace_when_bodies(node)
9
+ remove_else_branch(node)
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def remove_when_branches(node)
17
+ return if node.conditions.length < 2
18
+
19
+ node.conditions.each do |when_node|
20
+ add_mutation(
21
+ offset: when_node.location.start_offset,
22
+ length: when_node.location.length,
23
+ replacement: "",
24
+ node: when_node
25
+ )
26
+ end
27
+ end
28
+
29
+ def replace_when_bodies(node)
30
+ node.conditions.each do |when_node|
31
+ next if when_node.statements.nil? || when_node.statements.body.empty?
32
+
33
+ add_mutation(
34
+ offset: when_node.statements.location.start_offset,
35
+ length: when_node.statements.location.length,
36
+ replacement: "nil",
37
+ node: when_node
38
+ )
39
+ end
40
+ end
41
+
42
+ def remove_else_branch(node)
43
+ return if node.else_clause.nil?
44
+ return if node.else_clause.statements.nil?
45
+
46
+ start_offset = node.else_clause.else_keyword_loc.start_offset
47
+ end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
48
+ add_mutation(
49
+ offset: start_offset,
50
+ length: end_offset - start_offset,
51
+ replacement: "",
52
+ node: node.else_clause
53
+ )
54
+ end
55
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.name == :== && node.receiver && node.arguments
8
+ receiver_text = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
9
+ arg = node.arguments.arguments.first
10
+ arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
11
+
12
+ add_mutation(
13
+ offset: node.location.start_offset,
14
+ length: node.location.length,
15
+ replacement: "#{receiver_text}.equal?(#{arg_text})",
16
+ node: node
17
+ )
18
+ end
19
+
20
+ super
21
+ end
22
+ end
@@ -3,7 +3,7 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
6
- def initialize
6
+ def initialize(**options)
7
7
  super
8
8
  @consumed = Set.new
9
9
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::LambdaBody < Evilution::Mutator::Base
6
+ def visit_lambda_node(node)
7
+ if node.body
8
+ add_mutation(
9
+ offset: node.body.location.start_offset,
10
+ length: node.body.location.length,
11
+ replacement: "nil",
12
+ node: node
13
+ )
14
+ end
15
+
16
+ super
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::LoopFlip < Evilution::Mutator::Base
6
+ def visit_while_node(node)
7
+ add_mutation(
8
+ offset: node.keyword_loc.start_offset,
9
+ length: node.keyword_loc.length,
10
+ replacement: "until",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+
17
+ def visit_until_node(node)
18
+ add_mutation(
19
+ offset: node.keyword_loc.start_offset,
20
+ length: node.keyword_loc.length,
21
+ replacement: "while",
22
+ node: node
23
+ )
24
+
25
+ super
26
+ end
27
+ end
@@ -3,14 +3,18 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
6
+ REPLACEMENTS = %w[nil self super].freeze
7
+
6
8
  def visit_def_node(node)
7
9
  if node.body
8
- add_mutation(
9
- offset: node.body.location.start_offset,
10
- length: node.body.location.length,
11
- replacement: "nil",
12
- node: node
13
- )
10
+ REPLACEMENTS.each do |replacement|
11
+ add_mutation(
12
+ offset: node.body.location.start_offset,
13
+ length: node.body.location.length,
14
+ replacement: replacement,
15
+ node: node
16
+ )
17
+ end
14
18
  end
15
19
 
16
20
  super
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::PredicateReplacement < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.name.to_s.end_with?("?")
8
+ loc = node.location
9
+
10
+ add_mutation(
11
+ offset: loc.start_offset,
12
+ length: loc.length,
13
+ replacement: "true",
14
+ node: node
15
+ )
16
+
17
+ add_mutation(
18
+ offset: loc.start_offset,
19
+ length: loc.length,
20
+ replacement: "false",
21
+ node: node
22
+ )
23
+ end
24
+
25
+ super
26
+ end
27
+ end