evilution 0.16.0 → 0.17.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +19 -18
  4. data/CHANGELOG.md +23 -0
  5. data/docs/ast_pattern_syntax.md +210 -0
  6. data/lib/evilution/ast/pattern/filter.rb +25 -0
  7. data/lib/evilution/ast/pattern/matcher.rb +107 -0
  8. data/lib/evilution/ast/pattern/parser.rb +185 -0
  9. data/lib/evilution/ast/pattern.rb +4 -0
  10. data/lib/evilution/ast/source_surgeon.rb +3 -3
  11. data/lib/evilution/cli.rb +13 -1
  12. data/lib/evilution/config.rb +35 -2
  13. data/lib/evilution/hooks/loader.rb +35 -0
  14. data/lib/evilution/hooks/registry.rb +60 -0
  15. data/lib/evilution/hooks.rb +58 -0
  16. data/lib/evilution/integration/base.rb +4 -0
  17. data/lib/evilution/integration/rspec.rb +6 -2
  18. data/lib/evilution/isolation/fork.rb +5 -0
  19. data/lib/evilution/mutator/base.rb +4 -1
  20. data/lib/evilution/mutator/operator/index_assignment_removal.rb +18 -0
  21. data/lib/evilution/mutator/operator/index_to_dig.rb +58 -0
  22. data/lib/evilution/mutator/operator/index_to_fetch.rb +30 -0
  23. data/lib/evilution/mutator/operator/mixin_removal.rb +2 -1
  24. data/lib/evilution/mutator/operator/pattern_matching_alternative.rb +46 -0
  25. data/lib/evilution/mutator/operator/pattern_matching_array.rb +97 -0
  26. data/lib/evilution/mutator/operator/pattern_matching_guard.rb +44 -0
  27. data/lib/evilution/mutator/operator/superclass_removal.rb +2 -1
  28. data/lib/evilution/mutator/registry.rb +9 -3
  29. data/lib/evilution/parallel/pool.rb +3 -1
  30. data/lib/evilution/reporter/cli.rb +1 -0
  31. data/lib/evilution/reporter/html.rb +7 -0
  32. data/lib/evilution/reporter/json.rb +1 -0
  33. data/lib/evilution/reporter/suggestion.rb +87 -1
  34. data/lib/evilution/result/summary.rb +3 -2
  35. data/lib/evilution/runner.rb +21 -9
  36. data/lib/evilution/session/store.rb +5 -2
  37. data/lib/evilution/version.rb +1 -1
  38. data/lib/evilution.rb +12 -0
  39. metadata +16 -2
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::PatternMatchingAlternative < Evilution::Mutator::Base
6
+ def visit_alternation_pattern_node(node)
7
+ remove_left(node)
8
+ remove_right(node)
9
+ swap_order(node)
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def remove_left(node)
16
+ add_mutation(
17
+ offset: node.location.start_offset,
18
+ length: node.location.length,
19
+ replacement: source_for(node.right),
20
+ node: node
21
+ )
22
+ end
23
+
24
+ def remove_right(node)
25
+ add_mutation(
26
+ offset: node.location.start_offset,
27
+ length: node.location.length,
28
+ replacement: source_for(node.left),
29
+ node: node
30
+ )
31
+ end
32
+
33
+ def swap_order(node)
34
+ operator = @file_source.byteslice(node.operator_loc.start_offset, node.operator_loc.length)
35
+ add_mutation(
36
+ offset: node.location.start_offset,
37
+ length: node.location.length,
38
+ replacement: "#{source_for(node.right)} #{operator} #{source_for(node.left)}",
39
+ node: node
40
+ )
41
+ end
42
+
43
+ def source_for(node)
44
+ @file_source.byteslice(node.location.start_offset, node.location.length)
45
+ end
46
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::PatternMatchingArray < Evilution::Mutator::Base
6
+ def visit_array_pattern_node(node)
7
+ mutate_array_elements(node)
8
+ super
9
+ end
10
+
11
+ def visit_find_pattern_node(node)
12
+ mutate_find_elements(node)
13
+ super
14
+ end
15
+
16
+ private
17
+
18
+ def mutate_array_elements(node)
19
+ requireds = node.requireds
20
+ posts = node.posts
21
+ rest = node.rest
22
+ elements = requireds + posts
23
+ return if elements.empty?
24
+
25
+ elements.each_with_index do |_element, index|
26
+ remove_array_element(node, requireds, posts, rest, index) if elements.length > 1
27
+ wildcard_array_element(node, requireds, posts, rest, index)
28
+ end
29
+ end
30
+
31
+ def remove_array_element(node, requireds, posts, rest, skip_index)
32
+ parts = build_array_parts(requireds, posts, rest, skip_index: skip_index)
33
+ replace_pattern(node, parts)
34
+ end
35
+
36
+ def wildcard_array_element(node, requireds, posts, rest, wildcard_index)
37
+ parts = build_array_parts(requireds, posts, rest, wildcard_index: wildcard_index)
38
+ replace_pattern(node, parts)
39
+ end
40
+
41
+ def build_array_parts(requireds, posts, rest, skip_index: nil, wildcard_index: nil)
42
+ parts = []
43
+ requireds.each_with_index do |req, i|
44
+ next if i == skip_index
45
+
46
+ parts << (i == wildcard_index ? "_" : source_for(req))
47
+ end
48
+ parts << source_for(rest) if rest
49
+ posts.each_with_index do |post, i|
50
+ adjusted = requireds.length + i
51
+ next if adjusted == skip_index
52
+
53
+ parts << (adjusted == wildcard_index ? "_" : source_for(post))
54
+ end
55
+ parts
56
+ end
57
+
58
+ def mutate_find_elements(node)
59
+ return if node.requireds.empty?
60
+
61
+ node.requireds.each_with_index do |_element, index|
62
+ remove_find_element(node, index) if node.requireds.length > 1
63
+ wildcard_find_element(node, index)
64
+ end
65
+ end
66
+
67
+ def remove_find_element(node, skip_index)
68
+ parts = [source_for(node.left)]
69
+ node.requireds.each_with_index do |req, i|
70
+ parts << source_for(req) unless i == skip_index
71
+ end
72
+ parts << source_for(node.right)
73
+ replace_pattern(node, parts)
74
+ end
75
+
76
+ def wildcard_find_element(node, wildcard_index)
77
+ parts = [source_for(node.left)]
78
+ node.requireds.each_with_index do |req, i|
79
+ parts << (i == wildcard_index ? "_" : source_for(req))
80
+ end
81
+ parts << source_for(node.right)
82
+ replace_pattern(node, parts)
83
+ end
84
+
85
+ def replace_pattern(node, parts)
86
+ add_mutation(
87
+ offset: node.location.start_offset,
88
+ length: node.location.length,
89
+ replacement: "[#{parts.join(", ")}]",
90
+ node: node
91
+ )
92
+ end
93
+
94
+ def source_for(node)
95
+ @file_source.byteslice(node.location.start_offset, node.location.length)
96
+ end
97
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::PatternMatchingGuard < Evilution::Mutator::Base
6
+ def visit_in_node(node)
7
+ pattern = node.pattern
8
+ mutate_guard(pattern, node) if guarded?(pattern)
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def guarded?(pattern)
15
+ pattern.is_a?(Prism::IfNode) || pattern.is_a?(Prism::UnlessNode)
16
+ end
17
+
18
+ def mutate_guard(pattern, in_node)
19
+ guard_start = pattern.statements.location.start_offset + pattern.statements.location.length
20
+ guard_end = pattern.predicate.location.start_offset + pattern.predicate.location.length
21
+
22
+ remove_guard(guard_start, guard_end, in_node)
23
+ negate_guard(pattern, in_node)
24
+ end
25
+
26
+ def remove_guard(guard_start, guard_end, in_node)
27
+ add_mutation(
28
+ offset: guard_start,
29
+ length: guard_end - guard_start,
30
+ replacement: "",
31
+ node: in_node
32
+ )
33
+ end
34
+
35
+ def negate_guard(pattern, in_node)
36
+ pred_loc = pattern.predicate.location
37
+ add_mutation(
38
+ offset: pred_loc.start_offset,
39
+ length: pred_loc.length,
40
+ replacement: "!(#{@file_source.byteslice(pred_loc.start_offset, pred_loc.length)})",
41
+ node: in_node
42
+ )
43
+ end
44
+ end
@@ -5,10 +5,11 @@ require "prism"
5
5
  require_relative "../operator"
6
6
 
7
7
  class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
8
- def call(subject)
8
+ def call(subject, filter: nil)
9
9
  @subject = subject
10
10
  @file_source = File.read(subject.file_path)
11
11
  @mutations = []
12
+ @filter = filter
12
13
 
13
14
  tree = self.class.parsed_tree_for(subject.file_path, @file_source)
14
15
  enclosing = find_enclosing_class(tree, subject.line_number)
@@ -51,7 +51,13 @@ class Evilution::Mutator::Registry
51
51
  Evilution::Mutator::Operator::BitwiseReplacement,
52
52
  Evilution::Mutator::Operator::BitwiseComplement,
53
53
  Evilution::Mutator::Operator::ZsuperRemoval,
54
- Evilution::Mutator::Operator::ExplicitSuperMutation
54
+ Evilution::Mutator::Operator::ExplicitSuperMutation,
55
+ Evilution::Mutator::Operator::IndexToFetch,
56
+ Evilution::Mutator::Operator::IndexToDig,
57
+ Evilution::Mutator::Operator::IndexAssignmentRemoval,
58
+ Evilution::Mutator::Operator::PatternMatchingGuard,
59
+ Evilution::Mutator::Operator::PatternMatchingAlternative,
60
+ Evilution::Mutator::Operator::PatternMatchingArray
55
61
  ].each { |op| registry.register(op) }
56
62
  registry
57
63
  end
@@ -65,9 +71,9 @@ class Evilution::Mutator::Registry
65
71
  self
66
72
  end
67
73
 
68
- def mutations_for(subject)
74
+ def mutations_for(subject, filter: nil)
69
75
  @operators.flat_map do |operator_class|
70
- operator_class.new.call(subject)
76
+ operator_class.new.call(subject, filter: filter)
71
77
  end
72
78
  end
73
79
 
@@ -3,10 +3,11 @@
3
3
  require_relative "../parallel"
4
4
 
5
5
  class Evilution::Parallel::Pool
6
- def initialize(size:)
6
+ def initialize(size:, hooks: nil)
7
7
  raise ArgumentError, "pool size must be a positive integer, got #{size.inspect}" unless size.is_a?(Integer) && size >= 1
8
8
 
9
9
  @size = size
10
+ @hooks = hooks
10
11
  end
11
12
 
12
13
  def map(items, &block)
@@ -35,6 +36,7 @@ class Evilution::Parallel::Pool
35
36
  def fork_worker(item, read_io, write_io, &block)
36
37
  Process.fork do
37
38
  read_io.close
39
+ @hooks.fire(:worker_process_start) if @hooks
38
40
  result = block.call(item)
39
41
  Marshal.dump(result, write_io)
40
42
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -60,6 +60,7 @@ class Evilution::Reporter::CLI
60
60
  "#{summary.survived} survived, #{summary.timed_out} timed out"
61
61
  parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
62
62
  parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
63
+ parts += ", #{summary.skipped} skipped" if summary.skipped.positive?
63
64
  parts
64
65
  end
65
66
 
@@ -77,12 +77,19 @@ class Evilution::Reporter::HTML
77
77
  <div class="card"><span class="card-value">#{summary.errors}</span><span class="card-label">Errors</span></div>
78
78
  <div class="card"><span class="card-value">#{summary.neutral}</span><span class="card-label">Neutral</span></div>
79
79
  <div class="card"><span class="card-value">#{summary.equivalent}</span><span class="card-label">Equivalent</span></div>
80
+ #{build_skipped_card(summary)}
80
81
  <div class="card"><span class="card-value">#{format("%.2f", summary.duration)}s</span><span class="card-label">Duration</span></div>
81
82
  #{peak_html}
82
83
  </section>
83
84
  HTML
84
85
  end
85
86
 
87
+ def build_skipped_card(summary)
88
+ return "" unless summary.skipped.positive?
89
+
90
+ %(<div class="card"><span class="card-value">#{summary.skipped}</span><span class="card-label">Skipped</span></div>)
91
+ end
92
+
86
93
  def build_truncation_notice(summary)
87
94
  return "" unless summary.truncated?
88
95
 
@@ -46,6 +46,7 @@ class Evilution::Reporter::JSON
46
46
  duration: summary.duration.round(4)
47
47
  }
48
48
  data[:truncated] = true if summary.truncated?
49
+ data[:skipped] = summary.skipped if summary.skipped.positive?
49
50
  peak = summary.peak_memory_mb
50
51
  data[:peak_memory_mb] = peak.round(1) if peak
51
52
  data
@@ -42,7 +42,13 @@ class Evilution::Reporter::Suggestion
42
42
  "bitwise_replacement" => "Add a test that checks the exact bitwise result to distinguish &, |, and ^ operators",
43
43
  "bitwise_complement" => "Add a test that verifies the bitwise complement (~) result, not just the sign or magnitude",
44
44
  "zsuper_removal" => "Add a test that verifies inherited behavior from super is needed, not just the subclass logic",
45
- "explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters"
45
+ "explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters",
46
+ "index_to_fetch" => "Add a test that distinguishes [] (returns nil for missing keys) from .fetch (raises KeyError)",
47
+ "index_to_dig" => "Add a test that verifies chained [] access returns the correct nested value",
48
+ "index_assignment_removal" => "Add a test that verifies the []= assignment side effect is observable (the collection is modified)",
49
+ "pattern_matching_guard" => "Add a test with input that matches the pattern but fails the guard to verify filtering",
50
+ "pattern_matching_alternative" => "Add a test with input that matches only one specific alternative to verify each branch is reachable",
51
+ "pattern_matching_array" => "Add a test that verifies each element position in the array pattern matches the expected type or value"
46
52
  }.freeze
47
53
 
48
54
  CONCRETE_TEMPLATES = {
@@ -541,6 +547,86 @@ class Evilution::Reporter::Suggestion
541
547
  expect(result).to eq(expected)
542
548
  end
543
549
  RSPEC
550
+ },
551
+ "index_to_fetch" => lambda { |mutation|
552
+ method_name = parse_method_name(mutation.subject.name)
553
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
554
+ <<~RSPEC.strip
555
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
556
+ # #{mutation.file_path}:#{mutation.line}
557
+ it 'distinguishes [] from .fetch for missing keys in ##{method_name}' do
558
+ # Access a missing key: [] returns nil, .fetch raises KeyError
559
+ expect { subject.#{method_name}(collection_with_missing_key) }.to raise_error(KeyError)
560
+ end
561
+ RSPEC
562
+ },
563
+ "index_to_dig" => lambda { |mutation|
564
+ method_name = parse_method_name(mutation.subject.name)
565
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
566
+ <<~RSPEC.strip
567
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
568
+ # #{mutation.file_path}:#{mutation.line}
569
+ it 'verifies the chained [] access returns the correct nested value in ##{method_name}' do
570
+ # Assert the nested lookup produces the expected value
571
+ result = subject.#{method_name}(nested_collection)
572
+ expect(result).to eq(expected)
573
+ end
574
+ RSPEC
575
+ },
576
+ "index_assignment_removal" => lambda { |mutation|
577
+ method_name = parse_method_name(mutation.subject.name)
578
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
579
+ <<~RSPEC.strip
580
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
581
+ # #{mutation.file_path}:#{mutation.line}
582
+ it 'verifies the []= assignment modifies the collection in ##{method_name}' do
583
+ # Assert the collection contains the assigned value after the method runs
584
+ result = subject.#{method_name}(collection)
585
+ expect(result).to include(expected_key => expected_value)
586
+ end
587
+ RSPEC
588
+ },
589
+ "pattern_matching_guard" => lambda { |mutation|
590
+ method_name = parse_method_name(mutation.subject.name)
591
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
592
+ <<~RSPEC.strip
593
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
594
+ # #{mutation.file_path}:#{mutation.line}
595
+ it 'verifies the pattern guard filters correctly in ##{method_name}' do
596
+ # Test with input that matches the pattern but fails the guard condition
597
+ # The guard should prevent matching, routing to a different branch
598
+ result = subject.#{method_name}(input_matching_pattern_but_failing_guard)
599
+ expect(result).to eq(expected)
600
+ end
601
+ RSPEC
602
+ },
603
+ "pattern_matching_alternative" => lambda { |mutation|
604
+ method_name = parse_method_name(mutation.subject.name)
605
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
606
+ <<~RSPEC.strip
607
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
608
+ # #{mutation.file_path}:#{mutation.line}
609
+ it 'verifies each pattern alternative is reachable in ##{method_name}' do
610
+ # Test with input that matches only one specific alternative
611
+ # Each alternative should have a dedicated test case
612
+ result = subject.#{method_name}(input_for_specific_alternative)
613
+ expect(result).to eq(expected)
614
+ end
615
+ RSPEC
616
+ },
617
+ "pattern_matching_array" => lambda { |mutation|
618
+ method_name = parse_method_name(mutation.subject.name)
619
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
620
+ <<~RSPEC.strip
621
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
622
+ # #{mutation.file_path}:#{mutation.line}
623
+ it 'verifies each array pattern element matters in ##{method_name}' do
624
+ # Test with input where changing one element type causes a different match
625
+ # Each position in the array pattern should be validated
626
+ result = subject.#{method_name}(input_with_wrong_element_type)
627
+ expect(result).to eq(expected)
628
+ end
629
+ RSPEC
544
630
  }
545
631
  }.freeze
546
632
 
@@ -3,12 +3,13 @@
3
3
  require_relative "../result"
4
4
 
5
5
  class Evilution::Result::Summary
6
- attr_reader :results, :duration
6
+ attr_reader :results, :duration, :skipped
7
7
 
8
- def initialize(results:, duration: 0.0, truncated: false)
8
+ def initialize(results:, duration: 0.0, truncated: false, skipped: 0)
9
9
  @results = results
10
10
  @duration = duration
11
11
  @truncated = truncated
12
+ @skipped = skipped
12
13
  freeze
13
14
  end
14
15
 
@@ -20,13 +20,15 @@ require_relative "baseline"
20
20
  require_relative "cache"
21
21
  require_relative "parallel/pool"
22
22
  require_relative "session/store"
23
+ require_relative "ast/pattern/filter"
23
24
 
24
25
  class Evilution::Runner
25
26
  attr_reader :config
26
27
 
27
- def initialize(config: Evilution::Config.new, on_result: nil)
28
+ def initialize(config: Evilution::Config.new, on_result: nil, hooks: nil)
28
29
  @config = config
29
30
  @on_result = on_result
31
+ @hooks = hooks
30
32
  @parser = Evilution::AST::Parser.new
31
33
  @registry = Evilution::Mutator::Registry.default
32
34
  @isolator = build_isolator
@@ -41,7 +43,7 @@ class Evilution::Runner
41
43
 
42
44
  baseline_result = run_baseline(subjects)
43
45
 
44
- mutations = generate_mutations(subjects)
46
+ mutations, skipped_count = generate_mutations(subjects)
45
47
  equivalent_mutations, mutations = filter_equivalent(mutations)
46
48
  release_subject_nodes(subjects)
47
49
  clear_operator_caches
@@ -54,7 +56,8 @@ class Evilution::Runner
54
56
 
55
57
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
56
58
 
57
- summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated)
59
+ summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
60
+ skipped: skipped_count)
58
61
  output_report(summary)
59
62
  save_session(summary)
60
63
 
@@ -63,7 +66,7 @@ class Evilution::Runner
63
66
 
64
67
  private
65
68
 
66
- attr_reader :parser, :registry, :isolator, :cache, :on_result
69
+ attr_reader :parser, :registry, :isolator, :cache, :on_result, :hooks
67
70
 
68
71
  def parse_and_filter_subjects
69
72
  subjects = parse_subjects
@@ -169,9 +172,18 @@ class Evilution::Runner
169
172
  end
170
173
 
171
174
  def generate_mutations(subjects)
172
- subjects.flat_map do |subject|
173
- registry.mutations_for(subject)
175
+ filter = build_ignore_filter
176
+ mutations = subjects.flat_map do |subject|
177
+ registry.mutations_for(subject, filter: filter)
174
178
  end
179
+ [mutations, filter ? filter.skipped_count : 0]
180
+ end
181
+
182
+ def build_ignore_filter
183
+ patterns = config.ignore_patterns
184
+ return nil if patterns.nil? || patterns.empty?
185
+
186
+ Evilution::AST::Pattern::Filter.new(patterns)
175
187
  end
176
188
 
177
189
  def filter_equivalent(mutations)
@@ -240,7 +252,7 @@ class Evilution::Runner
240
252
 
241
253
  def run_mutations_parallel(mutations, baseline_result = nil)
242
254
  integration = build_integration
243
- pool = Evilution::Parallel::Pool.new(size: config.jobs)
255
+ pool = Evilution::Parallel::Pool.new(size: config.jobs, hooks: @hooks)
244
256
  worker_isolator = Evilution::Isolation::InProcess.new
245
257
  spec_resolver = baseline_result&.failed? ? Evilution::SpecResolver.new : nil
246
258
  state = { results: [], survived_count: 0, truncated: false, completed: 0 }
@@ -341,7 +353,7 @@ class Evilution::Runner
341
353
 
342
354
  def build_isolator
343
355
  case resolve_isolation
344
- when :fork then Evilution::Isolation::Fork.new
356
+ when :fork then Evilution::Isolation::Fork.new(hooks: @hooks)
345
357
  when :in_process then Evilution::Isolation::InProcess.new
346
358
  end
347
359
  end
@@ -356,7 +368,7 @@ class Evilution::Runner
356
368
  case config.integration
357
369
  when :rspec
358
370
  test_files = config.spec_files.empty? ? nil : config.spec_files
359
- Evilution::Integration::RSpec.new(test_files: test_files)
371
+ Evilution::Integration::RSpec.new(test_files: test_files, hooks: @hooks)
360
372
  else
361
373
  raise Evilution::Error, "unknown integration: #{config.integration}"
362
374
  end
@@ -69,12 +69,13 @@ class Evilution::Session::Store
69
69
  timed_out_count: summary.timed_out,
70
70
  error_count: summary.errors,
71
71
  neutral_count: summary.neutral,
72
- equivalent_count: summary.equivalent
72
+ equivalent_count: summary.equivalent,
73
+ skipped_count: summary.skipped
73
74
  }
74
75
  end
75
76
 
76
77
  def build_summary(summary)
77
- {
78
+ data = {
78
79
  total: summary.total,
79
80
  killed: summary.killed,
80
81
  survived: summary.survived,
@@ -85,6 +86,8 @@ class Evilution::Session::Store
85
86
  score: summary.score.round(4),
86
87
  duration: summary.duration.round(4)
87
88
  }
89
+ data[:skipped] = summary.skipped if summary.skipped.positive?
90
+ data
88
91
  end
89
92
 
90
93
  def build_mutation_detail(result)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -11,6 +11,12 @@ require_relative "evilution/parallel"
11
11
  require_relative "evilution/ast/source_surgeon"
12
12
  require_relative "evilution/ast/parser"
13
13
  require_relative "evilution/ast/inheritance_scanner"
14
+ require_relative "evilution/ast/pattern"
15
+ require_relative "evilution/ast/pattern/matcher"
16
+ require_relative "evilution/ast/pattern/parser"
17
+ require_relative "evilution/hooks"
18
+ require_relative "evilution/hooks/registry"
19
+ require_relative "evilution/hooks/loader"
14
20
  require_relative "evilution/mutator"
15
21
  require_relative "evilution/mutator/base"
16
22
  require_relative "evilution/mutator/operator"
@@ -60,6 +66,12 @@ require_relative "evilution/mutator/operator/bitwise_replacement"
60
66
  require_relative "evilution/mutator/operator/bitwise_complement"
61
67
  require_relative "evilution/mutator/operator/zsuper_removal"
62
68
  require_relative "evilution/mutator/operator/explicit_super_mutation"
69
+ require_relative "evilution/mutator/operator/index_to_fetch"
70
+ require_relative "evilution/mutator/operator/index_to_dig"
71
+ require_relative "evilution/mutator/operator/index_assignment_removal"
72
+ require_relative "evilution/mutator/operator/pattern_matching_guard"
73
+ require_relative "evilution/mutator/operator/pattern_matching_alternative"
74
+ require_relative "evilution/mutator/operator/pattern_matching_array"
63
75
  require_relative "evilution/mutator/registry"
64
76
  require_relative "evilution/equivalent"
65
77
  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.16.0
4
+ version: 0.17.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-03-29 00:00:00.000000000 Z
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -76,11 +76,16 @@ files:
76
76
  - README.md
77
77
  - Rakefile
78
78
  - claude-swarm.yml
79
+ - docs/ast_pattern_syntax.md
79
80
  - exe/evilution
80
81
  - lib/evilution.rb
81
82
  - lib/evilution/ast.rb
82
83
  - lib/evilution/ast/inheritance_scanner.rb
83
84
  - lib/evilution/ast/parser.rb
85
+ - lib/evilution/ast/pattern.rb
86
+ - lib/evilution/ast/pattern/filter.rb
87
+ - lib/evilution/ast/pattern/matcher.rb
88
+ - lib/evilution/ast/pattern/parser.rb
84
89
  - lib/evilution/ast/source_surgeon.rb
85
90
  - lib/evilution/baseline.rb
86
91
  - lib/evilution/cache.rb
@@ -97,6 +102,9 @@ files:
97
102
  - lib/evilution/equivalent/heuristic/noop_source.rb
98
103
  - lib/evilution/git.rb
99
104
  - lib/evilution/git/changed_files.rb
105
+ - lib/evilution/hooks.rb
106
+ - lib/evilution/hooks/loader.rb
107
+ - lib/evilution/hooks/registry.rb
100
108
  - lib/evilution/integration.rb
101
109
  - lib/evilution/integration/base.rb
102
110
  - lib/evilution/integration/rspec.rb
@@ -138,6 +146,9 @@ files:
138
146
  - lib/evilution/mutator/operator/float_literal.rb
139
147
  - lib/evilution/mutator/operator/global_variable_write.rb
140
148
  - lib/evilution/mutator/operator/hash_literal.rb
149
+ - lib/evilution/mutator/operator/index_assignment_removal.rb
150
+ - lib/evilution/mutator/operator/index_to_dig.rb
151
+ - lib/evilution/mutator/operator/index_to_fetch.rb
141
152
  - lib/evilution/mutator/operator/inline_rescue.rb
142
153
  - lib/evilution/mutator/operator/instance_variable_write.rb
143
154
  - lib/evilution/mutator/operator/integer_literal.rb
@@ -148,6 +159,9 @@ files:
148
159
  - lib/evilution/mutator/operator/negation_insertion.rb
149
160
  - lib/evilution/mutator/operator/next_statement.rb
150
161
  - lib/evilution/mutator/operator/nil_replacement.rb
162
+ - lib/evilution/mutator/operator/pattern_matching_alternative.rb
163
+ - lib/evilution/mutator/operator/pattern_matching_array.rb
164
+ - lib/evilution/mutator/operator/pattern_matching_guard.rb
151
165
  - lib/evilution/mutator/operator/range_replacement.rb
152
166
  - lib/evilution/mutator/operator/receiver_replacement.rb
153
167
  - lib/evilution/mutator/operator/redo_statement.rb