evilution 1.4.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 +70 -0
- data/CHANGELOG.md +50 -0
- data/README.md +64 -22
- data/docs/architecture.md +56 -12
- data/docs/isolation.md +82 -0
- data/lib/evilution/ast/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +20 -6
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +94 -31
- data/lib/evilution/ast/round_half_mode.rb +38 -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 +7 -7
- data/lib/evilution/cli/exit_guard.rb +26 -4
- data/lib/evilution/integration/base.rb +10 -2
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +59 -14
- data/lib/evilution/integration/loading/concern_redeclaration.rb +33 -14
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +3 -3
- data/lib/evilution/integration/loading/mutation_applier.rb +33 -6
- 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/isolation/fork.rb +82 -10
- data/lib/evilution/isolation/in_process.rb +79 -4
- data/lib/evilution/mcp/info_tool/status_glossary.rb +13 -2
- data/lib/evilution/mcp/info_tool.rb +3 -2
- data/lib/evilution/mutation.rb +18 -11
- data/lib/evilution/mutator/base.rb +27 -7
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +11 -81
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_removal.rb +2 -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/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 +1 -1
- 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/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_literal.rb +39 -9
- 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/operator_write_receiver_promotion.rb +38 -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/rescue_class_widening.rb +54 -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/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 +16 -24
- 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 +29 -2
- 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/runner/mutation_executor/strategy/parallel.rb +10 -0
- data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +20 -51
- data/lib/evilution/runner.rb +1 -1
- data/lib/evilution/subject.rb +18 -1
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +62 -0
- metadata +32 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/round_half_mode"
|
|
5
|
+
|
|
6
|
+
# Swap the tie-breaking mode of `round`: `amount.round(2, half: :even)`
|
|
7
|
+
# becomes `half: :up` and `half: :down`.
|
|
8
|
+
#
|
|
9
|
+
# The modes differ only for a value sitting exactly on a tie (`2.5`), so a
|
|
10
|
+
# survivor means no example rounds one — the case banker's rounding in money
|
|
11
|
+
# code exists for.
|
|
12
|
+
#
|
|
13
|
+
# Only a literal mode is swapped; one held in a variable is left alone.
|
|
14
|
+
# Integer, Float, Rational and BigDecimal all read `half:` the same way.
|
|
15
|
+
class Evilution::Mutator::Operator::RoundHalfModeSwap < Evilution::Mutator::Base
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
mode = Evilution::AST::RoundHalfMode.of(node)
|
|
18
|
+
swap(node, mode) if mode
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def swap(node, mode)
|
|
25
|
+
others = Evilution::AST::RoundHalfMode::MODES - [mode.unescaped.to_sym]
|
|
26
|
+
|
|
27
|
+
others.each do |other|
|
|
28
|
+
add_mutation(
|
|
29
|
+
offset: mode.location.start_offset,
|
|
30
|
+
length: mode.location.length,
|
|
31
|
+
replacement: other.inspect,
|
|
32
|
+
node: node
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/never_nil"
|
|
5
|
+
|
|
6
|
+
# Replace a plain call with safe navigation: `user.name` becomes
|
|
7
|
+
# `user&.name`, and the same for the `||=`, `&&=` and operator-write forms.
|
|
8
|
+
#
|
|
9
|
+
# The mutant only differs when the receiver is nil: the call is then skipped
|
|
10
|
+
# and the expression is nil, where the plain call raises NoMethodError or
|
|
11
|
+
# runs a method nil has (`nil.to_a` is `[]`). A survivor means no test
|
|
12
|
+
# reaches the call with a nil receiver and checks what comes of it.
|
|
13
|
+
#
|
|
14
|
+
# It touches nearly every call and survives wherever the receiver is never
|
|
15
|
+
# nil, so it is registered for the strict profile only. Receivers that can
|
|
16
|
+
# never be nil (`self`, literals other than `nil`, constants) are skipped,
|
|
17
|
+
# since the mutant is equivalent there.
|
|
18
|
+
class Evilution::Mutator::Operator::SafeNavigationInsertion < Evilution::Mutator::Base
|
|
19
|
+
# A constant could hold nil, but one that receives a call does not in
|
|
20
|
+
# practice.
|
|
21
|
+
CONSTANTS = [Prism::ConstantReadNode, Prism::ConstantPathNode].freeze
|
|
22
|
+
private_constant :CONSTANTS
|
|
23
|
+
|
|
24
|
+
def visit_call_node(node)
|
|
25
|
+
mutate_plain_call(node)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_call_or_write_node(node)
|
|
30
|
+
mutate_plain_call(node)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_call_and_write_node(node)
|
|
35
|
+
mutate_plain_call(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_call_operator_write_node(node)
|
|
40
|
+
mutate_plain_call(node)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def mutate_plain_call(node)
|
|
47
|
+
operator = node.call_operator_loc
|
|
48
|
+
return unless operator && operator.slice == "."
|
|
49
|
+
return if never_nil?(node.receiver)
|
|
50
|
+
|
|
51
|
+
add_mutation(offset: operator.start_offset, length: operator.length, replacement: "&.", node: node)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def never_nil?(receiver)
|
|
55
|
+
Evilution::AST::NeverNil.node?(receiver) || CONSTANTS.include?(receiver.class)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/never_nil"
|
|
4
5
|
|
|
5
6
|
# Replace safe navigation with a plain call: `user&.name` becomes
|
|
6
7
|
# `user.name`, and the same for the `||=`, `&&=` and operator-write forms.
|
|
@@ -12,29 +13,6 @@ require_relative "../operator"
|
|
|
12
13
|
# Receivers that can never be nil — `self` and literals — are skipped, since
|
|
13
14
|
# the mutant is equivalent there.
|
|
14
15
|
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
16
|
def visit_call_node(node)
|
|
39
17
|
mutate_safe_navigation(node)
|
|
40
18
|
super
|
|
@@ -59,7 +37,7 @@ class Evilution::Mutator::Operator::SafeNavigationRemoval < Evilution::Mutator::
|
|
|
59
37
|
|
|
60
38
|
def mutate_safe_navigation(node)
|
|
61
39
|
return unless node.safe_navigation?
|
|
62
|
-
return if
|
|
40
|
+
return if Evilution::AST::NeverNil.node?(node.receiver)
|
|
63
41
|
|
|
64
42
|
operator = node.call_operator_loc
|
|
65
43
|
add_mutation(offset: operator.start_offset, length: operator.length, replacement: ".", node: node)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Make a short-circuit operator evaluate both sides: `a && b` becomes
|
|
6
|
+
# `a & b`, `a || b` becomes `a | b`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example depends on the right side being skipped: it
|
|
9
|
+
# guards nothing (a nil receiver, a side effect), and both sides are plain
|
|
10
|
+
# booleans, since `&` and `|` give another value for anything else.
|
|
11
|
+
#
|
|
12
|
+
# `&` and `|` bind tighter than `&&` and `||`, so an operand that would
|
|
13
|
+
# regroup (`a == 1 && b` is not `a == 1 & b`) is parenthesized.
|
|
14
|
+
class Evilution::Mutator::Operator::ShortCircuitToEager < Evilution::Mutator::Base
|
|
15
|
+
def visit_and_node(node)
|
|
16
|
+
mutate_to_eager(node, "&")
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visit_or_node(node)
|
|
21
|
+
mutate_to_eager(node, "|")
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# An operand that only jumps (`a || return`) has no value to pass on; the
|
|
28
|
+
# result does not parse and is dropped.
|
|
29
|
+
def mutate_to_eager(node, operator)
|
|
30
|
+
replacement = "#{receiver_source(node.left)} #{operator} #{receiver_source(node.right)}"
|
|
31
|
+
|
|
32
|
+
replace_span(node:, target: node, replacement:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Take the wait out of a sleep whose duration is not a literal:
|
|
6
|
+
# `sleep delay` becomes `sleep 0`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means the pause decides nothing an example observes. A killed
|
|
9
|
+
# mutant means the suite depends on wall-clock time, which is worth knowing
|
|
10
|
+
# too.
|
|
11
|
+
#
|
|
12
|
+
# A literal duration (`sleep 5`, `sleep 0.5`) is left to the literal
|
|
13
|
+
# operators, which already take it to zero. Only `Kernel#sleep` is touched: a
|
|
14
|
+
# `sleep` sent to another receiver may be something else.
|
|
15
|
+
class Evilution::Mutator::Operator::SleepToZero < Evilution::Mutator::Base
|
|
16
|
+
LITERAL_DURATIONS = [Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode].freeze
|
|
17
|
+
KERNEL = %w[Kernel ::Kernel].freeze
|
|
18
|
+
|
|
19
|
+
def visit_call_node(node)
|
|
20
|
+
duration = duration_of(node)
|
|
21
|
+
add_mutation(offset: duration.location.start_offset, length: duration.location.length, replacement: "0", node:) if duration
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# The single argument of a `sleep` call, when it is one this operator
|
|
29
|
+
# replaces.
|
|
30
|
+
def duration_of(node)
|
|
31
|
+
return unless node.name == :sleep && kernel_call?(node) && node.arguments
|
|
32
|
+
|
|
33
|
+
arguments = node.arguments.arguments
|
|
34
|
+
return unless arguments.one?
|
|
35
|
+
|
|
36
|
+
duration = arguments.first
|
|
37
|
+
duration unless duration.is_a?(Prism::SplatNode) || LITERAL_DURATIONS.include?(duration.class)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def kernel_call?(node)
|
|
41
|
+
node.receiver.nil? || KERNEL.include?(node.receiver.slice)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -4,13 +4,15 @@ 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))
|
|
14
16
|
super
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -23,18 +25,18 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
23
25
|
# (`a, *b = x`) is still mutated: `a, b = x` parses and changes what `b`
|
|
24
26
|
# binds.
|
|
25
27
|
def visit_hash_pattern_node(node)
|
|
26
|
-
|
|
28
|
+
exempt_splats.add(node.rest) if node.rest
|
|
27
29
|
super
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def visit_array_pattern_node(node)
|
|
31
|
-
|
|
33
|
+
exempt_splats.add(node.rest) if node.rest
|
|
32
34
|
super
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def visit_find_pattern_node(node)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
exempt_splats.add(node.left)
|
|
39
|
+
exempt_splats.add(node.right)
|
|
38
40
|
super
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -42,22 +44,17 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
42
44
|
# `k: v` or another `**splat` precedes a `**opts` splat in the same call,
|
|
43
45
|
# demoting `**opts` to bare `opts` puts a positional after a keyword
|
|
44
46
|
# argument and Ruby rejects it (`bar(k: v, opts)` and `bar(**a, opts)` are
|
|
45
|
-
# syntax errors)
|
|
47
|
+
# syntax errors), so such splats are skipped.
|
|
46
48
|
# A splat that comes first (`bar(**opts, k: v)`) is still safe —
|
|
47
49
|
# positional-before-keyword is fine.
|
|
48
50
|
def visit_keyword_hash_node(node)
|
|
49
|
-
node.elements.drop(1).
|
|
50
|
-
kwarg_preceded_splats.add(el) if el.is_a?(Prism::AssocSplatNode)
|
|
51
|
-
end
|
|
52
|
-
|
|
51
|
+
exempt_splats.merge(node.elements.drop(1).grep(Prism::AssocSplatNode))
|
|
53
52
|
super
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def visit_assoc_splat_node(node)
|
|
57
56
|
return super if node.value.nil?
|
|
58
|
-
return super if
|
|
59
|
-
return super if kwarg_preceded_splats.include?(node)
|
|
60
|
-
return super if pattern_rests.include?(node)
|
|
57
|
+
return super if exempt_splats.include?(node)
|
|
61
58
|
|
|
62
59
|
mutate_remove_double_splat(node)
|
|
63
60
|
|
|
@@ -66,16 +63,11 @@ class Evilution::Mutator::Operator::SplatOperator < Evilution::Mutator::Base
|
|
|
66
63
|
|
|
67
64
|
private
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@kwarg_preceded_splats ||= Set.new.compare_by_identity
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def pattern_rests
|
|
78
|
-
@pattern_rests ||= 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
|
|
79
71
|
end
|
|
80
72
|
|
|
81
73
|
def mutate_remove_splat(node)
|
|
@@ -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
|