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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RetryRemoval < Evilution::Mutator::Base
6
+ def visit_retry_node(node)
7
+ add_mutation(
8
+ offset: node.location.start_offset,
9
+ length: node.location.length,
10
+ replacement: "nil",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+ end
@@ -29,7 +29,14 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
29
29
  to_i: [:to_s],
30
30
  to_f: [:to_i],
31
31
  to_a: [:to_h],
32
- to_h: [:to_a]
32
+ to_h: [:to_a],
33
+ downcase: [:upcase],
34
+ upcase: [:downcase],
35
+ strip: %i[lstrip rstrip],
36
+ lstrip: [:strip],
37
+ rstrip: [:strip],
38
+ chomp: [:chop],
39
+ chop: [:chomp]
33
40
  }.freeze
34
41
 
35
42
  def visit_call_node(node)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::StringInterpolation < Evilution::Mutator::Base
6
+ def visit_interpolated_string_node(node)
7
+ mutate_embedded_statements(node)
8
+ super
9
+ end
10
+
11
+ def visit_interpolated_symbol_node(node)
12
+ mutate_embedded_statements(node)
13
+ super
14
+ end
15
+
16
+ private
17
+
18
+ def mutate_embedded_statements(node)
19
+ node.parts.each do |part|
20
+ next unless part.is_a?(Prism::EmbeddedStatementsNode)
21
+ next if part.statements.nil? || part.statements.body.empty?
22
+
23
+ stmt = part.statements
24
+ add_mutation(
25
+ offset: stmt.location.start_offset,
26
+ length: stmt.location.length,
27
+ replacement: "nil",
28
+ node: part
29
+ )
30
+ end
31
+ end
32
+ end
@@ -3,7 +3,25 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::StringLiteral < Evilution::Mutator::Base
6
+ def initialize(skip_heredoc_literals: false, **rest)
7
+ super(**rest)
8
+ @skip_heredoc_literals = skip_heredoc_literals
9
+ end
10
+
11
+ def visit_interpolated_string_node(node)
12
+ return super unless node.heredoc?
13
+ return if @skip_heredoc_literals
14
+
15
+ node.parts.each do |part|
16
+ next if part.is_a?(Prism::StringNode)
17
+
18
+ visit(part)
19
+ end
20
+ end
21
+
6
22
  def visit_string_node(node)
23
+ return super if node.heredoc?
24
+
7
25
  replacement = node.content.empty? ? '"mutation"' : '""'
8
26
 
9
27
  add_mutation(
@@ -65,7 +65,16 @@ class Evilution::Mutator::Registry
65
65
  Evilution::Mutator::Operator::YieldStatement,
66
66
  Evilution::Mutator::Operator::SplatOperator,
67
67
  Evilution::Mutator::Operator::DefinedCheck,
68
- Evilution::Mutator::Operator::RegexCapture
68
+ Evilution::Mutator::Operator::RegexCapture,
69
+ Evilution::Mutator::Operator::LoopFlip,
70
+ Evilution::Mutator::Operator::StringInterpolation,
71
+ Evilution::Mutator::Operator::RetryRemoval,
72
+ Evilution::Mutator::Operator::CaseWhen,
73
+ Evilution::Mutator::Operator::PredicateReplacement,
74
+ Evilution::Mutator::Operator::EqualityToIdentity,
75
+ Evilution::Mutator::Operator::LambdaBody,
76
+ Evilution::Mutator::Operator::BeginUnwrap,
77
+ Evilution::Mutator::Operator::BlockParamRemoval
69
78
  ].each { |op| registry.register(op) }
70
79
  registry
71
80
  end
@@ -79,9 +88,10 @@ class Evilution::Mutator::Registry
79
88
  self
80
89
  end
81
90
 
82
- def mutations_for(subject, filter: nil)
91
+ def mutations_for(subject, filter: nil, operator_options: {})
83
92
  @operators.flat_map do |operator_class|
84
- operator_class.new.call(subject, filter: filter)
93
+ operator = build_operator(operator_class, operator_options)
94
+ operator.call(subject, filter: filter)
85
95
  end
86
96
  end
87
97
 
@@ -92,4 +102,10 @@ class Evilution::Mutator::Registry
92
102
  def operators
93
103
  @operators.dup
94
104
  end
105
+
106
+ private
107
+
108
+ def build_operator(operator_class, options)
109
+ operator_class.new(**options)
110
+ end
95
111
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Evilution::RelatedSpecHeuristic
4
+ RELATED_SPEC_DIRS = %w[
5
+ spec/requests
6
+ spec/integration
7
+ spec/features
8
+ spec/system
9
+ ].freeze
10
+
11
+ INCLUDES_PATTERN = /\bincludes\(/
12
+
13
+ def call(mutation)
14
+ return [] unless includes_mutation?(mutation)
15
+
16
+ domain = extract_domain(mutation.file_path)
17
+ return [] unless domain
18
+
19
+ find_related_specs(domain)
20
+ end
21
+
22
+ private
23
+
24
+ def includes_mutation?(mutation)
25
+ diff = mutation.diff
26
+ return false unless diff
27
+
28
+ diff.split("\n").any? { |line| line.start_with?("- ") && line.match?(INCLUDES_PATTERN) }
29
+ end
30
+
31
+ def extract_domain(file_path)
32
+ normalized = normalize_path(file_path)
33
+
34
+ # Strip common prefixes and get the relative path under app/ or lib/
35
+ relative = normalized
36
+ .delete_prefix("app/controllers/")
37
+ .delete_prefix("app/models/")
38
+ .delete_prefix("app/")
39
+ .delete_prefix("lib/")
40
+
41
+ # Remove .rb extension and _controller suffix
42
+ basename = relative.sub(/\.rb\z/, "")
43
+ basename = basename.sub(/_controller\z/, "")
44
+
45
+ basename.empty? ? nil : basename
46
+ end
47
+
48
+ def normalize_path(path)
49
+ path = path.delete_prefix("./")
50
+ path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
51
+ path
52
+ end
53
+
54
+ def find_related_specs(domain)
55
+ RELATED_SPEC_DIRS.flat_map { |dir| find_specs_in_dir(dir, domain) }.sort
56
+ end
57
+
58
+ def find_specs_in_dir(dir, domain)
59
+ return [] unless Dir.exist?(dir)
60
+
61
+ Dir.glob(File.join(dir, "**", "#{domain}_spec.rb"))
62
+ end
63
+ end
@@ -30,11 +30,12 @@ class Evilution::Reporter::CLI
30
30
  private
31
31
 
32
32
  def append_survived(lines, summary)
33
- return unless summary.survived_results.any?
33
+ gaps = summary.coverage_gaps
34
+ return unless gaps.any?
34
35
 
35
36
  lines << ""
36
- lines << "Survived mutations:"
37
- summary.survived_results.each { |result| lines << format_survived(result) }
37
+ lines << "Survived mutations (#{gaps.length} coverage gap#{"s" unless gaps.length == 1}):"
38
+ gaps.each { |gap| lines << format_coverage_gap(gap) }
38
39
  end
39
40
 
40
41
  def append_neutral(lines, summary)
@@ -90,11 +91,16 @@ class Evilution::Reporter::CLI
90
91
  "Efficiency: #{pct} killtime, #{rate} mutations/s"
91
92
  end
92
93
 
93
- def format_survived(result)
94
- mutation = result.mutation
95
- location = "#{mutation.file_path}:#{mutation.line}"
96
- diff_lines = mutation.diff.split("\n").map { |l| " #{l}" }.join("\n")
97
- " #{mutation.operator_name}: #{location} (#{mutation.subject.name})\n#{diff_lines}"
94
+ def format_coverage_gap(gap)
95
+ location = "#{gap.file_path}:#{gap.line}"
96
+ header = if gap.single?
97
+ " #{gap.primary_operator}: #{location} (#{gap.subject_name})"
98
+ else
99
+ operators = gap.operator_names.join(", ")
100
+ " #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
101
+ end
102
+ diff_lines = gap.primary_diff.split("\n").map { |l| " #{l}" }.join("\n")
103
+ "#{header}\n#{diff_lines}"
98
104
  end
99
105
 
100
106
  def format_neutral(result)
@@ -4,6 +4,7 @@ require "cgi"
4
4
  require_relative "suggestion"
5
5
 
6
6
  require_relative "../reporter"
7
+ require_relative "../result/coverage_gap_grouper"
7
8
 
8
9
  class Evilution::Reporter::HTML
9
10
  def initialize(baseline: nil)
@@ -156,15 +157,39 @@ class Evilution::Reporter::HTML
156
157
  def build_survived_details(survived)
157
158
  return "" if survived.empty?
158
159
 
159
- entries = survived.map { |r| build_survived_entry(r) }.join("\n")
160
+ gaps = Evilution::Result::CoverageGapGrouper.new.call(survived)
161
+ entries = gaps.map { |gap| build_gap_entry(gap) }.join("\n")
160
162
  <<~HTML
161
163
  <div class="survived-details">
162
- <h3>Survived Mutations</h3>
164
+ <h3>Coverage Gaps (#{gaps.length})</h3>
163
165
  #{entries}
164
166
  </div>
165
167
  HTML
166
168
  end
167
169
 
170
+ def build_gap_entry(gap)
171
+ if gap.single?
172
+ build_survived_entry(gap.mutation_results.first)
173
+ else
174
+ build_grouped_gap_entry(gap)
175
+ end
176
+ end
177
+
178
+ def build_grouped_gap_entry(gap)
179
+ operator_tags = gap.operator_names.map { |op| %(<span class="operator-tag">#{h(op)}</span>) }.join(" ")
180
+ entries_html = gap.mutation_results.map { |r| build_survived_entry(r) }.join("\n")
181
+ <<~HTML
182
+ <div class="coverage-gap">
183
+ <div class="gap-header">
184
+ <span class="location">#{h(gap.file_path)}:#{gap.line} (#{h(gap.subject_name)})</span>
185
+ <span class="gap-count">#{gap.count} mutations</span>
186
+ #{operator_tags}
187
+ </div>
188
+ #{entries_html}
189
+ </div>
190
+ HTML
191
+ end
192
+
168
193
  def build_survived_entry(result)
169
194
  mutation = result.mutation
170
195
  suggestion_text = @suggestion.suggestion_for(mutation)
@@ -307,6 +332,11 @@ class Evilution::Reporter::HTML
307
332
  .diff-removed { color: #f85149; display: block; }
308
333
  .diff-added { color: #3fb950; display: block; }
309
334
  .suggestion { color: #d29922; font-size: 0.8rem; margin-top: 0.5rem; font-style: italic; }
335
+ .coverage-gap { border: 1px solid #30363d; border-radius: 6px; padding: 0.75rem; margin-bottom: 0.75rem; background: #161b22; }
336
+ .gap-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.85rem; padding-bottom: 0.5rem; border-bottom: 1px solid #21262d; }
337
+ .gap-header .location { color: #58a6ff; font-family: monospace; }
338
+ .gap-count { background: #4a1a1a; color: #f85149; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: bold; }
339
+ .operator-tag { background: #21262d; color: #8b949e; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 4px; font-family: monospace; }
310
340
  .empty { color: #8b949e; text-align: center; padding: 2rem; }
311
341
  .baseline-comparison { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 2rem; }
312
342
  .baseline-comparison h2 { font-size: 1rem; color: #f0f6fc; margin-bottom: 0.75rem; }
@@ -24,6 +24,7 @@ class Evilution::Reporter::JSON
24
24
  timestamp: Time.now.iso8601,
25
25
  summary: build_summary(summary),
26
26
  survived: summary.survived_results.map { |r| build_mutation_detail(r) },
27
+ coverage_gaps: build_coverage_gaps(summary),
27
28
  killed: summary.killed_results.map { |r| build_mutation_detail(r) },
28
29
  neutral: summary.neutral_results.map { |r| build_mutation_detail(r) },
29
30
  timed_out: summary.results.select(&:timeout?).map { |r| build_mutation_detail(r) },
@@ -81,6 +82,19 @@ class Evilution::Reporter::JSON
81
82
  detail
82
83
  end
83
84
 
85
+ def build_coverage_gaps(summary)
86
+ summary.coverage_gaps.map do |gap|
87
+ {
88
+ file: gap.file_path,
89
+ subject: gap.subject_name,
90
+ line: gap.line,
91
+ operators: gap.operator_names,
92
+ count: gap.count,
93
+ mutations: gap.mutation_results.map { |r| build_mutation_detail(r) }
94
+ }
95
+ end
96
+ end
97
+
84
98
  def build_disabled_detail(mutation)
85
99
  {
86
100
  operator: mutation.operator_name,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+
5
+ class Evilution::Result::CoverageGap
6
+ attr_reader :file_path, :subject_name, :line, :mutation_results
7
+
8
+ def initialize(file_path:, subject_name:, line:, mutation_results:)
9
+ @file_path = file_path
10
+ @subject_name = subject_name
11
+ @line = line
12
+ @mutation_results = mutation_results.dup.freeze
13
+ freeze
14
+ end
15
+
16
+ def operator_names
17
+ mutation_results.map { |r| r.mutation.operator_name }.uniq
18
+ end
19
+
20
+ def primary_operator
21
+ mutation_results.first.mutation.operator_name
22
+ end
23
+
24
+ def primary_diff
25
+ mutation_results.first.mutation.diff
26
+ end
27
+
28
+ def count
29
+ mutation_results.length
30
+ end
31
+
32
+ def single?
33
+ count == 1
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "coverage_gap"
4
+
5
+ class Evilution::Result::CoverageGapGrouper
6
+ def call(survived_results)
7
+ grouped = survived_results.group_by do |r|
8
+ [r.mutation.file_path, r.mutation.subject.name, r.mutation.line]
9
+ end
10
+
11
+ gaps = grouped.map do |(file_path, subject_name, line), results|
12
+ Evilution::Result::CoverageGap.new(
13
+ file_path: file_path,
14
+ subject_name: subject_name,
15
+ line: line,
16
+ mutation_results: results
17
+ )
18
+ end
19
+
20
+ gaps.sort_by { |gap| [gap.file_path, gap.line, gap.subject_name] }
21
+ end
22
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../result"
4
+ require_relative "coverage_gap_grouper"
4
5
 
5
6
  class Evilution::Result::Summary
6
7
  attr_reader :results, :duration, :skipped, :disabled_mutations
@@ -73,6 +74,10 @@ class Evilution::Result::Summary
73
74
  results.select(&:equivalent?)
74
75
  end
75
76
 
77
+ def coverage_gaps
78
+ Evilution::Result::CoverageGapGrouper.new.call(survived_results)
79
+ end
80
+
76
81
  def killtime
77
82
  results.sum(0.0, &:duration)
78
83
  end
@@ -21,6 +21,7 @@ require_relative "cache"
21
21
  require_relative "parallel/pool"
22
22
  require_relative "session/store"
23
23
  require_relative "ast/pattern/filter"
24
+ require_relative "temp_dir_tracker"
24
25
  require_relative "disable_comment"
25
26
  require_relative "ast/sorbet_sig_detector"
26
27
 
@@ -42,6 +43,7 @@ class Evilution::Runner
42
43
  end
43
44
 
44
45
  def call
46
+ install_signal_handlers
45
47
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
48
 
47
49
  subjects = parse_and_filter_subjects
@@ -180,8 +182,9 @@ class Evilution::Runner
180
182
 
181
183
  def generate_mutations(subjects)
182
184
  filter = build_ignore_filter
185
+ operator_options = build_operator_options
183
186
  mutations = subjects.flat_map do |subject|
184
- registry.mutations_for(subject, filter: filter)
187
+ registry.mutations_for(subject, filter: filter, operator_options: operator_options)
185
188
  end
186
189
  skipped_count = filter ? filter.skipped_count : 0
187
190
 
@@ -252,6 +255,10 @@ class Evilution::Runner
252
255
  end
253
256
  end
254
257
 
258
+ def build_operator_options
259
+ { skip_heredoc_literals: config.skip_heredoc_literals? }
260
+ end
261
+
255
262
  def build_ignore_filter
256
263
  patterns = config.ignore_patterns
257
264
  return nil if patterns.nil? || patterns.empty?
@@ -432,6 +439,26 @@ class Evilution::Runner
432
439
  config.fail_fast? && survived_count >= config.fail_fast
433
440
  end
434
441
 
442
+ def install_signal_handlers
443
+ %w[INT TERM].each { |sig| install_signal_handler(sig) }
444
+ end
445
+
446
+ def install_signal_handler(sig)
447
+ prev_handler = Signal.trap(sig) do
448
+ Evilution::TempDirTracker.cleanup_all
449
+
450
+ case prev_handler
451
+ when Proc, Method
452
+ prev_handler.call
453
+ when "IGNORE"
454
+ # Do nothing — signal is ignored
455
+ else
456
+ Signal.trap(sig, "DEFAULT")
457
+ Process.kill(sig, Process.pid)
458
+ end
459
+ end
460
+ end
461
+
435
462
  def build_isolator
436
463
  case resolve_isolation
437
464
  when :fork then Evilution::Isolation::Fork.new(hooks: @hooks)
@@ -65,6 +65,7 @@ class Evilution::Session::Store
65
65
  git: git_context,
66
66
  summary: build_summary(summary),
67
67
  survived: summary.survived_results.map { |r| build_mutation_detail(r) },
68
+ coverage_gaps: build_coverage_gaps(summary),
68
69
  killed_count: summary.killed,
69
70
  timed_out_count: summary.timed_out,
70
71
  error_count: summary.errors,
@@ -101,6 +102,18 @@ class Evilution::Session::Store
101
102
  }
102
103
  end
103
104
 
105
+ def build_coverage_gaps(summary)
106
+ summary.coverage_gaps.map do |gap|
107
+ {
108
+ file: gap.file_path,
109
+ subject: gap.subject_name,
110
+ line: gap.line,
111
+ operators: gap.operator_names,
112
+ count: gap.count
113
+ }
114
+ end
115
+ end
116
+
104
117
  def git_context
105
118
  sha = `git rev-parse HEAD 2>/dev/null`.strip
106
119
  branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "monitor"
5
+ require_relative "version"
6
+
7
+ module Evilution::TempDirTracker
8
+ @dirs = Set.new
9
+ @monitor = Monitor.new
10
+ @at_exit_registered = false
11
+
12
+ def self.register(dir)
13
+ @monitor.synchronize do
14
+ @dirs << dir
15
+ register_at_exit unless @at_exit_registered
16
+ end
17
+ end
18
+
19
+ def self.unregister(dir)
20
+ @monitor.synchronize { @dirs.delete(dir) }
21
+ end
22
+
23
+ def self.cleanup_all
24
+ @monitor.synchronize do
25
+ @dirs.each { |d| FileUtils.rm_rf(d) }
26
+ @dirs.clear
27
+ end
28
+ end
29
+
30
+ def self.tracked_dirs
31
+ @monitor.synchronize { @dirs.dup }
32
+ end
33
+
34
+ def self.register_at_exit
35
+ at_exit { cleanup_all }
36
+ @at_exit_registered = true
37
+ end
38
+ private_class_method :register_at_exit
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.19.0"
4
+ VERSION = "0.21.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -81,6 +81,15 @@ require_relative "evilution/mutator/operator/yield_statement"
81
81
  require_relative "evilution/mutator/operator/splat_operator"
82
82
  require_relative "evilution/mutator/operator/defined_check"
83
83
  require_relative "evilution/mutator/operator/regex_capture"
84
+ require_relative "evilution/mutator/operator/loop_flip"
85
+ require_relative "evilution/mutator/operator/string_interpolation"
86
+ require_relative "evilution/mutator/operator/retry_removal"
87
+ require_relative "evilution/mutator/operator/case_when"
88
+ require_relative "evilution/mutator/operator/predicate_replacement"
89
+ require_relative "evilution/mutator/operator/equality_to_identity"
90
+ require_relative "evilution/mutator/operator/lambda_body"
91
+ require_relative "evilution/mutator/operator/begin_unwrap"
92
+ require_relative "evilution/mutator/operator/block_param_removal"
84
93
  require_relative "evilution/mutator/registry"
85
94
  require_relative "evilution/equivalent"
86
95
  require_relative "evilution/equivalent/heuristic"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-07 00:00:00.000000000 Z
11
+ date: 2026-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -102,6 +102,7 @@ files:
102
102
  - lib/evilution/equivalent/heuristic/dead_code.rb
103
103
  - lib/evilution/equivalent/heuristic/method_body_nil.rb
104
104
  - lib/evilution/equivalent/heuristic/noop_source.rb
105
+ - lib/evilution/equivalent/heuristic/void_context.rb
105
106
  - lib/evilution/git.rb
106
107
  - lib/evilution/git/changed_files.rb
107
108
  - lib/evilution/hooks.rb
@@ -109,6 +110,7 @@ files:
109
110
  - lib/evilution/hooks/registry.rb
110
111
  - lib/evilution/integration.rb
111
112
  - lib/evilution/integration/base.rb
113
+ - lib/evilution/integration/crash_detector.rb
112
114
  - lib/evilution/integration/rspec.rb
113
115
  - lib/evilution/isolation.rb
114
116
  - lib/evilution/isolation/fork.rb
@@ -130,12 +132,15 @@ files:
130
132
  - lib/evilution/mutator/operator/arithmetic_replacement.rb
131
133
  - lib/evilution/mutator/operator/array_literal.rb
132
134
  - lib/evilution/mutator/operator/bang_method.rb
135
+ - lib/evilution/mutator/operator/begin_unwrap.rb
133
136
  - lib/evilution/mutator/operator/bitwise_complement.rb
134
137
  - lib/evilution/mutator/operator/bitwise_replacement.rb
138
+ - lib/evilution/mutator/operator/block_param_removal.rb
135
139
  - lib/evilution/mutator/operator/block_removal.rb
136
140
  - lib/evilution/mutator/operator/boolean_literal_replacement.rb
137
141
  - lib/evilution/mutator/operator/boolean_operator_replacement.rb
138
142
  - lib/evilution/mutator/operator/break_statement.rb
143
+ - lib/evilution/mutator/operator/case_when.rb
139
144
  - lib/evilution/mutator/operator/class_variable_write.rb
140
145
  - lib/evilution/mutator/operator/collection_replacement.rb
141
146
  - lib/evilution/mutator/operator/collection_return.rb
@@ -146,6 +151,7 @@ files:
146
151
  - lib/evilution/mutator/operator/conditional_negation.rb
147
152
  - lib/evilution/mutator/operator/defined_check.rb
148
153
  - lib/evilution/mutator/operator/ensure_removal.rb
154
+ - lib/evilution/mutator/operator/equality_to_identity.rb
149
155
  - lib/evilution/mutator/operator/explicit_super_mutation.rb
150
156
  - lib/evilution/mutator/operator/float_literal.rb
151
157
  - lib/evilution/mutator/operator/global_variable_write.rb
@@ -157,7 +163,9 @@ files:
157
163
  - lib/evilution/mutator/operator/instance_variable_write.rb
158
164
  - lib/evilution/mutator/operator/integer_literal.rb
159
165
  - lib/evilution/mutator/operator/keyword_argument.rb
166
+ - lib/evilution/mutator/operator/lambda_body.rb
160
167
  - lib/evilution/mutator/operator/local_variable_assignment.rb
168
+ - lib/evilution/mutator/operator/loop_flip.rb
161
169
  - lib/evilution/mutator/operator/method_body_replacement.rb
162
170
  - lib/evilution/mutator/operator/method_call_removal.rb
163
171
  - lib/evilution/mutator/operator/mixin_removal.rb
@@ -168,6 +176,7 @@ files:
168
176
  - lib/evilution/mutator/operator/pattern_matching_alternative.rb
169
177
  - lib/evilution/mutator/operator/pattern_matching_array.rb
170
178
  - lib/evilution/mutator/operator/pattern_matching_guard.rb
179
+ - lib/evilution/mutator/operator/predicate_replacement.rb
171
180
  - lib/evilution/mutator/operator/range_replacement.rb
172
181
  - lib/evilution/mutator/operator/receiver_replacement.rb
173
182
  - lib/evilution/mutator/operator/redo_statement.rb
@@ -175,11 +184,13 @@ files:
175
184
  - lib/evilution/mutator/operator/regexp_mutation.rb
176
185
  - lib/evilution/mutator/operator/rescue_body_replacement.rb
177
186
  - lib/evilution/mutator/operator/rescue_removal.rb
187
+ - lib/evilution/mutator/operator/retry_removal.rb
178
188
  - lib/evilution/mutator/operator/return_value_removal.rb
179
189
  - lib/evilution/mutator/operator/scalar_return.rb
180
190
  - lib/evilution/mutator/operator/send_mutation.rb
181
191
  - lib/evilution/mutator/operator/splat_operator.rb
182
192
  - lib/evilution/mutator/operator/statement_deletion.rb
193
+ - lib/evilution/mutator/operator/string_interpolation.rb
183
194
  - lib/evilution/mutator/operator/string_literal.rb
184
195
  - lib/evilution/mutator/operator/superclass_removal.rb
185
196
  - lib/evilution/mutator/operator/symbol_literal.rb
@@ -189,6 +200,7 @@ files:
189
200
  - lib/evilution/parallel.rb
190
201
  - lib/evilution/parallel/pool.rb
191
202
  - lib/evilution/parallel/work_queue.rb
203
+ - lib/evilution/related_spec_heuristic.rb
192
204
  - lib/evilution/reporter.rb
193
205
  - lib/evilution/reporter/cli.rb
194
206
  - lib/evilution/reporter/html.rb
@@ -196,6 +208,8 @@ files:
196
208
  - lib/evilution/reporter/progress_bar.rb
197
209
  - lib/evilution/reporter/suggestion.rb
198
210
  - lib/evilution/result.rb
211
+ - lib/evilution/result/coverage_gap.rb
212
+ - lib/evilution/result/coverage_gap_grouper.rb
199
213
  - lib/evilution/result/mutation_result.rb
200
214
  - lib/evilution/result/summary.rb
201
215
  - lib/evilution/runner.rb
@@ -204,6 +218,7 @@ files:
204
218
  - lib/evilution/session/store.rb
205
219
  - lib/evilution/spec_resolver.rb
206
220
  - lib/evilution/subject.rb
221
+ - lib/evilution/temp_dir_tracker.rb
207
222
  - lib/evilution/version.rb
208
223
  - lib/tasks/memory_check.rake
209
224
  - script/memory_check