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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/regexp_pattern"
|
|
5
|
+
|
|
6
|
+
# Drop the `i` or `m` option of a regexp literal: `/admin/i` becomes `/admin/`,
|
|
7
|
+
# `/a.b/m` becomes `/a.b/`.
|
|
8
|
+
#
|
|
9
|
+
# Without `i` the pattern matches only the letter case it is written in;
|
|
10
|
+
# without `m` a `.` no longer matches a newline. A survivor means no example
|
|
11
|
+
# feeds the pattern input in another case, or a newline where a `.` stands.
|
|
12
|
+
#
|
|
13
|
+
# A flag is only dropped where it changes something: `i` needs a cased letter
|
|
14
|
+
# written in the pattern (escapes such as `\d` and `\w` do not count), and `m`
|
|
15
|
+
# needs a `.` outside a character class. Other options (`x`, `o`, encodings)
|
|
16
|
+
# are left alone.
|
|
17
|
+
class Evilution::Mutator::Operator::RegexpOptionRemoval < Evilution::Mutator::Base
|
|
18
|
+
def visit_regular_expression_node(node)
|
|
19
|
+
pattern = Evilution::AST::RegexpPattern.parse(node)
|
|
20
|
+
if pattern
|
|
21
|
+
drop_option(node, "i") if node.ignore_case? && cased_letter?(pattern)
|
|
22
|
+
drop_option(node, "m") if node.multi_line? && dot?(pattern)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def cased_letter?(pattern)
|
|
31
|
+
pattern.tokens.any? do |token|
|
|
32
|
+
token.type == :literal && token.text.each_char.any? { |char| char.swapcase != char }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dot?(pattern)
|
|
37
|
+
pattern.tokens.any? { |token| token.type == :meta && token.token == :dot }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The options follow the closing delimiter: `/im`, `}mi`.
|
|
41
|
+
def drop_option(node, option)
|
|
42
|
+
closing = node.closing_loc
|
|
43
|
+
|
|
44
|
+
add_mutation(
|
|
45
|
+
offset: closing.start_offset + closing.slice.index(option),
|
|
46
|
+
length: 1,
|
|
47
|
+
replacement: "",
|
|
48
|
+
node: node
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/regexp_pattern"
|
|
5
|
+
|
|
6
|
+
# Swap a regexp quantifier between "zero or more" and "one or more": `*`
|
|
7
|
+
# becomes `+` and `+` becomes `*`, keeping a lazy (`*?`) or possessive (`*+`)
|
|
8
|
+
# quantifier lazy or possessive.
|
|
9
|
+
#
|
|
10
|
+
# Only the empty case changes: `/\A\d*\z/` accepts "" and `/\A\d+\z/` does
|
|
11
|
+
# not. A survivor means no example checks how the pattern treats an absent
|
|
12
|
+
# repetition. RegexSimplification's quantifier removal changes the many case
|
|
13
|
+
# too; this isolates the empty one.
|
|
14
|
+
#
|
|
15
|
+
# Making a recursive `\g<name>` call mandatory can make the pattern recurse
|
|
16
|
+
# forever, so each mutant is compiled first and dropped if it does not.
|
|
17
|
+
class Evilution::Mutator::Operator::RegexpQuantifierMinimumSwap < Evilution::Mutator::Base
|
|
18
|
+
SWAPS = {
|
|
19
|
+
zero_or_more: "+", zero_or_more_reluctant: "+?", zero_or_more_possessive: "++",
|
|
20
|
+
one_or_more: "*", one_or_more_reluctant: "*?", one_or_more_possessive: "*+"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def visit_regular_expression_node(node)
|
|
24
|
+
pattern = Evilution::AST::RegexpPattern.parse(node)
|
|
25
|
+
pattern.tokens.each { |token| swap(node, pattern, token) } if pattern
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def swap(node, pattern, token)
|
|
32
|
+
return unless token.type == :quantifier
|
|
33
|
+
|
|
34
|
+
replacement = SWAPS[token.token]
|
|
35
|
+
return if replacement.nil?
|
|
36
|
+
return unless pattern.compiles?(token.start_offset, token.end_offset, replacement)
|
|
37
|
+
|
|
38
|
+
add_mutation(
|
|
39
|
+
offset: token.start_offset,
|
|
40
|
+
length: token.end_offset - token.start_offset,
|
|
41
|
+
replacement: replacement,
|
|
42
|
+
node: node
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Move the else clause of a rescue into the code it guards, keeping the rescue:
|
|
7
|
+
#
|
|
8
|
+
# begin begin
|
|
9
|
+
# fetch(id) fetch(id)
|
|
10
|
+
# rescue StandardError -> notify
|
|
11
|
+
# default rescue StandardError
|
|
12
|
+
# else default
|
|
13
|
+
# notify end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# The else code still runs only when the body succeeded; the one thing that
|
|
17
|
+
# changes is that an error it raises is now caught by the rescue, where an
|
|
18
|
+
# else clause is not protected. A survivor means no test lets the else code
|
|
19
|
+
# fail in a way the rescue would swallow.
|
|
20
|
+
#
|
|
21
|
+
# Only a rescue with at least one broad clause — a bare `rescue`, or one
|
|
22
|
+
# naming StandardError or Exception — is mutated. Under a rescue for specific
|
|
23
|
+
# classes the moved code changes behaviour only if it raises one of them,
|
|
24
|
+
# which it rarely does, so those mutants would mostly survive without saying
|
|
25
|
+
# anything. An empty else clause and a rescue around an empty body are
|
|
26
|
+
# skipped; ensure clauses and every rescue clause stay as written.
|
|
27
|
+
class Evilution::Mutator::Operator::RescueElseConcatenation < Evilution::Mutator::Base
|
|
28
|
+
BROAD_EXCEPTIONS = %w[StandardError ::StandardError Exception ::Exception].freeze
|
|
29
|
+
|
|
30
|
+
def visit_begin_node(node)
|
|
31
|
+
else_clause = node.else_clause
|
|
32
|
+
move_else(node, else_clause) if node.statements && else_clause && else_clause.statements && broad_rescue?(node)
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def broad_rescue?(node)
|
|
39
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.any? do |clause|
|
|
40
|
+
clause.exceptions.empty? || clause.exceptions.any? { |exception| BROAD_EXCEPTIONS.include?(exception.slice) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# One edit from the end of the body to the end of the else body: the else
|
|
45
|
+
# statements go first, then the rescue clauses as they were, with the
|
|
46
|
+
# whitespace (and a one-line `;`) that led into `else` trimmed off.
|
|
47
|
+
def move_else(node, else_clause)
|
|
48
|
+
body_end = node.statements.location.end_offset
|
|
49
|
+
continuation = Evilution::Mutator::RescueHandlers.continuation(@file_source, node.statements)
|
|
50
|
+
|
|
51
|
+
add_mutation(
|
|
52
|
+
offset: body_end,
|
|
53
|
+
length: else_clause.statements.location.end_offset - body_end,
|
|
54
|
+
replacement: "#{continuation}#{else_clause.statements.slice}#{rescue_clauses_text(body_end, else_clause)}",
|
|
55
|
+
node: else_clause
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def rescue_clauses_text(body_end, else_clause)
|
|
60
|
+
byteslice_source(body_end, else_clause.else_keyword_loc.start_offset - body_end).rstrip.delete_suffix(";")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Run a rescue handler after the code it protects as well, keeping the rescue:
|
|
7
|
+
#
|
|
8
|
+
# begin begin
|
|
9
|
+
# fetch(id) fetch(id)
|
|
10
|
+
# rescue NotFound -> rollback
|
|
11
|
+
# rollback rescue NotFound
|
|
12
|
+
# end rollback
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# The failure path is untouched; only a successful run changes, now also
|
|
16
|
+
# doing what the handler does. A survivor means the handler is harmless when
|
|
17
|
+
# nothing failed — the tests never show it is meant for the failure only.
|
|
18
|
+
# Dropping the rescue as well would repeat RescueRemoval and hide this signal
|
|
19
|
+
# behind any test of the failure path.
|
|
20
|
+
#
|
|
21
|
+
# With several rescue clauses each handler is used in its own mutant. An empty
|
|
22
|
+
# handler adds nothing and is skipped, as is one that only produces a value
|
|
23
|
+
# (`nil`, a literal, a variable, a bare `next`): run after the body it changes
|
|
24
|
+
# nothing but the return value, which RescueHandlerPromotion already probes.
|
|
25
|
+
# Handlers that only work inside the rescue are skipped too (see
|
|
26
|
+
# Mutator::RescueHandlers). A rescue modifier sits in value position, where a
|
|
27
|
+
# statement sequence does not fit, and is left alone.
|
|
28
|
+
class Evilution::Mutator::Operator::RescueHandlerConcatenation < Evilution::Mutator::Base
|
|
29
|
+
VALUE_TYPES = [
|
|
30
|
+
Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::IntegerNode, Prism::FloatNode,
|
|
31
|
+
Prism::StringNode, Prism::SymbolNode, Prism::SelfNode, Prism::ConstantReadNode,
|
|
32
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
def visit_begin_node(node)
|
|
36
|
+
append_handlers(node) if node.rescue_clause && node.statements
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def append_handlers(node)
|
|
43
|
+
body_end = node.statements.location.end_offset
|
|
44
|
+
separator = Evilution::Mutator::RescueHandlers.continuation(@file_source, node.statements)
|
|
45
|
+
|
|
46
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.each do |clause|
|
|
47
|
+
handler = clause.statements
|
|
48
|
+
next if handler.nil? || value_only?(handler)
|
|
49
|
+
next unless Evilution::Mutator::RescueHandlers.movable?(handler, clause.reference)
|
|
50
|
+
|
|
51
|
+
add_mutation(offset: body_end, length: 0, replacement: "#{separator}#{handler.slice}", node: clause)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def value_only?(handler)
|
|
56
|
+
handler.body.all? { |statement| value?(statement) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A method call may act, so only literals, plain reads and a bare `next` /
|
|
60
|
+
# `return` count.
|
|
61
|
+
def value?(statement)
|
|
62
|
+
case statement
|
|
63
|
+
when *VALUE_TYPES then true
|
|
64
|
+
when Prism::ArrayNode, Prism::HashNode then statement.elements.empty?
|
|
65
|
+
when Prism::NextNode, Prism::ReturnNode then statement.arguments.nil?
|
|
66
|
+
else false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Run a rescue handler instead of the code it protects:
|
|
7
|
+
# `begin; fetch(id); rescue NotFound; default; end` becomes
|
|
8
|
+
# `begin; default; end`, a method-level rescue does the same to the method
|
|
9
|
+
# body, and `fetch(id) rescue default` becomes `default`.
|
|
10
|
+
#
|
|
11
|
+
# The happy path never runs, so a survivor means the tests accept the
|
|
12
|
+
# fallback value where the real result was expected — they never check what
|
|
13
|
+
# the protected code returns.
|
|
14
|
+
#
|
|
15
|
+
# With several rescue clauses each handler is promoted in its own mutant. An
|
|
16
|
+
# else clause goes with the body, since it only runs when nothing is rescued;
|
|
17
|
+
# an ensure clause stays. An empty handler becomes `nil`, what a swallowed
|
|
18
|
+
# error returns.
|
|
19
|
+
#
|
|
20
|
+
# Handlers that only make sense inside the rescue are skipped, since promoted
|
|
21
|
+
# they fail for that reason alone; Mutator::RescueHandlers holds the rule.
|
|
22
|
+
class Evilution::Mutator::Operator::RescueHandlerPromotion < Evilution::Mutator::Base
|
|
23
|
+
def visit_begin_node(node)
|
|
24
|
+
promote_handlers(node) if node.rescue_clause && node.statements
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_rescue_modifier_node(node)
|
|
29
|
+
fallback = node.rescue_expression
|
|
30
|
+
replace(node, fallback.slice, node) if Evilution::Mutator::RescueHandlers.movable?(fallback, nil)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def promote_handlers(node)
|
|
37
|
+
start_offset = node.statements.location.start_offset
|
|
38
|
+
end_offset = protected_end(node)
|
|
39
|
+
|
|
40
|
+
Evilution::Mutator::RescueHandlers.new(node).clauses.each do |clause|
|
|
41
|
+
next unless Evilution::Mutator::RescueHandlers.movable?(clause.statements, clause.reference)
|
|
42
|
+
|
|
43
|
+
handler = clause.statements ? clause.statements.slice : "nil"
|
|
44
|
+
add_mutation(offset: start_offset, length: end_offset - start_offset, replacement: handler, node: clause)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The body, every rescue clause and an else clause are replaced; an ensure
|
|
49
|
+
# clause and the closing `end` stay. A rescue clause's location runs on
|
|
50
|
+
# through the clauses after it, and an else clause's up to the keyword that
|
|
51
|
+
# follows it, so the span ends at the last clause's own content.
|
|
52
|
+
def protected_end(node)
|
|
53
|
+
else_clause = node.else_clause
|
|
54
|
+
return Evilution::Mutator::RescueHandlers.new(node).clauses.last.location.end_offset if else_clause.nil?
|
|
55
|
+
|
|
56
|
+
return else_clause.statements.location.end_offset if else_clause.statements
|
|
57
|
+
|
|
58
|
+
else_clause.else_keyword_loc.end_offset
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def replace(target, replacement, node)
|
|
62
|
+
location = target.location
|
|
63
|
+
add_mutation(offset: location.start_offset, length: location.length, replacement: replacement, node: node)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../rescue_handlers"
|
|
5
|
+
|
|
6
|
+
# Drop the `return` keyword and keep its value: `return :neg if x.negative?`
|
|
7
|
+
# becomes `:neg if x.negative?`.
|
|
8
|
+
#
|
|
9
|
+
# Control flow continues past the point where the method used to leave, so a
|
|
10
|
+
# survivor means no test notices the code after a guard clause or an early
|
|
11
|
+
# return running anyway. ReturnValueRemoval keeps the early exit and drops the
|
|
12
|
+
# value; this keeps the value and drops the exit. Several values become an
|
|
13
|
+
# array, as `return` would make them: `return a, b` becomes `[a, b]`.
|
|
14
|
+
#
|
|
15
|
+
# A return in tail position — the method's last statement, the last statement
|
|
16
|
+
# of a branch that is itself in tail position, or the tail of a lambda — is
|
|
17
|
+
# skipped: its value is the result either way. A return inside a block always
|
|
18
|
+
# leaves the method, so it is never tail position. A bare `return` has no
|
|
19
|
+
# value to keep.
|
|
20
|
+
class Evilution::Mutator::Operator::ReturnKeywordRemoval < Evilution::Mutator::Base
|
|
21
|
+
# For each node that passes a value on, the parts whose value becomes its
|
|
22
|
+
# own. With an else clause a begin body's value is not the result, the
|
|
23
|
+
# else's is; each rescue handler's value can be the result too.
|
|
24
|
+
TAIL_PARTS = {
|
|
25
|
+
Prism::StatementsNode => ->(node) { [node.body.last] },
|
|
26
|
+
Prism::ParenthesesNode => ->(node) { [node.body] },
|
|
27
|
+
Prism::ElseNode => ->(node) { [node.statements] },
|
|
28
|
+
Prism::IfNode => ->(node) { [node.statements, node.subsequent] },
|
|
29
|
+
Prism::UnlessNode => ->(node) { [node.statements, node.else_clause] },
|
|
30
|
+
Prism::CaseNode => ->(node) { [*node.conditions.map(&:statements), node.else_clause] },
|
|
31
|
+
Prism::CaseMatchNode => ->(node) { [*node.conditions.map(&:statements), node.else_clause] },
|
|
32
|
+
Prism::BeginNode => lambda { |node|
|
|
33
|
+
[node.else_clause || node.statements, *Evilution::Mutator::RescueHandlers.new(node).clauses.map(&:statements)]
|
|
34
|
+
}
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def initialize(**options)
|
|
38
|
+
super
|
|
39
|
+
@tail_returns = Set.new.compare_by_identity
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def visit_def_node(node)
|
|
43
|
+
@tail_returns.merge(tail_returns(node.body))
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_lambda_node(node)
|
|
48
|
+
@tail_returns.merge(tail_returns(node.body))
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visit_return_node(node)
|
|
53
|
+
drop_keyword(node) if node.arguments && !@tail_returns.include?(node)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def drop_keyword(node)
|
|
60
|
+
arguments = node.arguments.arguments
|
|
61
|
+
value = if arguments.length == 1 && !arguments.first.is_a?(Prism::SplatNode)
|
|
62
|
+
arguments.first.slice
|
|
63
|
+
else
|
|
64
|
+
"[#{arguments.map(&:slice).join(", ")}]"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement: value, node: node)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The returns whose value is the value of `node` itself.
|
|
71
|
+
def tail_returns(node)
|
|
72
|
+
return [node] if node.is_a?(Prism::ReturnNode)
|
|
73
|
+
|
|
74
|
+
parts = TAIL_PARTS[node.class]
|
|
75
|
+
return [] if parts.nil?
|
|
76
|
+
|
|
77
|
+
parts.call(node).compact.flat_map { |part| tail_returns(part) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Turn a rightward pattern match into a pattern predicate: `value => [a, b]`
|
|
6
|
+
# becomes `value in [a, b]`.
|
|
7
|
+
#
|
|
8
|
+
# On a mismatch the original raises NoMatchingPatternError, while the predicate
|
|
9
|
+
# returns false and leaves the pattern's variables nil. A survivor means no
|
|
10
|
+
# example feeds the match a value it rejects, so the failure mode is untested.
|
|
11
|
+
class Evilution::Mutator::Operator::RightwardAssignment < Evilution::Mutator::Base
|
|
12
|
+
def visit_match_required_node(node)
|
|
13
|
+
replace_operator(node) unless irrefutable?(node.pattern)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def replace_operator(node)
|
|
20
|
+
location = node.operator_loc
|
|
21
|
+
|
|
22
|
+
add_mutation(
|
|
23
|
+
offset: location.start_offset,
|
|
24
|
+
length: location.length,
|
|
25
|
+
replacement: keyword_for(node),
|
|
26
|
+
node: node
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# `=>` is punctuation and may touch its neighbours (`value=>Integer`), but
|
|
31
|
+
# `in` is a word: written flush against them it would merge into a single
|
|
32
|
+
# name. A space is added on each side that has none.
|
|
33
|
+
def keyword_for(node)
|
|
34
|
+
location = node.operator_loc
|
|
35
|
+
before = node.value.location.end_offset == location.start_offset ? " " : ""
|
|
36
|
+
after = node.pattern.location.start_offset == location.end_offset ? " " : ""
|
|
37
|
+
|
|
38
|
+
"#{before}in#{after}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A bare name (`value => captured`, `value => _`) captures any value, so the
|
|
42
|
+
# match can never raise and the predicate form would behave the same.
|
|
43
|
+
def irrefutable?(pattern)
|
|
44
|
+
pattern.is_a?(Prism::LocalVariableTargetNode)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace safe navigation with a plain call: `user&.name` becomes
|
|
6
|
+
# `user.name`, and the same for the `||=`, `&&=` and operator-write forms.
|
|
7
|
+
#
|
|
8
|
+
# The mutant only differs when the receiver is nil, where it raises
|
|
9
|
+
# NoMethodError instead of short-circuiting. A survivor means no test ever
|
|
10
|
+
# reaches the call with a nil receiver, so the `&.` is unexercised.
|
|
11
|
+
#
|
|
12
|
+
# Receivers that can never be nil — `self` and literals — are skipped, since
|
|
13
|
+
# the mutant is equivalent there.
|
|
14
|
+
class Evilution::Mutator::Operator::SafeNavigationRemoval < Evilution::Mutator::Base
|
|
15
|
+
NEVER_NIL_RECEIVERS = [
|
|
16
|
+
Prism::SelfNode,
|
|
17
|
+
Prism::StringNode,
|
|
18
|
+
Prism::InterpolatedStringNode,
|
|
19
|
+
Prism::XStringNode,
|
|
20
|
+
Prism::InterpolatedXStringNode,
|
|
21
|
+
Prism::SymbolNode,
|
|
22
|
+
Prism::InterpolatedSymbolNode,
|
|
23
|
+
Prism::IntegerNode,
|
|
24
|
+
Prism::FloatNode,
|
|
25
|
+
Prism::RationalNode,
|
|
26
|
+
Prism::ImaginaryNode,
|
|
27
|
+
Prism::ArrayNode,
|
|
28
|
+
Prism::HashNode,
|
|
29
|
+
Prism::RangeNode,
|
|
30
|
+
Prism::RegularExpressionNode,
|
|
31
|
+
Prism::InterpolatedRegularExpressionNode,
|
|
32
|
+
Prism::TrueNode,
|
|
33
|
+
Prism::FalseNode,
|
|
34
|
+
Prism::LambdaNode
|
|
35
|
+
].freeze
|
|
36
|
+
private_constant :NEVER_NIL_RECEIVERS
|
|
37
|
+
|
|
38
|
+
def visit_call_node(node)
|
|
39
|
+
mutate_safe_navigation(node)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def visit_call_or_write_node(node)
|
|
44
|
+
mutate_safe_navigation(node)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visit_call_and_write_node(node)
|
|
49
|
+
mutate_safe_navigation(node)
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def visit_call_operator_write_node(node)
|
|
54
|
+
mutate_safe_navigation(node)
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def mutate_safe_navigation(node)
|
|
61
|
+
return unless node.safe_navigation?
|
|
62
|
+
return if NEVER_NIL_RECEIVERS.any? { |type| node.receiver.is_a?(type) }
|
|
63
|
+
|
|
64
|
+
operator = node.call_operator_loc
|
|
65
|
+
add_mutation(offset: operator.start_offset, length: operator.length, replacement: ".", node: node)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -25,22 +25,52 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
25
25
|
count: [:size],
|
|
26
26
|
select: [:filter],
|
|
27
27
|
filter: [:select],
|
|
28
|
-
to_s: [
|
|
29
|
-
to_i: [
|
|
28
|
+
to_s: %i[to_i to_str],
|
|
29
|
+
to_i: %i[to_s to_int],
|
|
30
30
|
to_f: [:to_i],
|
|
31
|
-
to_a: [
|
|
32
|
-
to_h: [
|
|
31
|
+
to_a: %i[to_h to_ary],
|
|
32
|
+
to_h: %i[to_a to_hash],
|
|
33
33
|
downcase: [:upcase],
|
|
34
34
|
upcase: [:downcase],
|
|
35
35
|
strip: %i[lstrip rstrip],
|
|
36
36
|
lstrip: [:strip],
|
|
37
37
|
rstrip: [:strip],
|
|
38
38
|
chomp: [:chop],
|
|
39
|
-
chop: [:chomp]
|
|
39
|
+
chop: [:chomp],
|
|
40
|
+
bytes: [:chars],
|
|
41
|
+
chars: [:bytes],
|
|
42
|
+
start_with?: [:end_with?],
|
|
43
|
+
end_with?: [:start_with?],
|
|
44
|
+
ceil: [:floor],
|
|
45
|
+
floor: [:ceil],
|
|
46
|
+
round: %i[floor ceil],
|
|
47
|
+
truncate: [:floor],
|
|
48
|
+
transform_keys: [:transform_values],
|
|
49
|
+
transform_values: [:transform_keys],
|
|
50
|
+
append: [:prepend],
|
|
51
|
+
prepend: [:append],
|
|
52
|
+
reverse_merge: [:merge],
|
|
53
|
+
even?: [:odd?],
|
|
54
|
+
odd?: [:even?],
|
|
55
|
+
zero?: [:nonzero?],
|
|
56
|
+
negative?: [:positive?],
|
|
57
|
+
positive?: [:negative?],
|
|
58
|
+
pred: [:succ],
|
|
59
|
+
succ: [:pred],
|
|
60
|
+
is_a?: [:instance_of?],
|
|
61
|
+
kind_of?: [:instance_of?]
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
# Swapped only when called with exactly one argument. Kept out of
|
|
65
|
+
# REPLACEMENTS, which symbol_to_proc_replacement also reads: `&:method`
|
|
66
|
+
# always calls with none, and a zero-argument `method` is usually an
|
|
67
|
+
# accessor such as `request.method` (the HTTP verb), not Object#method.
|
|
68
|
+
ONE_ARGUMENT_REPLACEMENTS = {
|
|
69
|
+
method: [:public_method]
|
|
40
70
|
}.freeze
|
|
41
71
|
|
|
42
72
|
def visit_call_node(node)
|
|
43
|
-
replacements =
|
|
73
|
+
replacements = replacements_for(node)
|
|
44
74
|
return super unless replacements
|
|
45
75
|
return super unless node.receiver
|
|
46
76
|
|
|
@@ -58,4 +88,12 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
58
88
|
|
|
59
89
|
super
|
|
60
90
|
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def replacements_for(node)
|
|
95
|
+
REPLACEMENTS.fetch(node.name) do
|
|
96
|
+
ONE_ARGUMENT_REPLACEMENTS[node.name] if node.arguments && node.arguments.arguments.length == 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
61
99
|
end
|
|
@@ -4,7 +4,7 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
6
6
|
def visit_splat_node(node)
|
|
7
|
-
mutate_remove_splat(node) if node.expression
|
|
7
|
+
mutate_remove_splat(node) if node.expression && !pattern_rests.include?(node)
|
|
8
8
|
|
|
9
9
|
super
|
|
10
10
|
end
|
|
@@ -14,20 +14,40 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
14
14
|
super
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# The rest of a pattern binds what is left over; it is not a splat in a
|
|
18
|
+
# call or a literal, so these splats are skipped. Dropping `**` from
|
|
19
|
+
# `{ key:, **opts }` is a syntax error. Dropping `*` from `[a, *rest]`
|
|
20
|
+
# parses, but it narrows the pattern to a fixed length. That mutant is
|
|
21
|
+
# deliberately not emitted, and the pattern operators keep a binding rest
|
|
22
|
+
# (`*rest`, `**opts`) as written. The rest of a multiple assignment
|
|
23
|
+
# (`a, *b = x`) is still mutated: `a, b = x` parses and changes what `b`
|
|
24
|
+
# binds.
|
|
25
|
+
def visit_hash_pattern_node(node)
|
|
26
|
+
pattern_rests.add(node.rest) if node.rest
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_array_pattern_node(node)
|
|
31
|
+
pattern_rests.add(node.rest) if node.rest
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_find_pattern_node(node)
|
|
36
|
+
pattern_rests.add(node.left)
|
|
37
|
+
pattern_rests.add(node.right)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
17
41
|
# KeywordHashNode wraps call-arg kwargs + `**splat`. When an explicit
|
|
18
|
-
# `k: v` precedes a `**opts` splat in the same call,
|
|
19
|
-
# bare `opts` puts a positional after a keyword
|
|
20
|
-
# (`bar(k: v, opts)`
|
|
21
|
-
# `visit_assoc_splat_node` skips them.
|
|
22
|
-
# (`bar(**opts, k: v)`)
|
|
42
|
+
# `k: v` or another `**splat` precedes a `**opts` splat in the same call,
|
|
43
|
+
# demoting `**opts` to bare `opts` puts a positional after a keyword
|
|
44
|
+
# argument and Ruby rejects it (`bar(k: v, opts)` and `bar(**a, opts)` are
|
|
45
|
+
# syntax errors). Mark such splats so `visit_assoc_splat_node` skips them.
|
|
46
|
+
# A splat that comes first (`bar(**opts, k: v)`) is still safe —
|
|
47
|
+
# positional-before-keyword is fine.
|
|
23
48
|
def visit_keyword_hash_node(node)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if el.is_a?(Prism::AssocSplatNode) && seen_kwarg
|
|
27
|
-
kwarg_preceded_splats.add(el)
|
|
28
|
-
elsif el.is_a?(Prism::AssocNode)
|
|
29
|
-
seen_kwarg = true
|
|
30
|
-
end
|
|
49
|
+
node.elements.drop(1).each do |el|
|
|
50
|
+
kwarg_preceded_splats.add(el) if el.is_a?(Prism::AssocSplatNode)
|
|
31
51
|
end
|
|
32
52
|
|
|
33
53
|
super
|
|
@@ -37,6 +57,7 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
37
57
|
return super if node.value.nil?
|
|
38
58
|
return super if hash_elements.include?(node)
|
|
39
59
|
return super if kwarg_preceded_splats.include?(node)
|
|
60
|
+
return super if pattern_rests.include?(node)
|
|
40
61
|
|
|
41
62
|
mutate_remove_double_splat(node)
|
|
42
63
|
|
|
@@ -53,6 +74,10 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
53
74
|
@kwarg_preceded_splats ||= Set.new.compare_by_identity
|
|
54
75
|
end
|
|
55
76
|
|
|
77
|
+
def pattern_rests
|
|
78
|
+
@pattern_rests ||= Set.new.compare_by_identity
|
|
79
|
+
end
|
|
80
|
+
|
|
56
81
|
def mutate_remove_splat(node)
|
|
57
82
|
add_mutation(
|
|
58
83
|
offset: node.location.start_offset,
|