evilution 0.27.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 +65 -0
- data/.rubocop_todo.yml +0 -1
- data/CHANGELOG.md +39 -0
- data/README.md +19 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +5 -4
- 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 +36 -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/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +27 -9
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config.rb +49 -32
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +43 -28
- data/lib/evilution/isolation/in_process.rb +10 -6
- 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 -3
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -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 -20
- data/lib/evilution/mutation.rb +60 -42
- data/lib/evilution/mutator/base.rb +23 -21
- 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/predicate_to_nil.rb +20 -0
- 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/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/process_cleanup.rb +19 -0
- 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/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- 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/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +16 -10
- data/lib/evilution/runner/diagnostics.rb +14 -11
- data/lib/evilution/runner/isolation_resolver.rb +12 -11
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +14 -20
- data/lib/evilution/runner/mutation_planner.rb +38 -19
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +22 -13
- data/lib/evilution/runner.rb +36 -34
- 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/lib/evilution.rb +1 -0
- data/script/memory_check +14 -6
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +15 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
|
@@ -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
|
|
@@ -13,27 +13,33 @@ class Evilution::Mutator::Operator::MixinRemoval < Evilution::Mutator::Base
|
|
|
13
13
|
@mutations = []
|
|
14
14
|
@filter = filter
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
enclosing = find_enclosing_scope(tree, subject.line_number)
|
|
16
|
+
enclosing = find_target_scope(subject)
|
|
18
17
|
return @mutations unless enclosing
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
return @mutations unless first_method_line == subject.line_number
|
|
22
|
-
|
|
23
|
-
find_mixin_calls(enclosing).each do |call_node|
|
|
24
|
-
add_mutation(
|
|
25
|
-
offset: call_node.location.start_offset,
|
|
26
|
-
length: call_node.location.length,
|
|
27
|
-
replacement: "",
|
|
28
|
-
node: call_node
|
|
29
|
-
)
|
|
30
|
-
end
|
|
31
|
-
|
|
19
|
+
find_mixin_calls(enclosing).each { |call_node| emit_mixin_removal(call_node) }
|
|
32
20
|
@mutations
|
|
33
21
|
end
|
|
34
22
|
|
|
35
23
|
private
|
|
36
24
|
|
|
25
|
+
def find_target_scope(subject)
|
|
26
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
27
|
+
enclosing = find_enclosing_scope(tree, subject.line_number)
|
|
28
|
+
return nil unless enclosing
|
|
29
|
+
return nil unless find_first_method_line(enclosing) == subject.line_number
|
|
30
|
+
|
|
31
|
+
enclosing
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emit_mixin_removal(call_node)
|
|
35
|
+
add_mutation(
|
|
36
|
+
offset: call_node.location.start_offset,
|
|
37
|
+
length: call_node.location.length,
|
|
38
|
+
replacement: "",
|
|
39
|
+
node: call_node
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def find_enclosing_scope(tree, target_line)
|
|
38
44
|
finder = ScopeFinder.new(target_line)
|
|
39
45
|
finder.visit(tree)
|
|
@@ -18,19 +18,18 @@ class Evilution::Mutator::Operator::MultipleAssignment < Evilution::Mutator::Bas
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
20
|
def mutate_target_removal(node, lefts, values)
|
|
21
|
-
lefts.each_index
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
21
|
+
lefts.each_index { |i| emit_target_removal_at(node, lefts, values, i) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def emit_target_removal_at(node, lefts, values, i)
|
|
25
|
+
remaining_lefts = lefts.each_with_index.filter_map { |l, j| l.slice if j != i }
|
|
26
|
+
remaining_values = values.each_with_index.filter_map { |v, j| v.slice if j != i }
|
|
27
|
+
add_mutation(
|
|
28
|
+
offset: node.location.start_offset,
|
|
29
|
+
length: node.location.length,
|
|
30
|
+
replacement: "#{remaining_lefts.join(", ")} = #{remaining_values.join(", ")}",
|
|
31
|
+
node: node
|
|
32
|
+
)
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
def mutate_swap(node, lefts, values)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PredicateToNil < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
7
|
+
if node.name.to_s.end_with?("?")
|
|
8
|
+
loc = node.location
|
|
9
|
+
|
|
10
|
+
add_mutation(
|
|
11
|
+
offset: loc.start_offset,
|
|
12
|
+
length: loc.length,
|
|
13
|
+
replacement: "nil",
|
|
14
|
+
node: node
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,19 +5,22 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::ReceiverReplacement < Evilution::Mutator::Base
|
|
6
6
|
def visit_call_node(node)
|
|
7
7
|
if node.receiver.is_a?(Prism::SelfNode)
|
|
8
|
-
call_without_self = @file_source.byteslice(
|
|
9
|
-
node.message_loc.start_offset,
|
|
10
|
-
node.location.start_offset + node.location.length - node.message_loc.start_offset
|
|
11
|
-
)
|
|
12
|
-
|
|
13
8
|
add_mutation(
|
|
14
9
|
offset: node.location.start_offset,
|
|
15
10
|
length: node.location.length,
|
|
16
|
-
replacement:
|
|
11
|
+
replacement: call_without_self_text(node),
|
|
17
12
|
node: node
|
|
18
13
|
)
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
super
|
|
22
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def call_without_self_text(node)
|
|
22
|
+
message_start = node.message_loc.start_offset
|
|
23
|
+
call_end = node.location.start_offset + node.location.length
|
|
24
|
+
@file_source.byteslice(message_start, call_end - message_start)
|
|
25
|
+
end
|
|
23
26
|
end
|
|
@@ -19,31 +19,21 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def remove_quantifiers(node, content, content_offset)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
scan_regex_positions(content) do |kind, i|
|
|
23
|
+
case kind
|
|
24
|
+
when :backslash then 2
|
|
25
|
+
when :class_open then class_skip(content, i)
|
|
26
|
+
when :char then emit_quantifier_at(node, content, content_offset, i)
|
|
27
27
|
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
31
|
+
def emit_quantifier_at(node, content, content_offset, i)
|
|
32
|
+
match = match_quantifier(content, i)
|
|
33
|
+
return 1 if match.nil?
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
add_mutation(
|
|
37
|
-
offset: content_offset + i,
|
|
38
|
-
length: match.length,
|
|
39
|
-
replacement: "",
|
|
40
|
-
node: node
|
|
41
|
-
)
|
|
42
|
-
i += match.length
|
|
43
|
-
else
|
|
44
|
-
i += 1
|
|
45
|
-
end
|
|
46
|
-
end
|
|
35
|
+
add_mutation(offset: content_offset + i, length: match.length, replacement: "", node: node)
|
|
36
|
+
match.length
|
|
47
37
|
end
|
|
48
38
|
|
|
49
39
|
def match_quantifier(content, pos)
|
|
@@ -58,40 +48,29 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
58
48
|
end
|
|
59
49
|
|
|
60
50
|
def remove_anchors(node, content, content_offset)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
length: anchor.length,
|
|
69
|
-
replacement: "",
|
|
70
|
-
node: node
|
|
71
|
-
)
|
|
72
|
-
i += anchor.length
|
|
73
|
-
else
|
|
74
|
-
i += 2
|
|
75
|
-
end
|
|
76
|
-
next
|
|
51
|
+
scan_regex_positions(content) do |kind, i|
|
|
52
|
+
case kind
|
|
53
|
+
when :backslash then try_emit_backslash_anchor(node, content, content_offset, i)
|
|
54
|
+
when :class_open then class_skip(content, i)
|
|
55
|
+
when :char
|
|
56
|
+
try_emit_caret_dollar(node, content, content_offset, i)
|
|
57
|
+
1
|
|
77
58
|
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
62
|
+
def try_emit_backslash_anchor(node, content, content_offset, i)
|
|
63
|
+
anchor = match_backslash_anchor(content, i)
|
|
64
|
+
return 2 if anchor.nil?
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
length: 1,
|
|
88
|
-
replacement: "",
|
|
89
|
-
node: node
|
|
90
|
-
)
|
|
91
|
-
end
|
|
66
|
+
add_mutation(offset: content_offset + i, length: anchor.length, replacement: "", node: node)
|
|
67
|
+
anchor.length
|
|
68
|
+
end
|
|
92
69
|
|
|
93
|
-
|
|
94
|
-
|
|
70
|
+
def try_emit_caret_dollar(node, content, content_offset, i)
|
|
71
|
+
return unless %w[^ $].include?(content[i])
|
|
72
|
+
|
|
73
|
+
add_mutation(offset: content_offset + i, length: 1, replacement: "", node: node)
|
|
95
74
|
end
|
|
96
75
|
|
|
97
76
|
def match_backslash_anchor(content, pos)
|
|
@@ -104,18 +83,13 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
104
83
|
end
|
|
105
84
|
|
|
106
85
|
def remove_character_class_ranges(node, content, content_offset)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
next
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
if content[i] == "["
|
|
86
|
+
scan_regex_positions(content) do |kind, i|
|
|
87
|
+
case kind
|
|
88
|
+
when :backslash then 2
|
|
89
|
+
when :class_open
|
|
115
90
|
scan_ranges_in_class(node, content, content_offset, i)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
i += 1
|
|
91
|
+
class_skip(content, i)
|
|
92
|
+
when :char then 1
|
|
119
93
|
end
|
|
120
94
|
end
|
|
121
95
|
end
|
|
@@ -153,17 +127,38 @@ class Evilution::Mutator::Operator::RegexSimplification < Evilution::Mutator::Ba
|
|
|
153
127
|
)
|
|
154
128
|
end
|
|
155
129
|
|
|
130
|
+
# Walks `content` yielding (kind, position) for each significant token:
|
|
131
|
+
# :backslash for an escape sequence, :class_open for `[`, :char for any
|
|
132
|
+
# other byte. The block returns the number of characters to advance from
|
|
133
|
+
# `position` — callers decide how to handle each case (skip, emit a
|
|
134
|
+
# mutation, descend into a character class, etc.).
|
|
135
|
+
def scan_regex_positions(content)
|
|
136
|
+
i = 0
|
|
137
|
+
while i < content.length
|
|
138
|
+
advance = case content[i]
|
|
139
|
+
when "\\" then yield(:backslash, i)
|
|
140
|
+
when "[" then yield(:class_open, i)
|
|
141
|
+
else yield(:char, i)
|
|
142
|
+
end
|
|
143
|
+
i += advance
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def class_skip(content, pos)
|
|
148
|
+
skip_character_class(content, pos) - pos
|
|
149
|
+
end
|
|
150
|
+
|
|
156
151
|
def skip_character_class(content, pos)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
i += 1 if i < content.length && content[i] == "]"
|
|
152
|
+
scan_to_class_close(content, skip_class_prefix(content, pos))
|
|
153
|
+
end
|
|
160
154
|
|
|
155
|
+
def scan_to_class_close(content, start)
|
|
156
|
+
i = start
|
|
161
157
|
while i < content.length
|
|
162
158
|
return i + 1 if content[i] == "]"
|
|
163
159
|
|
|
164
160
|
i += content[i] == "\\" ? 2 : 1
|
|
165
161
|
end
|
|
166
|
-
|
|
167
162
|
i
|
|
168
163
|
end
|
|
169
164
|
end
|
|
@@ -72,14 +72,15 @@ class Evilution::Mutator::Operator::RescueBodyReplacement < Evilution::Mutator::
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def rescue_line_end(node)
|
|
75
|
-
if node.reference
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
loc = if node.reference
|
|
76
|
+
node.reference.location
|
|
77
|
+
elsif node.exceptions.any?
|
|
78
|
+
node.exceptions.last.location
|
|
79
|
+
else
|
|
80
|
+
node.keyword_loc
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
loc.start_offset + loc.length
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def indentation_of(offset)
|
|
@@ -20,13 +20,10 @@ class Evilution::Mutator::Operator::RescueRemoval < Evilution::Mutator::Base
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
def rescue_end_offset(node)
|
|
23
|
-
if node.subsequent
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else
|
|
28
|
-
node.keyword_loc.start_offset + node.keyword_loc.length
|
|
29
|
-
end
|
|
23
|
+
return line_start_before(node.subsequent.keyword_loc.start_offset) if node.subsequent
|
|
24
|
+
|
|
25
|
+
loc = node.statements ? node.statements.location : node.keyword_loc
|
|
26
|
+
loc.start_offset + loc.length
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def line_start_before(offset)
|
|
@@ -11,29 +11,35 @@ class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
|
11
11
|
@mutations = []
|
|
12
12
|
@filter = filter
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
14
|
+
enclosing = find_target_class(subject)
|
|
16
15
|
return @mutations unless enclosing
|
|
17
|
-
return @mutations unless enclosing.superclass
|
|
18
|
-
|
|
19
|
-
first_method_line = find_first_method_line(enclosing)
|
|
20
|
-
return @mutations unless first_method_line == subject.line_number
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
add_mutation(
|
|
26
|
-
offset: name_end,
|
|
27
|
-
length: superclass_end - name_end,
|
|
28
|
-
replacement: "",
|
|
29
|
-
node: enclosing
|
|
30
|
-
)
|
|
17
|
+
offset, length = superclass_range(enclosing)
|
|
18
|
+
add_mutation(offset: offset, length: length, replacement: "", node: enclosing)
|
|
31
19
|
|
|
32
20
|
@mutations
|
|
33
21
|
end
|
|
34
22
|
|
|
35
23
|
private
|
|
36
24
|
|
|
25
|
+
def find_target_class(subject)
|
|
26
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
27
|
+
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
28
|
+
return nil unless enclosing && enclosing.superclass
|
|
29
|
+
return nil unless find_first_method_line(enclosing) == subject.line_number
|
|
30
|
+
|
|
31
|
+
enclosing
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def superclass_range(class_node)
|
|
35
|
+
name_loc = class_node.constant_path.location
|
|
36
|
+
superclass_loc = class_node.superclass.location
|
|
37
|
+
name_end = name_loc.start_offset + name_loc.length
|
|
38
|
+
superclass_end = superclass_loc.start_offset + superclass_loc.length
|
|
39
|
+
|
|
40
|
+
[name_end, superclass_end - name_end]
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def find_enclosing_class(tree, target_line)
|
|
38
44
|
finder = ClassFinder.new(target_line)
|
|
39
45
|
finder.visit(tree)
|
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
require_relative "../mutator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Registry
|
|
6
|
+
STRICT_EXTRA_OPERATORS = [
|
|
7
|
+
Evilution::Mutator::Operator::PredicateToNil
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
def self.for_profile(profile)
|
|
11
|
+
unless profile.is_a?(Symbol) || profile.is_a?(String)
|
|
12
|
+
raise ArgumentError, "unknown profile: #{profile.inspect} (expected :default or :strict)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
case profile.to_sym
|
|
16
|
+
when :default then default
|
|
17
|
+
when :strict
|
|
18
|
+
registry = default
|
|
19
|
+
STRICT_EXTRA_OPERATORS.each { |op| registry.register(op) }
|
|
20
|
+
registry
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "unknown profile: #{profile.inspect} (expected :default or :strict)"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
6
26
|
def self.default
|
|
7
27
|
registry = new
|
|
8
28
|
[
|
|
@@ -10,12 +10,16 @@ module Evilution::Parallel::WorkQueue::Channel::Frame
|
|
|
10
10
|
[payload.bytesize].pack("N") + payload
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Marshal.load is safe here: payload originates from a sibling worker the
|
|
14
|
+
# parent itself forked, transferred over a private pipe inside our process
|
|
15
|
+
# tree. No external/untrusted input ever reaches this code. See
|
|
16
|
+
# .rubocop.yml (Security/MarshalLoad) for the full rationale.
|
|
13
17
|
def decode(header, payload)
|
|
14
18
|
return nil if header.nil? || header.bytesize < 4
|
|
15
19
|
|
|
16
20
|
length = header.unpack1("N")
|
|
17
21
|
return nil if payload.nil? || payload.bytesize < length
|
|
18
22
|
|
|
19
|
-
Marshal.load(payload)
|
|
23
|
+
Marshal.load(payload)
|
|
20
24
|
end
|
|
21
25
|
end
|