evilution 0.17.0 → 0.19.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 +103 -33
- data/CHANGELOG.md +50 -0
- data/README.md +144 -50
- data/lib/evilution/ast/sorbet_sig_detector.rb +52 -0
- data/lib/evilution/baseline.rb +9 -1
- data/lib/evilution/cli.rb +398 -23
- data/lib/evilution/config.rb +10 -2
- data/lib/evilution/disable_comment.rb +90 -0
- data/lib/evilution/integration/rspec.rb +74 -5
- data/lib/evilution/isolation/fork.rb +10 -6
- data/lib/evilution/isolation/in_process.rb +14 -10
- data/lib/evilution/mcp/session_diff_tool.rb +5 -35
- data/lib/evilution/mutator/operator/collection_return.rb +33 -0
- data/lib/evilution/mutator/operator/defined_check.rb +16 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +91 -0
- data/lib/evilution/mutator/operator/multiple_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/regex_capture.rb +43 -0
- data/lib/evilution/mutator/operator/scalar_return.rb +37 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +46 -0
- data/lib/evilution/mutator/operator/yield_statement.rb +51 -0
- data/lib/evilution/mutator/registry.rb +9 -1
- data/lib/evilution/parallel/pool.rb +7 -53
- data/lib/evilution/parallel/work_queue.rb +265 -0
- data/lib/evilution/reporter/cli.rb +21 -1
- data/lib/evilution/reporter/html.rb +69 -3
- data/lib/evilution/reporter/json.rb +23 -2
- data/lib/evilution/reporter/suggestion.rb +29 -1
- data/lib/evilution/result/mutation_result.rb +5 -2
- data/lib/evilution/result/summary.rb +19 -2
- data/lib/evilution/runner.rb +123 -12
- data/lib/evilution/session/diff.rb +85 -0
- data/lib/evilution/spec_resolver.rb +13 -1
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +11 -0
- data/script/memory_check +22 -0
- metadata +14 -2
|
@@ -12,6 +12,8 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
12
12
|
def initialize(test_files: nil, hooks: nil)
|
|
13
13
|
@test_files = test_files
|
|
14
14
|
@rspec_loaded = false
|
|
15
|
+
@spec_resolver = Evilution::SpecResolver.new
|
|
16
|
+
@warned_files = Set.new
|
|
15
17
|
super(hooks: hooks)
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -98,9 +100,14 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
98
100
|
# process exits after each run.
|
|
99
101
|
#
|
|
100
102
|
# This integration can also be invoked directly (e.g. in specs or alternative
|
|
101
|
-
# runners) without fork isolation.
|
|
102
|
-
#
|
|
103
|
-
|
|
103
|
+
# runners) without fork isolation. clear_examples reuses the existing World
|
|
104
|
+
# and Configuration (avoiding per-run instance growth) while clearing loaded
|
|
105
|
+
# example groups, constants, and configuration state.
|
|
106
|
+
if ::RSpec.respond_to?(:clear_examples)
|
|
107
|
+
::RSpec.clear_examples
|
|
108
|
+
else
|
|
109
|
+
::RSpec.reset
|
|
110
|
+
end
|
|
104
111
|
|
|
105
112
|
out = StringIO.new
|
|
106
113
|
err = StringIO.new
|
|
@@ -108,11 +115,60 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
108
115
|
args = build_args(mutation)
|
|
109
116
|
command = "rspec #{args.join(" ")}"
|
|
110
117
|
|
|
118
|
+
eg_before = snapshot_example_groups
|
|
111
119
|
status = ::RSpec::Core::Runner.run(args, out, err)
|
|
112
120
|
|
|
113
121
|
{ passed: status.zero?, test_command: command }
|
|
114
122
|
rescue StandardError => e
|
|
115
123
|
{ passed: false, error: e.message, test_command: command }
|
|
124
|
+
ensure
|
|
125
|
+
release_rspec_state(eg_before)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def snapshot_example_groups
|
|
129
|
+
groups = Set.new
|
|
130
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
131
|
+
groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
|
|
132
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
133
|
+
end
|
|
134
|
+
groups
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def release_rspec_state(eg_before)
|
|
138
|
+
release_example_groups(eg_before)
|
|
139
|
+
# Remove ExampleGroups constants so the named reference is dropped.
|
|
140
|
+
# We avoid a full RSpec.reset here because it creates new World and
|
|
141
|
+
# Configuration instances each call; the pre-run reset already handles
|
|
142
|
+
# that. Instead, clear the world's example_groups array (which holds
|
|
143
|
+
# direct class references) and the source cache.
|
|
144
|
+
::RSpec::ExampleGroups.remove_all_constants if defined?(::RSpec::ExampleGroups)
|
|
145
|
+
release_world_example_groups
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def release_example_groups(eg_before)
|
|
149
|
+
return unless eg_before
|
|
150
|
+
|
|
151
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
152
|
+
next unless klass < ::RSpec::Core::ExampleGroup
|
|
153
|
+
next if eg_before.include?(klass.object_id)
|
|
154
|
+
|
|
155
|
+
# Remove nested module constants (LetDefinitions, NamedSubjectPreventSuper)
|
|
156
|
+
klass.constants(false).each do |const|
|
|
157
|
+
klass.send(:remove_const, const)
|
|
158
|
+
rescue NameError # rubocop:disable Lint/SuppressedException
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
klass.instance_variables.each do |ivar|
|
|
162
|
+
klass.remove_instance_variable(ivar)
|
|
163
|
+
end
|
|
164
|
+
rescue TypeError # rubocop:disable Lint/SuppressedException
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def release_world_example_groups
|
|
169
|
+
world = ::RSpec.world
|
|
170
|
+
world.instance_variable_get(:@example_groups).clear if world.instance_variable_defined?(:@example_groups)
|
|
171
|
+
world.instance_variable_set(:@sources_by_path, {}) if world.instance_variable_defined?(:@sources_by_path)
|
|
116
172
|
end
|
|
117
173
|
|
|
118
174
|
def build_args(mutation)
|
|
@@ -123,7 +179,20 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
123
179
|
def resolve_test_files(mutation)
|
|
124
180
|
return test_files if test_files
|
|
125
181
|
|
|
126
|
-
resolved =
|
|
127
|
-
|
|
182
|
+
resolved = @spec_resolver.call(mutation.file_path)
|
|
183
|
+
if resolved
|
|
184
|
+
[resolved]
|
|
185
|
+
else
|
|
186
|
+
warn_unresolved_spec(mutation.file_path)
|
|
187
|
+
["spec"]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def warn_unresolved_spec(file_path)
|
|
192
|
+
return if @warned_files.include?(file_path)
|
|
193
|
+
|
|
194
|
+
@warned_files << file_path
|
|
195
|
+
warn "[evilution] No matching spec found for #{file_path}, running full suite. " \
|
|
196
|
+
"Use --spec to specify the spec file."
|
|
128
197
|
end
|
|
129
198
|
end
|
|
@@ -16,6 +16,7 @@ class Evilution::Isolation::Fork
|
|
|
16
16
|
def call(mutation:, test_command:, timeout:)
|
|
17
17
|
sandbox_dir = Dir.mktmpdir("evilution-run")
|
|
18
18
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
parent_rss = Evilution::Memory.rss_kb
|
|
19
20
|
read_io, write_io = IO.pipe
|
|
20
21
|
|
|
21
22
|
pid = ::Process.fork do
|
|
@@ -33,7 +34,7 @@ class Evilution::Isolation::Fork
|
|
|
33
34
|
result = wait_for_result(pid, read_io, timeout)
|
|
34
35
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
35
36
|
|
|
36
|
-
build_mutation_result(mutation, result, duration)
|
|
37
|
+
build_mutation_result(mutation, result, duration, parent_rss)
|
|
37
38
|
ensure
|
|
38
39
|
read_io&.close
|
|
39
40
|
write_io&.close
|
|
@@ -67,10 +68,12 @@ class Evilution::Isolation::Fork
|
|
|
67
68
|
if read_io.wait_readable(timeout)
|
|
68
69
|
data = read_io.read
|
|
69
70
|
::Process.wait(pid)
|
|
70
|
-
return { timeout: false }.merge(Marshal.load(data)) unless data.empty? # rubocop:disable Security/MarshalLoad
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if data.empty?
|
|
73
|
+
{ timeout: false, passed: false, error: "empty result from child" }
|
|
74
|
+
else
|
|
75
|
+
{ timeout: false }.merge(Marshal.load(data)) # rubocop:disable Security/MarshalLoad
|
|
76
|
+
end
|
|
74
77
|
else
|
|
75
78
|
terminate_child(pid)
|
|
76
79
|
{ timeout: true }
|
|
@@ -90,7 +93,7 @@ class Evilution::Isolation::Fork
|
|
|
90
93
|
::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
def build_mutation_result(mutation, result, duration)
|
|
96
|
+
def build_mutation_result(mutation, result, duration, parent_rss_kb)
|
|
94
97
|
status = if result[:timeout]
|
|
95
98
|
:timeout
|
|
96
99
|
elsif result[:error]
|
|
@@ -106,7 +109,8 @@ class Evilution::Isolation::Fork
|
|
|
106
109
|
status: status,
|
|
107
110
|
duration: duration,
|
|
108
111
|
test_command: result[:test_command],
|
|
109
|
-
child_rss_kb: result[:child_rss_kb]
|
|
112
|
+
child_rss_kb: result[:child_rss_kb],
|
|
113
|
+
parent_rss_kb: parent_rss_kb
|
|
110
114
|
)
|
|
111
115
|
end
|
|
112
116
|
end
|
|
@@ -7,6 +7,13 @@ require_relative "../result/mutation_result"
|
|
|
7
7
|
require_relative "../isolation"
|
|
8
8
|
|
|
9
9
|
class Evilution::Isolation::InProcess
|
|
10
|
+
@null_out = File.open(File::NULL, "w")
|
|
11
|
+
@null_err = File.open(File::NULL, "w")
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :null_out, :null_err
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def call(mutation:, test_command:, timeout:)
|
|
11
18
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
12
19
|
rss_before = Evilution::Memory.rss_kb
|
|
@@ -15,7 +22,7 @@ class Evilution::Isolation::InProcess
|
|
|
15
22
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
16
23
|
delta = compute_memory_delta(rss_before, rss_after, result)
|
|
17
24
|
|
|
18
|
-
build_mutation_result(mutation, result, duration, rss_after, delta)
|
|
25
|
+
build_mutation_result(mutation, result, duration, rss_before, rss_after, delta)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
private
|
|
@@ -34,13 +41,9 @@ class Evilution::Isolation::InProcess
|
|
|
34
41
|
def suppress_output
|
|
35
42
|
saved_stdout = $stdout
|
|
36
43
|
saved_stderr = $stderr
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
$stderr = null_err
|
|
41
|
-
yield
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
+
$stdout = self.class.null_out
|
|
45
|
+
$stderr = self.class.null_err
|
|
46
|
+
yield
|
|
44
47
|
ensure
|
|
45
48
|
$stdout = saved_stdout
|
|
46
49
|
$stderr = saved_stderr
|
|
@@ -53,7 +56,7 @@ class Evilution::Isolation::InProcess
|
|
|
53
56
|
rss_after - rss_before
|
|
54
57
|
end
|
|
55
58
|
|
|
56
|
-
def build_mutation_result(mutation, result, duration, rss_after, memory_delta_kb)
|
|
59
|
+
def build_mutation_result(mutation, result, duration, rss_before, rss_after, memory_delta_kb)
|
|
57
60
|
status = if result[:timeout]
|
|
58
61
|
:timeout
|
|
59
62
|
elsif result[:error]
|
|
@@ -70,7 +73,8 @@ class Evilution::Isolation::InProcess
|
|
|
70
73
|
duration: duration,
|
|
71
74
|
test_command: result[:test_command],
|
|
72
75
|
child_rss_kb: rss_after,
|
|
73
|
-
memory_delta_kb: memory_delta_kb
|
|
76
|
+
memory_delta_kb: memory_delta_kb,
|
|
77
|
+
parent_rss_kb: rss_before
|
|
74
78
|
)
|
|
75
79
|
end
|
|
76
80
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "mcp"
|
|
5
5
|
require_relative "../session/store"
|
|
6
|
+
require_relative "../session/diff"
|
|
6
7
|
|
|
7
8
|
require_relative "../mcp"
|
|
8
9
|
|
|
@@ -33,7 +34,10 @@ class Evilution::MCP::SessionDiffTool < MCP::Tool
|
|
|
33
34
|
base_data = store.load(base)
|
|
34
35
|
head_data = store.load(head)
|
|
35
36
|
|
|
36
|
-
::
|
|
37
|
+
diff = Evilution::Session::Diff.new
|
|
38
|
+
result = diff.call(base_data, head_data)
|
|
39
|
+
|
|
40
|
+
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(result.to_h) }])
|
|
37
41
|
rescue Evilution::Error => e
|
|
38
42
|
error_response("not_found", e.message)
|
|
39
43
|
rescue ::JSON::ParserError => e
|
|
@@ -45,40 +49,6 @@ class Evilution::MCP::SessionDiffTool < MCP::Tool
|
|
|
45
49
|
|
|
46
50
|
private
|
|
47
51
|
|
|
48
|
-
def build_diff(base_data, head_data)
|
|
49
|
-
base_survivors = base_data["survived"] || []
|
|
50
|
-
head_survivors = head_data["survived"] || []
|
|
51
|
-
|
|
52
|
-
base_keys = base_survivors.to_set { |m| mutation_key(m) }
|
|
53
|
-
head_keys = head_survivors.to_set { |m| mutation_key(m) }
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
"summary" => build_summary_diff(base_data, head_data),
|
|
57
|
-
"fixed" => base_survivors.reject { |m| head_keys.include?(mutation_key(m)) },
|
|
58
|
-
"new_survivors" => head_survivors.reject { |m| base_keys.include?(mutation_key(m)) },
|
|
59
|
-
"persistent" => head_survivors.select { |m| base_keys.include?(mutation_key(m)) }
|
|
60
|
-
}
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def build_summary_diff(base_data, head_data)
|
|
64
|
-
base_summary = base_data["summary"] || {}
|
|
65
|
-
head_summary = head_data["summary"] || {}
|
|
66
|
-
base_score = base_summary["score"] || 0.0
|
|
67
|
-
head_score = head_summary["score"] || 0.0
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
"base_score" => base_score,
|
|
71
|
-
"head_score" => head_score,
|
|
72
|
-
"score_delta" => (head_score - base_score).round(4),
|
|
73
|
-
"base_survived" => base_summary["survived"] || 0,
|
|
74
|
-
"head_survived" => head_summary["survived"] || 0
|
|
75
|
-
}
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def mutation_key(mutation)
|
|
79
|
-
[mutation["operator"], mutation["file"], mutation["line"], mutation["subject"]]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
52
|
def error_response(type, message)
|
|
83
53
|
::MCP::Tool::Response.new(
|
|
84
54
|
[{ type: "text", text: ::JSON.generate({ error: { type: type, message: message } }) }],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::CollectionReturn < Evilution::Mutator::Base
|
|
6
|
+
def visit_def_node(node)
|
|
7
|
+
body = node.body
|
|
8
|
+
if body.is_a?(Prism::StatementsNode) && body.body.length > 1
|
|
9
|
+
return_node = body.body.last
|
|
10
|
+
replacement = collection_replacement(return_node)
|
|
11
|
+
|
|
12
|
+
if replacement
|
|
13
|
+
add_mutation(
|
|
14
|
+
offset: body.location.start_offset,
|
|
15
|
+
length: body.location.length,
|
|
16
|
+
replacement: replacement,
|
|
17
|
+
node: node
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def collection_replacement(node)
|
|
26
|
+
case node
|
|
27
|
+
when Prism::ArrayNode
|
|
28
|
+
"[]" if node.elements.any?
|
|
29
|
+
when Prism::HashNode
|
|
30
|
+
"{}" if node.elements.any?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::DefinedCheck < Evilution::Mutator::Base
|
|
6
|
+
def visit_defined_node(node)
|
|
7
|
+
add_mutation(
|
|
8
|
+
offset: node.location.start_offset,
|
|
9
|
+
length: node.location.length,
|
|
10
|
+
replacement: "true",
|
|
11
|
+
node: node
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
6
|
+
def visit_def_node(node)
|
|
7
|
+
params = node.parameters
|
|
8
|
+
if params
|
|
9
|
+
mutate_optional_keyword_defaults(params)
|
|
10
|
+
mutate_optional_keyword_removal(params)
|
|
11
|
+
mutate_keyword_rest_removal(params)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def mutate_optional_keyword_defaults(params)
|
|
20
|
+
params.keywords.each do |kw|
|
|
21
|
+
next unless kw.is_a?(Prism::OptionalKeywordParameterNode)
|
|
22
|
+
|
|
23
|
+
name_loc = kw.name_loc
|
|
24
|
+
kw_loc = kw.location
|
|
25
|
+
|
|
26
|
+
add_mutation(
|
|
27
|
+
offset: kw_loc.start_offset,
|
|
28
|
+
length: kw_loc.length,
|
|
29
|
+
replacement: @file_source[name_loc.start_offset...name_loc.end_offset],
|
|
30
|
+
node: kw
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mutate_optional_keyword_removal(params)
|
|
36
|
+
all_params = collect_all_params(params)
|
|
37
|
+
return if all_params.length < 2
|
|
38
|
+
|
|
39
|
+
params.keywords.each do |kw|
|
|
40
|
+
next unless kw.is_a?(Prism::OptionalKeywordParameterNode)
|
|
41
|
+
|
|
42
|
+
remaining = all_params.reject { |p| p.equal?(kw) }
|
|
43
|
+
replacement = remaining.map(&:slice).join(", ")
|
|
44
|
+
|
|
45
|
+
add_mutation(
|
|
46
|
+
offset: params.location.start_offset,
|
|
47
|
+
length: params.location.length,
|
|
48
|
+
replacement: replacement,
|
|
49
|
+
node: kw
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mutate_keyword_rest_removal(params)
|
|
55
|
+
kr = params.keyword_rest
|
|
56
|
+
return unless kr.is_a?(Prism::KeywordRestParameterNode)
|
|
57
|
+
|
|
58
|
+
all_params = collect_all_params(params)
|
|
59
|
+
|
|
60
|
+
if all_params.length < 2
|
|
61
|
+
add_mutation(
|
|
62
|
+
offset: kr.location.start_offset,
|
|
63
|
+
length: kr.location.length,
|
|
64
|
+
replacement: "",
|
|
65
|
+
node: kr
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
remaining = all_params.reject { |p| p.equal?(kr) }
|
|
69
|
+
replacement = remaining.map(&:slice).join(", ")
|
|
70
|
+
|
|
71
|
+
add_mutation(
|
|
72
|
+
offset: params.location.start_offset,
|
|
73
|
+
length: params.location.length,
|
|
74
|
+
replacement: replacement,
|
|
75
|
+
node: kr
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def collect_all_params(params)
|
|
81
|
+
result = []
|
|
82
|
+
result.concat(params.requireds)
|
|
83
|
+
result.concat(params.optionals)
|
|
84
|
+
result << params.rest if params.rest
|
|
85
|
+
result.concat(params.posts)
|
|
86
|
+
result.concat(params.keywords)
|
|
87
|
+
result << params.keyword_rest if params.keyword_rest
|
|
88
|
+
result << params.block if params.block
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::MultipleAssignment < Evilution::Mutator::Base
|
|
6
|
+
def visit_multi_write_node(node)
|
|
7
|
+
lefts = node.lefts
|
|
8
|
+
values = node.value.is_a?(Prism::ArrayNode) ? node.value.elements : nil
|
|
9
|
+
|
|
10
|
+
if values && lefts.length == values.length && lefts.length >= 2 && node.rest.nil?
|
|
11
|
+
mutate_target_removal(node, lefts, values)
|
|
12
|
+
mutate_swap(node, lefts, values) if lefts.length == 2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def mutate_target_removal(node, lefts, values)
|
|
21
|
+
lefts.each_index do |i|
|
|
22
|
+
remaining_lefts = lefts.each_with_index.filter_map { |l, j| l.slice if j != i }
|
|
23
|
+
remaining_values = values.each_with_index.filter_map { |v, j| v.slice if j != i }
|
|
24
|
+
|
|
25
|
+
replacement = "#{remaining_lefts.join(", ")} = #{remaining_values.join(", ")}"
|
|
26
|
+
|
|
27
|
+
add_mutation(
|
|
28
|
+
offset: node.location.start_offset,
|
|
29
|
+
length: node.location.length,
|
|
30
|
+
replacement: replacement,
|
|
31
|
+
node: node
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mutate_swap(node, lefts, values)
|
|
37
|
+
swapped_lefts = "#{lefts[1].slice}, #{lefts[0].slice}"
|
|
38
|
+
replacement = "#{swapped_lefts} = #{values.map(&:slice).join(", ")}"
|
|
39
|
+
|
|
40
|
+
add_mutation(
|
|
41
|
+
offset: node.location.start_offset,
|
|
42
|
+
length: node.location.length,
|
|
43
|
+
replacement: replacement,
|
|
44
|
+
node: node
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::RegexCapture < Evilution::Mutator::Base
|
|
6
|
+
def visit_numbered_reference_read_node(node)
|
|
7
|
+
mutate_replace_with_nil(node)
|
|
8
|
+
mutate_swap_number(node)
|
|
9
|
+
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def mutate_replace_with_nil(node)
|
|
16
|
+
add_mutation(
|
|
17
|
+
offset: node.location.start_offset,
|
|
18
|
+
length: node.location.length,
|
|
19
|
+
replacement: "nil",
|
|
20
|
+
node: node
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mutate_swap_number(node)
|
|
25
|
+
number = node.number
|
|
26
|
+
|
|
27
|
+
if number > 1
|
|
28
|
+
add_mutation(
|
|
29
|
+
offset: node.location.start_offset,
|
|
30
|
+
length: node.location.length,
|
|
31
|
+
replacement: "$#{number - 1}",
|
|
32
|
+
node: node
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
add_mutation(
|
|
37
|
+
offset: node.location.start_offset,
|
|
38
|
+
length: node.location.length,
|
|
39
|
+
replacement: "$#{number + 1}",
|
|
40
|
+
node: node
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::ScalarReturn < Evilution::Mutator::Base
|
|
6
|
+
def visit_def_node(node)
|
|
7
|
+
body = node.body
|
|
8
|
+
if body.is_a?(Prism::StatementsNode) && body.body.length > 1
|
|
9
|
+
return_node = body.body.last
|
|
10
|
+
replacement = scalar_replacement(return_node)
|
|
11
|
+
|
|
12
|
+
if replacement
|
|
13
|
+
add_mutation(
|
|
14
|
+
offset: body.location.start_offset,
|
|
15
|
+
length: body.location.length,
|
|
16
|
+
replacement: replacement,
|
|
17
|
+
node: node
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def scalar_replacement(node)
|
|
28
|
+
case node
|
|
29
|
+
when Prism::StringNode
|
|
30
|
+
'""' unless node.content.empty?
|
|
31
|
+
when Prism::IntegerNode
|
|
32
|
+
"0" unless node.value.zero?
|
|
33
|
+
when Prism::FloatNode
|
|
34
|
+
"0.0" unless node.value.zero?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
6
|
+
def visit_splat_node(node)
|
|
7
|
+
mutate_remove_splat(node) if node.expression
|
|
8
|
+
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_hash_node(node)
|
|
13
|
+
node.elements.each { |el| hash_elements.add(el) }
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_assoc_splat_node(node)
|
|
18
|
+
mutate_remove_double_splat(node) if node.value && !hash_elements.include?(node)
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def hash_elements
|
|
26
|
+
@hash_elements ||= Set.new.compare_by_identity
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mutate_remove_splat(node)
|
|
30
|
+
add_mutation(
|
|
31
|
+
offset: node.location.start_offset,
|
|
32
|
+
length: node.location.length,
|
|
33
|
+
replacement: node.expression.slice,
|
|
34
|
+
node: node
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mutate_remove_double_splat(node)
|
|
39
|
+
add_mutation(
|
|
40
|
+
offset: node.location.start_offset,
|
|
41
|
+
length: node.location.length,
|
|
42
|
+
replacement: node.value.slice,
|
|
43
|
+
node: node
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::YieldStatement < Evilution::Mutator::Base
|
|
6
|
+
def visit_yield_node(node)
|
|
7
|
+
mutate_remove_yield(node)
|
|
8
|
+
|
|
9
|
+
if node.arguments
|
|
10
|
+
mutate_remove_arguments(node)
|
|
11
|
+
mutate_replace_value_with_nil(node)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def mutate_remove_yield(node)
|
|
20
|
+
add_mutation(
|
|
21
|
+
offset: node.location.start_offset,
|
|
22
|
+
length: node.location.length,
|
|
23
|
+
replacement: "nil",
|
|
24
|
+
node: node
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def mutate_remove_arguments(node)
|
|
29
|
+
add_mutation(
|
|
30
|
+
offset: node.location.start_offset,
|
|
31
|
+
length: node.location.length,
|
|
32
|
+
replacement: "yield",
|
|
33
|
+
node: node
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mutate_replace_value_with_nil(node)
|
|
38
|
+
replacement = if node.lparen_loc
|
|
39
|
+
"yield(nil)"
|
|
40
|
+
else
|
|
41
|
+
"yield nil"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
add_mutation(
|
|
45
|
+
offset: node.location.start_offset,
|
|
46
|
+
length: node.location.length,
|
|
47
|
+
replacement: replacement,
|
|
48
|
+
node: node
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -57,7 +57,15 @@ class Evilution::Mutator::Registry
|
|
|
57
57
|
Evilution::Mutator::Operator::IndexAssignmentRemoval,
|
|
58
58
|
Evilution::Mutator::Operator::PatternMatchingGuard,
|
|
59
59
|
Evilution::Mutator::Operator::PatternMatchingAlternative,
|
|
60
|
-
Evilution::Mutator::Operator::PatternMatchingArray
|
|
60
|
+
Evilution::Mutator::Operator::PatternMatchingArray,
|
|
61
|
+
Evilution::Mutator::Operator::CollectionReturn,
|
|
62
|
+
Evilution::Mutator::Operator::ScalarReturn,
|
|
63
|
+
Evilution::Mutator::Operator::KeywordArgument,
|
|
64
|
+
Evilution::Mutator::Operator::MultipleAssignment,
|
|
65
|
+
Evilution::Mutator::Operator::YieldStatement,
|
|
66
|
+
Evilution::Mutator::Operator::SplatOperator,
|
|
67
|
+
Evilution::Mutator::Operator::DefinedCheck,
|
|
68
|
+
Evilution::Mutator::Operator::RegexCapture
|
|
61
69
|
].each { |op| registry.register(op) }
|
|
62
70
|
registry
|
|
63
71
|
end
|