evilution 1.3.0 → 1.5.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 +132 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +127 -0
- data/README.md +136 -38
- data/docs/architecture.md +135 -11
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +121 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +41 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +187 -21
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +14 -15
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/round_half_mode.rb +38 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/source_surgeon.rb +2 -3
- data/lib/evilution/ast/syntax_check.rb +56 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +67 -30
- data/lib/evilution/cli/exit_guard.rb +26 -4
- data/lib/evilution/cli/parser/file_args.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +1 -1
- data/lib/evilution/cli.rb +3 -2
- data/lib/evilution/config/validators/spec_mappings.rb +2 -1
- data/lib/evilution/config.rb +3 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +45 -0
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +11 -1
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +177 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +80 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +22 -15
- data/lib/evilution/integration/loading/mutation_applier.rb +44 -1
- data/lib/evilution/integration/loading/own_methods.rb +87 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
- data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
- data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard.rb +4 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +84 -11
- data/lib/evilution/isolation/in_process.rb +100 -8
- data/lib/evilution/mcp/info_tool/status_glossary.rb +15 -4
- data/lib/evilution/mcp/info_tool.rb +3 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +21 -9
- data/lib/evilution/mutator/base.rb +69 -5
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +56 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +10 -2
- data/lib/evilution/mutator/operator/array_literal.rb +29 -0
- data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
- data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
- data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
- data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
- data/lib/evilution/mutator/operator/float_literal.rb +54 -19
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
- data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
- data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
- data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
- data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
- data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
- data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
- data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +41 -24
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/string_literal.rb +29 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
- data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
- data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
- data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
- data/lib/evilution/mutator/primitives.rb +24 -2
- data/lib/evilution/mutator/receiver_positions.rb +79 -0
- data/lib/evilution/mutator/registry.rb +58 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
- data/lib/evilution/parallel/work_queue/channel.rb +58 -5
- data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
- data/lib/evilution/parallel/work_queue/worker.rb +13 -4
- data/lib/evilution/parallel/work_queue.rb +15 -9
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +9 -4
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +57 -43
- data/lib/evilution/runner.rb +5 -2
- data/lib/evilution/subject.rb +25 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +90 -0
- data/script/memory_check +62 -16
- metadata +113 -4
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Stop forwarding one part of the arguments a method passes through unnamed:
|
|
6
|
+
# `target.call(*, **, &)` becomes `target.call(**, &)`, and so on for each part.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means that part never reaches an assertion — nothing checks that
|
|
9
|
+
# the positional arguments, the keywords or the block arrive at the other end.
|
|
10
|
+
#
|
|
11
|
+
# Two spellings are handled. Anonymous `*` and `**` are dropped from the
|
|
12
|
+
# argument list they are written in. A `...` cannot be split where it stands,
|
|
13
|
+
# because `*`, `**` and `&` may not be written under a `...` signature, so the
|
|
14
|
+
# signature is spelled out as `(*, **, &)` along with every use, and one use
|
|
15
|
+
# loses one part: `def f(...) = g(...)` becomes `def f(*, **, &) = g(**, &)`.
|
|
16
|
+
#
|
|
17
|
+
# Named splats are left to SplatOperator and KeywordArgument, and an anonymous
|
|
18
|
+
# `&` written on its own to BlockPassRemoval.
|
|
19
|
+
class Evilution::Mutator::Operator::ForwardedArgumentDrop < Evilution::Mutator::Base
|
|
20
|
+
PARTS = ["*", "**", "&"].freeze
|
|
21
|
+
ALL_PARTS = PARTS.join(", ").freeze
|
|
22
|
+
|
|
23
|
+
def visit_def_node(node)
|
|
24
|
+
drop_forwarded_parts(node)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_call_node(node)
|
|
29
|
+
drop_anonymous_arguments(node)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_super_node(node)
|
|
34
|
+
drop_anonymous_arguments(node)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_yield_node(node)
|
|
39
|
+
drop_anonymous_arguments(node)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def drop_anonymous_arguments(node)
|
|
46
|
+
return if node.arguments.nil?
|
|
47
|
+
|
|
48
|
+
items = argument_items(node.arguments)
|
|
49
|
+
# Dropping the only argument leaves an empty list, which is the mutant
|
|
50
|
+
# ArgumentListRemoval already emits.
|
|
51
|
+
return if items.length < 2
|
|
52
|
+
|
|
53
|
+
items.each_index do |index|
|
|
54
|
+
emit_argument_drop(node, items, index) if anonymous?(items[index])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Keywords sit together in one KeywordHashNode, `**` among them; they are
|
|
59
|
+
# listed individually so each can be dropped on its own.
|
|
60
|
+
def argument_items(arguments)
|
|
61
|
+
arguments.arguments.flat_map do |argument|
|
|
62
|
+
argument.is_a?(Prism::KeywordHashNode) ? argument.elements : [argument]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def anonymous?(item)
|
|
67
|
+
case item
|
|
68
|
+
when Prism::SplatNode then item.expression.nil?
|
|
69
|
+
when Prism::AssocSplatNode then item.value.nil?
|
|
70
|
+
else false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def emit_argument_drop(node, items, index)
|
|
75
|
+
remaining = items.map(&:slice)
|
|
76
|
+
remaining.delete_at(index)
|
|
77
|
+
location = node.arguments.location
|
|
78
|
+
|
|
79
|
+
add_mutation(
|
|
80
|
+
offset: location.start_offset,
|
|
81
|
+
length: location.length,
|
|
82
|
+
replacement: remaining.join(", "),
|
|
83
|
+
node: node
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def drop_forwarded_parts(node)
|
|
88
|
+
return if node.body.nil?
|
|
89
|
+
|
|
90
|
+
uses = forwarding_uses(node.body)
|
|
91
|
+
uses.each do |target|
|
|
92
|
+
PARTS.each { |part| emit_part_drop(node, uses, target, part) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Every `...` the method forwards. A method defined in the body is not
|
|
97
|
+
# searched: its `...` forwards that method's own arguments.
|
|
98
|
+
def forwarding_uses(node)
|
|
99
|
+
return [] if node.is_a?(Prism::DefNode)
|
|
100
|
+
return [node] if node.is_a?(Prism::ForwardingArgumentsNode)
|
|
101
|
+
|
|
102
|
+
node.compact_child_nodes.flat_map { |child| forwarding_uses(child) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A `...` can only be forwarded by the method that declares it, so a use
|
|
106
|
+
# found in the body means the signature ends in the matching parameter.
|
|
107
|
+
def emit_part_drop(node, uses, target, part)
|
|
108
|
+
edits = uses.map do |use|
|
|
109
|
+
[use.location, use.equal?(target) ? (PARTS - [part]).join(", ") : ALL_PARTS]
|
|
110
|
+
end
|
|
111
|
+
edits << [node.parameters.keyword_rest.location, ALL_PARTS]
|
|
112
|
+
|
|
113
|
+
emit_rewrite(node, edits.sort_by { |location, _text| location.start_offset })
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The signature and the uses are apart in the source, while a mutation
|
|
117
|
+
# replaces one contiguous range, so the range runs from the first edit to
|
|
118
|
+
# the last and is rewritten with every edit applied.
|
|
119
|
+
def emit_rewrite(node, edits)
|
|
120
|
+
start_offset = edits.first.first.start_offset
|
|
121
|
+
length = edits.last.first.end_offset - start_offset
|
|
122
|
+
|
|
123
|
+
add_mutation(
|
|
124
|
+
offset: start_offset,
|
|
125
|
+
length: length,
|
|
126
|
+
replacement: apply_edits(byteslice_source(start_offset, length), edits, start_offset),
|
|
127
|
+
node: node
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Applied last to first, so an edit never shifts the offsets of those still
|
|
132
|
+
# to come.
|
|
133
|
+
def apply_edits(text, edits, base_offset)
|
|
134
|
+
edits.reverse_each do |location, replacement|
|
|
135
|
+
text = text.byteslice(0, location.start_offset - base_offset) +
|
|
136
|
+
replacement +
|
|
137
|
+
text.byteslice((location.end_offset - base_offset)..)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
text
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/class_body"
|
|
7
|
+
|
|
8
|
+
# Drop the `freeze` of a constant defined in a class or module body:
|
|
9
|
+
# `LIST = %w[a b].freeze` becomes `LIST = %w[a b]`.
|
|
10
|
+
#
|
|
11
|
+
# A survivor means no example tries to change the shared value, so nothing
|
|
12
|
+
# shows that it has to stay as it is.
|
|
13
|
+
#
|
|
14
|
+
# A constant is defined in the class body, outside every method, where no
|
|
15
|
+
# subject reaches it. The mutation is attributed to the method anchoring the
|
|
16
|
+
# enclosing body (see Evilution::AST::ClassBody); a top-level constant has no
|
|
17
|
+
# body to be attributed to. A `freeze` inside a method is left to the generic
|
|
18
|
+
# call operators, which already drop it.
|
|
19
|
+
class Evilution::Mutator::Operator::FreezeRemoval < Evilution::Mutator::Base
|
|
20
|
+
CONSTANT_WRITES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode].freeze
|
|
21
|
+
|
|
22
|
+
# Values that are frozen whether or not `freeze` is called.
|
|
23
|
+
ALWAYS_FROZEN = [
|
|
24
|
+
Prism::SymbolNode, Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
|
|
25
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::RangeNode, Prism::RegularExpressionNode
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# The comments a file opens with, where the magic comment has to be.
|
|
29
|
+
LEADING_COMMENTS = /\A(?:[ \t]*(?:#.*)?\n)*/
|
|
30
|
+
FROZEN_STRING_LITERALS = /^[ \t]*#.*frozen[_-]string[_-]literal:[ \t]*true/i
|
|
31
|
+
private_constant :CONSTANT_WRITES, :ALWAYS_FROZEN, :LEADING_COMMENTS, :FROZEN_STRING_LITERALS
|
|
32
|
+
|
|
33
|
+
def call(subject, filter: nil)
|
|
34
|
+
@subject = subject
|
|
35
|
+
@file_source = File.read(subject.file_path)
|
|
36
|
+
@mutations = []
|
|
37
|
+
@filter = filter
|
|
38
|
+
|
|
39
|
+
constant_writes_for(subject).each { |write| freezes_in(write).each { |freeze_call| remove(freeze_call) } }
|
|
40
|
+
@mutations
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def remove(freeze_call)
|
|
46
|
+
add_mutation(
|
|
47
|
+
offset: freeze_call.location.start_offset,
|
|
48
|
+
length: freeze_call.location.length,
|
|
49
|
+
replacement: freeze_call.receiver.slice,
|
|
50
|
+
node: freeze_call
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def constant_writes_for(subject)
|
|
55
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
56
|
+
|
|
57
|
+
Evilution::AST::ClassBody.anchored_at(tree, subject.line_number).flat_map do |body|
|
|
58
|
+
body.declarations { |node| CONSTANT_WRITES.include?(node.class) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Every `freeze` call under the node, outermost first.
|
|
63
|
+
def freezes_in(node)
|
|
64
|
+
nested = node.compact_child_nodes.flat_map { |child| freezes_in(child) }
|
|
65
|
+
removable_freeze?(node) ? [node, *nested] : nested
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def removable_freeze?(node)
|
|
69
|
+
return false unless node.is_a?(Prism::CallNode) && node.name == :freeze
|
|
70
|
+
return false if node.receiver.nil? || node.arguments
|
|
71
|
+
|
|
72
|
+
!frozen_anyway?(node.receiver)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A group has the value of its last expression: `(1..2)`.
|
|
76
|
+
def frozen_anyway?(receiver)
|
|
77
|
+
receiver = receiver.body.body.last while receiver.is_a?(Prism::ParenthesesNode) && receiver.body
|
|
78
|
+
return true if ALWAYS_FROZEN.include?(receiver.class)
|
|
79
|
+
|
|
80
|
+
receiver.is_a?(Prism::StringNode) && frozen_string_literals?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def frozen_string_literals?
|
|
84
|
+
@file_source[LEADING_COMMENTS].match?(FROZEN_STRING_LITERALS)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::HashLiteral < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
8
|
+
RENAMED_KEY = "__evilution_mutated__"
|
|
9
|
+
|
|
6
10
|
def visit_hash_node(node)
|
|
7
11
|
if node.elements.any?
|
|
8
12
|
add_mutation(
|
|
@@ -18,8 +22,55 @@ class Evilution::Mutator::Operator::HashLiteral < Evilution::Mutator::Base
|
|
|
18
22
|
replacement: "nil",
|
|
19
23
|
node: node
|
|
20
24
|
)
|
|
25
|
+
|
|
26
|
+
mutate_delete_pairs(node.elements)
|
|
27
|
+
mutate_rename_keys(node.elements)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
super
|
|
24
31
|
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Delete each key/value pair in turn: a survivor means no example checks
|
|
36
|
+
# that key. A single element is left to the `{}` replacement. A `**splat`
|
|
37
|
+
# is not a pair and stays. So does a pair holding a heredoc: its body sits
|
|
38
|
+
# after the closing brace, so one cut cannot take both, and the cut that
|
|
39
|
+
# does not parse is dropped.
|
|
40
|
+
def mutate_delete_pairs(elements)
|
|
41
|
+
return if elements.one?
|
|
42
|
+
|
|
43
|
+
elements.each_with_index do |element, index|
|
|
44
|
+
delete_element(elements, index) if element.is_a?(Prism::AssocNode)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Rename each label key in turn (`a: 1`, `"a b": 1`): a survivor means no
|
|
49
|
+
# example reads the value by that name. A key written with a rocket
|
|
50
|
+
# (`:a => 1`, `"a" => 1`) is a literal of its own, and SymbolLiteral and
|
|
51
|
+
# StringLiteral already replace it.
|
|
52
|
+
def mutate_rename_keys(elements)
|
|
53
|
+
elements.each do |element|
|
|
54
|
+
rename_key(element) if element.is_a?(Prism::AssocNode) && label?(element.key)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def label?(key)
|
|
59
|
+
key.is_a?(Prism::SymbolNode) && !key.closing.nil? && key.closing.end_with?(":")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A shorthand pair (`{ x: }`) reads its value from the key, so the value is
|
|
63
|
+
# written out to stay what it was.
|
|
64
|
+
def rename_key(pair)
|
|
65
|
+
key = pair.key
|
|
66
|
+
replacement = "#{RENAMED_KEY}:"
|
|
67
|
+
replacement += " #{key.unescaped}" if pair.value.is_a?(Prism::ImplicitNode)
|
|
68
|
+
|
|
69
|
+
add_mutation(
|
|
70
|
+
offset: key.location.start_offset,
|
|
71
|
+
length: key.location.length,
|
|
72
|
+
replacement: replacement,
|
|
73
|
+
node: key
|
|
74
|
+
)
|
|
75
|
+
end
|
|
25
76
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Take the tail of a list with `drop` instead of a range index:
|
|
6
|
+
# `list[n..-1]` and the endless `list[n..]` / `list[n...]` become
|
|
7
|
+
# `list.drop(n)`.
|
|
8
|
+
#
|
|
9
|
+
# The two differ only past the end: a start beyond the last element gives
|
|
10
|
+
# `nil` from the index and an empty array from `drop`. A survivor means no
|
|
11
|
+
# test reaches that boundary.
|
|
12
|
+
#
|
|
13
|
+
# A literal start of zero is skipped, never being out of range, and so is a
|
|
14
|
+
# negative literal, which `drop` rejects with ArgumentError. A range that
|
|
15
|
+
# stops short of the last element (`n...-1`, `n..-2`) is no tail at all. A
|
|
16
|
+
# read in void statement position is left to statement_deletion, and index
|
|
17
|
+
# writes are another method altogether.
|
|
18
|
+
#
|
|
19
|
+
# The receiver's type is unknown here: on a String the mutant raises
|
|
20
|
+
# NoMethodError, which any test reaching the line kills.
|
|
21
|
+
class Evilution::Mutator::Operator::IndexRangeToDrop < Evilution::Mutator::Base
|
|
22
|
+
LAST_INDEX = -1
|
|
23
|
+
private_constant :LAST_INDEX
|
|
24
|
+
|
|
25
|
+
def call(subject, **)
|
|
26
|
+
@void_statements = Set.new
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_statements_node(node)
|
|
31
|
+
@void_statements.merge(node.body[...-1])
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_call_node(node)
|
|
36
|
+
start = tail_start(node)
|
|
37
|
+
replace_span(node: node, target: node, replacement: drop_call(node, start)) if start
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Where the tail begins, when `node` reads from there to the last element.
|
|
44
|
+
# A beginless range has no start to hand to `drop`, and answers nil too.
|
|
45
|
+
def tail_start(node)
|
|
46
|
+
return unless node.name == :[] && !@void_statements.include?(node)
|
|
47
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [Prism::RangeNode => range]]
|
|
48
|
+
|
|
49
|
+
range.left if to_the_end?(range) && droppable_start?(range.left)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_the_end?(range)
|
|
53
|
+
stop = range.right
|
|
54
|
+
return true if stop.nil?
|
|
55
|
+
|
|
56
|
+
!range.exclude_end? && stop.is_a?(Prism::IntegerNode) && stop.value == LAST_INDEX
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def droppable_start?(start)
|
|
60
|
+
!start.is_a?(Prism::IntegerNode) || start.value.positive?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drop_call(node, start)
|
|
64
|
+
operator = node.safe_navigation? ? "&." : "."
|
|
65
|
+
"#{source_of(node.receiver)}#{operator}drop(#{source_of(start)})"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Send an index read to `self` instead of its receiver: `hash[key]` becomes
|
|
6
|
+
# `self[key]`, arguments untouched.
|
|
7
|
+
#
|
|
8
|
+
# The survivor it targets lives in a class that defines its own `#[]` — a
|
|
9
|
+
# collection or a wrapper that also holds the thing it indexes — where the
|
|
10
|
+
# tests never tell the two lookups apart. Anywhere else the mutant raises
|
|
11
|
+
# NoMethodError, which any test reaching the line kills.
|
|
12
|
+
#
|
|
13
|
+
# Index writes are left alone: `a[b] = c` is a different method, and the
|
|
14
|
+
# operator-write forms (`a[b] ||= c`) are not calls at all.
|
|
15
|
+
class Evilution::Mutator::Operator::IndexReceiverToSelf < Evilution::Mutator::Base
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
replace_span(node: node, target: node.receiver, replacement: "self") if node.name == :[]
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Ask whether the key is there instead of reading it: `config[:size]`
|
|
6
|
+
# becomes `config.key?(:size)`.
|
|
7
|
+
#
|
|
8
|
+
# The value turns into a boolean. A survivor means the tests only notice
|
|
9
|
+
# that something came back — a condition taken, a truthy result — and never
|
|
10
|
+
# what the lookup returned.
|
|
11
|
+
#
|
|
12
|
+
# Only a read with one plain argument qualifies, `key?` taking exactly one.
|
|
13
|
+
# An integer or range index is skipped: it points at an Array or a String,
|
|
14
|
+
# which has no `key?`, so the mutant would only raise NoMethodError. A read
|
|
15
|
+
# in void statement position is left to statement_deletion, its value being
|
|
16
|
+
# discarded either way. Index writes are another method altogether.
|
|
17
|
+
class Evilution::Mutator::Operator::IndexToKeyPredicate < Evilution::Mutator::Base
|
|
18
|
+
# Argument shapes `key?` cannot take as its one key.
|
|
19
|
+
NON_KEY_ARGUMENTS = [
|
|
20
|
+
Prism::SplatNode,
|
|
21
|
+
Prism::KeywordHashNode,
|
|
22
|
+
Prism::BlockArgumentNode,
|
|
23
|
+
Prism::ForwardingArgumentsNode,
|
|
24
|
+
Prism::IntegerNode,
|
|
25
|
+
Prism::RangeNode
|
|
26
|
+
].freeze
|
|
27
|
+
private_constant :NON_KEY_ARGUMENTS
|
|
28
|
+
|
|
29
|
+
def call(subject, **)
|
|
30
|
+
@void_statements = Set.new
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_statements_node(node)
|
|
35
|
+
@void_statements.merge(node.body[...-1])
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_call_node(node)
|
|
40
|
+
key = key_argument(node)
|
|
41
|
+
replace_span(node: node, target: node, replacement: key_predicate(node, key)) if key
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def key_argument(node)
|
|
48
|
+
return unless node.name == :[] && !@void_statements.include?(node)
|
|
49
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [argument]]
|
|
50
|
+
|
|
51
|
+
argument unless NON_KEY_ARGUMENTS.any? { |type| argument.is_a?(type) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def key_predicate(node, key)
|
|
55
|
+
operator = node.safe_navigation? ? "&." : "."
|
|
56
|
+
"#{source_of(node.receiver)}#{operator}key?(#{source_of(key)})"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace an index write with the value it assigns: `cache[key] = value`
|
|
6
|
+
# becomes `value`.
|
|
7
|
+
#
|
|
8
|
+
# An index write evaluates to the assigned value, so the mutant returns just
|
|
9
|
+
# what the original did — only the store is gone. A survivor means the tests
|
|
10
|
+
# check what was computed and never that it was kept. That is what sets it
|
|
11
|
+
# apart from deleting the statement or turning it into `nil`, both of which
|
|
12
|
+
# also change the result.
|
|
13
|
+
#
|
|
14
|
+
# The result is kept for the assignment syntax only. The method-call
|
|
15
|
+
# spelling is mutated as well, but there it may change: `cache.[]=(key,
|
|
16
|
+
# value)` returns whatever `[]=` returns, and `cache&.[]=(key, value)` is
|
|
17
|
+
# `nil`, with `value` never evaluated, when the receiver is `nil`.
|
|
18
|
+
#
|
|
19
|
+
# A write in void statement position is skipped: with its value discarded,
|
|
20
|
+
# the mutant is statement_deletion's. Operator writes (`a[b] ||= c`) and
|
|
21
|
+
# index targets of a multiple assignment are not calls and are left alone.
|
|
22
|
+
class Evilution::Mutator::Operator::IndexWriteToValue < Evilution::Mutator::Base
|
|
23
|
+
def call(subject, **)
|
|
24
|
+
@void_statements = Set.new
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_statements_node(node)
|
|
29
|
+
@void_statements.merge(node.body[...-1])
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_call_node(node)
|
|
34
|
+
promote_child(node, argument_nodes(node).last) if node.name == :[]= && !@void_statements.include?(node)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Turn a division into `fdiv`: `a / b` becomes `a.fdiv(b)`.
|
|
6
|
+
#
|
|
7
|
+
# Between integers `/` drops the remainder while `fdiv` keeps it as a float,
|
|
8
|
+
# and for exact types such as Rational or BigDecimal `fdiv` falls back to
|
|
9
|
+
# Float. A survivor means no example divides values that leave a remainder,
|
|
10
|
+
# or none checks what happens to it.
|
|
11
|
+
#
|
|
12
|
+
# A float literal on either side is skipped: float division and `fdiv` give
|
|
13
|
+
# the same answer, so the mutant would change nothing.
|
|
14
|
+
class Evilution::Mutator::Operator::IntegerDivisionToFdiv < Evilution::Mutator::Base
|
|
15
|
+
# Receivers that `.fdiv` can follow directly. Anything else is an operator
|
|
16
|
+
# expression, which `.fdiv` would bind into, so it is grouped first.
|
|
17
|
+
SIMPLE_RECEIVER_TYPES = [
|
|
18
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
19
|
+
Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode,
|
|
20
|
+
Prism::IntegerNode, Prism::ParenthesesNode, Prism::SelfNode
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# A method name, as opposed to an operator such as `*` or `-@`.
|
|
24
|
+
METHOD_NAME = /\A[a-zA-Z_]\w*[?!]?\z/
|
|
25
|
+
|
|
26
|
+
def visit_call_node(node)
|
|
27
|
+
replace_division(node) if division?(node)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# The explicit form (`a./(b)`) is left alone: it is rare, and rewriting it
|
|
34
|
+
# gains nothing over the operator form. The operator form always has a
|
|
35
|
+
# receiver and exactly one divisor.
|
|
36
|
+
def division?(node)
|
|
37
|
+
return false unless node.name == :/ && node.call_operator_loc.nil?
|
|
38
|
+
|
|
39
|
+
[node.receiver, *node.arguments.arguments].none?(Prism::FloatNode)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def replace_division(node)
|
|
43
|
+
divisor = node.arguments.arguments.first
|
|
44
|
+
|
|
45
|
+
add_mutation(
|
|
46
|
+
offset: node.location.start_offset,
|
|
47
|
+
length: node.location.length,
|
|
48
|
+
replacement: "#{receiver_text(node.receiver)}.fdiv(#{divisor.slice})",
|
|
49
|
+
node: node
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def receiver_text(receiver)
|
|
54
|
+
simple_receiver?(receiver) ? receiver.slice : "(#{receiver.slice})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def simple_receiver?(receiver)
|
|
58
|
+
return true if SIMPLE_RECEIVER_TYPES.include?(receiver.class)
|
|
59
|
+
|
|
60
|
+
receiver.is_a?(Prism::CallNode) && receiver.name.to_s.match?(METHOD_NAME)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -3,23 +3,53 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::IntegerLiteral < Evilution::Mutator::Base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
8
|
+
# One value past each integer width, keyed by the boundary it crosses:
|
|
9
|
+
# int8, uint8, int16, uint16, int32, uint32, int64. A literal is replaced
|
|
10
|
+
# with the sentinel of the first boundary above its magnitude, so a
|
|
11
|
+
# survivor means nothing checks that the value still fits its width. The
|
|
12
|
+
# sentinels are safe primes: arithmetic, masking or shifting does not land
|
|
13
|
+
# on one by coincidence.
|
|
14
|
+
WIDTH_SENTINELS = {
|
|
15
|
+
2**7 => 167,
|
|
16
|
+
2**8 => 467,
|
|
17
|
+
2**15 => 55_487,
|
|
18
|
+
2**16 => 108_503,
|
|
19
|
+
2**31 => 2_667_278_543,
|
|
20
|
+
2**32 => 7_980_081_959,
|
|
21
|
+
2**63 => 15_508_464_536_481_899_903
|
|
22
|
+
}.freeze
|
|
15
23
|
|
|
24
|
+
def visit_integer_node(node)
|
|
25
|
+
replacements_for(node.value).each { |value| add_mutation_with_replacement(node, value.to_s) }
|
|
16
26
|
add_mutation_with_replacement(node, "nil")
|
|
17
27
|
|
|
18
28
|
super
|
|
19
29
|
end
|
|
20
30
|
|
|
31
|
+
# A complex literal (`5i`) is replaced as a whole by ComplexLiteral. Its
|
|
32
|
+
# numeric part is not a literal of its own: `nili` is not a value.
|
|
33
|
+
def visit_imaginary_node(_node); end
|
|
34
|
+
|
|
21
35
|
private
|
|
22
36
|
|
|
37
|
+
def replacements_for(value)
|
|
38
|
+
[*neighbours_of(value), width_sentinel_for(value)].compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def neighbours_of(value)
|
|
42
|
+
return [1, -1] if value.zero?
|
|
43
|
+
return [0] if value == 1
|
|
44
|
+
|
|
45
|
+
[0, value + 1, value - 1].uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def width_sentinel_for(value)
|
|
49
|
+
_boundary, sentinel = WIDTH_SENTINELS.find { |boundary, _| boundary > value.abs }
|
|
50
|
+
sentinel
|
|
51
|
+
end
|
|
52
|
+
|
|
23
53
|
def add_mutation_with_replacement(node, replacement)
|
|
24
54
|
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement:, node:)
|
|
25
55
|
end
|
|
@@ -8,7 +8,7 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
8
8
|
if params
|
|
9
9
|
mutate_optional_keyword_defaults(params)
|
|
10
10
|
mutate_optional_keyword_removal(params)
|
|
11
|
-
mutate_keyword_rest_removal(
|
|
11
|
+
mutate_keyword_rest_removal(node)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
super
|
|
@@ -51,9 +51,11 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def mutate_keyword_rest_removal(
|
|
54
|
+
def mutate_keyword_rest_removal(node)
|
|
55
|
+
params = node.parameters
|
|
55
56
|
kr = params.keyword_rest
|
|
56
57
|
return unless kr.is_a?(Prism::KeywordRestParameterNode)
|
|
58
|
+
return if anonymous_rest_used?(node.body)
|
|
57
59
|
|
|
58
60
|
all_params = collect_all_params(params)
|
|
59
61
|
if all_params.length < 2
|
|
@@ -63,6 +65,27 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
67
|
|
|
68
|
+
# An anonymous `**` in the body (`bar(**)`, `{ ** }`) only parses while the
|
|
69
|
+
# signature declares one, so removing it there would leave the body
|
|
70
|
+
# unparseable. Finding one also tells the rest is anonymous: Ruby rejects it
|
|
71
|
+
# next to a named rest. A named rest is safe to remove: without it the body
|
|
72
|
+
# reads an undefined name, which parses and fails at runtime.
|
|
73
|
+
def anonymous_rest_used?(body)
|
|
74
|
+
return false if body.nil?
|
|
75
|
+
|
|
76
|
+
uses_anonymous_rest?(body)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A nested def is not searched: an anonymous `**` inside it belongs to that
|
|
80
|
+
# method's own signature. Blocks and lambdas share the enclosing method's
|
|
81
|
+
# parameters, so they are.
|
|
82
|
+
def uses_anonymous_rest?(node)
|
|
83
|
+
return false if node.is_a?(Prism::DefNode)
|
|
84
|
+
return true if node.is_a?(Prism::AssocSplatNode) && node.value.nil?
|
|
85
|
+
|
|
86
|
+
node.compact_child_nodes.any? { |child| uses_anonymous_rest?(child) }
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
def emit_remove_only_kr(kr)
|
|
67
90
|
add_mutation(
|
|
68
91
|
offset: kr.location.start_offset,
|