evilution 0.13.0 → 0.15.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +17 -17
  4. data/CHANGELOG.md +39 -0
  5. data/lib/evilution/ast/inheritance_scanner.rb +70 -0
  6. data/lib/evilution/ast/parser.rb +73 -68
  7. data/lib/evilution/ast/source_surgeon.rb +7 -9
  8. data/lib/evilution/ast.rb +4 -0
  9. data/lib/evilution/baseline.rb +73 -75
  10. data/lib/evilution/cache.rb +75 -77
  11. data/lib/evilution/cli.rb +412 -173
  12. data/lib/evilution/config.rb +141 -136
  13. data/lib/evilution/equivalent/detector.rb +29 -27
  14. data/lib/evilution/equivalent/heuristic/alias_swap.rb +32 -33
  15. data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
  16. data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
  17. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  18. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  19. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  20. data/lib/evilution/equivalent/heuristic.rb +6 -0
  21. data/lib/evilution/equivalent.rb +4 -0
  22. data/lib/evilution/git/changed_files.rb +35 -37
  23. data/lib/evilution/git.rb +4 -0
  24. data/lib/evilution/integration/base.rb +5 -7
  25. data/lib/evilution/integration/rspec.rb +114 -116
  26. data/lib/evilution/integration.rb +4 -0
  27. data/lib/evilution/isolation/fork.rb +98 -100
  28. data/lib/evilution/isolation/in_process.rb +59 -61
  29. data/lib/evilution/isolation.rb +4 -0
  30. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  31. data/lib/evilution/mcp/server.rb +12 -11
  32. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  33. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  34. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  35. data/lib/evilution/mcp.rb +4 -0
  36. data/lib/evilution/memory/leak_check.rb +80 -84
  37. data/lib/evilution/memory.rb +34 -36
  38. data/lib/evilution/mutation.rb +40 -42
  39. data/lib/evilution/mutator/base.rb +62 -48
  40. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  41. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  42. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  43. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  44. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  45. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  46. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  47. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  48. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  49. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  50. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  51. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  52. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  53. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  54. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  55. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  56. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  57. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  58. data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
  59. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  60. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  61. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  62. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  63. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  64. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  65. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  66. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  67. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  68. data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
  69. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  70. data/lib/evilution/mutator/operator.rb +6 -0
  71. data/lib/evilution/mutator/registry.rb +56 -56
  72. data/lib/evilution/mutator.rb +4 -0
  73. data/lib/evilution/parallel/pool.rb +56 -58
  74. data/lib/evilution/parallel.rb +4 -0
  75. data/lib/evilution/reporter/cli.rb +99 -101
  76. data/lib/evilution/reporter/html.rb +242 -244
  77. data/lib/evilution/reporter/json.rb +57 -59
  78. data/lib/evilution/reporter/suggestion.rb +354 -328
  79. data/lib/evilution/reporter.rb +4 -0
  80. data/lib/evilution/result/mutation_result.rb +43 -46
  81. data/lib/evilution/result/summary.rb +80 -81
  82. data/lib/evilution/result.rb +4 -0
  83. data/lib/evilution/runner.rb +401 -316
  84. data/lib/evilution/session/store.rb +147 -0
  85. data/lib/evilution/session.rb +4 -0
  86. data/lib/evilution/spec_resolver.rb +49 -47
  87. data/lib/evilution/subject.rb +14 -16
  88. data/lib/evilution/version.rb +1 -1
  89. data/lib/evilution.rb +16 -0
  90. metadata +24 -2
@@ -2,174 +2,179 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- module Evilution
6
- class Config
7
- CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
8
-
9
- DEFAULTS = {
10
- timeout: 30,
11
- format: :text,
12
- target: nil,
13
- min_score: 0.0,
14
- integration: :rspec,
15
- verbose: false,
16
- quiet: false,
17
- jobs: 1,
18
- fail_fast: nil,
19
- baseline: true,
20
- isolation: :auto,
21
- incremental: false,
22
- suggest_tests: false,
23
- line_ranges: {},
24
- spec_files: []
25
- }.freeze
26
-
27
- attr_reader :target_files, :timeout, :format,
28
- :target, :min_score, :integration, :verbose, :quiet,
29
- :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
30
- :line_ranges, :spec_files
31
-
32
- def initialize(**options)
33
- file_options = options.delete(:skip_config_file) ? {} : load_config_file
34
- merged = DEFAULTS.merge(file_options).merge(options)
35
- assign_attributes(merged)
36
- freeze
37
- end
5
+ class Evilution::Config
6
+ CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
7
+
8
+ DEFAULTS = {
9
+ timeout: 30,
10
+ format: :text,
11
+ target: nil,
12
+ min_score: 0.0,
13
+ integration: :rspec,
14
+ verbose: false,
15
+ quiet: false,
16
+ jobs: 1,
17
+ fail_fast: nil,
18
+ baseline: true,
19
+ isolation: :auto,
20
+ incremental: false,
21
+ suggest_tests: false,
22
+ save_session: false,
23
+ line_ranges: {},
24
+ spec_files: []
25
+ }.freeze
26
+
27
+ attr_reader :target_files, :timeout, :format,
28
+ :target, :min_score, :integration, :verbose, :quiet,
29
+ :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
30
+ :save_session, :line_ranges, :spec_files
31
+
32
+ def initialize(**options)
33
+ file_options = options.delete(:skip_config_file) ? {} : load_config_file
34
+ merged = DEFAULTS.merge(file_options).merge(options)
35
+ assign_attributes(merged)
36
+ freeze
37
+ end
38
38
 
39
- def json?
40
- format == :json
41
- end
39
+ def json?
40
+ format == :json
41
+ end
42
42
 
43
- def text?
44
- format == :text
45
- end
43
+ def text?
44
+ format == :text
45
+ end
46
46
 
47
- def html?
48
- format == :html
49
- end
47
+ def html?
48
+ format == :html
49
+ end
50
50
 
51
- def line_ranges?
52
- !line_ranges.empty?
53
- end
51
+ def line_ranges?
52
+ !line_ranges.empty?
53
+ end
54
54
 
55
- def target?
56
- !target.nil?
57
- end
55
+ def target?
56
+ !target.nil?
57
+ end
58
58
 
59
- def fail_fast?
60
- !fail_fast.nil?
61
- end
59
+ def fail_fast?
60
+ !fail_fast.nil?
61
+ end
62
62
 
63
- def baseline?
64
- baseline
65
- end
63
+ def baseline?
64
+ baseline
65
+ end
66
66
 
67
- def incremental?
68
- incremental
69
- end
67
+ def incremental?
68
+ incremental
69
+ end
70
70
 
71
- def suggest_tests?
72
- suggest_tests
73
- end
71
+ def suggest_tests?
72
+ suggest_tests
73
+ end
74
74
 
75
- def self.file_options
76
- CONFIG_FILES.each do |path|
77
- next unless File.exist?(path)
75
+ def save_session?
76
+ save_session
77
+ end
78
78
 
79
- data = YAML.safe_load_file(path, symbolize_names: true)
80
- return data.is_a?(Hash) ? data : {}
81
- rescue Psych::SyntaxError, Psych::DisallowedClass => e
82
- raise ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
83
- rescue SystemCallError => e
84
- raise ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
85
- end
79
+ def self.file_options
80
+ CONFIG_FILES.each do |path|
81
+ next unless File.exist?(path)
86
82
 
87
- {}
83
+ data = YAML.safe_load_file(path, symbolize_names: true)
84
+ return data.is_a?(Hash) ? data : {}
85
+ rescue Psych::SyntaxError, Psych::DisallowedClass => e
86
+ raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
87
+ rescue SystemCallError => e
88
+ raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
88
89
  end
89
90
 
90
- # Generates a default config file template.
91
- def self.default_template
92
- <<~YAML
93
- # Evilution configuration
94
- # See: https://github.com/marinazzio/evilution
91
+ {}
92
+ end
95
93
 
96
- # Per-mutation timeout in seconds (default: 30)
97
- # timeout: 30
94
+ # Generates a default config file template.
95
+ def self.default_template
96
+ <<~YAML
97
+ # Evilution configuration
98
+ # See: https://github.com/marinazzio/evilution
98
99
 
99
- # Output format: text or json (default: text)
100
- # format: text
100
+ # Per-mutation timeout in seconds (default: 30)
101
+ # timeout: 30
101
102
 
102
- # Minimum mutation score to pass (0.0 to 1.0, default: 0.0)
103
- # min_score: 0.0
103
+ # Output format: text or json (default: text)
104
+ # format: text
104
105
 
105
- # Test integration: rspec (default: rspec)
106
- # integration: rspec
106
+ # Minimum mutation score to pass (0.0 to 1.0, default: 0.0)
107
+ # min_score: 0.0
107
108
 
108
- # Number of parallel workers (default: 1)
109
- # jobs: 1
109
+ # Test integration: rspec (default: rspec)
110
+ # integration: rspec
110
111
 
111
- # Stop after N surviving mutants (default: disabled)
112
- # fail_fast: 1
112
+ # Number of parallel workers (default: 1)
113
+ # jobs: 1
113
114
 
114
- # Generate concrete RSpec test code in suggestions (default: false)
115
- # suggest_tests: false
116
- YAML
117
- end
115
+ # Stop after N surviving mutants (default: disabled)
116
+ # fail_fast: 1
118
117
 
119
- private
118
+ # Generate concrete RSpec test code in suggestions (default: false)
119
+ # suggest_tests: false
120
+ YAML
121
+ end
120
122
 
121
- def validate_fail_fast(value)
122
- return nil if value.nil?
123
+ private
123
124
 
124
- value = Integer(value)
125
- raise ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
125
+ def validate_fail_fast(value)
126
+ return nil if value.nil?
126
127
 
127
- value
128
- rescue ::ArgumentError, ::TypeError
129
- raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
130
- end
128
+ value = Integer(value)
129
+ raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
131
130
 
132
- def assign_attributes(merged)
133
- @target_files = Array(merged[:target_files])
134
- @timeout = merged[:timeout]
135
- @format = merged[:format].to_sym
136
- @target = merged[:target]
137
- @min_score = merged[:min_score].to_f
138
- @integration = merged[:integration].to_sym
139
- @verbose = merged[:verbose]
140
- @quiet = merged[:quiet]
141
- @jobs = validate_jobs(merged[:jobs])
142
- @fail_fast = validate_fail_fast(merged[:fail_fast])
143
- @baseline = merged[:baseline]
144
- @isolation = validate_isolation(merged[:isolation])
145
- @incremental = merged[:incremental]
146
- @suggest_tests = merged[:suggest_tests]
147
- @line_ranges = merged[:line_ranges] || {}
148
- @spec_files = Array(merged[:spec_files])
149
- end
131
+ value
132
+ rescue ::ArgumentError, ::TypeError
133
+ raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
134
+ end
135
+
136
+ def assign_attributes(merged) # rubocop:disable Metrics/AbcSize
137
+ @target_files = Array(merged[:target_files])
138
+ @timeout = merged[:timeout]
139
+ @format = merged[:format].to_sym
140
+ @target = merged[:target]
141
+ @min_score = merged[:min_score].to_f
142
+ @integration = merged[:integration].to_sym
143
+ @verbose = merged[:verbose]
144
+ @quiet = merged[:quiet]
145
+ @jobs = validate_jobs(merged[:jobs])
146
+ @fail_fast = validate_fail_fast(merged[:fail_fast])
147
+ @baseline = merged[:baseline]
148
+ @isolation = validate_isolation(merged[:isolation])
149
+ @incremental = merged[:incremental]
150
+ @suggest_tests = merged[:suggest_tests]
151
+ @save_session = merged[:save_session]
152
+ @line_ranges = merged[:line_ranges] || {}
153
+ @spec_files = Array(merged[:spec_files])
154
+ end
150
155
 
151
- def validate_isolation(value)
152
- raise ConfigError, "isolation must be auto, fork, or in_process, got nil" if value.nil?
156
+ def validate_isolation(value)
157
+ raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got nil" if value.nil?
153
158
 
154
- value = value.to_sym
155
- raise ConfigError, "isolation must be auto, fork, or in_process, got #{value.inspect}" unless %i[auto fork in_process].include?(value)
159
+ value = value.to_sym
160
+ raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got #{value.inspect}" unless %i[auto fork
161
+ in_process].include?(value)
156
162
 
157
- value
158
- end
163
+ value
164
+ end
159
165
 
160
- def validate_jobs(value)
161
- raise ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
166
+ def validate_jobs(value)
167
+ raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
162
168
 
163
- value = Integer(value)
164
- raise ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
169
+ value = Integer(value)
170
+ raise Evilution::ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
165
171
 
166
- value
167
- rescue ::ArgumentError, ::TypeError
168
- raise ConfigError, "jobs must be a positive integer, got #{value.inspect}"
169
- end
172
+ value
173
+ rescue ::ArgumentError, ::TypeError
174
+ raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}"
175
+ end
170
176
 
171
- def load_config_file
172
- self.class.file_options
173
- end
177
+ def load_config_file
178
+ self.class.file_options
174
179
  end
175
180
  end
@@ -4,39 +4,41 @@ require_relative "heuristic/noop_source"
4
4
  require_relative "heuristic/method_body_nil"
5
5
  require_relative "heuristic/alias_swap"
6
6
  require_relative "heuristic/dead_code"
7
+ require_relative "heuristic/arithmetic_identity"
8
+ require_relative "heuristic/comment_marking"
7
9
 
8
- module Evilution
9
- module Equivalent
10
- class Detector
11
- def initialize(heuristics: nil)
12
- @heuristics = heuristics || default_heuristics
13
- end
10
+ require_relative "../equivalent"
14
11
 
15
- def call(mutations)
16
- equivalent = []
17
- remaining = []
12
+ class Evilution::Equivalent::Detector
13
+ def initialize(heuristics: nil)
14
+ @heuristics = heuristics || default_heuristics
15
+ end
18
16
 
19
- mutations.each do |mutation|
20
- if @heuristics.any? { |h| h.match?(mutation) }
21
- equivalent << mutation
22
- else
23
- remaining << mutation
24
- end
25
- end
17
+ def call(mutations)
18
+ equivalent = []
19
+ remaining = []
26
20
 
27
- [equivalent, remaining]
21
+ mutations.each do |mutation|
22
+ if @heuristics.any? { |h| h.match?(mutation) }
23
+ equivalent << mutation
24
+ else
25
+ remaining << mutation
28
26
  end
27
+ end
29
28
 
30
- private
29
+ [equivalent, remaining]
30
+ end
31
31
 
32
- def default_heuristics
33
- [
34
- Heuristic::NoopSource.new,
35
- Heuristic::MethodBodyNil.new,
36
- Heuristic::AliasSwap.new,
37
- Heuristic::DeadCode.new
38
- ]
39
- end
40
- end
32
+ private
33
+
34
+ def default_heuristics
35
+ [
36
+ Evilution::Equivalent::Heuristic::NoopSource.new,
37
+ Evilution::Equivalent::Heuristic::MethodBodyNil.new,
38
+ Evilution::Equivalent::Heuristic::AliasSwap.new,
39
+ Evilution::Equivalent::Heuristic::DeadCode.new,
40
+ Evilution::Equivalent::Heuristic::ArithmeticIdentity.new,
41
+ Evilution::Equivalent::Heuristic::CommentMarking.new
42
+ ]
41
43
  end
42
44
  end
@@ -1,37 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Equivalent
5
- module Heuristic
6
- class AliasSwap
7
- ALIAS_PAIRS = Set[
8
- Set[:detect, :find],
9
- Set[:length, :size],
10
- Set[:collect, :map]
11
- ].freeze
12
-
13
- def match?(mutation)
14
- return false unless mutation.operator_name == "send_mutation"
15
-
16
- diff = mutation.diff
17
- removed = extract_method(diff, "- ")
18
- added = extract_method(diff, "+ ")
19
- return false unless removed && added
20
-
21
- pair = Set[removed.to_sym, added.to_sym]
22
- ALIAS_PAIRS.include?(pair)
23
- end
24
-
25
- private
26
-
27
- def extract_method(diff, prefix)
28
- line = diff.split("\n").find { |l| l.start_with?(prefix) }
29
- return nil unless line
30
-
31
- match = line.match(/\.(\w+)(?:[\s(]|$)/)
32
- match && match[1]
33
- end
34
- end
35
- end
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::AliasSwap
6
+ ALIAS_PAIRS = Set[
7
+ Set[:detect, :find],
8
+ Set[:length, :size],
9
+ Set[:collect, :map],
10
+ Set[:count, :length]
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
+ diff = mutation.diff
19
+ removed = extract_method(diff, "- ")
20
+ added = extract_method(diff, "+ ")
21
+ return false unless removed && added
22
+
23
+ pair = Set[removed.to_sym, added.to_sym]
24
+ ALIAS_PAIRS.include?(pair)
25
+ end
26
+
27
+ private
28
+
29
+ def extract_method(diff, prefix)
30
+ line = diff.split("\n").find { |l| l.start_with?(prefix) }
31
+ return nil unless line
32
+
33
+ match = line.match(/\.(\w+)(?:[\s(]|$)/)
34
+ match && match[1]
36
35
  end
37
36
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::ArithmeticIdentity
6
+ # Patterns where the original expression is an arithmetic identity operation.
7
+ # "x + 0" is identity (equals x), so mutating the 0 to something else means
8
+ # the original was a no-op — if the test doesn't catch it, it's likely equivalent.
9
+ ADDITIVE_IDENTITY = /[\w)\].]+\s*[+-]\s*0\b|\b0\s*\+\s*[\w(\[]/
10
+ MULTIPLICATIVE_IDENTITY = %r{[\w)\].]+\s*[*/]\s*1\b|\b1\s*\*\s*[\w(\[]}
11
+ EXPONENT_IDENTITY = /[\w)\].]+\s*\*\*\s*1\b/
12
+
13
+ def match?(mutation)
14
+ return false unless mutation.operator_name == "integer_literal"
15
+
16
+ removed = diff_line(mutation.diff, "- ")
17
+ return false unless removed
18
+
19
+ content = removed.sub(/^- /, "")
20
+ content.match?(ADDITIVE_IDENTITY) ||
21
+ content.match?(MULTIPLICATIVE_IDENTITY) ||
22
+ content.match?(EXPONENT_IDENTITY)
23
+ end
24
+
25
+ private
26
+
27
+ def diff_line(diff, prefix)
28
+ diff.split("\n").find { |l| l.start_with?(prefix) }
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::CommentMarking
6
+ MARKER = /#\s*evilution:equivalent\b/
7
+
8
+ def match?(mutation)
9
+ source = mutation.original_source
10
+ return false unless source
11
+
12
+ lines = source.lines
13
+ line_index = mutation.line - 1
14
+ return false if line_index.negative? || line_index >= lines.length
15
+
16
+ return true if lines[line_index].match?(MARKER)
17
+ return true if line_index.positive? && lines[line_index - 1].match?(MARKER)
18
+
19
+ false
20
+ end
21
+ end
@@ -1,51 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Equivalent
5
- module Heuristic
6
- class DeadCode
7
- def match?(mutation)
8
- return false unless mutation.operator_name == "statement_deletion"
9
-
10
- node = mutation.subject.node
11
- return false unless node
12
-
13
- body = node.body
14
- return false unless body.is_a?(Prism::StatementsNode)
15
-
16
- statements = body.body
17
- unreachable_lines = find_unreachable_lines(statements)
18
- unreachable_lines.include?(mutation.line)
19
- end
20
-
21
- private
22
-
23
- def find_unreachable_lines(statements)
24
- lines = Set.new
25
- found_unconditional_return = false
26
-
27
- statements.each do |stmt|
28
- if found_unconditional_return
29
- collect_lines(stmt, lines)
30
- elsif unconditional_return?(stmt)
31
- found_unconditional_return = true
32
- end
33
- end
34
-
35
- lines
36
- end
37
-
38
- def unconditional_return?(node)
39
- node.is_a?(Prism::ReturnNode) ||
40
- (node.is_a?(Prism::CallNode) && node.name == :raise)
41
- end
42
-
43
- def collect_lines(node, lines)
44
- start_line = node.location.start_line
45
- end_line = node.location.end_line
46
- (start_line..end_line).each { |l| lines.add(l) }
47
- end
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::DeadCode
6
+ def match?(mutation)
7
+ return false unless mutation.operator_name == "statement_deletion"
8
+
9
+ node = mutation.subject.node
10
+ return false unless node
11
+
12
+ body = node.body
13
+ return false unless body.is_a?(Prism::StatementsNode)
14
+
15
+ statements = body.body
16
+ unreachable_lines = find_unreachable_lines(statements)
17
+ unreachable_lines.include?(mutation.line)
18
+ end
19
+
20
+ private
21
+
22
+ def find_unreachable_lines(statements)
23
+ lines = Set.new
24
+ found_unconditional_return = false
25
+
26
+ statements.each do |stmt|
27
+ if found_unconditional_return
28
+ collect_lines(stmt, lines)
29
+ elsif unconditional_return?(stmt)
30
+ found_unconditional_return = true
48
31
  end
49
32
  end
33
+
34
+ lines
35
+ end
36
+
37
+ def unconditional_return?(node)
38
+ node.is_a?(Prism::ReturnNode) ||
39
+ (node.is_a?(Prism::CallNode) && node.name == :raise)
40
+ end
41
+
42
+ def collect_lines(node, lines)
43
+ start_line = node.location.start_line
44
+ end_line = node.location.end_line
45
+ (start_line..end_line).each { |l| lines.add(l) }
50
46
  end
51
47
  end
@@ -1,23 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Equivalent
5
- module Heuristic
6
- class MethodBodyNil
7
- def match?(mutation)
8
- return false unless mutation.operator_name == "method_body_replacement"
3
+ require_relative "../heuristic"
9
4
 
10
- node = mutation.subject.node
11
- return false unless node
5
+ class Evilution::Equivalent::Heuristic::MethodBodyNil
6
+ def match?(mutation)
7
+ return false unless mutation.operator_name == "method_body_replacement"
12
8
 
13
- body = node.body
14
- return true if body.nil? || body.is_a?(Prism::NilNode)
9
+ node = mutation.subject.node
10
+ return false unless node
15
11
 
16
- return body.body.first.is_a?(Prism::NilNode) if body.is_a?(Prism::StatementsNode) && body.body.length == 1
12
+ body = node.body
13
+ return true if body.nil? || body.is_a?(Prism::NilNode)
17
14
 
18
- false
19
- end
20
- end
21
- end
15
+ return body.body.first.is_a?(Prism::NilNode) if body.is_a?(Prism::StatementsNode) && body.body.length == 1
16
+
17
+ false
22
18
  end
23
19
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Equivalent
5
- module Heuristic
6
- class NoopSource
7
- def match?(mutation)
8
- mutation.original_source == mutation.mutated_source
9
- end
10
- end
11
- end
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::NoopSource
6
+ def match?(mutation)
7
+ mutation.original_source == mutation.mutated_source
12
8
  end
13
9
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../equivalent"
4
+
5
+ module Evilution::Equivalent::Heuristic
6
+ end