evilution 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +132 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +127 -0
- data/README.md +136 -38
- data/docs/architecture.md +135 -11
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +121 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +41 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +187 -21
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +14 -15
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/round_half_mode.rb +38 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/source_surgeon.rb +2 -3
- data/lib/evilution/ast/syntax_check.rb +56 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +67 -30
- data/lib/evilution/cli/exit_guard.rb +26 -4
- data/lib/evilution/cli/parser/file_args.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +1 -1
- data/lib/evilution/cli.rb +3 -2
- data/lib/evilution/config/validators/spec_mappings.rb +2 -1
- data/lib/evilution/config.rb +3 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +45 -0
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +11 -1
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +177 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +80 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +22 -15
- data/lib/evilution/integration/loading/mutation_applier.rb +44 -1
- data/lib/evilution/integration/loading/own_methods.rb +87 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
- data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
- data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard.rb +4 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +84 -11
- data/lib/evilution/isolation/in_process.rb +100 -8
- data/lib/evilution/mcp/info_tool/status_glossary.rb +15 -4
- data/lib/evilution/mcp/info_tool.rb +3 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +21 -9
- data/lib/evilution/mutator/base.rb +69 -5
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +56 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +10 -2
- data/lib/evilution/mutator/operator/array_literal.rb +29 -0
- data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
- data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
- data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
- data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
- data/lib/evilution/mutator/operator/float_literal.rb +54 -19
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
- data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
- data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
- data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
- data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
- data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
- data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
- data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +41 -24
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/string_literal.rb +29 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
- data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
- data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
- data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
- data/lib/evilution/mutator/primitives.rb +24 -2
- data/lib/evilution/mutator/receiver_positions.rb +79 -0
- data/lib/evilution/mutator/registry.rb +58 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
- data/lib/evilution/parallel/work_queue/channel.rb +58 -5
- data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
- data/lib/evilution/parallel/work_queue/worker.rb +13 -4
- data/lib/evilution/parallel/work_queue.rb +15 -9
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +9 -4
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +57 -43
- data/lib/evilution/runner.rb +5 -2
- data/lib/evilution/subject.rb +25 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +90 -0
- data/script/memory_check +62 -16
- metadata +113 -4
|
@@ -4,39 +4,57 @@ 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 && !exempt_splats.include?(node)
|
|
8
8
|
|
|
9
9
|
super
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Inside a hash literal `**opts` merges a hash in; a bare `opts` is not an
|
|
13
|
+
# element (`{ opts }` is a syntax error), so these splats are skipped.
|
|
12
14
|
def visit_hash_node(node)
|
|
13
|
-
node.elements.
|
|
15
|
+
exempt_splats.merge(node.elements.grep(Prism::AssocSplatNode))
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The rest of a pattern binds what is left over; it is not a splat in a
|
|
20
|
+
# call or a literal, so these splats are skipped. Dropping `**` from
|
|
21
|
+
# `{ key:, **opts }` is a syntax error. Dropping `*` from `[a, *rest]`
|
|
22
|
+
# parses, but it narrows the pattern to a fixed length. That mutant is
|
|
23
|
+
# deliberately not emitted, and the pattern operators keep a binding rest
|
|
24
|
+
# (`*rest`, `**opts`) as written. The rest of a multiple assignment
|
|
25
|
+
# (`a, *b = x`) is still mutated: `a, b = x` parses and changes what `b`
|
|
26
|
+
# binds.
|
|
27
|
+
def visit_hash_pattern_node(node)
|
|
28
|
+
exempt_splats.add(node.rest) if node.rest
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_array_pattern_node(node)
|
|
33
|
+
exempt_splats.add(node.rest) if node.rest
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_find_pattern_node(node)
|
|
38
|
+
exempt_splats.add(node.left)
|
|
39
|
+
exempt_splats.add(node.right)
|
|
14
40
|
super
|
|
15
41
|
end
|
|
16
42
|
|
|
17
43
|
# 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
|
-
#
|
|
22
|
-
# (`bar(**opts, k: v)`)
|
|
44
|
+
# `k: v` or another `**splat` precedes a `**opts` splat in the same call,
|
|
45
|
+
# demoting `**opts` to bare `opts` puts a positional after a keyword
|
|
46
|
+
# argument and Ruby rejects it (`bar(k: v, opts)` and `bar(**a, opts)` are
|
|
47
|
+
# syntax errors), so such splats are skipped.
|
|
48
|
+
# A splat that comes first (`bar(**opts, k: v)`) is still safe —
|
|
49
|
+
# positional-before-keyword is fine.
|
|
23
50
|
def visit_keyword_hash_node(node)
|
|
24
|
-
|
|
25
|
-
node.elements.each do |el|
|
|
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
|
|
31
|
-
end
|
|
32
|
-
|
|
51
|
+
exempt_splats.merge(node.elements.drop(1).grep(Prism::AssocSplatNode))
|
|
33
52
|
super
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
def visit_assoc_splat_node(node)
|
|
37
56
|
return super if node.value.nil?
|
|
38
|
-
return super if
|
|
39
|
-
return super if kwarg_preceded_splats.include?(node)
|
|
57
|
+
return super if exempt_splats.include?(node)
|
|
40
58
|
|
|
41
59
|
mutate_remove_double_splat(node)
|
|
42
60
|
|
|
@@ -45,12 +63,11 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
45
63
|
|
|
46
64
|
private
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@kwarg_preceded_splats ||= Set.new.compare_by_identity
|
|
66
|
+
# Splats that are left as written: removing the `*` or `**` would not
|
|
67
|
+
# parse, or would change what a pattern matches. Each visitor that fills
|
|
68
|
+
# the set says why.
|
|
69
|
+
def exempt_splats
|
|
70
|
+
@exempt_splats ||= Set.new.compare_by_identity
|
|
54
71
|
end
|
|
55
72
|
|
|
56
73
|
def mutate_remove_splat(node)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap two adjacent commands: `charge(card); send_receipt(user)` becomes
|
|
6
|
+
# `send_receipt(user); charge(card)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example depends on the order the two side effects
|
|
9
|
+
# happen in. Most pairs of statements could swap without anyone noticing, so
|
|
10
|
+
# only pairs that are likely to matter are touched:
|
|
11
|
+
#
|
|
12
|
+
# - both statements are commands: they call a method, append, write an index,
|
|
13
|
+
# yield or call super. Assigning a local or instance variable stores a
|
|
14
|
+
# value, and statements without such effects swap freely.
|
|
15
|
+
# - neither writes a variable the other reads or writes. Such a pair fails at
|
|
16
|
+
# once when swapped, which tests nothing about order.
|
|
17
|
+
# - neither contains control flow (`return`, `break`, `next`, `redo`, `retry`,
|
|
18
|
+
# `raise`, `fail`), which would skip the other statement.
|
|
19
|
+
# - the second is not the last statement of its body, which gives the body
|
|
20
|
+
# its value.
|
|
21
|
+
# - neither contains a heredoc, whose body sits outside the statement's
|
|
22
|
+
# source range.
|
|
23
|
+
# - they are not writes to different literal keys of the same receiver
|
|
24
|
+
# (`opts[:a] = 1; opts[:b] = 2`), which cannot affect each other.
|
|
25
|
+
class Evilution::Mutator::Operator::StatementReorder < Evilution::Mutator::Base
|
|
26
|
+
CONTROL_FLOW_TYPES = [
|
|
27
|
+
Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode
|
|
28
|
+
].freeze
|
|
29
|
+
RAISING_METHODS = %i[raise fail].freeze
|
|
30
|
+
|
|
31
|
+
# Calls that change their receiver without a method name: `a << b`, `a[k] = v`.
|
|
32
|
+
MUTATING_OPERATORS = %i[<< []=].freeze
|
|
33
|
+
METHOD_NAME = /\A[a-zA-Z_]/
|
|
34
|
+
|
|
35
|
+
EFFECT_TYPES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
|
|
36
|
+
|
|
37
|
+
# Statements that store a value rather than act.
|
|
38
|
+
VALUE_WRITE_TYPES = [
|
|
39
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
40
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
41
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
42
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# Nodes that set a variable, and nodes that read one.
|
|
46
|
+
WRITE_TYPES = [
|
|
47
|
+
*VALUE_WRITE_TYPES, Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
48
|
+
Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
|
|
49
|
+
Prism::ClassVariableAndWriteNode, Prism::ClassVariableTargetNode,
|
|
50
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
51
|
+
Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
|
|
52
|
+
].freeze
|
|
53
|
+
LITERAL_KEY_TYPES = [Prism::SymbolNode, Prism::StringNode, Prism::IntegerNode].freeze
|
|
54
|
+
|
|
55
|
+
READ_TYPES = [
|
|
56
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
57
|
+
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
def visit_statements_node(node)
|
|
61
|
+
statements = node.body
|
|
62
|
+
# The last statement stays put, so pairs end one before it.
|
|
63
|
+
statements[0...-1].each_cons(2) do |first, second|
|
|
64
|
+
emit_swap(first, second) if swappable?(first, second)
|
|
65
|
+
end
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def swappable?(first, second)
|
|
72
|
+
[first, second].all? { |statement| command?(statement) && !control_flow?(statement) && !heredoc?(statement) } &&
|
|
73
|
+
independent?(first, second) && !distinct_key_writes?(first, second)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def command?(statement)
|
|
77
|
+
return false if VALUE_WRITE_TYPES.include?(statement.class)
|
|
78
|
+
|
|
79
|
+
nodes_in(statement).any? { |node| effect?(node) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def effect?(node)
|
|
83
|
+
return true if EFFECT_TYPES.include?(node.class)
|
|
84
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
85
|
+
|
|
86
|
+
node.name.to_s.match?(METHOD_NAME) || MUTATING_OPERATORS.include?(node.name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def control_flow?(statement)
|
|
90
|
+
nodes_in(statement).any? do |node|
|
|
91
|
+
CONTROL_FLOW_TYPES.include?(node.class) ||
|
|
92
|
+
(node.is_a?(Prism::CallNode) && node.receiver.nil? && RAISING_METHODS.include?(node.name))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def heredoc?(statement)
|
|
97
|
+
nodes_in(statement).any? do |node|
|
|
98
|
+
node.respond_to?(:heredoc?) && node.heredoc?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def distinct_key_writes?(first, second)
|
|
103
|
+
writes = [index_write(first), index_write(second)]
|
|
104
|
+
return false if writes.include?(nil)
|
|
105
|
+
return false unless writes.first.receiver.slice == writes.last.receiver.slice
|
|
106
|
+
|
|
107
|
+
distinct_literal_keys?(writes.map { |write| write.arguments.arguments.first })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def distinct_literal_keys?(keys)
|
|
111
|
+
keys.all? { |key| LITERAL_KEY_TYPES.include?(key.class) } && keys.first.slice != keys.last.slice
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `h[k] = v` on its own or under a modifier condition (`h[k] = v if v`).
|
|
115
|
+
def index_write(statement)
|
|
116
|
+
if statement.is_a?(Prism::IfNode) || statement.is_a?(Prism::UnlessNode)
|
|
117
|
+
body = statement.statements
|
|
118
|
+
statement = body.body.first if body && body.body.length == 1
|
|
119
|
+
end
|
|
120
|
+
statement if statement.is_a?(Prism::CallNode) && statement.name == :[]=
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def independent?(first, second)
|
|
124
|
+
first_writes = names(first, WRITE_TYPES)
|
|
125
|
+
second_writes = names(second, WRITE_TYPES)
|
|
126
|
+
|
|
127
|
+
!first_writes.intersect?(names(second, READ_TYPES) | second_writes) &&
|
|
128
|
+
!second_writes.intersect?(names(first, READ_TYPES))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def names(statement, types)
|
|
132
|
+
nodes_in(statement).filter_map { |node| node.name if types.include?(node.class) }.to_set
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def nodes_in(node)
|
|
136
|
+
[node, *node.compact_child_nodes.flat_map { |child| nodes_in(child) }]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Whatever separates the two statements — a line break and indentation, or
|
|
140
|
+
# `; ` — stays between them.
|
|
141
|
+
def emit_swap(first, second)
|
|
142
|
+
start_offset = first.location.start_offset
|
|
143
|
+
end_offset = second.location.end_offset
|
|
144
|
+
separator = byteslice_source(first.location.end_offset, second.location.start_offset - first.location.end_offset)
|
|
145
|
+
|
|
146
|
+
add_mutation(
|
|
147
|
+
offset: start_offset,
|
|
148
|
+
length: end_offset - start_offset,
|
|
149
|
+
replacement: "#{second.slice}#{separator}#{first.slice}",
|
|
150
|
+
node: first
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::StringLiteral < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
6
8
|
def initialize(skip_heredoc_literals: false, **rest)
|
|
7
9
|
super(**rest)
|
|
8
10
|
@skip_heredoc_literals = skip_heredoc_literals
|
|
@@ -32,6 +34,11 @@ class Evilution::Mutator::Operator::StringLiteral < Evilution::Mutator::Base
|
|
|
32
34
|
return
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# The composed value as a whole, next to the chunks and interpolations
|
|
38
|
+
# inside it. A word of `%W[]` has no quotes of its own and is not a
|
|
39
|
+
# literal: `nil` there would be the word "nil".
|
|
40
|
+
emit_string_mutations(node) if node.opening_loc
|
|
41
|
+
|
|
35
42
|
super
|
|
36
43
|
end
|
|
37
44
|
|
|
@@ -59,11 +66,33 @@ class Evilution::Mutator::Operator::StringLiteral < Evilution::Mutator::Base
|
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def visit_interpolated_x_string_node(node)
|
|
69
|
+
return if node.heredoc? && @skip_heredoc_literals
|
|
70
|
+
|
|
71
|
+
mutate_command_to_nil(node)
|
|
62
72
|
visit_non_string_parts(node)
|
|
63
73
|
end
|
|
64
74
|
|
|
75
|
+
def visit_x_string_node(node)
|
|
76
|
+
mutate_command_to_nil(node)
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
|
|
65
80
|
private
|
|
66
81
|
|
|
82
|
+
# A command literal (`` `ls` ``, `%x(ls)`) is only ever replaced as a whole,
|
|
83
|
+
# by `nil`: the command then does not run. Its text is never rewritten, as
|
|
84
|
+
# a changed command would run for real wherever an example runs it.
|
|
85
|
+
def mutate_command_to_nil(node)
|
|
86
|
+
return if node.heredoc?
|
|
87
|
+
|
|
88
|
+
add_mutation(
|
|
89
|
+
offset: node.location.start_offset,
|
|
90
|
+
length: node.location.length,
|
|
91
|
+
replacement: "nil",
|
|
92
|
+
node: node
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
67
96
|
def visit_non_string_parts(node)
|
|
68
97
|
node.parts.each do |part|
|
|
69
98
|
next if part.is_a?(Prism::StringNode)
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/class_body"
|
|
6
7
|
|
|
8
|
+
# Drop the superclass from a class definition: `class Admin < User` becomes
|
|
9
|
+
# `class Admin`.
|
|
10
|
+
#
|
|
11
|
+
# The definition line belongs to no method, so the mutant is attributed to
|
|
12
|
+
# the method anchoring the class body (see Evilution::AST::ClassBody).
|
|
7
13
|
class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
8
14
|
def call(subject, filter: nil)
|
|
9
15
|
@subject = subject
|
|
@@ -11,24 +17,22 @@ class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
|
11
17
|
@mutations = []
|
|
12
18
|
@filter = filter
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
add_mutation(offset: offset, length: length, replacement: "", node: enclosing)
|
|
20
|
+
classes_with_superclass_for(subject).each do |class_node|
|
|
21
|
+
offset, length = superclass_range(class_node)
|
|
22
|
+
add_mutation(offset: offset, length: length, replacement: "", node: class_node)
|
|
23
|
+
end
|
|
19
24
|
|
|
20
25
|
@mutations
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
private
|
|
24
29
|
|
|
25
|
-
def
|
|
30
|
+
def classes_with_superclass_for(subject)
|
|
26
31
|
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
27
|
-
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
28
|
-
return nil unless enclosing && enclosing.superclass
|
|
29
|
-
return nil unless find_first_method_line(enclosing) == subject.line_number
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Evilution::AST::ClassBody.anchored_at(tree, subject.line_number).map(&:node).select do |scope|
|
|
34
|
+
scope.is_a?(Prism::ClassNode) && scope.superclass
|
|
35
|
+
end
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def superclass_range(class_node)
|
|
@@ -39,34 +43,4 @@ class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
|
39
43
|
|
|
40
44
|
[name_end, superclass_end - name_end]
|
|
41
45
|
end
|
|
42
|
-
|
|
43
|
-
def find_enclosing_class(tree, target_line)
|
|
44
|
-
finder = ClassFinder.new(target_line)
|
|
45
|
-
finder.visit(tree)
|
|
46
|
-
finder.result
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def find_first_method_line(class_node)
|
|
50
|
-
return nil unless class_node.body
|
|
51
|
-
|
|
52
|
-
class_node.body.body.each do |node|
|
|
53
|
-
return node.location.start_line if node.is_a?(Prism::DefNode)
|
|
54
|
-
end
|
|
55
|
-
nil
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Visitor to find the ClassNode enclosing a given line number.
|
|
59
|
-
class ClassFinder < Prism::Visitor
|
|
60
|
-
attr_reader :result
|
|
61
|
-
|
|
62
|
-
def initialize(target_line)
|
|
63
|
-
@target_line = target_line
|
|
64
|
-
@result = nil
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def visit_class_node(node)
|
|
68
|
-
@result = node if @target_line.between?(node.location.start_line, node.location.end_line)
|
|
69
|
-
super
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
46
|
end
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/round_half_mode"
|
|
4
5
|
|
|
5
6
|
class Evilution::Mutator::Operator::SymbolLiteral < Evilution::Mutator::Base
|
|
7
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
8
|
+
|
|
9
|
+
def call(subject, filter: nil)
|
|
10
|
+
@round_half_modes = []
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The tie-breaking mode of `round` (`half: :even`) is left to
|
|
15
|
+
# RoundHalfModeSwap: a made-up symbol there only raises, and nil rounds
|
|
16
|
+
# like `:up`.
|
|
17
|
+
def visit_call_node(node)
|
|
18
|
+
mode = Evilution::AST::RoundHalfMode.of(node)
|
|
19
|
+
@round_half_modes << mode if mode
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
6
23
|
def visit_symbol_node(node)
|
|
7
|
-
return super if label_form?(node)
|
|
24
|
+
return super if label_form?(node) || round_half_mode?(node)
|
|
8
25
|
|
|
9
26
|
add_mutation(
|
|
10
27
|
offset: node.location.start_offset,
|
|
@@ -23,10 +40,38 @@ class Evilution::Mutator::Operator::SymbolLiteral < Evilution::Mutator::Base
|
|
|
23
40
|
super
|
|
24
41
|
end
|
|
25
42
|
|
|
43
|
+
# An interpolated symbol (`:"visit_#{type}"`) as a whole. A label key
|
|
44
|
+
# (`"a#{x}": 1`) cannot be replaced by a value, and a word of `%I[]` has no
|
|
45
|
+
# quotes of its own: `nil` there would be the symbol `:nil`.
|
|
46
|
+
def visit_interpolated_symbol_node(node)
|
|
47
|
+
return super if node.opening_loc.nil? || label_form?(node)
|
|
48
|
+
|
|
49
|
+
add_mutation(
|
|
50
|
+
offset: node.location.start_offset,
|
|
51
|
+
length: node.location.length,
|
|
52
|
+
replacement: ':""',
|
|
53
|
+
node: node
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
add_mutation(
|
|
57
|
+
offset: node.location.start_offset,
|
|
58
|
+
length: node.location.length,
|
|
59
|
+
replacement: "nil",
|
|
60
|
+
node: node
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
26
66
|
private
|
|
27
67
|
|
|
68
|
+
def round_half_mode?(node)
|
|
69
|
+
@round_half_modes.any? { |mode| mode.equal?(node) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `a:` closes with `:`, a quoted label (`"a b":`) with `":`.
|
|
28
73
|
def label_form?(node)
|
|
29
74
|
closing = node.closing_loc
|
|
30
|
-
!closing.nil? && closing.slice
|
|
75
|
+
!closing.nil? && closing.slice.end_with?(":")
|
|
31
76
|
end
|
|
32
77
|
end
|
|
@@ -29,7 +29,7 @@ class Evilution::Mutator::Operator::ToIToInteger < Evilution::Mutator::Base
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def rewrite(node)
|
|
32
|
-
arguments = node
|
|
32
|
+
arguments = argument_nodes(node)
|
|
33
33
|
return if arguments.length > 1 || arguments.any?(Prism::SplatNode)
|
|
34
34
|
|
|
35
35
|
parts = [node.receiver, *arguments].map { |part| source_of(part) }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a variable read with `nil`: a local, an instance, class or global
|
|
6
|
+
# variable, or `self`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means the value read never reaches an assertion — the tests
|
|
9
|
+
# would pass whatever the variable held. Where the read is passed as a
|
|
10
|
+
# positional argument or interpolated into a string, the mutant is the one
|
|
11
|
+
# argument_nil_substitution or string_interpolation already emits, and the
|
|
12
|
+
# two are counted once.
|
|
13
|
+
#
|
|
14
|
+
# Positions where `nil` says nothing new are skipped:
|
|
15
|
+
#
|
|
16
|
+
# - the receiver of a call, which turns into `nil.name` — a NoMethodError
|
|
17
|
+
# any test that reaches it kills — and likewise the receiver of a write
|
|
18
|
+
# (`counter.total += 1`, `cache[key] ||= value`);
|
|
19
|
+
# - a void statement (every statement of a body except the last), which
|
|
20
|
+
# statement_deletion deletes;
|
|
21
|
+
# - the condition of an `if`, `unless`, `while` or `until`, where `nil` is
|
|
22
|
+
# the `false` conditional_negation already tries;
|
|
23
|
+
# - the left operand of `&&` or `||`, where `nil && b` is the whole
|
|
24
|
+
# expression turned to nil and `nil || b` is `b` promoted;
|
|
25
|
+
# - a block-pass argument, `&nil` being the call without its block;
|
|
26
|
+
# - a double-splatted read, `**nil` raising before Ruby 3.4;
|
|
27
|
+
# - the value a shorthand hash (`name:`) leaves unwritten.
|
|
28
|
+
class Evilution::Mutator::Operator::VariableReadToNil < Evilution::Mutator::Base
|
|
29
|
+
include Evilution::Mutator::ReceiverPositions
|
|
30
|
+
|
|
31
|
+
def call(subject, **)
|
|
32
|
+
@skipped_nodes = Set.new
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def visit_statements_node(node)
|
|
37
|
+
@skipped_nodes.merge(node.body[...-1])
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_if_node(node)
|
|
42
|
+
@skipped_nodes.add(node.predicate)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit_unless_node(node)
|
|
47
|
+
@skipped_nodes.add(node.predicate)
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def visit_while_node(node)
|
|
52
|
+
@skipped_nodes.add(node.predicate)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def visit_until_node(node)
|
|
57
|
+
@skipped_nodes.add(node.predicate)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit_and_node(node)
|
|
62
|
+
@skipped_nodes.add(node.left)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def visit_or_node(node)
|
|
67
|
+
@skipped_nodes.add(node.left)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def visit_block_argument_node(node)
|
|
72
|
+
@skipped_nodes.add(node.expression)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_assoc_splat_node(node)
|
|
77
|
+
@skipped_nodes.add(node.value)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The read inside has no source of its own: it shares the key's.
|
|
82
|
+
def visit_implicit_node(_node); end
|
|
83
|
+
|
|
84
|
+
def visit_local_variable_read_node(node)
|
|
85
|
+
nilify(node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def visit_instance_variable_read_node(node)
|
|
89
|
+
nilify(node)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def visit_class_variable_read_node(node)
|
|
93
|
+
nilify(node)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visit_global_variable_read_node(node)
|
|
97
|
+
nilify(node)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def visit_self_node(node)
|
|
101
|
+
nilify(node)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def receiver_found(node)
|
|
107
|
+
@skipped_nodes.add(node)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def nilify(node)
|
|
111
|
+
mutate_to_nil(node) unless @skipped_nodes.include?(node)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/class_body"
|
|
7
|
+
|
|
8
|
+
# Drop a visibility declaration from a class or module body: `private`,
|
|
9
|
+
# `protected` and `module_function`, bare or naming methods. A definition the
|
|
10
|
+
# declaration wraps is kept: `private def a; end` becomes `def a; end`.
|
|
11
|
+
#
|
|
12
|
+
# A survivor means no example checks what the declaration decides: that a
|
|
13
|
+
# private method cannot be called from outside, or that a module function
|
|
14
|
+
# can.
|
|
15
|
+
#
|
|
16
|
+
# Declarations sit in the class body, outside every method, where no subject
|
|
17
|
+
# reaches them. They are attributed to the method anchoring the enclosing
|
|
18
|
+
# body (see Evilution::AST::ClassBody); a top-level declaration has no body
|
|
19
|
+
# to be attributed to. A call inside a method, or one sent to another
|
|
20
|
+
# receiver, is not a declaration.
|
|
21
|
+
class Evilution::Mutator::Operator::VisibilityRemoval < Evilution::Mutator::Base
|
|
22
|
+
DECLARATIONS = %i[private protected module_function].freeze
|
|
23
|
+
|
|
24
|
+
# What a declaration can wrap and has to leave behind: a method definition,
|
|
25
|
+
# or a call (`private attr_reader :name`, `private helper_names`). The call
|
|
26
|
+
# runs with or without the declaration, so it stays.
|
|
27
|
+
WRAPPED = [Prism::DefNode, Prism::CallNode].freeze
|
|
28
|
+
|
|
29
|
+
def call(subject, filter: nil)
|
|
30
|
+
@subject = subject
|
|
31
|
+
@file_source = File.read(subject.file_path)
|
|
32
|
+
@mutations = []
|
|
33
|
+
@filter = filter
|
|
34
|
+
|
|
35
|
+
declarations_for(subject).each { |declaration| remove(declaration) }
|
|
36
|
+
@mutations
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def remove(declaration)
|
|
42
|
+
replacement = replacement_for(argument_nodes(declaration))
|
|
43
|
+
return if replacement.nil?
|
|
44
|
+
|
|
45
|
+
add_mutation(
|
|
46
|
+
offset: declaration.location.start_offset,
|
|
47
|
+
length: declaration.location.length,
|
|
48
|
+
replacement: replacement,
|
|
49
|
+
node: declaration
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# What is left of the declaration: nothing when it only names methods, the
|
|
54
|
+
# definition when it wraps one. A definition among several arguments
|
|
55
|
+
# (`private attr_reader(:a), :b`) cannot be left behind on its own, so that
|
|
56
|
+
# declaration is not mutated.
|
|
57
|
+
def replacement_for(arguments)
|
|
58
|
+
return "" if arguments.none? { |argument| WRAPPED.include?(argument.class) }
|
|
59
|
+
|
|
60
|
+
arguments.first.slice if arguments.one?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def declarations_for(subject)
|
|
64
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
65
|
+
|
|
66
|
+
Evilution::AST::ClassBody.anchored_at(tree, subject.line_number).flat_map do |body|
|
|
67
|
+
body.declarations { |node| declaration?(node) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def declaration?(node)
|
|
72
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && DECLARATIONS.include?(node.name)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require_relative "../mutator"
|
|
4
4
|
|
|
5
5
|
# Mutation shapes shared across the operator families: replacing an
|
|
6
|
-
# expression with `nil`,
|
|
7
|
-
# of
|
|
6
|
+
# expression with `nil`, replacing an expression with the source of one of
|
|
7
|
+
# its children, and deleting one element of a list. All are built on
|
|
8
|
+
# Base#add_mutation, so the
|
|
8
9
|
# equivalent-mutant filter and the heredoc-span guards still apply.
|
|
9
10
|
#
|
|
10
11
|
# Unlike a bare add_mutation call, these skip rather than emit when the
|
|
@@ -30,6 +31,20 @@ module Evilution::Mutator::Primitives
|
|
|
30
31
|
replace_span(node: node, target: target, replacement: source_of(child))
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
# Delete the element at `index` from a list written in source order (the
|
|
35
|
+
# elements of an array or hash literal), attributing the mutation to the
|
|
36
|
+
# element. It goes with the separator after it; the last one goes with the
|
|
37
|
+
# separator before it, so a trailing comma stays where it was. The list
|
|
38
|
+
# must hold another element to take the separator from.
|
|
39
|
+
def delete_element(elements, index)
|
|
40
|
+
element = elements[index]
|
|
41
|
+
following = elements[index + 1]
|
|
42
|
+
offset = following ? element.location.start_offset : elements[index - 1].location.end_offset
|
|
43
|
+
stop = following ? following.location.start_offset : element.location.end_offset
|
|
44
|
+
|
|
45
|
+
add_mutation(offset:, length: stop - offset, replacement: "", node: element, skip_unparseable: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
33
48
|
def replace_span(node:, target:, replacement:)
|
|
34
49
|
return nil if target.nil?
|
|
35
50
|
|
|
@@ -45,6 +60,13 @@ module Evilution::Mutator::Primitives
|
|
|
45
60
|
)
|
|
46
61
|
end
|
|
47
62
|
|
|
63
|
+
# The arguments of a call, `super`, `yield` or the like, in source order.
|
|
64
|
+
# Prism gives nil, not an empty list, where there are none; this gives the
|
|
65
|
+
# empty list, so a reader can count or iterate without asking first.
|
|
66
|
+
def argument_nodes(node)
|
|
67
|
+
node.arguments ? node.arguments.arguments : []
|
|
68
|
+
end
|
|
69
|
+
|
|
48
70
|
def source_of(child)
|
|
49
71
|
location = child.location
|
|
50
72
|
byteslice_source(location.start_offset, location.length)
|