evilution 0.28.0 → 0.29.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/interactions.jsonl +52 -0
- data/CHANGELOG.md +7 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +9 -11
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +29 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/normalizer.rb +10 -5
- data/lib/evilution/config.rb +10 -10
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +27 -17
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool.rb +7 -1
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +27 -14
- data/lib/evilution/mcp/session_tool.rb +27 -18
- data/lib/evilution/mutation.rb +13 -15
- data/lib/evilution/mutator/base.rb +17 -15
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
- data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/json.rb +52 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
- data/lib/evilution/runner/baseline_runner.rb +15 -8
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +11 -9
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
- data/lib/evilution/runner/mutation_executor.rb +2 -0
- data/lib/evilution/runner/mutation_planner.rb +37 -17
- data/lib/evilution/runner/subject_pipeline.rb +21 -11
- data/lib/evilution/runner.rb +3 -3
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +11 -5
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +3 -2
data/lib/evilution/mutation.rb
CHANGED
|
@@ -74,22 +74,17 @@ class Evilution::Mutation
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def compute_diff
|
|
77
|
-
|
|
78
|
-
mutated_lines = mutated_source.lines
|
|
79
|
-
diffs = ::Diff::LCS.diff(original_lines, mutated_lines)
|
|
80
|
-
|
|
77
|
+
diffs = ::Diff::LCS.diff(original_source.lines, mutated_source.lines)
|
|
81
78
|
return "" if diffs.empty?
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
end
|
|
80
|
+
diffs.flatten(1).filter_map { |change| format_diff_change(change) }.join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_diff_change(change)
|
|
84
|
+
case change.action
|
|
85
|
+
when "-" then "- #{change.element.chomp}"
|
|
86
|
+
when "+" then "+ #{change.element.chomp}"
|
|
91
87
|
end
|
|
92
|
-
result.join("\n")
|
|
93
88
|
end
|
|
94
89
|
|
|
95
90
|
def compute_unified_diff
|
|
@@ -97,15 +92,18 @@ class Evilution::Mutation
|
|
|
97
92
|
|
|
98
93
|
original_lines = @slice.original.lines
|
|
99
94
|
mutated_lines = @slice.mutated.lines
|
|
100
|
-
body = ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
|
|
101
95
|
[
|
|
102
96
|
"--- a/#{file_path}",
|
|
103
97
|
"+++ b/#{file_path}",
|
|
104
98
|
"@@ -#{line},#{original_lines.length} +#{line},#{mutated_lines.length} @@",
|
|
105
|
-
|
|
99
|
+
unified_diff_body(original_lines, mutated_lines)
|
|
106
100
|
].reject(&:empty?).join("\n")
|
|
107
101
|
end
|
|
108
102
|
|
|
103
|
+
def unified_diff_body(original_lines, mutated_lines)
|
|
104
|
+
::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
|
|
105
|
+
end
|
|
106
|
+
|
|
109
107
|
def format_sdiff_change(change)
|
|
110
108
|
case change.action
|
|
111
109
|
when "=" then " #{change.old_element.chomp}"
|
|
@@ -5,6 +5,9 @@ require "prism"
|
|
|
5
5
|
require_relative "../mutator"
|
|
6
6
|
|
|
7
7
|
class Evilution::Mutator::Base < Prism::Visitor
|
|
8
|
+
AffectedSlices = Data.define(:original, :mutated)
|
|
9
|
+
private_constant :AffectedSlices
|
|
10
|
+
|
|
8
11
|
attr_reader :mutations
|
|
9
12
|
|
|
10
13
|
def initialize(**_options)
|
|
@@ -28,25 +31,24 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
28
31
|
return if @filter && @filter.skip?(node)
|
|
29
32
|
|
|
30
33
|
surgery = Evilution::AST::SourceSurgeon.apply(
|
|
31
|
-
@file_source,
|
|
32
|
-
offset: offset,
|
|
33
|
-
length: length,
|
|
34
|
-
replacement: replacement
|
|
34
|
+
@file_source, offset: offset, length: length, replacement: replacement
|
|
35
35
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
original_slice, mutated_slice = slice_affected_lines(
|
|
39
|
-
mutated_source: mutated_source,
|
|
36
|
+
slices = slice_affected_lines(
|
|
37
|
+
mutated_source: surgery.source,
|
|
40
38
|
offset: offset,
|
|
41
39
|
length: length,
|
|
42
40
|
replacement_bytesize: replacement.bytesize
|
|
43
41
|
)
|
|
44
42
|
|
|
45
|
-
@mutations <<
|
|
43
|
+
@mutations << build_mutation_record(node, surgery, slices)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_mutation_record(node, surgery, slices)
|
|
47
|
+
Evilution::Mutation.new(
|
|
46
48
|
subject: @subject,
|
|
47
49
|
operator_name: self.class.operator_name,
|
|
48
|
-
sources: Evilution::Mutation::Sources.new(original: @file_source, mutated:
|
|
49
|
-
slice: Evilution::Mutation::Slice.new(original:
|
|
50
|
+
sources: Evilution::Mutation::Sources.new(original: @file_source, mutated: surgery.source),
|
|
51
|
+
slice: Evilution::Mutation::Slice.new(original: slices.original, mutated: slices.mutated),
|
|
50
52
|
location: Evilution::Mutation::Location.new(
|
|
51
53
|
file_path: @subject.file_path,
|
|
52
54
|
line: node.location.start_line,
|
|
@@ -64,10 +66,10 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
64
66
|
orig_line_end = line_end_byte(@file_source, [offset + length - 1, line_start].max)
|
|
65
67
|
mut_line_end = line_end_byte(mutated_source, [offset + replacement_bytesize - 1, line_start].max)
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
@file_source.byteslice(line_start, orig_line_end - line_start),
|
|
69
|
-
mutated_source.byteslice(line_start, mut_line_end - line_start)
|
|
70
|
-
|
|
69
|
+
AffectedSlices.new(
|
|
70
|
+
original: @file_source.byteslice(line_start, orig_line_end - line_start),
|
|
71
|
+
mutated: mutated_source.byteslice(line_start, mut_line_end - line_start)
|
|
72
|
+
)
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def line_start_byte(source, offset)
|
|
@@ -12,26 +12,23 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
|
|
|
12
12
|
|
|
13
13
|
def visit_call_node(node)
|
|
14
14
|
args = node.arguments&.arguments
|
|
15
|
-
|
|
16
|
-
if mutable?(node, args)
|
|
17
|
-
args.each_index do |i|
|
|
18
|
-
parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
|
|
19
|
-
replacement = parts.join(", ")
|
|
20
|
-
|
|
21
|
-
add_mutation(
|
|
22
|
-
offset: node.arguments.location.start_offset,
|
|
23
|
-
length: node.arguments.location.length,
|
|
24
|
-
replacement: replacement,
|
|
25
|
-
node: node
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
15
|
+
args.each_index { |i| emit_nil_substitution(node, args, i) } if mutable?(node, args)
|
|
29
16
|
|
|
30
17
|
super
|
|
31
18
|
end
|
|
32
19
|
|
|
33
20
|
private
|
|
34
21
|
|
|
22
|
+
def emit_nil_substitution(node, args, i)
|
|
23
|
+
parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: node.arguments.location.start_offset,
|
|
26
|
+
length: node.arguments.location.length,
|
|
27
|
+
replacement: parts.join(", "),
|
|
28
|
+
node: node
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
35
32
|
def mutable?(node, args)
|
|
36
33
|
args && args.length >= 1 && positional_only?(args) && node.name != :[]=
|
|
37
34
|
end
|
|
@@ -12,26 +12,23 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
12
12
|
|
|
13
13
|
def visit_call_node(node)
|
|
14
14
|
args = node.arguments&.arguments
|
|
15
|
-
|
|
16
|
-
if mutable?(node, args)
|
|
17
|
-
args.each_index do |i|
|
|
18
|
-
remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
|
|
19
|
-
replacement = remaining.join(", ")
|
|
20
|
-
|
|
21
|
-
add_mutation(
|
|
22
|
-
offset: node.arguments.location.start_offset,
|
|
23
|
-
length: node.arguments.location.length,
|
|
24
|
-
replacement:,
|
|
25
|
-
node:
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
15
|
+
args.each_index { |i| emit_argument_removal(node, args, i) } if mutable?(node, args)
|
|
29
16
|
|
|
30
17
|
super
|
|
31
18
|
end
|
|
32
19
|
|
|
33
20
|
private
|
|
34
21
|
|
|
22
|
+
def emit_argument_removal(node, args, i)
|
|
23
|
+
remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: node.arguments.location.start_offset,
|
|
26
|
+
length: node.arguments.location.length,
|
|
27
|
+
replacement: remaining.join(", "),
|
|
28
|
+
node: node
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
35
32
|
def mutable?(node, args)
|
|
36
33
|
args && args.length >= 2 && positional_only?(args) && node.name != :[]=
|
|
37
34
|
end
|
|
@@ -4,18 +4,30 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::BeginUnwrap < Evilution::Mutator::Base
|
|
6
6
|
def visit_begin_node(node)
|
|
7
|
-
return super
|
|
8
|
-
return super if node.statements.nil?
|
|
9
|
-
return super if node.begin_keyword_loc.nil?
|
|
7
|
+
return super unless unwrappable?(node)
|
|
10
8
|
|
|
11
|
-
body_text = @file_source.byteslice(node.statements.location.start_offset, node.statements.location.length)
|
|
12
9
|
add_mutation(
|
|
13
10
|
offset: node.location.start_offset,
|
|
14
11
|
length: node.location.length,
|
|
15
|
-
replacement: body_text,
|
|
12
|
+
replacement: body_text(node),
|
|
16
13
|
node: node
|
|
17
14
|
)
|
|
18
15
|
|
|
19
16
|
super
|
|
20
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def unwrappable?(node)
|
|
22
|
+
return false if node.rescue_clause || node.else_clause || node.ensure_clause
|
|
23
|
+
return false if node.statements.nil?
|
|
24
|
+
return false if node.begin_keyword_loc.nil?
|
|
25
|
+
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def body_text(node)
|
|
30
|
+
loc = node.statements.location
|
|
31
|
+
@file_source.byteslice(loc.start_offset, loc.length)
|
|
32
|
+
end
|
|
21
33
|
end
|
|
@@ -5,27 +5,34 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::BitwiseComplement < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if node.name == :~ && node.receiver && node.arguments.nil?
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Remove ~: replace entire ~expr with just the receiver expression
|
|
12
|
-
receiver_source = byteslice_source(receiver_loc.start_offset, receiver_loc.length)
|
|
13
|
-
add_mutation(
|
|
14
|
-
offset: node.location.start_offset,
|
|
15
|
-
length: node.location.length,
|
|
16
|
-
replacement: receiver_source,
|
|
17
|
-
node: node
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# Swap ~ with unary minus
|
|
21
|
-
add_mutation(
|
|
22
|
-
offset: loc.start_offset,
|
|
23
|
-
length: loc.length,
|
|
24
|
-
replacement: "-",
|
|
25
|
-
node: node
|
|
26
|
-
)
|
|
8
|
+
emit_remove_complement(node)
|
|
9
|
+
emit_swap_to_minus(node)
|
|
27
10
|
end
|
|
28
11
|
|
|
29
12
|
super
|
|
30
13
|
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Replace `~expr` with just `expr`.
|
|
18
|
+
def emit_remove_complement(node)
|
|
19
|
+
receiver_loc = node.receiver.location
|
|
20
|
+
add_mutation(
|
|
21
|
+
offset: node.location.start_offset,
|
|
22
|
+
length: node.location.length,
|
|
23
|
+
replacement: byteslice_source(receiver_loc.start_offset, receiver_loc.length),
|
|
24
|
+
node: node
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Swap `~` with unary minus.
|
|
29
|
+
def emit_swap_to_minus(node)
|
|
30
|
+
loc = node.message_loc
|
|
31
|
+
add_mutation(
|
|
32
|
+
offset: loc.start_offset,
|
|
33
|
+
length: loc.length,
|
|
34
|
+
replacement: "-",
|
|
35
|
+
node: node
|
|
36
|
+
)
|
|
37
|
+
end
|
|
31
38
|
end
|
|
@@ -38,14 +38,7 @@ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def remove_block_param(node)
|
|
41
|
-
|
|
42
|
-
params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
|
|
43
|
-
block_rel = block_loc.start_offset - node.parameters.location.start_offset
|
|
44
|
-
|
|
45
|
-
# Find the comma before the block param and remove ", &block"
|
|
46
|
-
comma_pos = params_text.rindex(",", block_rel - 1)
|
|
47
|
-
remove_start = node.parameters.location.start_offset + comma_pos
|
|
48
|
-
remove_end = block_loc.start_offset + block_loc.length
|
|
41
|
+
remove_start, remove_end = block_param_removal_range(node)
|
|
49
42
|
|
|
50
43
|
add_mutation(
|
|
51
44
|
offset: remove_start,
|
|
@@ -54,4 +47,21 @@ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
|
|
|
54
47
|
node: node
|
|
55
48
|
)
|
|
56
49
|
end
|
|
50
|
+
|
|
51
|
+
# Range covering ", &block" — from the comma before the block param to the end of the block param.
|
|
52
|
+
def block_param_removal_range(node)
|
|
53
|
+
params_loc = node.parameters.location
|
|
54
|
+
block_loc = node.parameters.block.location
|
|
55
|
+
comma_pos = params_text(params_loc).rindex(",", block_loc.start_offset - params_loc.start_offset - 1)
|
|
56
|
+
|
|
57
|
+
[params_loc.start_offset + comma_pos, end_offset(block_loc)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def params_text(params_loc)
|
|
61
|
+
@file_source.byteslice(params_loc.start_offset, params_loc.length)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def end_offset(loc)
|
|
65
|
+
loc.start_offset + loc.length
|
|
66
|
+
end
|
|
57
67
|
end
|
|
@@ -4,27 +4,31 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::BlockPassRemoval < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
call_start = node.location.start_offset
|
|
10
|
-
node_end = call_start + node.location.length
|
|
11
|
-
block_end = block_node.location.start_offset + block_node.location.length
|
|
12
|
-
|
|
13
|
-
prefix = @file_source.byteslice(call_start...block_node.location.start_offset).rstrip
|
|
14
|
-
suffix = @file_source.byteslice(block_end...node_end)
|
|
15
|
-
|
|
16
|
-
# Clean up: remove trailing comma from prefix, remove empty parens
|
|
17
|
-
prefix = prefix.sub(/,\s*\z/, "")
|
|
18
|
-
replacement = "#{prefix}#{suffix}".sub(/\(\s*\)/, "")
|
|
19
|
-
|
|
7
|
+
block_node = node.block
|
|
8
|
+
if block_node.is_a?(Prism::BlockArgumentNode)
|
|
20
9
|
add_mutation(
|
|
21
|
-
offset:
|
|
10
|
+
offset: node.location.start_offset,
|
|
22
11
|
length: node.location.length,
|
|
23
|
-
replacement:
|
|
12
|
+
replacement: build_replacement(node, block_node),
|
|
24
13
|
node: node
|
|
25
14
|
)
|
|
26
15
|
end
|
|
27
16
|
|
|
28
17
|
super
|
|
29
18
|
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Drop the block-pass argument plus the trailing comma it leaves behind, and
|
|
23
|
+
# collapse the resulting `()` if the block-pass was the only argument.
|
|
24
|
+
def build_replacement(node, block_node)
|
|
25
|
+
call_start = node.location.start_offset
|
|
26
|
+
node_end = call_start + node.location.length
|
|
27
|
+
block_start = block_node.location.start_offset
|
|
28
|
+
block_end = block_start + block_node.location.length
|
|
29
|
+
|
|
30
|
+
prefix = @file_source.byteslice(call_start...block_start).rstrip.sub(/,\s*\z/, "")
|
|
31
|
+
suffix = @file_source.byteslice(block_end...node_end)
|
|
32
|
+
"#{prefix}#{suffix}".sub(/\(\s*\)/, "")
|
|
33
|
+
end
|
|
30
34
|
end
|
|
@@ -40,16 +40,18 @@ class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def remove_else_branch(node)
|
|
43
|
-
|
|
44
|
-
return if
|
|
43
|
+
else_clause = node.else_clause
|
|
44
|
+
return if else_clause.nil? || else_clause.statements.nil?
|
|
45
|
+
|
|
46
|
+
start_offset = else_clause.else_keyword_loc.start_offset
|
|
47
|
+
stmts_loc = else_clause.statements.location
|
|
48
|
+
end_offset = stmts_loc.start_offset + stmts_loc.length
|
|
45
49
|
|
|
46
|
-
start_offset = node.else_clause.else_keyword_loc.start_offset
|
|
47
|
-
end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
|
|
48
50
|
add_mutation(
|
|
49
51
|
offset: start_offset,
|
|
50
52
|
length: end_offset - start_offset,
|
|
51
53
|
replacement: "",
|
|
52
|
-
node:
|
|
54
|
+
node: else_clause
|
|
53
55
|
)
|
|
54
56
|
end
|
|
55
57
|
end
|
|
@@ -4,29 +4,29 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::ConditionalBranch < Evilution::Mutator::Base
|
|
6
6
|
def visit_if_node(node)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
replacement: "nil",
|
|
12
|
-
node: node
|
|
13
|
-
)
|
|
14
|
-
elsif node.statements && node.subsequent&.statements
|
|
15
|
-
add_mutation(
|
|
16
|
-
offset: node.statements.location.start_offset,
|
|
17
|
-
length: node.statements.location.length,
|
|
18
|
-
replacement: "nil",
|
|
19
|
-
node: node
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
add_mutation(
|
|
23
|
-
offset: node.subsequent.statements.location.start_offset,
|
|
24
|
-
length: node.subsequent.statements.location.length,
|
|
25
|
-
replacement: "nil",
|
|
26
|
-
node: node
|
|
27
|
-
)
|
|
28
|
-
end
|
|
7
|
+
return super unless node.statements
|
|
8
|
+
|
|
9
|
+
add_nil_mutation(node.statements, node)
|
|
10
|
+
add_nil_mutation_to_else(node.subsequent, node)
|
|
29
11
|
|
|
30
12
|
super
|
|
31
13
|
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def add_nil_mutation_to_else(subsequent, node)
|
|
18
|
+
return unless subsequent.is_a?(Prism::ElseNode)
|
|
19
|
+
return if subsequent.statements.nil?
|
|
20
|
+
|
|
21
|
+
add_nil_mutation(subsequent.statements, node)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_nil_mutation(statements, node)
|
|
25
|
+
add_mutation(
|
|
26
|
+
offset: statements.location.start_offset,
|
|
27
|
+
length: statements.location.length,
|
|
28
|
+
replacement: "nil",
|
|
29
|
+
node: node
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
32
|
end
|
|
@@ -5,9 +5,8 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if node.name == :== && node.receiver && node.arguments
|
|
8
|
-
receiver_text =
|
|
9
|
-
|
|
10
|
-
arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
|
|
8
|
+
receiver_text = loc_text(node.receiver.location)
|
|
9
|
+
arg_text = loc_text(node.arguments.arguments.first.location)
|
|
11
10
|
|
|
12
11
|
add_mutation(
|
|
13
12
|
offset: node.location.start_offset,
|
|
@@ -19,4 +18,10 @@ class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Bas
|
|
|
19
18
|
|
|
20
19
|
super
|
|
21
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def loc_text(loc)
|
|
25
|
+
@file_source.byteslice(loc.start_offset, loc.length)
|
|
26
|
+
end
|
|
22
27
|
end
|
|
@@ -23,25 +23,29 @@ class Evilution::Mutator::Operator::ExplicitSuperMutation < Evilution::Mutator::
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def mutate_arguments(node, args)
|
|
26
|
-
|
|
26
|
+
emit_remove_all_args(node)
|
|
27
|
+
return unless args.length >= 2
|
|
28
|
+
|
|
29
|
+
args.each_index { |i| emit_remove_arg_at(node, args, i) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# super(a, b) -> super()
|
|
33
|
+
def emit_remove_all_args(node)
|
|
27
34
|
add_mutation(
|
|
28
35
|
offset: node.arguments.location.start_offset,
|
|
29
36
|
length: node.arguments.location.length,
|
|
30
37
|
replacement: "",
|
|
31
38
|
node: node
|
|
32
39
|
)
|
|
40
|
+
end
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
replacement: remaining.join(", "),
|
|
43
|
-
node: node
|
|
44
|
-
)
|
|
45
|
-
end
|
|
42
|
+
def emit_remove_arg_at(node, args, i)
|
|
43
|
+
remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
|
|
44
|
+
add_mutation(
|
|
45
|
+
offset: node.arguments.location.start_offset,
|
|
46
|
+
length: node.arguments.location.length,
|
|
47
|
+
replacement: remaining.join(", "),
|
|
48
|
+
node: node
|
|
49
|
+
)
|
|
46
50
|
end
|
|
47
51
|
end
|
|
@@ -5,13 +5,10 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::IndexToAt < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if indexable?(node)
|
|
8
|
-
receiver_source = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
|
|
9
|
-
arg_source = @file_source.byteslice(node.arguments.location.start_offset, node.arguments.location.length)
|
|
10
|
-
|
|
11
8
|
add_mutation(
|
|
12
9
|
offset: node.location.start_offset,
|
|
13
10
|
length: node.location.length,
|
|
14
|
-
replacement: "#{
|
|
11
|
+
replacement: "#{loc_text(node.receiver.location)}.at(#{loc_text(node.arguments.location)})",
|
|
15
12
|
node: node
|
|
16
13
|
)
|
|
17
14
|
end
|
|
@@ -21,6 +18,10 @@ class Evilution::Mutator::Operator::IndexToAt < Evilution::Mutator::Base
|
|
|
21
18
|
|
|
22
19
|
private
|
|
23
20
|
|
|
21
|
+
def loc_text(loc)
|
|
22
|
+
@file_source.byteslice(loc.start_offset, loc.length)
|
|
23
|
+
end
|
|
24
|
+
|
|
24
25
|
def indexable?(node)
|
|
25
26
|
node.name == :[] &&
|
|
26
27
|
node.receiver &&
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
|
|
6
|
+
Chain = Data.define(:root, :args)
|
|
7
|
+
private_constant :Chain
|
|
8
|
+
|
|
6
9
|
def initialize(**options)
|
|
7
10
|
super
|
|
8
11
|
@consumed = Set.new
|
|
@@ -10,14 +13,11 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
|
|
|
10
13
|
|
|
11
14
|
def visit_call_node(node)
|
|
12
15
|
if chain_head?(node)
|
|
13
|
-
|
|
14
|
-
root_source = byteslice_source(root.location.start_offset, root.location.length)
|
|
15
|
-
arg_sources = args.map { |a| byteslice_source(a.location.start_offset, a.location.length) }
|
|
16
|
-
|
|
16
|
+
chain = collect_chain(node)
|
|
17
17
|
add_mutation(
|
|
18
18
|
offset: node.location.start_offset,
|
|
19
19
|
length: node.location.length,
|
|
20
|
-
replacement:
|
|
20
|
+
replacement: dig_replacement(chain),
|
|
21
21
|
node: node
|
|
22
22
|
)
|
|
23
23
|
end
|
|
@@ -27,6 +27,12 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
|
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
|
+
def dig_replacement(chain)
|
|
31
|
+
root_source = byteslice_source(chain.root.location.start_offset, chain.root.location.length)
|
|
32
|
+
arg_sources = chain.args.map { |a| byteslice_source(a.location.start_offset, a.location.length) }
|
|
33
|
+
"#{root_source}.dig(#{arg_sources.join(", ")})"
|
|
34
|
+
end
|
|
35
|
+
|
|
30
36
|
def chain_head?(node)
|
|
31
37
|
return false if @consumed.include?(node.object_id)
|
|
32
38
|
return false unless single_arg_index?(node)
|
|
@@ -53,6 +59,6 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
|
|
|
53
59
|
current = current.receiver
|
|
54
60
|
end
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
Chain.new(root: current, args: args)
|
|
57
63
|
end
|
|
58
64
|
end
|
|
@@ -5,13 +5,10 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::IndexToFetch < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if indexable?(node)
|
|
8
|
-
receiver_source = byteslice_source(node.receiver.location.start_offset, node.receiver.location.length)
|
|
9
|
-
arg_source = byteslice_source(node.arguments.location.start_offset, node.arguments.location.length)
|
|
10
|
-
|
|
11
8
|
add_mutation(
|
|
12
9
|
offset: node.location.start_offset,
|
|
13
10
|
length: node.location.length,
|
|
14
|
-
replacement: "#{
|
|
11
|
+
replacement: "#{loc_text(node.receiver.location)}.fetch(#{loc_text(node.arguments.location)})",
|
|
15
12
|
node: node
|
|
16
13
|
)
|
|
17
14
|
end
|
|
@@ -21,6 +18,10 @@ class Evilution::Mutator::Operator::IndexToFetch < Evilution::Mutator::Base
|
|
|
21
18
|
|
|
22
19
|
private
|
|
23
20
|
|
|
21
|
+
def loc_text(loc)
|
|
22
|
+
byteslice_source(loc.start_offset, loc.length)
|
|
23
|
+
end
|
|
24
|
+
|
|
24
25
|
def indexable?(node)
|
|
25
26
|
node.name == :[] &&
|
|
26
27
|
node.receiver &&
|
|
@@ -56,36 +56,41 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
56
56
|
return unless kr.is_a?(Prism::KeywordRestParameterNode)
|
|
57
57
|
|
|
58
58
|
all_params = collect_all_params(params)
|
|
59
|
-
|
|
60
59
|
if all_params.length < 2
|
|
61
|
-
|
|
62
|
-
offset: kr.location.start_offset,
|
|
63
|
-
length: kr.location.length,
|
|
64
|
-
replacement: "",
|
|
65
|
-
node: kr
|
|
66
|
-
)
|
|
60
|
+
emit_remove_only_kr(kr)
|
|
67
61
|
else
|
|
68
|
-
|
|
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
|
-
)
|
|
62
|
+
emit_remove_kr_with_remaining(params, all_params, kr)
|
|
77
63
|
end
|
|
78
64
|
end
|
|
79
65
|
|
|
66
|
+
def emit_remove_only_kr(kr)
|
|
67
|
+
add_mutation(
|
|
68
|
+
offset: kr.location.start_offset,
|
|
69
|
+
length: kr.location.length,
|
|
70
|
+
replacement: "",
|
|
71
|
+
node: kr
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit_remove_kr_with_remaining(params, all_params, kr)
|
|
76
|
+
remaining = all_params.reject { |p| p.equal?(kr) }
|
|
77
|
+
add_mutation(
|
|
78
|
+
offset: params.location.start_offset,
|
|
79
|
+
length: params.location.length,
|
|
80
|
+
replacement: remaining.map(&:slice).join(", "),
|
|
81
|
+
node: kr
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
80
85
|
def collect_all_params(params)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
[
|
|
87
|
+
*params.requireds,
|
|
88
|
+
*params.optionals,
|
|
89
|
+
params.rest,
|
|
90
|
+
*params.posts,
|
|
91
|
+
*params.keywords,
|
|
92
|
+
params.keyword_rest,
|
|
93
|
+
params.block
|
|
94
|
+
].compact
|
|
90
95
|
end
|
|
91
96
|
end
|