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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap the values of neighbouring keyword arguments, keeping the keys:
|
|
6
|
+
# `compute(x: a, y: b)` becomes `compute(x: b, y: a)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example tells the two values apart — the keyword form
|
|
9
|
+
# of the defect ArgumentOrderPermutation probes for positional arguments.
|
|
10
|
+
#
|
|
11
|
+
# Only keyword arguments written in the call are swapped: a braced hash is a
|
|
12
|
+
# single positional argument whose pairs are data. A double splat stays in
|
|
13
|
+
# place, and identical values are not swapped, since that would reproduce the
|
|
14
|
+
# original call. A shorthand key (`compute(a:, b:)`) has no value of its own
|
|
15
|
+
# to move, so the swap spells it out: `compute(a: b, b: a)`.
|
|
16
|
+
class Evilution::Mutator::Operator::KeywordValueSwap < Evilution::Mutator::Base
|
|
17
|
+
def visit_call_node(node)
|
|
18
|
+
swap_keyword_values(node)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_super_node(node)
|
|
23
|
+
swap_keyword_values(node)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_yield_node(node)
|
|
28
|
+
swap_keyword_values(node)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def swap_keyword_values(node)
|
|
35
|
+
return if node.arguments.nil?
|
|
36
|
+
|
|
37
|
+
node.arguments.arguments.grep(Prism::KeywordHashNode).each do |keywords|
|
|
38
|
+
pairs = keywords.elements.grep(Prism::AssocNode)
|
|
39
|
+
pairs.each_cons(2) do |left, right|
|
|
40
|
+
emit_swap(node, left, right) unless value_text(left) == value_text(right)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whatever separates the two pairs — the comma, a double splat, line breaks,
|
|
46
|
+
# comments — stays between them, so the call keeps its layout.
|
|
47
|
+
def emit_swap(node, left, right)
|
|
48
|
+
start_offset = left.location.start_offset
|
|
49
|
+
|
|
50
|
+
add_mutation(
|
|
51
|
+
offset: start_offset,
|
|
52
|
+
length: right.location.end_offset - start_offset,
|
|
53
|
+
replacement: "#{key_text(left)}#{value_text(right)}#{source_between(left, right)}" \
|
|
54
|
+
"#{key_text(right)}#{value_text(left)}",
|
|
55
|
+
node: node
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def source_between(left, right)
|
|
60
|
+
byteslice_source(left.location.end_offset, right.location.start_offset - left.location.end_offset)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The key with everything up to its value: `x: `, `"x" => `.
|
|
64
|
+
def key_text(pair)
|
|
65
|
+
return "#{pair.key.slice} " if pair.value.is_a?(Prism::ImplicitNode)
|
|
66
|
+
|
|
67
|
+
byteslice_source(pair.location.start_offset, pair.value.location.start_offset - pair.location.start_offset)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A shorthand key stands for the local variable or method of the same name.
|
|
71
|
+
def value_text(pair)
|
|
72
|
+
value = pair.value
|
|
73
|
+
value.is_a?(Prism::ImplicitNode) ? value.value.name.to_s : value.slice
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Give a `case/in` that has no `else` an empty one.
|
|
6
|
+
#
|
|
7
|
+
# Without an else, a value no pattern matches raises NoMatchingPatternError;
|
|
8
|
+
# with an empty one the expression quietly yields nil. A survivor means no
|
|
9
|
+
# example feeds the match a value outside its patterns, so nothing asserts
|
|
10
|
+
# that such a value is rejected.
|
|
11
|
+
#
|
|
12
|
+
# The opposite edit, removing an else that is there, belongs to CaseIn.
|
|
13
|
+
class Evilution::Mutator::Operator::NoMatchingPatternElse < Evilution::Mutator::Base
|
|
14
|
+
def visit_case_match_node(node)
|
|
15
|
+
add_else(node) if node.else_clause.nil? && !exhaustive?(node)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def add_else(node)
|
|
22
|
+
offset = node.end_keyword_loc.start_offset
|
|
23
|
+
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: offset,
|
|
26
|
+
length: 0,
|
|
27
|
+
replacement: else_clause_before(offset),
|
|
28
|
+
node: node
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# An `end` on a line of its own gets the else on the line above, indented
|
|
33
|
+
# alike. Otherwise the case is written on one line and the else joins it.
|
|
34
|
+
def else_clause_before(offset)
|
|
35
|
+
line_start = line_start_byte(@file_source, offset)
|
|
36
|
+
indentation = byteslice_source(line_start, offset - line_start)
|
|
37
|
+
|
|
38
|
+
indentation.strip.empty? ? "else\n#{indentation}" : "else; "
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A bare name (`in other`, `in _`) matches every value, so an else after it
|
|
42
|
+
# could never run and adding one would change nothing. A guarded capture is
|
|
43
|
+
# not a bare name: Prism wraps it in the guard's conditional.
|
|
44
|
+
def exhaustive?(node)
|
|
45
|
+
node.conditions.any? { |clause| clause.pattern.is_a?(Prism::LocalVariableTargetNode) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap the numbered parameters of a block: `pairs.map { _1 - _2 }` becomes
|
|
6
|
+
# `pairs.map { _2 - _1 }`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example tells the two values apart, so the block could
|
|
9
|
+
# take them in either order unnoticed.
|
|
10
|
+
#
|
|
11
|
+
# Only parameters the body reads are swapped with each other, each with its
|
|
12
|
+
# neighbour. Swapping in a parameter the body never reads (`{ _1 }` to
|
|
13
|
+
# `{ _2 }`) would change how many values the block takes, which is a different
|
|
14
|
+
# mutation from reordering them.
|
|
15
|
+
class Evilution::Mutator::Operator::NumberedParameterSwap < Evilution::Mutator::Base
|
|
16
|
+
NUMBERED_NAME = /\A_[1-9]\z/
|
|
17
|
+
|
|
18
|
+
def visit_block_node(node)
|
|
19
|
+
swap_parameters(node)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_lambda_node(node)
|
|
24
|
+
swap_parameters(node)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def swap_parameters(node)
|
|
31
|
+
return unless node.parameters.is_a?(Prism::NumberedParametersNode)
|
|
32
|
+
|
|
33
|
+
reads = numbered_reads(node.body)
|
|
34
|
+
names = reads.map { |read| read.name.to_s }.uniq.sort
|
|
35
|
+
names.each_cons(2) { |left, right| emit_swap(node, reads, left, right) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every read of a numbered parameter in the body. Blocks nested in it are
|
|
39
|
+
# searched too: a numbered parameter read there can only be this block's,
|
|
40
|
+
# since Ruby rejects a nested block that declares its own. A method defined
|
|
41
|
+
# in the body opens a new scope, whose blocks have parameters of their own.
|
|
42
|
+
def numbered_reads(node)
|
|
43
|
+
return [] if node.is_a?(Prism::DefNode)
|
|
44
|
+
return [node] if node.is_a?(Prism::LocalVariableReadNode) && node.name.to_s.match?(NUMBERED_NAME)
|
|
45
|
+
|
|
46
|
+
node.compact_child_nodes.flat_map { |child| numbered_reads(child) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The reads are scattered through the body, while a mutation replaces one
|
|
50
|
+
# contiguous range, so the range runs from the first affected read to the
|
|
51
|
+
# last and is rewritten with the two names exchanged.
|
|
52
|
+
def emit_swap(node, reads, left, right)
|
|
53
|
+
affected = reads.select { |read| [left, right].include?(read.name.to_s) }
|
|
54
|
+
.sort_by { |read| read.location.start_offset }
|
|
55
|
+
start_offset = affected.first.location.start_offset
|
|
56
|
+
end_offset = affected.last.location.end_offset
|
|
57
|
+
|
|
58
|
+
add_mutation(
|
|
59
|
+
offset: start_offset,
|
|
60
|
+
length: end_offset - start_offset,
|
|
61
|
+
replacement: exchange(affected, start_offset, end_offset, left => right, right => left),
|
|
62
|
+
node: node
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def exchange(affected, start_offset, end_offset, renames)
|
|
67
|
+
text = byteslice_source(start_offset, end_offset - start_offset)
|
|
68
|
+
|
|
69
|
+
affected.reverse_each do |read|
|
|
70
|
+
location = read.location
|
|
71
|
+
text = text.byteslice(0, location.start_offset - start_offset) +
|
|
72
|
+
renames.fetch(read.name.to_s) +
|
|
73
|
+
text.byteslice((location.end_offset - start_offset)..)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
text
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Shift a count held in a variable down by one: `n.times` becomes
|
|
6
|
+
# `(n - 1).times`, `items.first(n)` becomes `items.first(n - 1)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example checks the last iteration or the last element,
|
|
9
|
+
# the classic fencepost slip. A literal count is left to IntegerLiteral, which
|
|
10
|
+
# already shifts it; this operator reaches the counts that have no literal.
|
|
11
|
+
class Evilution::Mutator::Operator::OffByOneBoundary < Evilution::Mutator::Base
|
|
12
|
+
# Methods whose receiver is the count.
|
|
13
|
+
RECEIVER_COUNT_METHODS = %i[times].freeze
|
|
14
|
+
|
|
15
|
+
# Methods whose single argument is the count or the bound.
|
|
16
|
+
ARGUMENT_COUNT_METHODS = %i[upto downto take first last drop each_slice each_cons].freeze
|
|
17
|
+
|
|
18
|
+
# Arguments that stand in the list without being a value: a splat,
|
|
19
|
+
# keywords, forwarded arguments. Subtracting from them would not parse.
|
|
20
|
+
NON_VALUE_ARGUMENT_TYPES = [
|
|
21
|
+
Prism::SplatNode, Prism::KeywordHashNode, Prism::ForwardingArgumentsNode
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def visit_call_node(node)
|
|
25
|
+
shift_count(node) unless node.safe_navigation?
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def shift_count(node)
|
|
32
|
+
count = count_of(node)
|
|
33
|
+
return if count.nil? || count.is_a?(Prism::IntegerNode)
|
|
34
|
+
|
|
35
|
+
add_mutation(
|
|
36
|
+
offset: count.location.start_offset,
|
|
37
|
+
length: count.location.length,
|
|
38
|
+
replacement: lowered(node, count),
|
|
39
|
+
node: node
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def count_of(node)
|
|
44
|
+
if RECEIVER_COUNT_METHODS.include?(node.name)
|
|
45
|
+
node.receiver if node.arguments.nil?
|
|
46
|
+
elsif ARGUMENT_COUNT_METHODS.include?(node.name)
|
|
47
|
+
single_argument(node)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def single_argument(node)
|
|
52
|
+
return nil if node.arguments.nil?
|
|
53
|
+
|
|
54
|
+
arguments = node.arguments.arguments
|
|
55
|
+
return nil unless arguments.length == 1
|
|
56
|
+
return nil if NON_VALUE_ARGUMENT_TYPES.include?(arguments.first.class)
|
|
57
|
+
|
|
58
|
+
arguments.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A receiver is followed by `.method`, so the subtraction is grouped; an
|
|
62
|
+
# argument stands on its own and only needs grouping when it is not a
|
|
63
|
+
# primary expression.
|
|
64
|
+
def lowered(node, count)
|
|
65
|
+
subtraction = "#{receiver_source(count)} - 1"
|
|
66
|
+
count.equal?(node.receiver) ? "(#{subtraction})" : subtraction
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -12,21 +12,32 @@ require_relative "../operator"
|
|
|
12
12
|
# that pass the argument are unaffected, which is what separates this from a
|
|
13
13
|
# mutation that breaks every call site.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# Optional parameters have to sit side by side, so only the ends of a run of
|
|
16
|
+
# them are mutated; a required parameter in the middle would split it in two.
|
|
17
|
+
# The first can always join the required parameters before it. The last can
|
|
18
|
+
# join those after it, unless a rest parameter follows: a required parameter
|
|
19
|
+
# cannot stand between an optional one and `*rest`. The rest of the signature
|
|
20
|
+
# stays as written.
|
|
17
21
|
# KeywordArgument owns optional keyword parameters. Blocks are left alone: a
|
|
18
22
|
# block ignores arity, so a missing argument arrives as nil rather than raising,
|
|
19
23
|
# and making its parameter required would change nothing.
|
|
20
24
|
class Evilution::Mutator::Operator::OptionalParameterToRequired < Evilution::Mutator::Base
|
|
21
25
|
def visit_def_node(node)
|
|
22
26
|
parameters = node.parameters
|
|
23
|
-
parameters.
|
|
27
|
+
mutable_optionals(parameters).each { |optional| require_parameter(optional) } if parameters
|
|
24
28
|
|
|
25
29
|
super
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
34
|
+
def mutable_optionals(parameters)
|
|
35
|
+
optionals = parameters.optionals
|
|
36
|
+
return optionals.first(1) if parameters.rest
|
|
37
|
+
|
|
38
|
+
[optionals.first, optionals.last].compact.uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
30
41
|
def require_parameter(node)
|
|
31
42
|
location = node.location
|
|
32
43
|
name_loc = node.name_loc
|
|
@@ -83,14 +83,25 @@ class Evilution::Mutator::Operator::PatternMatchingArray < Evilution::Mutator::B
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def replace_pattern(node, parts)
|
|
86
|
+
constant = node.constant ? source_for(node.constant) : ""
|
|
87
|
+
opening, closing = delimiters(node)
|
|
88
|
+
|
|
86
89
|
add_mutation(
|
|
87
90
|
offset: node.location.start_offset,
|
|
88
91
|
length: node.location.length,
|
|
89
|
-
replacement: "
|
|
92
|
+
replacement: "#{constant}#{opening}#{parts.join(", ")}#{closing}",
|
|
90
93
|
node: node
|
|
91
94
|
)
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
# A pattern written without brackets (`in a, b`) has no delimiters of its
|
|
98
|
+
# own, so the mutant is given square ones.
|
|
99
|
+
def delimiters(node)
|
|
100
|
+
return ["[", "]"] if node.opening_loc.nil?
|
|
101
|
+
|
|
102
|
+
[node.opening_loc.slice, node.closing_loc.slice]
|
|
103
|
+
end
|
|
104
|
+
|
|
94
105
|
def source_for(node)
|
|
95
106
|
@file_source.byteslice(node.location.start_offset, node.location.length)
|
|
96
107
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Widen a pattern so it accepts input the original rejects:
|
|
6
|
+
#
|
|
7
|
+
# in [a, b] -> in [a, b, *] longer arrays
|
|
8
|
+
# in { age: Integer } -> in { age: _ } any value under the key
|
|
9
|
+
# in { name: String, age: Integer }
|
|
10
|
+
# -> in { name: String } hashes without the key
|
|
11
|
+
# in { kind: :a, **nil } -> in { kind: :a } hashes with other keys
|
|
12
|
+
#
|
|
13
|
+
# A survivor means no example feeds the pattern one of the shapes it is written
|
|
14
|
+
# to turn away.
|
|
15
|
+
#
|
|
16
|
+
# Elements of array and find patterns are widened by PatternMatchingArray. Here
|
|
17
|
+
# the pattern itself is opened up, and every edit keeps the variables the
|
|
18
|
+
# pattern binds, so the branch body still runs as written.
|
|
19
|
+
class Evilution::Mutator::Operator::PatternWildcardWidening < Evilution::Mutator::Base
|
|
20
|
+
WILDCARD = "_"
|
|
21
|
+
UNWIDENABLE_VALUE_TYPES = [Prism::LocalVariableTargetNode, Prism::ImplicitNode].freeze
|
|
22
|
+
|
|
23
|
+
def visit_array_pattern_node(node)
|
|
24
|
+
append_rest(node)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_hash_pattern_node(node)
|
|
29
|
+
widen_pairs(node)
|
|
30
|
+
drop_closing_rest(node)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# A pattern with a rest is open already, at whichever end the rest sits, and
|
|
37
|
+
# an empty one has no element to append after.
|
|
38
|
+
def append_rest(node)
|
|
39
|
+
return unless node.rest.nil?
|
|
40
|
+
return if node.requireds.empty?
|
|
41
|
+
|
|
42
|
+
add_mutation(
|
|
43
|
+
offset: node.requireds.last.location.end_offset,
|
|
44
|
+
length: 0,
|
|
45
|
+
replacement: ", *",
|
|
46
|
+
node: node
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def widen_pairs(node)
|
|
51
|
+
node.elements.each_with_index do |pair, index|
|
|
52
|
+
next if binds_variable?(pair.value)
|
|
53
|
+
|
|
54
|
+
wildcard_value(node, pair)
|
|
55
|
+
# `in {}` matches the empty hash only, so dropping the last key would
|
|
56
|
+
# narrow the pattern rather than widen it.
|
|
57
|
+
drop_pair(node, index) if node.elements.length > 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A bare name reaching here is an underscore one, which already accepts any
|
|
62
|
+
# value; rewriting it to `_` would change nothing. A shorthand key
|
|
63
|
+
# (`_ignored:`) has no value pattern of its own: its implicit value spans the
|
|
64
|
+
# key, so writing a wildcard there would rename the key instead.
|
|
65
|
+
def wildcard_value(node, pair)
|
|
66
|
+
return if UNWIDENABLE_VALUE_TYPES.include?(pair.value.class)
|
|
67
|
+
|
|
68
|
+
location = pair.value.location
|
|
69
|
+
|
|
70
|
+
add_mutation(
|
|
71
|
+
offset: location.start_offset,
|
|
72
|
+
length: location.length,
|
|
73
|
+
replacement: WILDCARD,
|
|
74
|
+
node: node
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def drop_pair(node, index)
|
|
79
|
+
remaining = node.elements.dup
|
|
80
|
+
remaining.delete_at(index)
|
|
81
|
+
replace_members(node, remaining + [node.rest].compact)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `**nil` forbids keys the pattern does not name. On its own it leaves a
|
|
85
|
+
# pattern that matches the empty hash only, with or without it.
|
|
86
|
+
def drop_closing_rest(node)
|
|
87
|
+
return unless node.rest.is_a?(Prism::NoKeywordsParameterNode)
|
|
88
|
+
return if node.elements.empty?
|
|
89
|
+
|
|
90
|
+
replace_members(node, node.elements)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Rewrites what sits between the delimiters, so braces, parentheses and a
|
|
94
|
+
# constant in front of them stay as written.
|
|
95
|
+
def replace_members(node, members)
|
|
96
|
+
all = node.elements + [node.rest].compact
|
|
97
|
+
start_offset = all.first.location.start_offset
|
|
98
|
+
end_offset = all.last.location.end_offset
|
|
99
|
+
|
|
100
|
+
add_mutation(
|
|
101
|
+
offset: start_offset,
|
|
102
|
+
length: end_offset - start_offset,
|
|
103
|
+
replacement: members.map(&:slice).join(", "),
|
|
104
|
+
node: node
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whether the value pattern captures into a variable the branch body could
|
|
109
|
+
# read. Taking such a pair away, or widening it to a wildcard, would break
|
|
110
|
+
# the body on the missing name whatever the tests assert. An underscore name
|
|
111
|
+
# is a wildcard by convention and is not counted.
|
|
112
|
+
def binds_variable?(node)
|
|
113
|
+
return !node.name.start_with?(WILDCARD) if node.is_a?(Prism::LocalVariableTargetNode)
|
|
114
|
+
|
|
115
|
+
node.compact_child_nodes.any? { |child| binds_variable?(child) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Drop the pin of a pattern variable: `in ^expected` becomes `in expected`.
|
|
6
|
+
#
|
|
7
|
+
# A pinned variable compares the matched value against an existing binding;
|
|
8
|
+
# without the pin the same name captures whatever is there, so the pattern
|
|
9
|
+
# matches every value. A survivor means no example feeds the pattern a value
|
|
10
|
+
# that differs from the pinned one.
|
|
11
|
+
class Evilution::Mutator::Operator::PinOperatorRemoval < Evilution::Mutator::Base
|
|
12
|
+
def initialize(**options)
|
|
13
|
+
super
|
|
14
|
+
@alternation_depth = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# An alternative pattern (`^a | ^b`) may not capture, at any depth inside it,
|
|
18
|
+
# so no pin under one has an unpinned form. Tracked here rather than left to
|
|
19
|
+
# the parse check: older Prism releases accept the capture that Ruby rejects.
|
|
20
|
+
def visit_alternation_pattern_node(node)
|
|
21
|
+
@alternation_depth += 1
|
|
22
|
+
super
|
|
23
|
+
ensure
|
|
24
|
+
@alternation_depth -= 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_pinned_variable_node(node)
|
|
28
|
+
remove_pin(node) if @alternation_depth.zero?
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Other pins without an unpinned form are caught by the parse check. Only a
|
|
35
|
+
# local name can stand as a capturing pattern, so `^@expected` and
|
|
36
|
+
# `^$expected` have none, and neither does a numbered parameter (`^_1`).
|
|
37
|
+
# Those rewrites carry no signal, so they are dropped rather than reported
|
|
38
|
+
# as unparseable.
|
|
39
|
+
def remove_pin(node)
|
|
40
|
+
add_mutation(
|
|
41
|
+
offset: node.location.start_offset,
|
|
42
|
+
length: node.location.length,
|
|
43
|
+
replacement: node.variable.slice,
|
|
44
|
+
node: node,
|
|
45
|
+
skip_unparseable: true
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Turn a proc into a lambda: `proc { |a, b| ... }` and `Proc.new { ... }`
|
|
6
|
+
# become `lambda { ... }`, with the block itself untouched.
|
|
7
|
+
#
|
|
8
|
+
# A lambda checks arity (wrong argument counts raise ArgumentError instead
|
|
9
|
+
# of padding with nil or dropping extras), does not auto-splat a single
|
|
10
|
+
# array argument, and `return` inside it returns from the lambda rather than
|
|
11
|
+
# from the enclosing method. A survivor means no test relies on any of that.
|
|
12
|
+
#
|
|
13
|
+
# Only a literal block with no arguments qualifies: `proc(&blk)` and
|
|
14
|
+
# `Proc.new(x) { }` are skipped.
|
|
15
|
+
class Evilution::Mutator::Operator::ProcToLambda < Evilution::Mutator::Base
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
rewrite(node) if proc_constructor?(node) && node.block.is_a?(Prism::BlockNode) && node.arguments.nil?
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def proc_constructor?(node)
|
|
24
|
+
case node.name
|
|
25
|
+
when :proc then node.receiver.nil?
|
|
26
|
+
when :new then proc_constant?(node.receiver)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def proc_constant?(receiver)
|
|
31
|
+
case receiver
|
|
32
|
+
when Prism::ConstantReadNode then receiver.name == :Proc
|
|
33
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Proc
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Replace the call head — `proc`, `Proc.new` or `Proc.new()` — leaving the
|
|
38
|
+
# block in place.
|
|
39
|
+
def rewrite(node)
|
|
40
|
+
head_end = (node.closing_loc || node.message_loc).end_offset
|
|
41
|
+
add_mutation(offset: node.start_offset, length: head_end - node.start_offset, replacement: "lambda", node: node)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap a constructor for a sibling on the same receiver constant:
|
|
6
|
+
# `Date.parse(value)` becomes `Date.iso8601(value)`, `Date.strptime(value)`,
|
|
7
|
+
# `Date.rfc2822(value)` and the rest. Keyed on receiver and selector, which
|
|
8
|
+
# the plain selector tables cannot express — `JSON.parse` is untouched.
|
|
9
|
+
#
|
|
10
|
+
# `parse` accepts almost any date-like string; the siblings accept one
|
|
11
|
+
# format (strptime's default is ISO 8601) or, like `jd` / `civil`, numbers
|
|
12
|
+
# only. A survivor means the tests only ever feed input every parser agrees
|
|
13
|
+
# on, so an overly lenient parse goes unnoticed.
|
|
14
|
+
#
|
|
15
|
+
# Only the constant itself (`Date`, `::Date`) matches, not a namespaced
|
|
16
|
+
# `Acme::Date`.
|
|
17
|
+
class Evilution::Mutator::Operator::ReceiverConstructorSwap < Evilution::Mutator::Base
|
|
18
|
+
DATE_PARSE_SIBLINGS = %i[jd civil strptime iso8601 rfc3339 xmlschema rfc2822 rfc822 httpdate jisx0301].freeze
|
|
19
|
+
private_constant :DATE_PARSE_SIBLINGS
|
|
20
|
+
|
|
21
|
+
# receiver constant => { selector => replacements }. Time has no jd / civil
|
|
22
|
+
# / rfc3339 / jisx0301, and its strptime requires a format argument.
|
|
23
|
+
REPLACEMENTS = {
|
|
24
|
+
Date: { parse: DATE_PARSE_SIBLINGS },
|
|
25
|
+
DateTime: { parse: DATE_PARSE_SIBLINGS },
|
|
26
|
+
Time: { parse: %i[iso8601 xmlschema rfc2822 rfc822 httpdate] }
|
|
27
|
+
}.freeze
|
|
28
|
+
private_constant :REPLACEMENTS
|
|
29
|
+
|
|
30
|
+
def visit_call_node(node)
|
|
31
|
+
replacements_for(node).each do |replacement|
|
|
32
|
+
location = node.message_loc
|
|
33
|
+
add_mutation(offset: location.start_offset, length: location.length, replacement: replacement.to_s, node: node)
|
|
34
|
+
end
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def replacements_for(node)
|
|
41
|
+
selectors = REPLACEMENTS[receiver_constant(node.receiver)]
|
|
42
|
+
selectors ? selectors.fetch(node.name, []) : []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def receiver_constant(receiver)
|
|
46
|
+
case receiver
|
|
47
|
+
when Prism::ConstantReadNode then receiver.name
|
|
48
|
+
when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Rewrite a `+` reduction into `sum`: `values.reduce(:+)` becomes
|
|
6
|
+
# `values.sum`, `values.inject(10, :+)` becomes `values.sum(10)`, and the
|
|
7
|
+
# same for the block-pass form `inject(&:+)`.
|
|
8
|
+
#
|
|
9
|
+
# The two differ on an empty collection (nil vs 0), on non-numeric elements
|
|
10
|
+
# (`reduce` concatenates strings and arrays, `sum` raises TypeError without a
|
|
11
|
+
# matching initial value) and on float precision (`sum` compensates rounding
|
|
12
|
+
# error). A survivor means none of those edges is tested.
|
|
13
|
+
class Evilution::Mutator::Operator::ReduceToSum < Evilution::Mutator::Base
|
|
14
|
+
SELECTORS = %i[reduce inject].freeze
|
|
15
|
+
private_constant :SELECTORS
|
|
16
|
+
|
|
17
|
+
NON_VALUE_ARGUMENTS = [Prism::SplatNode, Prism::KeywordHashNode].freeze
|
|
18
|
+
private_constant :NON_VALUE_ARGUMENTS
|
|
19
|
+
|
|
20
|
+
def visit_call_node(node)
|
|
21
|
+
rewrite(node) if node.receiver && SELECTORS.include?(node.name)
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def rewrite(node)
|
|
28
|
+
initial = initial_arguments(node)
|
|
29
|
+
return unless initial && initial.length <= 1
|
|
30
|
+
return if initial.any? { |argument| NON_VALUE_ARGUMENTS.any? { |type| argument.is_a?(type) } }
|
|
31
|
+
|
|
32
|
+
# A literal block never reaches here, so the call ends at its argument
|
|
33
|
+
# list or block-pass: cut from the selector to the end of the call.
|
|
34
|
+
start = node.message_loc.start_offset
|
|
35
|
+
replacement = initial.empty? ? "sum" : "sum(#{source_of(initial.first)})"
|
|
36
|
+
add_mutation(offset: start, length: node.end_offset - start, replacement: replacement, node: node)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The arguments besides the `+` operand — at most an initial value — when
|
|
40
|
+
# the call is a `+` reduction; nil otherwise.
|
|
41
|
+
def initial_arguments(node)
|
|
42
|
+
arguments = node.arguments ? node.arguments.arguments : []
|
|
43
|
+
|
|
44
|
+
if plus_block_pass?(node.block)
|
|
45
|
+
arguments unless plus_symbol?(arguments.last)
|
|
46
|
+
elsif node.block.nil? && plus_symbol?(arguments.last)
|
|
47
|
+
arguments[...-1]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def plus_block_pass?(block)
|
|
52
|
+
block.is_a?(Prism::BlockArgumentNode) && plus_symbol?(block.expression)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def plus_symbol?(node)
|
|
56
|
+
node.is_a?(Prism::SymbolNode) && node.unescaped == "+"
|
|
57
|
+
end
|
|
58
|
+
end
|