evilution 0.14.0 → 0.16.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +41 -41
  4. data/CHANGELOG.md +43 -0
  5. data/lib/evilution/ast/inheritance_scanner.rb +70 -0
  6. data/lib/evilution/ast/parser.rb +10 -6
  7. data/lib/evilution/cli.rb +6 -1
  8. data/lib/evilution/config.rb +7 -1
  9. data/lib/evilution/equivalent/detector.rb +5 -1
  10. data/lib/evilution/equivalent/heuristic/alias_swap.rb +5 -2
  11. data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
  12. data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
  13. data/lib/evilution/mutator/base.rb +16 -0
  14. data/lib/evilution/mutator/operator/bang_method.rb +48 -0
  15. data/lib/evilution/mutator/operator/bitwise_complement.rb +31 -0
  16. data/lib/evilution/mutator/operator/bitwise_replacement.rb +30 -0
  17. data/lib/evilution/mutator/operator/break_statement.rb +50 -0
  18. data/lib/evilution/mutator/operator/class_variable_write.rb +25 -0
  19. data/lib/evilution/mutator/operator/collection_replacement.rb +25 -1
  20. data/lib/evilution/mutator/operator/ensure_removal.rb +27 -0
  21. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +47 -0
  22. data/lib/evilution/mutator/operator/global_variable_write.rb +25 -0
  23. data/lib/evilution/mutator/operator/inline_rescue.rb +39 -0
  24. data/lib/evilution/mutator/operator/instance_variable_write.rb +25 -0
  25. data/lib/evilution/mutator/operator/local_variable_assignment.rb +16 -0
  26. data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
  27. data/lib/evilution/mutator/operator/next_statement.rb +50 -0
  28. data/lib/evilution/mutator/operator/redo_statement.rb +18 -0
  29. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +94 -0
  30. data/lib/evilution/mutator/operator/rescue_removal.rb +37 -0
  31. data/lib/evilution/mutator/operator/send_mutation.rb +11 -2
  32. data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
  33. data/lib/evilution/mutator/operator/zsuper_removal.rb +16 -0
  34. data/lib/evilution/mutator/registry.rb +19 -1
  35. data/lib/evilution/reporter/progress_bar.rb +84 -0
  36. data/lib/evilution/reporter/suggestion.rb +253 -1
  37. data/lib/evilution/runner.rb +105 -19
  38. data/lib/evilution/version.rb +1 -1
  39. data/lib/evilution.rb +20 -0
  40. metadata +24 -2
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "config"
4
4
  require_relative "ast/parser"
5
+ require_relative "ast/inheritance_scanner"
5
6
  require_relative "memory"
6
7
  require_relative "mutator/registry"
7
8
  require_relative "isolation/fork"
@@ -35,9 +36,7 @@ class Evilution::Runner
35
36
  def call
36
37
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
38
 
38
- subjects = parse_subjects
39
- subjects = filter_by_target(subjects) if config.target?
40
- subjects = filter_by_line_ranges(subjects) if config.line_ranges?
39
+ subjects = parse_and_filter_subjects
41
40
  log_memory("after parse_subjects", "#{subjects.length} subjects")
42
41
 
43
42
  baseline_result = run_baseline(subjects)
@@ -45,6 +44,7 @@ class Evilution::Runner
45
44
  mutations = generate_mutations(subjects)
46
45
  equivalent_mutations, mutations = filter_equivalent(mutations)
47
46
  release_subject_nodes(subjects)
47
+ clear_operator_caches
48
48
  results, truncated = run_mutations(mutations, baseline_result)
49
49
  results += equivalent_mutations.map do |m|
50
50
  m.strip_sources!
@@ -65,28 +65,98 @@ class Evilution::Runner
65
65
 
66
66
  attr_reader :parser, :registry, :isolator, :cache, :on_result
67
67
 
68
+ def parse_and_filter_subjects
69
+ subjects = parse_subjects
70
+ subjects = filter_by_descendants(subjects) if descendants_target?
71
+ subjects = filter_by_target(subjects) if method_target?
72
+ subjects = filter_by_line_ranges(subjects) if config.line_ranges?
73
+ subjects
74
+ end
75
+
68
76
  def parse_subjects
69
77
  files = resolve_target_files
70
78
  files.flat_map { |file| parser.call(file) }
71
79
  end
72
80
 
73
81
  def resolve_target_files
82
+ return resolve_source_glob if source_glob_target?
74
83
  return config.target_files unless config.target_files.empty?
75
84
 
76
85
  Evilution::Git::ChangedFiles.new.call
77
86
  end
78
87
 
88
+ def source_glob_target?
89
+ config.target&.start_with?("source:")
90
+ end
91
+
92
+ def descendants_target?
93
+ config.target&.start_with?("descendants:")
94
+ end
95
+
96
+ def method_target?
97
+ config.target? && !source_glob_target? && !descendants_target?
98
+ end
99
+
100
+ def resolve_source_glob
101
+ pattern = config.target.delete_prefix("source:")
102
+ files = Dir.glob(pattern)
103
+ raise Evilution::Error, "no files found matching '#{pattern}'" if files.empty?
104
+
105
+ files.sort
106
+ end
107
+
108
+ def filter_by_descendants(subjects)
109
+ base_name = config.target.delete_prefix("descendants:")
110
+ files = resolve_target_files
111
+ inheritance = Evilution::AST::InheritanceScanner.call(files)
112
+ class_names = resolve_descendant_set(base_name, inheritance)
113
+ raise Evilution::Error, "no classes found matching '#{config.target}'" if class_names.empty?
114
+
115
+ subjects.select { |s| class_names.include?(s.name.split(/[#.]/).first) }
116
+ end
117
+
118
+ def resolve_descendant_set(base_name, inheritance)
119
+ descendants = Set.new
120
+ known = inheritance.key?(base_name) || inheritance.value?(base_name)
121
+ return descendants unless known
122
+
123
+ descendants.add(base_name)
124
+ changed = true
125
+ while changed
126
+ changed = false
127
+ inheritance.each do |child, parent|
128
+ next unless descendants.include?(parent)
129
+ next if descendants.include?(child)
130
+
131
+ descendants.add(child)
132
+ changed = true
133
+ end
134
+ end
135
+ descendants
136
+ end
137
+
79
138
  def filter_by_target(subjects)
80
- matched = if config.target.include?("#")
81
- subjects.select { |s| s.name == config.target }
82
- else
83
- subjects.select { |s| s.name.start_with?("#{config.target}#") }
84
- end
139
+ matched = subjects.select(&target_matcher)
85
140
  raise Evilution::Error, "no method found matching '#{config.target}'" if matched.empty?
86
141
 
87
142
  matched
88
143
  end
89
144
 
145
+ def target_matcher
146
+ target = config.target
147
+ if target.end_with?("*")
148
+ prefix = target.chomp("*")
149
+ ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
150
+ elsif target.end_with?("#", ".")
151
+ prefix = target
152
+ ->(s) { s.name.start_with?(prefix) }
153
+ elsif target.include?("#") || target.include?(".")
154
+ ->(s) { s.name == target }
155
+ else
156
+ ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
157
+ end
158
+ end
159
+
90
160
  def filter_by_line_ranges(subjects)
91
161
  subjects.select do |subject|
92
162
  range = config.line_ranges[subject.file_path]
@@ -112,6 +182,10 @@ class Evilution::Runner
112
182
  subjects.each(&:release_node!)
113
183
  end
114
184
 
185
+ def clear_operator_caches
186
+ Evilution::Mutator::Base.clear_parse_cache!
187
+ end
188
+
115
189
  def equivalent_result(mutation)
116
190
  Evilution::Result::MutationResult.new(mutation: mutation, status: :equivalent, duration: 0.0)
117
191
  end
@@ -127,11 +201,14 @@ class Evilution::Runner
127
201
  end
128
202
 
129
203
  def run_mutations(mutations, baseline_result = nil)
130
- if config.jobs > 1
131
- run_mutations_parallel(mutations, baseline_result)
132
- else
133
- run_mutations_sequential(mutations, baseline_result)
134
- end
204
+ @progress_bar = build_progress_bar(mutations.length)
205
+ result = if config.jobs > 1
206
+ run_mutations_parallel(mutations, baseline_result)
207
+ else
208
+ run_mutations_sequential(mutations, baseline_result)
209
+ end
210
+ @progress_bar&.finish
211
+ result
135
212
  end
136
213
 
137
214
  def run_mutations_sequential(mutations, baseline_result = nil)
@@ -150,9 +227,7 @@ class Evilution::Runner
150
227
  result = neutralize_if_baseline_failed(result, baseline_result, spec_resolver)
151
228
  results << result
152
229
  survived_count += 1 if result.survived?
153
- on_result&.call(result)
154
- log_progress(index + 1, result.status)
155
- log_mutation_diagnostics(result)
230
+ notify_result(result, index + 1)
156
231
 
157
232
  if config.fail_fast? && survived_count >= config.fail_fast
158
233
  truncated = true
@@ -207,9 +282,7 @@ class Evilution::Runner
207
282
  state[:results] << result
208
283
  state[:survived_count] += 1 if result.survived?
209
284
  state[:completed] += 1
210
- on_result&.call(result)
211
- log_progress(state[:completed], result.status)
212
- log_mutation_diagnostics(result)
285
+ notify_result(result, state[:completed])
213
286
  end
214
287
 
215
288
  log_memory("after batch", "#{state[:completed]} complete")
@@ -373,6 +446,19 @@ class Evilution::Runner
373
446
  warn "[evilution] failed to save session: #{e.message}" unless config.quiet
374
447
  end
375
448
 
449
+ def notify_result(result, index)
450
+ on_result&.call(result)
451
+ @progress_bar&.tick(status: result.status)
452
+ log_progress(index, result.status)
453
+ log_mutation_diagnostics(result)
454
+ end
455
+
456
+ def build_progress_bar(total)
457
+ return nil if !config.progress? || config.quiet || config.verbose || !config.text? || !$stderr.tty?
458
+
459
+ Evilution::Reporter::ProgressBar.new(total: total, output: $stderr)
460
+ end
461
+
376
462
  def build_reporter
377
463
  case config.format
378
464
  when :json
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.14.0"
4
+ VERSION = "0.16.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "evilution/ast"
10
10
  require_relative "evilution/parallel"
11
11
  require_relative "evilution/ast/source_surgeon"
12
12
  require_relative "evilution/ast/parser"
13
+ require_relative "evilution/ast/inheritance_scanner"
13
14
  require_relative "evilution/mutator"
14
15
  require_relative "evilution/mutator/base"
15
16
  require_relative "evilution/mutator/operator"
@@ -41,6 +42,24 @@ require_relative "evilution/mutator/operator/receiver_replacement"
41
42
  require_relative "evilution/mutator/operator/send_mutation"
42
43
  require_relative "evilution/mutator/operator/argument_nil_substitution"
43
44
  require_relative "evilution/mutator/operator/compound_assignment"
45
+ require_relative "evilution/mutator/operator/mixin_removal"
46
+ require_relative "evilution/mutator/operator/superclass_removal"
47
+ require_relative "evilution/mutator/operator/local_variable_assignment"
48
+ require_relative "evilution/mutator/operator/instance_variable_write"
49
+ require_relative "evilution/mutator/operator/class_variable_write"
50
+ require_relative "evilution/mutator/operator/global_variable_write"
51
+ require_relative "evilution/mutator/operator/rescue_removal"
52
+ require_relative "evilution/mutator/operator/rescue_body_replacement"
53
+ require_relative "evilution/mutator/operator/inline_rescue"
54
+ require_relative "evilution/mutator/operator/ensure_removal"
55
+ require_relative "evilution/mutator/operator/break_statement"
56
+ require_relative "evilution/mutator/operator/next_statement"
57
+ require_relative "evilution/mutator/operator/redo_statement"
58
+ require_relative "evilution/mutator/operator/bang_method"
59
+ require_relative "evilution/mutator/operator/bitwise_replacement"
60
+ require_relative "evilution/mutator/operator/bitwise_complement"
61
+ require_relative "evilution/mutator/operator/zsuper_removal"
62
+ require_relative "evilution/mutator/operator/explicit_super_mutation"
44
63
  require_relative "evilution/mutator/registry"
45
64
  require_relative "evilution/equivalent"
46
65
  require_relative "evilution/equivalent/heuristic"
@@ -63,6 +82,7 @@ require_relative "evilution/reporter/json"
63
82
  require_relative "evilution/reporter/cli"
64
83
  require_relative "evilution/reporter/html"
65
84
  require_relative "evilution/reporter/suggestion"
85
+ require_relative "evilution/reporter/progress_bar"
66
86
  require_relative "evilution/spec_resolver"
67
87
  require_relative "evilution/baseline"
68
88
  require_relative "evilution/cache"
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.14.0
4
+ version: 0.16.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-28 00:00:00.000000000 Z
11
+ date: 2026-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -79,6 +79,7 @@ files:
79
79
  - exe/evilution
80
80
  - lib/evilution.rb
81
81
  - lib/evilution/ast.rb
82
+ - lib/evilution/ast/inheritance_scanner.rb
82
83
  - lib/evilution/ast/parser.rb
83
84
  - lib/evilution/ast/source_surgeon.rb
84
85
  - lib/evilution/baseline.rb
@@ -89,6 +90,8 @@ files:
89
90
  - lib/evilution/equivalent/detector.rb
90
91
  - lib/evilution/equivalent/heuristic.rb
91
92
  - lib/evilution/equivalent/heuristic/alias_swap.rb
93
+ - lib/evilution/equivalent/heuristic/arithmetic_identity.rb
94
+ - lib/evilution/equivalent/heuristic/comment_marking.rb
92
95
  - lib/evilution/equivalent/heuristic/dead_code.rb
93
96
  - lib/evilution/equivalent/heuristic/method_body_nil.rb
94
97
  - lib/evilution/equivalent/heuristic/noop_source.rb
@@ -116,30 +119,48 @@ files:
116
119
  - lib/evilution/mutator/operator/argument_removal.rb
117
120
  - lib/evilution/mutator/operator/arithmetic_replacement.rb
118
121
  - lib/evilution/mutator/operator/array_literal.rb
122
+ - lib/evilution/mutator/operator/bang_method.rb
123
+ - lib/evilution/mutator/operator/bitwise_complement.rb
124
+ - lib/evilution/mutator/operator/bitwise_replacement.rb
119
125
  - lib/evilution/mutator/operator/block_removal.rb
120
126
  - lib/evilution/mutator/operator/boolean_literal_replacement.rb
121
127
  - lib/evilution/mutator/operator/boolean_operator_replacement.rb
128
+ - lib/evilution/mutator/operator/break_statement.rb
129
+ - lib/evilution/mutator/operator/class_variable_write.rb
122
130
  - lib/evilution/mutator/operator/collection_replacement.rb
123
131
  - lib/evilution/mutator/operator/comparison_replacement.rb
124
132
  - lib/evilution/mutator/operator/compound_assignment.rb
125
133
  - lib/evilution/mutator/operator/conditional_branch.rb
126
134
  - lib/evilution/mutator/operator/conditional_flip.rb
127
135
  - lib/evilution/mutator/operator/conditional_negation.rb
136
+ - lib/evilution/mutator/operator/ensure_removal.rb
137
+ - lib/evilution/mutator/operator/explicit_super_mutation.rb
128
138
  - lib/evilution/mutator/operator/float_literal.rb
139
+ - lib/evilution/mutator/operator/global_variable_write.rb
129
140
  - lib/evilution/mutator/operator/hash_literal.rb
141
+ - lib/evilution/mutator/operator/inline_rescue.rb
142
+ - lib/evilution/mutator/operator/instance_variable_write.rb
130
143
  - lib/evilution/mutator/operator/integer_literal.rb
144
+ - lib/evilution/mutator/operator/local_variable_assignment.rb
131
145
  - lib/evilution/mutator/operator/method_body_replacement.rb
132
146
  - lib/evilution/mutator/operator/method_call_removal.rb
147
+ - lib/evilution/mutator/operator/mixin_removal.rb
133
148
  - lib/evilution/mutator/operator/negation_insertion.rb
149
+ - lib/evilution/mutator/operator/next_statement.rb
134
150
  - lib/evilution/mutator/operator/nil_replacement.rb
135
151
  - lib/evilution/mutator/operator/range_replacement.rb
136
152
  - lib/evilution/mutator/operator/receiver_replacement.rb
153
+ - lib/evilution/mutator/operator/redo_statement.rb
137
154
  - lib/evilution/mutator/operator/regexp_mutation.rb
155
+ - lib/evilution/mutator/operator/rescue_body_replacement.rb
156
+ - lib/evilution/mutator/operator/rescue_removal.rb
138
157
  - lib/evilution/mutator/operator/return_value_removal.rb
139
158
  - lib/evilution/mutator/operator/send_mutation.rb
140
159
  - lib/evilution/mutator/operator/statement_deletion.rb
141
160
  - lib/evilution/mutator/operator/string_literal.rb
161
+ - lib/evilution/mutator/operator/superclass_removal.rb
142
162
  - lib/evilution/mutator/operator/symbol_literal.rb
163
+ - lib/evilution/mutator/operator/zsuper_removal.rb
143
164
  - lib/evilution/mutator/registry.rb
144
165
  - lib/evilution/parallel.rb
145
166
  - lib/evilution/parallel/pool.rb
@@ -147,6 +168,7 @@ files:
147
168
  - lib/evilution/reporter/cli.rb
148
169
  - lib/evilution/reporter/html.rb
149
170
  - lib/evilution/reporter/json.rb
171
+ - lib/evilution/reporter/progress_bar.rb
150
172
  - lib/evilution/reporter/suggestion.rb
151
173
  - lib/evilution/result.rb
152
174
  - lib/evilution/result/mutation_result.rb