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.
- checksums.yaml +4 -4
- data/.beads/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +17 -17
- data/CHANGELOG.md +39 -0
- data/lib/evilution/ast/inheritance_scanner.rb +70 -0
- data/lib/evilution/ast/parser.rb +73 -68
- data/lib/evilution/ast/source_surgeon.rb +7 -9
- data/lib/evilution/ast.rb +4 -0
- data/lib/evilution/baseline.rb +73 -75
- data/lib/evilution/cache.rb +75 -77
- data/lib/evilution/cli.rb +412 -173
- data/lib/evilution/config.rb +141 -136
- data/lib/evilution/equivalent/detector.rb +29 -27
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +32 -33
- data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
- data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
- data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
- data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
- data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
- data/lib/evilution/equivalent/heuristic.rb +6 -0
- data/lib/evilution/equivalent.rb +4 -0
- data/lib/evilution/git/changed_files.rb +35 -37
- data/lib/evilution/git.rb +4 -0
- data/lib/evilution/integration/base.rb +5 -7
- data/lib/evilution/integration/rspec.rb +114 -116
- data/lib/evilution/integration.rb +4 -0
- data/lib/evilution/isolation/fork.rb +98 -100
- data/lib/evilution/isolation/in_process.rb +59 -61
- data/lib/evilution/isolation.rb +4 -0
- data/lib/evilution/mcp/mutate_tool.rb +172 -143
- data/lib/evilution/mcp/server.rb +12 -11
- data/lib/evilution/mcp/session_diff_tool.rb +89 -0
- data/lib/evilution/mcp/session_list_tool.rb +46 -0
- data/lib/evilution/mcp/session_show_tool.rb +53 -0
- data/lib/evilution/mcp.rb +4 -0
- data/lib/evilution/memory/leak_check.rb +80 -84
- data/lib/evilution/memory.rb +34 -36
- data/lib/evilution/mutation.rb +40 -42
- data/lib/evilution/mutator/base.rb +62 -48
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
- data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
- data/lib/evilution/mutator/operator/array_literal.rb +18 -22
- data/lib/evilution/mutator/operator/block_removal.rb +16 -20
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
- data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
- data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
- data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
- data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
- data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
- data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
- data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
- data/lib/evilution/mutator/operator/float_literal.rb +22 -26
- data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
- data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
- data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
- data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
- data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
- data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
- data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
- data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
- data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
- data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
- data/lib/evilution/mutator/operator/string_literal.rb +18 -22
- data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
- data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
- data/lib/evilution/mutator/operator.rb +6 -0
- data/lib/evilution/mutator/registry.rb +56 -56
- data/lib/evilution/mutator.rb +4 -0
- data/lib/evilution/parallel/pool.rb +56 -58
- data/lib/evilution/parallel.rb +4 -0
- data/lib/evilution/reporter/cli.rb +99 -101
- data/lib/evilution/reporter/html.rb +242 -244
- data/lib/evilution/reporter/json.rb +57 -59
- data/lib/evilution/reporter/suggestion.rb +354 -328
- data/lib/evilution/reporter.rb +4 -0
- data/lib/evilution/result/mutation_result.rb +43 -46
- data/lib/evilution/result/summary.rb +80 -81
- data/lib/evilution/result.rb +4 -0
- data/lib/evilution/runner.rb +401 -316
- data/lib/evilution/session/store.rb +147 -0
- data/lib/evilution/session.rb +4 -0
- data/lib/evilution/spec_resolver.rb +49 -47
- data/lib/evilution/subject.rb +14 -16
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +16 -0
- metadata +24 -2
data/lib/evilution/config.rb
CHANGED
|
@@ -2,174 +2,179 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
def json?
|
|
40
|
+
format == :json
|
|
41
|
+
end
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
def text?
|
|
44
|
+
format == :text
|
|
45
|
+
end
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
def html?
|
|
48
|
+
format == :html
|
|
49
|
+
end
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
def line_ranges?
|
|
52
|
+
!line_ranges.empty?
|
|
53
|
+
end
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
def target?
|
|
56
|
+
!target.nil?
|
|
57
|
+
end
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
def fail_fast?
|
|
60
|
+
!fail_fast.nil?
|
|
61
|
+
end
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
def baseline?
|
|
64
|
+
baseline
|
|
65
|
+
end
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
def incremental?
|
|
68
|
+
incremental
|
|
69
|
+
end
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
def suggest_tests?
|
|
72
|
+
suggest_tests
|
|
73
|
+
end
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
def save_session?
|
|
76
|
+
save_session
|
|
77
|
+
end
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
<<~YAML
|
|
93
|
-
# Evilution configuration
|
|
94
|
-
# See: https://github.com/marinazzio/evilution
|
|
91
|
+
{}
|
|
92
|
+
end
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
100
|
+
# Per-mutation timeout in seconds (default: 30)
|
|
101
|
+
# timeout: 30
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
# Output format: text or json (default: text)
|
|
104
|
+
# format: text
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
# Minimum mutation score to pass (0.0 to 1.0, default: 0.0)
|
|
107
|
+
# min_score: 0.0
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
# Test integration: rspec (default: rspec)
|
|
110
|
+
# integration: rspec
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
# Number of parallel workers (default: 1)
|
|
113
|
+
# jobs: 1
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
YAML
|
|
117
|
-
end
|
|
115
|
+
# Stop after N surviving mutants (default: disabled)
|
|
116
|
+
# fail_fast: 1
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
# Generate concrete RSpec test code in suggestions (default: false)
|
|
119
|
+
# suggest_tests: false
|
|
120
|
+
YAML
|
|
121
|
+
end
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
return nil if value.nil?
|
|
123
|
+
private
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
def validate_fail_fast(value)
|
|
126
|
+
return nil if value.nil?
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
163
|
+
value
|
|
164
|
+
end
|
|
159
165
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
169
|
+
value = Integer(value)
|
|
170
|
+
raise Evilution::ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
value
|
|
173
|
+
rescue ::ArgumentError, ::TypeError
|
|
174
|
+
raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}"
|
|
175
|
+
end
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
class Evilution::Equivalent::Detector
|
|
13
|
+
def initialize(heuristics: nil)
|
|
14
|
+
@heuristics = heuristics || default_heuristics
|
|
15
|
+
end
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else
|
|
23
|
-
remaining << mutation
|
|
24
|
-
end
|
|
25
|
-
end
|
|
17
|
+
def call(mutations)
|
|
18
|
+
equivalent = []
|
|
19
|
+
remaining = []
|
|
26
20
|
|
|
27
|
-
|
|
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
|
-
|
|
29
|
+
[equivalent, remaining]
|
|
30
|
+
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
5
|
+
class Evilution::Equivalent::Heuristic::MethodBodyNil
|
|
6
|
+
def match?(mutation)
|
|
7
|
+
return false unless mutation.operator_name == "method_body_replacement"
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
node = mutation.subject.node
|
|
10
|
+
return false unless node
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
body = node.body
|
|
13
|
+
return true if body.nil? || body.is_a?(Prism::NilNode)
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|