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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/class_body"
|
|
6
7
|
|
|
7
8
|
# Drop an alias declaration from a class or module body: `alias length size`
|
|
8
9
|
# and `alias_method :count, :size` are removed.
|
|
@@ -12,11 +13,11 @@ require_relative "../operator"
|
|
|
12
13
|
# asserts.
|
|
13
14
|
#
|
|
14
15
|
# Declarations sit in the class body, outside every method, where no subject
|
|
15
|
-
# reaches them. They are attributed to the
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# reaches them. They are attributed to the method anchoring the enclosing
|
|
17
|
+
# body (see Evilution::AST::ClassBody); a top-level alias has no body to be
|
|
18
|
+
# attributed to. An `alias_method` call inside a method is left to the
|
|
19
|
+
# generic call operators, and global-variable aliases (`alias $new $old`)
|
|
20
|
+
# are not method surface.
|
|
20
21
|
class Evilution::Mutator::Operator::AliasRemoval < Evilution::Mutator::Base
|
|
21
22
|
def call(subject, filter: nil)
|
|
22
23
|
@subject = subject
|
|
@@ -41,86 +42,15 @@ class Evilution::Mutator::Operator::AliasRemoval < Evilution::Mutator::Base
|
|
|
41
42
|
|
|
42
43
|
def declarations_for(subject)
|
|
43
44
|
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
44
|
-
collector = DeclarationCollector.new
|
|
45
|
-
collector.visit(tree)
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
Evilution::AST::ClassBody.anchored_at(tree, subject.line_number).flat_map do |body|
|
|
47
|
+
body.declarations { |node| declaration?(node) }
|
|
49
48
|
end
|
|
50
49
|
end
|
|
51
50
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
scope.compact_child_nodes.each { |child| finder.visit(child) }
|
|
55
|
-
finder.line
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Collects alias declarations written in a class, module or singleton class
|
|
59
|
-
# body, each paired with the innermost such scope. Method bodies are not
|
|
60
|
-
# searched, and a top-level alias has no scope to attribute it to.
|
|
61
|
-
class DeclarationCollector < Prism::Visitor
|
|
62
|
-
attr_reader :declarations
|
|
63
|
-
|
|
64
|
-
def initialize
|
|
65
|
-
super
|
|
66
|
-
@declarations = []
|
|
67
|
-
@scopes = []
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def visit_class_node(node)
|
|
71
|
-
within(node) { super }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def visit_module_node(node)
|
|
75
|
-
within(node) { super }
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def visit_singleton_class_node(node)
|
|
79
|
-
within(node) { super }
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def visit_def_node(_node); end
|
|
83
|
-
|
|
84
|
-
def visit_alias_method_node(node)
|
|
85
|
-
record(node)
|
|
86
|
-
super
|
|
87
|
-
end
|
|
51
|
+
def declaration?(node)
|
|
52
|
+
return true if node.is_a?(Prism::AliasMethodNode)
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
# there still belongs to the enclosing scope.
|
|
91
|
-
def visit_call_node(node)
|
|
92
|
-
record(node) if node.name == :alias_method && node.receiver.nil?
|
|
93
|
-
super
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def record(node)
|
|
99
|
-
@declarations << [node, @scopes.last] unless @scopes.empty?
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def within(scope)
|
|
103
|
-
@scopes.push(scope)
|
|
104
|
-
yield
|
|
105
|
-
ensure
|
|
106
|
-
@scopes.pop
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Finds the line of the first method written in a scope. Nested classes,
|
|
111
|
-
# modules and singleton classes are not searched: their methods are subjects
|
|
112
|
-
# of that inner scope.
|
|
113
|
-
class FirstMethodFinder < Prism::Visitor
|
|
114
|
-
attr_reader :line
|
|
115
|
-
|
|
116
|
-
def visit_class_node(_node); end
|
|
117
|
-
|
|
118
|
-
def visit_module_node(_node); end
|
|
119
|
-
|
|
120
|
-
def visit_singleton_class_node(_node); end
|
|
121
|
-
|
|
122
|
-
def visit_def_node(node)
|
|
123
|
-
@line = node.location.start_line if @line.nil?
|
|
124
|
-
end
|
|
54
|
+
node.is_a?(Prism::CallNode) && node.name == :alias_method && node.receiver.nil?
|
|
125
55
|
end
|
|
126
56
|
end
|
|
@@ -11,7 +11,7 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
|
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
13
|
def visit_call_node(node)
|
|
14
|
-
args = node
|
|
14
|
+
args = argument_nodes(node)
|
|
15
15
|
args.each_index { |i| emit_nil_substitution(node, args, i) } if mutable?(node, args)
|
|
16
16
|
|
|
17
17
|
super
|
|
@@ -30,7 +30,7 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def mutable?(node, args)
|
|
33
|
-
|
|
33
|
+
positional_only?(args) && node.name != :[]=
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def positional_only?(args)
|
|
@@ -12,7 +12,7 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
14
|
def visit_call_node(node)
|
|
15
|
-
args = node
|
|
15
|
+
args = argument_nodes(node)
|
|
16
16
|
args.each_index { |i| emit_argument_removal(node, args, i) } if mutable?(node, args)
|
|
17
17
|
|
|
18
18
|
super
|
|
@@ -31,7 +31,7 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def mutable?(node, args)
|
|
34
|
-
args
|
|
34
|
+
args.length >= 2 && positional_only?(args) && node.name != :[]= && !member_list?(node)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# DataStructMember owns the member list of `Data.define` / `Struct.new` and
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::ArrayLiteral < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
6
8
|
def visit_array_node(node)
|
|
7
9
|
if node.opening_loc && node.elements.any?
|
|
8
10
|
add_mutation(
|
|
@@ -18,8 +20,35 @@ class Evilution::Mutator::Operator::ArrayLiteral < Evilution::Mutator::Base
|
|
|
18
20
|
replacement: "nil",
|
|
19
21
|
node: node
|
|
20
22
|
)
|
|
23
|
+
|
|
24
|
+
mutate_delete_elements(node.elements)
|
|
25
|
+
mutate_promote_only_element(node)
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
super
|
|
24
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Delete each element in turn: a survivor means no example checks that
|
|
34
|
+
# element. A single element is left to the `[]` replacement. A heredoc
|
|
35
|
+
# element stays: its body sits after the closing bracket, so one cut cannot
|
|
36
|
+
# take both, and the cut that does not parse is dropped.
|
|
37
|
+
def mutate_delete_elements(elements)
|
|
38
|
+
return if elements.one?
|
|
39
|
+
|
|
40
|
+
elements.each_index { |index| delete_element(elements, index) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `[x]` becomes `x`: a survivor means the wrapping does not matter, as when
|
|
44
|
+
# the caller flattens or splats the value anyway. The words of `%w[a]` are
|
|
45
|
+
# not written as expressions and stay. An element that is not an expression
|
|
46
|
+
# on its own (`*x`, the bare keywords of `[a: 1]`) does not parse once
|
|
47
|
+
# promoted and is dropped, and so is a heredoc, whose body cannot follow
|
|
48
|
+
# the cut.
|
|
49
|
+
def mutate_promote_only_element(node)
|
|
50
|
+
return unless node.elements.one? && node.opening == "["
|
|
51
|
+
|
|
52
|
+
replace_span(node:, target: node, replacement: receiver_source(node.elements.first))
|
|
53
|
+
end
|
|
25
54
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/class_body"
|
|
7
|
+
|
|
8
|
+
# Mutate an attribute declaration in a class or module body two ways.
|
|
9
|
+
#
|
|
10
|
+
# Widening: `attr_reader :name` and `attr_writer :name` become
|
|
11
|
+
# `attr_accessor :name`. A survivor means no example checks that the missing
|
|
12
|
+
# half is missing: that the attribute cannot be written, or cannot be read.
|
|
13
|
+
#
|
|
14
|
+
# Removal: an `attr_reader`, `attr_writer` or `attr_accessor` declaration is
|
|
15
|
+
# dropped. A survivor means no example uses any of the methods it defines.
|
|
16
|
+
#
|
|
17
|
+
# Declarations sit in the class body, outside every method, where no subject
|
|
18
|
+
# reaches them. They are attributed to the method anchoring the enclosing
|
|
19
|
+
# body (see Evilution::AST::ClassBody); a top-level declaration has no body
|
|
20
|
+
# to be attributed to. A declaration wrapped in a visibility call
|
|
21
|
+
# (`private attr_reader :name`) is left alone: removing it would leave a bare
|
|
22
|
+
# `private` behind.
|
|
23
|
+
class Evilution::Mutator::Operator::AttrAccessorSwap < Evilution::Mutator::Base
|
|
24
|
+
WIDENED = "attr_accessor"
|
|
25
|
+
NARROW = %i[attr_reader attr_writer].freeze
|
|
26
|
+
DECLARATIONS = [*NARROW, :attr_accessor].freeze
|
|
27
|
+
|
|
28
|
+
def call(subject, filter: nil)
|
|
29
|
+
@subject = subject
|
|
30
|
+
@file_source = File.read(subject.file_path)
|
|
31
|
+
@mutations = []
|
|
32
|
+
@filter = filter
|
|
33
|
+
|
|
34
|
+
declarations_for(subject).each do |declaration|
|
|
35
|
+
widen(declaration) if NARROW.include?(declaration.name)
|
|
36
|
+
remove(declaration)
|
|
37
|
+
end
|
|
38
|
+
@mutations
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def widen(declaration)
|
|
44
|
+
name = declaration.message_loc
|
|
45
|
+
add_mutation(offset: name.start_offset, length: name.length, replacement: WIDENED, node: declaration)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove(declaration)
|
|
49
|
+
add_mutation(
|
|
50
|
+
offset: declaration.location.start_offset,
|
|
51
|
+
length: declaration.location.length,
|
|
52
|
+
replacement: "",
|
|
53
|
+
node: declaration
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def declarations_for(subject)
|
|
58
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
59
|
+
|
|
60
|
+
Evilution::AST::ClassBody.anchored_at(tree, subject.line_number).flat_map do |body|
|
|
61
|
+
body.declarations { |node| declaration?(node) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def declaration?(node)
|
|
66
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && DECLARATIONS.include?(node.name) && !node.arguments.nil?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -13,9 +13,13 @@ require_relative "../operator"
|
|
|
13
13
|
# (every statement of a body except the last) has no value to replace;
|
|
14
14
|
# deleting it is statement_deletion's job. A call used as the receiver of
|
|
15
15
|
# another call turns into `nil.foo`, a NoMethodError any test that reaches it
|
|
16
|
-
# kills; the outer call is nil-ified instead.
|
|
17
|
-
# (`
|
|
16
|
+
# kills; the outer call is nil-ified instead. The receiver of a write
|
|
17
|
+
# (`user.account.total += 1`, `cache[key] ||= value`) is skipped likewise. An
|
|
18
|
+
# attribute or index write (`a.b = c`, `a[i] = c`) is a side effect, not a
|
|
19
|
+
# value.
|
|
18
20
|
class Evilution::Mutator::Operator::CallToNil < Evilution::Mutator::Base
|
|
21
|
+
include Evilution::Mutator::ReceiverPositions
|
|
22
|
+
|
|
19
23
|
def call(subject, **)
|
|
20
24
|
@skipped_calls = Set.new
|
|
21
25
|
super
|
|
@@ -27,8 +31,13 @@ class Evilution::Mutator::Operator::CallToNil < Evilution::Mutator::Base
|
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def visit_call_node(node)
|
|
30
|
-
@skipped_calls.add(node.receiver)
|
|
31
34
|
mutate_to_nil(node) unless @skipped_calls.include?(node) || node.attribute_write?
|
|
32
35
|
super
|
|
33
36
|
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def receiver_found(node)
|
|
41
|
+
@skipped_calls.add(node)
|
|
42
|
+
end
|
|
34
43
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a complex literal (`5i`) with `0i`, `1i`, its two neighbours along
|
|
6
|
+
# the imaginary axis (`6i`, `4i`) and `nil`.
|
|
7
|
+
#
|
|
8
|
+
# The literal is replaced as a whole: IntegerLiteral and FloatLiteral leave
|
|
9
|
+
# its numeric part alone.
|
|
10
|
+
class Evilution::Mutator::Operator::ComplexLiteral < Evilution::Mutator::Base
|
|
11
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
12
|
+
|
|
13
|
+
def visit_imaginary_node(node)
|
|
14
|
+
replacements_for(node.numeric).each { |part| add_mutation_with_replacement(node, "#{part}i") }
|
|
15
|
+
add_mutation_with_replacement(node, "nil")
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Equal values are emitted once, the integer first: `1i` over `1.0i`.
|
|
23
|
+
def replacements_for(numeric)
|
|
24
|
+
part = numeric.value
|
|
25
|
+
[0, 1, *neighbours_of(numeric)].reject { |candidate| candidate == part }.uniq(&:to_r)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A rational part (`3ri`) gets none: its neighbours have no literal of
|
|
29
|
+
# their own in general.
|
|
30
|
+
def neighbours_of(numeric)
|
|
31
|
+
return [] if numeric.is_a?(Prism::RationalNode)
|
|
32
|
+
|
|
33
|
+
[numeric.value + 1, numeric.value - 1]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_mutation_with_replacement(node, replacement)
|
|
37
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement:, node:)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::CompoundAssignment < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
6
8
|
REPLACEMENTS = {
|
|
7
9
|
:+ => %i[- *],
|
|
8
10
|
:- => [:+],
|
|
@@ -37,6 +39,51 @@ class Evilution::Mutator::Operator::CompoundAssignment < Evilution::Mutator::Bas
|
|
|
37
39
|
super
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# `obj.count += 1`: the target is read through `count` and written
|
|
43
|
+
# through `count=`.
|
|
44
|
+
def visit_call_operator_write_node(node)
|
|
45
|
+
mutate_operator_write(node)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `counts[key] += 1`: read through `[]` and written through `[]=`.
|
|
50
|
+
def visit_index_operator_write_node(node)
|
|
51
|
+
mutate_operator_write(node)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `TOTAL += 1` is written outside any method and is a constant-write
|
|
56
|
+
# subject of its own, like the assignment `TOTAL = 1` before it.
|
|
57
|
+
def visit_constant_operator_write_node(node)
|
|
58
|
+
mutate_operator_write(node)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit_constant_path_operator_write_node(node)
|
|
63
|
+
mutate_operator_write(node)
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def visit_call_and_write_node(node)
|
|
68
|
+
mutate_logical_write(node, "||=")
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visit_call_or_write_node(node)
|
|
73
|
+
mutate_logical_write(node, "&&=")
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_index_and_write_node(node)
|
|
78
|
+
mutate_logical_write(node, "||=")
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_index_or_write_node(node)
|
|
83
|
+
mutate_logical_write(node, "&&=")
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
40
87
|
def visit_local_variable_and_write_node(node)
|
|
41
88
|
mutate_logical_write(node, "||=")
|
|
42
89
|
super
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Strip the namespace off a constant path: `Config::LIMIT` becomes `LIMIT`.
|
|
6
|
+
# A longer path is stripped at each level, so `App::Config::LIMIT` gives
|
|
7
|
+
# both `LIMIT` and `Config::LIMIT`.
|
|
8
|
+
#
|
|
9
|
+
# The bare name is looked up from where the code stands instead of inside
|
|
10
|
+
# the namespace. It either finds another constant or raises NameError; a
|
|
11
|
+
# survivor means the same constant is reachable without the qualifier, or
|
|
12
|
+
# that nothing depends on which one is found. A namespace given by an
|
|
13
|
+
# expression (`self.class::LIMIT`) is stripped too — there the survivor says
|
|
14
|
+
# no test overrides the constant in a subclass.
|
|
15
|
+
#
|
|
16
|
+
# A top-level path (`::LIMIT`) has no namespace to strip. The target of an
|
|
17
|
+
# operator write (`Config::LIMIT ||= x`) is left alone: bare, `LIMIT ||= x`
|
|
18
|
+
# is a dynamic constant assignment inside a method. Prism lets it through,
|
|
19
|
+
# where parse.y rejects it with a SyntaxError, so whether the mutant exists
|
|
20
|
+
# would depend on the parser the interpreter compiles with.
|
|
21
|
+
class Evilution::Mutator::Operator::ConstantNamespaceStrip < Evilution::Mutator::Base
|
|
22
|
+
def call(subject, **)
|
|
23
|
+
@write_targets = Set.new
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_constant_path_or_write_node(node)
|
|
28
|
+
@write_targets.add(node.target)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_constant_path_and_write_node(node)
|
|
33
|
+
@write_targets.add(node.target)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_constant_path_operator_write_node(node)
|
|
38
|
+
@write_targets.add(node.target)
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def visit_constant_path_node(node)
|
|
43
|
+
replace_span(node: node, target: node, replacement: name_source(node)) if strippable?(node)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def strippable?(node)
|
|
50
|
+
node.parent && !@write_targets.include?(node)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def name_source(node)
|
|
54
|
+
location = node.name_loc
|
|
55
|
+
byteslice_source(location.start_offset, location.length)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a constant reference with `nil`: `MAX` becomes `nil`, and a path
|
|
6
|
+
# such as `Config::MAX` goes as a whole.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means the constant's value never reaches an assertion — the
|
|
9
|
+
# tests would pass whatever it held.
|
|
10
|
+
#
|
|
11
|
+
# Four positions are skipped as noise. A constant used as the receiver of a
|
|
12
|
+
# call turns into `nil.new`, a NoMethodError any test that reaches it kills;
|
|
13
|
+
# so does the receiver of a write (`Counter.total += 1`, `CACHE[key] ||= v`).
|
|
14
|
+
# The namespace of a path would leave `nil::MAX`, a TypeError; the path is
|
|
15
|
+
# nil-ified instead. A constant in void statement position (every statement
|
|
16
|
+
# of a body except the last) has no value to replace; deleting it is
|
|
17
|
+
# statement_deletion's job. And the exception classes of a `rescue` clause
|
|
18
|
+
# belong to the rescue operators — `rescue nil` only fails once something is
|
|
19
|
+
# raised, and then with a TypeError of its own.
|
|
20
|
+
class Evilution::Mutator::Operator::ConstantReadToNil < Evilution::Mutator::Base
|
|
21
|
+
include Evilution::Mutator::ReceiverPositions
|
|
22
|
+
|
|
23
|
+
def call(subject, **)
|
|
24
|
+
@skipped_nodes = Set.new
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_statements_node(node)
|
|
29
|
+
@skipped_nodes.merge(node.body[...-1])
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_rescue_node(node)
|
|
34
|
+
@skipped_nodes.merge(node.exceptions)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_constant_path_node(node)
|
|
39
|
+
@skipped_nodes.add(node.parent)
|
|
40
|
+
nilify(node)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_constant_read_node(node)
|
|
45
|
+
nilify(node)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def receiver_found(node)
|
|
52
|
+
@skipped_nodes.add(node)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def nilify(node)
|
|
56
|
+
mutate_to_nil(node) unless @skipped_nodes.include?(node)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Assign `nil` to a constant instead of its value: `LIMIT = 10` becomes
|
|
6
|
+
# `LIMIT = nil`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means nothing the tests run depends on what the constant holds.
|
|
9
|
+
# The literal operators go on to change the value piece by piece; this one
|
|
10
|
+
# takes it away whole, whatever it is — a literal, a call, a lambda.
|
|
11
|
+
#
|
|
12
|
+
# It works on constant-write subjects, each being one assignment, and does
|
|
13
|
+
# not look inside the value: a constant assigned there (in the block of a
|
|
14
|
+
# `Class.new`) is a subject of its own. A constant already assigned `nil`
|
|
15
|
+
# has nothing to lose. An operator write (`LIMIT += 1`) is left to
|
|
16
|
+
# compound_assignment, which replaces the whole of it with `nil`.
|
|
17
|
+
#
|
|
18
|
+
# Code that read the constant while the file first loaded keeps the old
|
|
19
|
+
# value (see Subject), so only what reads it when called sees the mutant.
|
|
20
|
+
class Evilution::Mutator::Operator::ConstantWriteToNil < Evilution::Mutator::Base
|
|
21
|
+
def self.subject_kinds
|
|
22
|
+
%i[constant_write]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visit_constant_write_node(node)
|
|
26
|
+
mutate_to_nil(node, target: node.value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_constant_path_write_node(node)
|
|
30
|
+
mutate_to_nil(node, target: node.value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -38,7 +38,7 @@ class Evilution::Mutator::Operator::DataStructMember < Evilution::Mutator::Base
|
|
|
38
38
|
private
|
|
39
39
|
|
|
40
40
|
def mutate_members(node)
|
|
41
|
-
arguments = node
|
|
41
|
+
arguments = argument_nodes(node)
|
|
42
42
|
members = member_indexes(arguments)
|
|
43
43
|
# A lone member has no neighbour to swap with, and dropping it would leave
|
|
44
44
|
# `Struct.new()`, which raises on Rubies that require at least one member.
|
|
@@ -5,8 +5,8 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::ExplicitSuperMutation < Evilution::Mutator::Base
|
|
6
6
|
def visit_super_node(node)
|
|
7
7
|
replace_with_zsuper(node)
|
|
8
|
-
args = node
|
|
9
|
-
mutate_arguments(node, args)
|
|
8
|
+
args = argument_nodes(node)
|
|
9
|
+
mutate_arguments(node, args) unless args.empty?
|
|
10
10
|
|
|
11
11
|
super
|
|
12
12
|
end
|
|
@@ -3,27 +3,62 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::FloatLiteral < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
8
|
+
# NaN fails every comparison and equality; the infinities break arithmetic
|
|
9
|
+
# on bounds. None of them can be reached from `0.0`, `1.0` or `nil`.
|
|
10
|
+
SPECIAL_VALUES = ["Float::NAN", "Float::INFINITY", "-Float::INFINITY"].freeze
|
|
11
|
+
|
|
12
|
+
# A guard wraps the pattern of its branch: `in Float if x > 1.0`.
|
|
13
|
+
GUARDS = [Prism::IfNode, Prism::UnlessNode].freeze
|
|
14
|
+
|
|
6
15
|
def visit_float_node(node)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
add_mutation(
|
|
14
|
-
offset: node.location.start_offset,
|
|
15
|
-
length: node.location.length,
|
|
16
|
-
replacement: replacement,
|
|
17
|
-
node: node
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
add_mutation(
|
|
21
|
-
offset: node.location.start_offset,
|
|
22
|
-
length: node.location.length,
|
|
23
|
-
replacement: "nil",
|
|
24
|
-
node: node
|
|
25
|
-
)
|
|
16
|
+
add_mutation_with_replacement(node, node.value == 0.0 ? "1.0" : "0.0")
|
|
17
|
+
SPECIAL_VALUES.each { |value| add_mutation_with_replacement(node, value) } unless pattern_literals.include?(node)
|
|
18
|
+
add_mutation_with_replacement(node, "nil")
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
26
22
|
|
|
23
|
+
# A pattern takes literals, not expressions: `in -Float::INFINITY` and
|
|
24
|
+
# `in Float::NAN..2.0` do not parse. The floats of a pattern keep the
|
|
25
|
+
# literal replacements only. A guard and a pinned expression (`^(x + 1.0)`)
|
|
26
|
+
# are ordinary code.
|
|
27
|
+
def visit_in_node(node)
|
|
28
|
+
pattern = node.pattern
|
|
29
|
+
pattern = pattern.statements if GUARDS.include?(pattern.class)
|
|
30
|
+
mark_pattern_literals(pattern)
|
|
27
31
|
super
|
|
28
32
|
end
|
|
33
|
+
|
|
34
|
+
def visit_match_predicate_node(node)
|
|
35
|
+
mark_pattern_literals(node.pattern)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_match_required_node(node)
|
|
40
|
+
mark_pattern_literals(node.pattern)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A complex literal (`5i`) is replaced as a whole by ComplexLiteral. Its
|
|
45
|
+
# numeric part is not a literal of its own: `nili` is not a value.
|
|
46
|
+
def visit_imaginary_node(_node); end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def pattern_literals
|
|
51
|
+
@pattern_literals ||= Set.new.compare_by_identity
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mark_pattern_literals(node)
|
|
55
|
+
return if node.is_a?(Prism::PinnedExpressionNode)
|
|
56
|
+
|
|
57
|
+
pattern_literals.add(node) if node.is_a?(Prism::FloatNode)
|
|
58
|
+
node.compact_child_nodes.each { |child| mark_pattern_literals(child) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_mutation_with_replacement(node, replacement)
|
|
62
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement:, node:)
|
|
63
|
+
end
|
|
29
64
|
end
|