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
|
@@ -50,8 +50,10 @@ class Evilution::MCP::SessionTool < MCP::Tool
|
|
|
50
50
|
|
|
51
51
|
VALID_ACTIONS = %w[list show diff].freeze
|
|
52
52
|
|
|
53
|
+
LimitResult = Data.define(:limit, :error)
|
|
54
|
+
private_constant :LimitResult
|
|
55
|
+
|
|
53
56
|
class << self
|
|
54
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
|
55
57
|
def call(server_context:, action: nil, results_dir: nil, limit: nil, path: nil, base: nil, head: nil)
|
|
56
58
|
return error_response("config_error", "action is required") unless action
|
|
57
59
|
return error_response("config_error", "unknown action: #{action}") unless VALID_ACTIONS.include?(action)
|
|
@@ -62,33 +64,32 @@ class Evilution::MCP::SessionTool < MCP::Tool
|
|
|
62
64
|
when "diff" then diff_action(base: base, head: head, results_dir: results_dir)
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
70
|
def list_action(results_dir:, limit:)
|
|
70
|
-
|
|
71
|
-
return error_response("config_error",
|
|
71
|
+
result = normalize_limit(limit)
|
|
72
|
+
return error_response("config_error", result.error) if result.error
|
|
72
73
|
|
|
73
74
|
store_opts = {}
|
|
74
75
|
store_opts[:results_dir] = results_dir if results_dir
|
|
75
76
|
store = Evilution::Session::Store.new(**store_opts)
|
|
76
77
|
entries = store.list
|
|
77
|
-
entries = entries.first(
|
|
78
|
+
entries = entries.first(result.limit) unless result.limit.nil?
|
|
78
79
|
|
|
79
80
|
payload = entries.map { |e| e.transform_keys(&:to_s) }
|
|
80
81
|
success_response(payload)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def normalize_limit(limit)
|
|
84
|
-
return
|
|
85
|
+
return LimitResult.new(limit: nil, error: nil) if limit.nil?
|
|
85
86
|
|
|
86
87
|
coerced = Integer(limit)
|
|
87
|
-
return
|
|
88
|
+
return LimitResult.new(limit: nil, error: "limit must be a non-negative integer") if coerced.negative?
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
LimitResult.new(limit: coerced, error: nil)
|
|
90
91
|
rescue ArgumentError, TypeError
|
|
91
|
-
|
|
92
|
+
LimitResult.new(limit: nil, error: "limit must be a non-negative integer")
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
def show_action(path:, results_dir:)
|
|
@@ -109,19 +110,11 @@ class Evilution::MCP::SessionTool < MCP::Tool
|
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def diff_action(base:, head:, results_dir:)
|
|
112
|
-
return error_response("config_error", "base is required") unless base
|
|
113
|
-
return error_response("config_error", "head is required") unless head
|
|
114
|
-
|
|
115
113
|
dir = results_dir || Evilution::Session::Store::DEFAULT_DIR
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
store = Evilution::Session::Store.new(results_dir: dir)
|
|
120
|
-
base_data = store.load(base)
|
|
121
|
-
head_data = store.load(head)
|
|
114
|
+
validation = validate_diff_args(base, head, dir)
|
|
115
|
+
return validation if validation
|
|
122
116
|
|
|
123
|
-
|
|
124
|
-
result = diff.call(base_data, head_data)
|
|
117
|
+
result = load_and_diff(base, head, dir)
|
|
125
118
|
success_response(result.to_h)
|
|
126
119
|
rescue Evilution::Error => e
|
|
127
120
|
error_response("not_found", e.message)
|
|
@@ -131,6 +124,20 @@ class Evilution::MCP::SessionTool < MCP::Tool
|
|
|
131
124
|
error_response("runtime_error", e.message)
|
|
132
125
|
end
|
|
133
126
|
|
|
127
|
+
def validate_diff_args(base, head, dir)
|
|
128
|
+
return error_response("config_error", "base is required") unless base
|
|
129
|
+
return error_response("config_error", "head is required") unless head
|
|
130
|
+
return error_response("config_error", "base must be under results directory") unless within?(base, dir)
|
|
131
|
+
return error_response("config_error", "head must be under results directory") unless within?(head, dir)
|
|
132
|
+
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def load_and_diff(base, head, dir)
|
|
137
|
+
store = Evilution::Session::Store.new(results_dir: dir)
|
|
138
|
+
Evilution::Session::Diff.new.call(store.load(base), store.load(head))
|
|
139
|
+
end
|
|
140
|
+
|
|
134
141
|
def within?(path, results_dir)
|
|
135
142
|
resolved_root = canonical_path(results_dir)
|
|
136
143
|
resolved_path = canonical_path(path)
|
data/lib/evilution/mutation.rb
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "diff/lcs"
|
|
4
|
+
require_relative "../evilution"
|
|
4
5
|
|
|
5
6
|
class Evilution::Mutation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
parse_status: :ok)
|
|
14
|
-
# rubocop:enable Metrics/ParameterLists
|
|
7
|
+
Sources = Data.define(:original, :mutated)
|
|
8
|
+
Slice = Data.define(:original, :mutated)
|
|
9
|
+
Location = Data.define(:file_path, :line, :column)
|
|
10
|
+
|
|
11
|
+
attr_reader :subject, :operator_name, :parse_status, :location
|
|
12
|
+
|
|
13
|
+
def initialize(subject:, operator_name:, sources:, location:, slice: nil, parse_status: :ok)
|
|
15
14
|
@subject = subject
|
|
16
15
|
@operator_name = operator_name
|
|
17
|
-
@
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@mutated_slice = mutated_slice
|
|
21
|
-
@file_path = file_path
|
|
22
|
-
@line = line
|
|
23
|
-
@column = column
|
|
16
|
+
@sources = sources
|
|
17
|
+
@location = location
|
|
18
|
+
@slice = slice
|
|
24
19
|
@parse_status = parse_status
|
|
25
20
|
@diff = nil
|
|
26
21
|
end
|
|
27
22
|
|
|
23
|
+
def original_source
|
|
24
|
+
@sources&.original
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mutated_source
|
|
28
|
+
@sources&.mutated
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def original_slice
|
|
32
|
+
@slice&.original
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mutated_slice
|
|
36
|
+
@slice&.mutated
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def file_path
|
|
40
|
+
@location.file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line
|
|
44
|
+
@location.line
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def column
|
|
48
|
+
@location.column
|
|
49
|
+
end
|
|
50
|
+
|
|
28
51
|
def unparseable?
|
|
29
52
|
@parse_status == :unparseable
|
|
30
53
|
end
|
|
@@ -41,45 +64,46 @@ class Evilution::Mutation
|
|
|
41
64
|
|
|
42
65
|
def strip_sources!
|
|
43
66
|
diff # ensure diff is cached before clearing sources
|
|
44
|
-
@
|
|
45
|
-
|
|
67
|
+
@sources = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s
|
|
71
|
+
"#{operator_name}: #{file_path}:#{line}"
|
|
46
72
|
end
|
|
47
73
|
|
|
48
74
|
private
|
|
49
75
|
|
|
50
76
|
def compute_diff
|
|
51
|
-
|
|
52
|
-
mutated_lines = mutated_source.lines
|
|
53
|
-
diffs = ::Diff::LCS.diff(original_lines, mutated_lines)
|
|
54
|
-
|
|
77
|
+
diffs = ::Diff::LCS.diff(original_source.lines, mutated_source.lines)
|
|
55
78
|
return "" if diffs.empty?
|
|
56
79
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
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}"
|
|
65
87
|
end
|
|
66
|
-
result.join("\n")
|
|
67
88
|
end
|
|
68
89
|
|
|
69
90
|
def compute_unified_diff
|
|
70
|
-
return nil if @
|
|
91
|
+
return nil if @slice.nil?
|
|
71
92
|
|
|
72
|
-
original_lines = @
|
|
73
|
-
mutated_lines = @
|
|
74
|
-
body = ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
|
|
93
|
+
original_lines = @slice.original.lines
|
|
94
|
+
mutated_lines = @slice.mutated.lines
|
|
75
95
|
[
|
|
76
96
|
"--- a/#{file_path}",
|
|
77
97
|
"+++ b/#{file_path}",
|
|
78
98
|
"@@ -#{line},#{original_lines.length} +#{line},#{mutated_lines.length} @@",
|
|
79
|
-
|
|
99
|
+
unified_diff_body(original_lines, mutated_lines)
|
|
80
100
|
].reject(&:empty?).join("\n")
|
|
81
101
|
end
|
|
82
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
|
+
|
|
83
107
|
def format_sdiff_change(change)
|
|
84
108
|
case change.action
|
|
85
109
|
when "=" then " #{change.old_element.chomp}"
|
|
@@ -88,10 +112,4 @@ class Evilution::Mutation
|
|
|
88
112
|
when "!" then "-#{change.old_element.chomp}\n+#{change.new_element.chomp}"
|
|
89
113
|
end
|
|
90
114
|
end
|
|
91
|
-
|
|
92
|
-
public
|
|
93
|
-
|
|
94
|
-
def to_s
|
|
95
|
-
"#{operator_name}: #{file_path}:#{line}"
|
|
96
|
-
end
|
|
97
115
|
end
|
|
@@ -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,31 +31,30 @@ 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
sources: Evilution::Mutation::Sources.new(original: @file_source, mutated: surgery.source),
|
|
51
|
+
slice: Evilution::Mutation::Slice.new(original: slices.original, mutated: slices.mutated),
|
|
52
|
+
location: Evilution::Mutation::Location.new(
|
|
53
|
+
file_path: @subject.file_path,
|
|
54
|
+
line: node.location.start_line,
|
|
55
|
+
column: node.location.start_column
|
|
56
|
+
),
|
|
57
|
+
parse_status: surgery.status
|
|
56
58
|
)
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -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
|