evilution 0.28.0 → 0.30.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 +106 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +49 -0
- data/README.md +194 -8
- data/docs/versioning.md +53 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/heredoc_span.rb +99 -0
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +15 -2
- data/lib/evilution/cli/commands/compare.rb +13 -0
- 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 +12 -12
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +31 -3
- 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/file_loader.rb +40 -1
- data/lib/evilution/config.rb +21 -11
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/equivalent/heuristic/dead_code.rb +8 -1
- data/lib/evilution/feedback/setup_warning.rb +79 -0
- data/lib/evilution/gem_detector.rb +132 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +190 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +35 -15
- data/lib/evilution/integration/loading/redefinition_recovery.rb +58 -1
- data/lib/evilution/integration/minitest.rb +60 -16
- data/lib/evilution/integration/rspec/result_builder.rb +20 -1
- data/lib/evilution/integration/rspec.rb +20 -1
- data/lib/evilution/isolation/fork.rb +104 -27
- 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/response_formatter.rb +14 -1
- data/lib/evilution/mcp/info_tool.rb +10 -2
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +4 -2
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +58 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +49 -17
- data/lib/evilution/mcp/session_tool.rb +34 -22
- data/lib/evilution/mcp.rb +6 -0
- data/lib/evilution/mutation.rb +26 -16
- data/lib/evilution/mutator/base.rb +66 -16
- data/lib/evilution/mutator/operator/argument_method_call_replacement.rb +59 -0
- 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 +50 -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 +36 -14
- data/lib/evilution/mutator/operator/index_to_at.rb +18 -5
- 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/last_expression_removal.rb +46 -0
- 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 +38 -7
- 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 +58 -12
- data/lib/evilution/mutator/operator/splat_operator.rb +28 -1
- data/lib/evilution/mutator/operator/string_literal.rb +83 -6
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/mutator/registry.rb +2 -0
- 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/error_rate_warning.rb +29 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json.rb +54 -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/result/mutation_result.rb +12 -6
- data/lib/evilution/runner/baseline_runner.rb +20 -9
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +75 -12
- 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 +53 -16
- 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/session/schema.rb +44 -0
- data/lib/evilution/session/store.rb +5 -1
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +2 -0
- data/schema/evilution.config.schema.json +205 -0
- data/script/build_runtime_snapshot +88 -0
- data/script/memory_check +11 -5
- data/script/run_self_baseline +79 -0
- data/script/run_self_validation +54 -0
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +16 -2
|
@@ -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,47 @@ 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
|
+
#
|
|
34
|
+
# The argument list's byte range covers only the args themselves; the
|
|
35
|
+
# separator (`,` + whitespace) between the last arg and a following
|
|
36
|
+
# `&block` (or the closing `)` after a trailing comma) is owned by the
|
|
37
|
+
# SuperNode itself. Replacing only `arguments.location` with `""` leaves
|
|
38
|
+
# `super(, &block)` or `super(,)`. Extend the removal range to the start
|
|
39
|
+
# of the block argument when present, otherwise to the closing paren — so
|
|
40
|
+
# the separator goes along with the args.
|
|
41
|
+
def emit_remove_all_args(node)
|
|
42
|
+
start_offset = node.arguments.location.start_offset
|
|
43
|
+
end_offset = trailing_args_boundary(node)
|
|
44
|
+
|
|
27
45
|
add_mutation(
|
|
28
|
-
offset:
|
|
29
|
-
length:
|
|
46
|
+
offset: start_offset,
|
|
47
|
+
length: end_offset - start_offset,
|
|
30
48
|
replacement: "",
|
|
31
49
|
node: node
|
|
32
50
|
)
|
|
51
|
+
end
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
def trailing_args_boundary(node)
|
|
54
|
+
return node.block.location.start_offset if node.block
|
|
55
|
+
return node.rparen_loc.start_offset if node.rparen_loc
|
|
56
|
+
|
|
57
|
+
node.arguments.location.end_offset
|
|
58
|
+
end
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
args.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
end
|
|
60
|
+
def emit_remove_arg_at(node, args, i)
|
|
61
|
+
remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
|
|
62
|
+
add_mutation(
|
|
63
|
+
offset: node.arguments.location.start_offset,
|
|
64
|
+
length: node.arguments.location.length,
|
|
65
|
+
replacement: remaining.join(", "),
|
|
66
|
+
node: node
|
|
67
|
+
)
|
|
46
68
|
end
|
|
47
69
|
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,10 +18,26 @@ 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
|
+
|
|
25
|
+
# EV-pn5y / GH #1173: Hash has no #at method, so the symbol/string keys that
|
|
26
|
+
# almost always indicate a Hash receiver are skipped here — otherwise the
|
|
27
|
+
# mutated source crashes with NoMethodError instead of yielding a measurable
|
|
28
|
+
# mutation. Integer literals and variable/expression keys are still mutated;
|
|
29
|
+
# if the receiver in those cases turns out to be a Hash the mutation will
|
|
30
|
+
# still raise NoMethodError at runtime, but those shapes are far rarer in
|
|
31
|
+
# practice and the AST gives no reliable receiver-type signal to filter on.
|
|
24
32
|
def indexable?(node)
|
|
25
33
|
node.name == :[] &&
|
|
26
34
|
node.receiver &&
|
|
27
35
|
node.arguments &&
|
|
28
|
-
node.arguments.arguments.length == 1
|
|
36
|
+
node.arguments.arguments.length == 1 &&
|
|
37
|
+
!hash_key_shape?(node.arguments.arguments.first)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def hash_key_shape?(arg)
|
|
41
|
+
arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
29
42
|
end
|
|
30
43
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Removes a trailing literal expression (true/false/nil/integer/symbol) from
|
|
6
|
+
# a method body. Targets the idiomatic Ruby pattern `def foo?; side_effect;
|
|
7
|
+
# true; end` where the explicit literal return value is the high-signal
|
|
8
|
+
# behavior under test — dropping it makes the method return whatever the
|
|
9
|
+
# preceding statement evaluates to. Strong against predicates and
|
|
10
|
+
# command-query split methods.
|
|
11
|
+
class Evilution::Mutator::Operator::LastExpressionRemoval < Evilution::Mutator::Base
|
|
12
|
+
LITERAL_NODE_TYPES = [
|
|
13
|
+
Prism::TrueNode,
|
|
14
|
+
Prism::FalseNode,
|
|
15
|
+
Prism::NilNode,
|
|
16
|
+
Prism::IntegerNode,
|
|
17
|
+
Prism::SymbolNode
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def visit_def_node(node)
|
|
21
|
+
last_literal = trailing_literal(node)
|
|
22
|
+
if last_literal
|
|
23
|
+
add_mutation(
|
|
24
|
+
offset: last_literal.location.start_offset,
|
|
25
|
+
length: last_literal.location.length,
|
|
26
|
+
replacement: "",
|
|
27
|
+
node: last_literal
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def trailing_literal(node)
|
|
37
|
+
body = node.body
|
|
38
|
+
return nil unless body.is_a?(Prism::StatementsNode)
|
|
39
|
+
return nil if body.body.empty?
|
|
40
|
+
|
|
41
|
+
last = body.body.last
|
|
42
|
+
return nil unless LITERAL_NODE_TYPES.any? { |t| last.is_a?(t) }
|
|
43
|
+
|
|
44
|
+
last
|
|
45
|
+
end
|
|
46
|
+
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)
|
|
@@ -3,21 +3,52 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::ReceiverReplacement < Evilution::Mutator::Base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
# Ruby reserved words. A call like `self.class` — when stripped of its
|
|
7
|
+
# `self.` receiver — becomes the bare token `class`, which the parser reads
|
|
8
|
+
# as the class-definition keyword rather than a method call. Producing this
|
|
9
|
+
# mutation guarantees an unparseable result. Skip when the call's method
|
|
10
|
+
# name collides with any reserved keyword.
|
|
11
|
+
RUBY_RESERVED_KEYWORDS = %i[
|
|
12
|
+
BEGIN END __ENCODING__ __FILE__ __LINE__
|
|
13
|
+
alias and begin break case class def defined? do else elsif end
|
|
14
|
+
ensure false for if in module next nil not or redo rescue retry
|
|
15
|
+
return self super then true undef unless until when while yield
|
|
16
|
+
].to_set.freeze
|
|
12
17
|
|
|
18
|
+
def visit_call_node(node)
|
|
19
|
+
if eligible_self_call?(node)
|
|
13
20
|
add_mutation(
|
|
14
21
|
offset: node.location.start_offset,
|
|
15
22
|
length: node.location.length,
|
|
16
|
-
replacement:
|
|
23
|
+
replacement: call_without_self_text(node),
|
|
17
24
|
node: node
|
|
18
25
|
)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
super
|
|
22
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def eligible_self_call?(node)
|
|
34
|
+
return false unless node.receiver.is_a?(Prism::SelfNode)
|
|
35
|
+
return false if reserved_keyword_method?(node.name)
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# `self.class = value` is a writer call whose Prism `name` is `:class=` —
|
|
41
|
+
# not `:class` — so a literal lookup in RUBY_RESERVED_KEYWORDS misses it,
|
|
42
|
+
# and stripping the receiver leaves `class = value` (parse error). Normalize
|
|
43
|
+
# the trailing `=` from writer forms before comparing.
|
|
44
|
+
def reserved_keyword_method?(name)
|
|
45
|
+
base = name.to_s.chomp("=").to_sym
|
|
46
|
+
RUBY_RESERVED_KEYWORDS.include?(base)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call_without_self_text(node)
|
|
50
|
+
message_start = node.message_loc.start_offset
|
|
51
|
+
call_end = node.location.start_offset + node.location.length
|
|
52
|
+
@file_source.byteslice(message_start, call_end - message_start)
|
|
53
|
+
end
|
|
23
54
|
end
|