evilution 1.2.0 → 1.4.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 +100 -0
- data/.rubocop_todo.yml +14 -0
- data/CHANGELOG.md +124 -0
- data/README.md +142 -28
- data/docs/architecture.md +123 -9
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +39 -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/included_block.rb +27 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/parser.rb +120 -16
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +15 -15
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -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 +101 -42
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +4 -1
- data/lib/evilution/cli/parser/options_builder.rb +4 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +8 -2
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +3 -1
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- 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 +46 -1
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +2 -0
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- 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/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +10 -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 +2 -1
- data/lib/evilution/isolation/in_process.rb +21 -4
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -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 +8 -3
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- 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/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -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/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/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- 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_anchor_to_predicate.rb +136 -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_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/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +54 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/rails_warmup.rb +61 -0
- 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/html/report.rb +1 -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/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +26 -11
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
- 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.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.rb +46 -1
- data/lib/evilution/runner.rb +4 -1
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +51 -0
- data/script/memory_check +62 -16
- metadata +108 -4
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Make the first step of a `dig` strict: `config.dig(:db, :host)` becomes
|
|
6
|
+
# `config.fetch(:db).dig(:host)`. The reverse of index_to_dig.
|
|
7
|
+
#
|
|
8
|
+
# `dig` returns nil when the first key is missing or holds nil; the fetch
|
|
9
|
+
# chain raises there instead (KeyError / IndexError, or NoMethodError on
|
|
10
|
+
# `nil.dig`). A survivor means no test reaches the lookup with that key
|
|
11
|
+
# absent.
|
|
12
|
+
#
|
|
13
|
+
# Only calls with two or more positional keys qualify; the remaining keys
|
|
14
|
+
# keep their original formatting.
|
|
15
|
+
class Evilution::Mutator::Operator::DigToFetchChain < Evilution::Mutator::Base
|
|
16
|
+
NON_POSITIONAL = [Prism::SplatNode, Prism::KeywordHashNode, Prism::ForwardingArgumentsNode].freeze
|
|
17
|
+
private_constant :NON_POSITIONAL
|
|
18
|
+
|
|
19
|
+
def visit_call_node(node)
|
|
20
|
+
rewrite(node) if node.name == :dig && node.receiver && node.block.nil?
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Replace `dig(first, ` with `fetch(first).dig(` — up to the second key —
|
|
27
|
+
# keeping whatever opened the argument list (`(`, a newline, a space).
|
|
28
|
+
def rewrite(node)
|
|
29
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [first, second, *]]
|
|
30
|
+
return unless positional_keys?(node.arguments.arguments)
|
|
31
|
+
|
|
32
|
+
start = node.message_loc.start_offset
|
|
33
|
+
add_mutation(offset: start, length: second.start_offset - start, replacement: fetch_prefix(node, first), node: node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def positional_keys?(keys)
|
|
37
|
+
keys.none? { |key| NON_POSITIONAL.any? { |type| key.is_a?(type) } }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fetch_prefix(node, first)
|
|
41
|
+
message_end = node.message_loc.end_offset
|
|
42
|
+
opener = byteslice_source(message_end, first.start_offset - message_end)
|
|
43
|
+
"fetch(#{source_of(first)}).dig#{opener}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a double negation with its operand: `!!value` becomes `value`, and
|
|
6
|
+
# the same for the `not not value` keyword form.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no test distinguishes a truthy object from `true` (or nil
|
|
9
|
+
# from `false`) — the boolean coercion is never observed.
|
|
10
|
+
class Evilution::Mutator::Operator::DoubleNegationRemoval < Evilution::Mutator::Base
|
|
11
|
+
def visit_call_node(node)
|
|
12
|
+
inner = node.receiver
|
|
13
|
+
promote_child(node, inner.receiver) if negation?(node) && negation?(inner)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def negation?(node)
|
|
20
|
+
node.is_a?(Prism::CallNode) && node.name == :!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Resolve a dynamic dispatch with a literal symbol into a direct call:
|
|
6
|
+
# `target.send(:reset)` becomes `target.reset`, and the same for `__send__`
|
|
7
|
+
# and `public_send`. Remaining arguments, a block-pass and a literal block
|
|
8
|
+
# stay attached.
|
|
9
|
+
#
|
|
10
|
+
# The two forms differ only in visibility checks, so the mutant is emitted
|
|
11
|
+
# only where those checks can disagree:
|
|
12
|
+
#
|
|
13
|
+
# - send / __send__ on an explicit receiver other than self: the direct call
|
|
14
|
+
# raises NoMethodError for a private method. With an implicit or self
|
|
15
|
+
# receiver a private method resolves either way (Ruby 2.7+).
|
|
16
|
+
# - public_send with an implicit or self receiver: the direct call reaches a
|
|
17
|
+
# private method that public_send refuses. On any other receiver both
|
|
18
|
+
# refuse it, and only a protected method called from inside its class
|
|
19
|
+
# would differ.
|
|
20
|
+
#
|
|
21
|
+
# method_missing is reached the same way by both forms.
|
|
22
|
+
class Evilution::Mutator::Operator::DynamicDispatchResolution < Evilution::Mutator::Base
|
|
23
|
+
SEND_SELECTORS = %i[send __send__].freeze
|
|
24
|
+
private_constant :SEND_SELECTORS
|
|
25
|
+
|
|
26
|
+
OPERATOR_NAMES = %w[
|
|
27
|
+
+ - * / % ** == != < > <= >= <=> === =~ !~ ! [] []= << >> & | ^ ~ +@ -@
|
|
28
|
+
].to_set.freeze
|
|
29
|
+
private_constant :OPERATOR_NAMES
|
|
30
|
+
|
|
31
|
+
METHOD_NAME = /\A[[:alpha:]_][[:alnum:]_]*[?!=]?\z/
|
|
32
|
+
private_constant :METHOD_NAME
|
|
33
|
+
|
|
34
|
+
def visit_call_node(node)
|
|
35
|
+
selector = literal_selector(node)
|
|
36
|
+
resolve(node, selector) if selector && visibility_can_differ?(node)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def literal_selector(node)
|
|
43
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [Prism::SymbolNode => symbol, *]]
|
|
44
|
+
|
|
45
|
+
name = symbol.unescaped
|
|
46
|
+
name if name.match?(METHOD_NAME) || OPERATOR_NAMES.include?(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visibility_can_differ?(node)
|
|
50
|
+
self_receiver = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
51
|
+
|
|
52
|
+
if SEND_SELECTORS.include?(node.name)
|
|
53
|
+
!self_receiver
|
|
54
|
+
else
|
|
55
|
+
node.name == :public_send && self_receiver
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve(node, selector)
|
|
60
|
+
start = node.message_loc.start_offset
|
|
61
|
+
following = following_item(node)
|
|
62
|
+
|
|
63
|
+
if following
|
|
64
|
+
replacement = "#{selector}#{node.opening_loc ? "(" : " "}"
|
|
65
|
+
finish = following.start_offset
|
|
66
|
+
else
|
|
67
|
+
replacement = selector
|
|
68
|
+
finish = node.closing_loc ? node.closing_loc.end_offset : node.arguments.end_offset
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
add_mutation(offset: start, length: finish - start, replacement: replacement, node: node)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The argument after the selector, or the block-pass when the selector is
|
|
75
|
+
# the only argument.
|
|
76
|
+
def following_item(node)
|
|
77
|
+
node.arguments.arguments[1] || (node.block if node.block.is_a?(Prism::BlockArgumentNode))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swallow the error of a statement that raises by convention:
|
|
6
|
+
# `record.save!` becomes `record.save! rescue nil`.
|
|
7
|
+
#
|
|
8
|
+
# The mutant behaves like the original until the statement fails; then it
|
|
9
|
+
# carries on with nil instead of raising. A survivor means no example makes
|
|
10
|
+
# that statement fail and checks the error comes out. It is the inverse of
|
|
11
|
+
# RescueRemoval: handling is added where there is none.
|
|
12
|
+
#
|
|
13
|
+
# Only calls whose failure is part of their contract are reached: bang
|
|
14
|
+
# methods, `fetch`, and the conversion functions `Integer`, `Float` and
|
|
15
|
+
# `Rational`. Wrapping every statement would mostly report that no test makes
|
|
16
|
+
# a log line raise. Statements already covered by a rescue are left alone, as
|
|
17
|
+
# is `raise`, whose swallowed form behaves as deleting the statement.
|
|
18
|
+
class Evilution::Mutator::Operator::ExceptionSwallow < Evilution::Mutator::Base
|
|
19
|
+
CONVERSION_FUNCTIONS = %i[Integer Float Rational].freeze
|
|
20
|
+
|
|
21
|
+
# A bang method name, as opposed to the `!` and `!=` operators.
|
|
22
|
+
BANG_METHOD = /\w!\z/
|
|
23
|
+
|
|
24
|
+
# Bangs that do not raise: Ruby's in-place methods mark a change to the
|
|
25
|
+
# receiver, and exit! ends the process without raising.
|
|
26
|
+
NON_RAISING_BANGS = %i[
|
|
27
|
+
uniq! sort! sort_by! select! filter! reject! map! collect! compact! flatten!
|
|
28
|
+
shuffle! reverse! rotate! slice! merge! transform_keys! transform_values!
|
|
29
|
+
gsub! sub! strip! lstrip! rstrip! chomp! chop! squeeze! tr! tr_s! delete!
|
|
30
|
+
downcase! upcase! capitalize! swapcase! unicode_normalize! scrub! encode!
|
|
31
|
+
exit!
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Assignments whose value is the statement's call.
|
|
35
|
+
WRITE_TYPES = [
|
|
36
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
37
|
+
Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
def visit_statements_node(node)
|
|
41
|
+
node.body.each { |statement| swallow(statement) }
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Everything under a `begin` (or a `def` or block body) with a rescue clause
|
|
46
|
+
# is already handled there, so the visitor does not descend into it.
|
|
47
|
+
def visit_begin_node(node)
|
|
48
|
+
super if node.rescue_clause.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def swallow(statement)
|
|
54
|
+
call = statement_call(statement)
|
|
55
|
+
return unless call && raising?(call)
|
|
56
|
+
|
|
57
|
+
add_mutation(
|
|
58
|
+
offset: statement.location.end_offset,
|
|
59
|
+
length: 0,
|
|
60
|
+
replacement: " rescue nil",
|
|
61
|
+
node: call
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def statement_call(statement)
|
|
66
|
+
statement = statement.value if WRITE_TYPES.include?(statement.class)
|
|
67
|
+
statement if statement.is_a?(Prism::CallNode)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def raising?(call)
|
|
71
|
+
name = call.name
|
|
72
|
+
return !NON_RAISING_BANGS.include?(name) if name.to_s.match?(BANG_METHOD)
|
|
73
|
+
return raising_fetch?(call) if name == :fetch
|
|
74
|
+
|
|
75
|
+
call.receiver.nil? && CONVERSION_FUNCTIONS.include?(name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# With a default value or a block, fetch returns that instead of raising.
|
|
79
|
+
def raising_fetch?(call)
|
|
80
|
+
return false if call.receiver.nil? || call.block
|
|
81
|
+
|
|
82
|
+
call.arguments.nil? || call.arguments.arguments.length == 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Loosen the conversion specifiers of a literal format string, one at a time:
|
|
6
|
+
#
|
|
7
|
+
# format("%05d", n) -> format("%d", n) flags and width dropped
|
|
8
|
+
# sprintf("%.2f", x) -> sprintf("%f", x) precision dropped
|
|
9
|
+
# -> sprintf("%s", x) rendered as a plain string
|
|
10
|
+
#
|
|
11
|
+
# A survivor means the output is produced but its shape — padding, decimals,
|
|
12
|
+
# number base — is never asserted.
|
|
13
|
+
#
|
|
14
|
+
# Reaches the format string of receiverless `format`, `sprintf` and `printf`
|
|
15
|
+
# and the string receiver of `"..." % args`, when it is a plain literal. Integer
|
|
16
|
+
# conversions (`%d`, `%i`, `%u`) are not turned into `%s`: for an integer
|
|
17
|
+
# argument both print the same digits.
|
|
18
|
+
class Evilution::Mutator::Operator::FormatSpecifierSwap < Evilution::Mutator::Base
|
|
19
|
+
FORMAT_METHODS = %i[format sprintf printf].freeze
|
|
20
|
+
|
|
21
|
+
# `%%` and `%{name}` are matched so they are not mistaken for the start of
|
|
22
|
+
# a specifier; neither is mutated.
|
|
23
|
+
SPECIFIER = /
|
|
24
|
+
%(?:
|
|
25
|
+
%
|
|
26
|
+
| \{\w+\}
|
|
27
|
+
| (?<name><\w+>)?(?<flags>[-+\ 0\#]*)(?<width>\d+|\*)?(?:\.(?<precision>\d+|\*))?(?<type>[a-zA-Z])
|
|
28
|
+
)
|
|
29
|
+
/x
|
|
30
|
+
|
|
31
|
+
# Conversions whose output differs from `%s` for the values they take.
|
|
32
|
+
STRINGIFIABLE_TYPES = %w[f e E g G a A x X o b B].freeze
|
|
33
|
+
|
|
34
|
+
def visit_call_node(node)
|
|
35
|
+
format_string = format_string_of(node)
|
|
36
|
+
mutate_specifiers(node, format_string) if format_string
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def format_string_of(node)
|
|
43
|
+
candidate =
|
|
44
|
+
if node.receiver.nil? && FORMAT_METHODS.include?(node.name)
|
|
45
|
+
first_argument(node)
|
|
46
|
+
elsif node.name == :%
|
|
47
|
+
node.receiver
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
candidate if plain_literal?(candidate)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def first_argument(node)
|
|
54
|
+
node.arguments.arguments.first if node.arguments
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A heredoc's content sits after the line that opens it, so edits inside
|
|
58
|
+
# it are left alone along with interpolated strings. Only `%w[]` elements
|
|
59
|
+
# lack an opening delimiter, and those never stand here on their own.
|
|
60
|
+
def plain_literal?(node)
|
|
61
|
+
node.is_a?(Prism::StringNode) && !node.opening_loc.slice.start_with?("<<")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def mutate_specifiers(node, format_string)
|
|
65
|
+
specifiers(format_string).each do |match, offset|
|
|
66
|
+
variants(match).each do |replacement|
|
|
67
|
+
add_mutation(offset: offset, length: match[0].bytesize, replacement: replacement, node: node)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Each conversion specifier with its offset in the file. The scan runs over
|
|
73
|
+
# the source text between the quotes, so offsets line up with the file even
|
|
74
|
+
# when the string holds escapes.
|
|
75
|
+
def specifiers(format_string)
|
|
76
|
+
content = format_string.content_loc
|
|
77
|
+
text = byteslice_source(content.start_offset, content.length)
|
|
78
|
+
|
|
79
|
+
text.to_enum(:scan, SPECIFIER).filter_map do
|
|
80
|
+
match = Regexp.last_match
|
|
81
|
+
[match, content.start_offset + match.byteoffset(0).first] unless match[:type].nil?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A `*` width or precision consumes an argument of its own, so dropping it
|
|
86
|
+
# would shift every argument after it.
|
|
87
|
+
def variants(match)
|
|
88
|
+
return [] if [match[:width], match[:precision]].include?("*")
|
|
89
|
+
|
|
90
|
+
name = match[:name]
|
|
91
|
+
replacements = []
|
|
92
|
+
replacements << "%#{name}#{match[:type]}" if modifiers?(match)
|
|
93
|
+
replacements << "%#{name}s" if STRINGIFIABLE_TYPES.include?(match[:type])
|
|
94
|
+
replacements
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def modifiers?(match)
|
|
98
|
+
!match[:flags].empty? || !match[:width].nil? || !match[:precision].nil?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Rewrite an inequality with stricter comparisons: `a != b` becomes
|
|
6
|
+
# `!a.eql?(b)` and `!a.equal?(b)`.
|
|
7
|
+
#
|
|
8
|
+
# `eql?` adds a type check (`1 != 1.0` is false, `!1.eql?(1.0)` is true) and
|
|
9
|
+
# `equal?` compares identity (two equal strings or arrays are still
|
|
10
|
+
# different objects). A survivor means no test separates value equality from
|
|
11
|
+
# the stricter forms, so the comparison in use is never pinned down.
|
|
12
|
+
#
|
|
13
|
+
# Comparisons against nil, true, false or a symbol literal are skipped: those
|
|
14
|
+
# are singletons, so all three forms always agree.
|
|
15
|
+
class Evilution::Mutator::Operator::InequalityToNegatedIdentity < Evilution::Mutator::Base
|
|
16
|
+
SELECTORS = %i[eql? equal?].freeze
|
|
17
|
+
private_constant :SELECTORS
|
|
18
|
+
|
|
19
|
+
SINGLETON_LITERALS = [Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::SymbolNode].freeze
|
|
20
|
+
private_constant :SINGLETON_LITERALS
|
|
21
|
+
|
|
22
|
+
def visit_call_node(node)
|
|
23
|
+
rewrite(node) if node.name == :!=
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def rewrite(node)
|
|
30
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [argument]]
|
|
31
|
+
return if [node.receiver, argument].any? { |operand| singleton_literal?(operand) }
|
|
32
|
+
|
|
33
|
+
left = receiver_source(node.receiver)
|
|
34
|
+
SELECTORS.each do |selector|
|
|
35
|
+
add_mutation(offset: node.start_offset, length: node.location.length,
|
|
36
|
+
replacement: "!#{left}.#{selector}(#{source_of(argument)})", node: node)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def singleton_literal?(operand)
|
|
41
|
+
SINGLETON_LITERALS.any? { |type| operand.is_a?(type) }
|
|
42
|
+
end
|
|
43
|
+
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
|
|
@@ -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,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Drop one keyword argument at a call site: `build(name: "x", size: 2)`
|
|
6
|
+
# becomes `build(size: 2)` and `build(name: "x")`. Applies to any argument
|
|
7
|
+
# list — calls, `super` and `yield`.
|
|
8
|
+
#
|
|
9
|
+
# A survivor means the callee never consumes that keyword, or its default
|
|
10
|
+
# happens to match. argument_removal and argument_nil_substitution skip
|
|
11
|
+
# keyword arguments entirely.
|
|
12
|
+
#
|
|
13
|
+
# Only `key: value` pairs are dropped; a `**opts` double splat stays, though
|
|
14
|
+
# it still counts as a neighbour when removing the separator. A lone keyword
|
|
15
|
+
# that is the whole argument list is left to argument_list_removal.
|
|
16
|
+
class Evilution::Mutator::Operator::KeywordArgumentRemoval < Evilution::Mutator::Base
|
|
17
|
+
def visit_arguments_node(node)
|
|
18
|
+
node.arguments.each_with_index do |argument, index|
|
|
19
|
+
next unless argument.is_a?(Prism::KeywordHashNode)
|
|
20
|
+
|
|
21
|
+
drop_keywords(argument, index.zero? ? nil : node.arguments[index - 1])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def drop_keywords(hash, previous_argument)
|
|
30
|
+
elements = hash.elements
|
|
31
|
+
return if elements.length == 1 && previous_argument.nil?
|
|
32
|
+
|
|
33
|
+
elements.each_with_index do |element, index|
|
|
34
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
35
|
+
|
|
36
|
+
drop_element(element, elements[index + 1], index.zero? ? previous_argument : elements[index - 1])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Cut from the element to the next one, or — for the last element — from
|
|
41
|
+
# the end of whatever precedes it, so the separating comma goes too.
|
|
42
|
+
def drop_element(element, following, preceding)
|
|
43
|
+
start, finish = following ? [element.start_offset, following.start_offset] : [preceding.end_offset, element.end_offset]
|
|
44
|
+
|
|
45
|
+
add_mutation(offset: start, length: finish - start, replacement: "", node: element, skip_unparseable: true)
|
|
46
|
+
end
|
|
47
|
+
end
|