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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +70 -0
  3. data/CHANGELOG.md +50 -0
  4. data/README.md +64 -22
  5. data/docs/architecture.md +56 -12
  6. data/docs/isolation.md +82 -0
  7. data/lib/evilution/ast/class_body.rb +101 -0
  8. data/lib/evilution/ast/included_block.rb +20 -6
  9. data/lib/evilution/ast/never_nil.rb +36 -0
  10. data/lib/evilution/ast/parser.rb +94 -31
  11. data/lib/evilution/ast/round_half_mode.rb +38 -0
  12. data/lib/evilution/ast/source_surgeon.rb +2 -3
  13. data/lib/evilution/ast/syntax_check.rb +56 -0
  14. data/lib/evilution/ast/uncovered_code.rb +7 -7
  15. data/lib/evilution/cli/exit_guard.rb +26 -4
  16. data/lib/evilution/integration/base.rb +10 -2
  17. data/lib/evilution/integration/loading/body_call_neutralizer.rb +59 -14
  18. data/lib/evilution/integration/loading/concern_redeclaration.rb +33 -14
  19. data/lib/evilution/integration/loading/concern_state_cleaner.rb +3 -3
  20. data/lib/evilution/integration/loading/mutation_applier.rb +33 -6
  21. data/lib/evilution/integration/loading/own_methods.rb +87 -0
  22. data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
  23. data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
  24. data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
  25. data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
  26. data/lib/evilution/isolation/fork.rb +82 -10
  27. data/lib/evilution/isolation/in_process.rb +79 -4
  28. data/lib/evilution/mcp/info_tool/status_glossary.rb +13 -2
  29. data/lib/evilution/mcp/info_tool.rb +3 -2
  30. data/lib/evilution/mutation.rb +18 -11
  31. data/lib/evilution/mutator/base.rb +27 -7
  32. data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
  33. data/lib/evilution/mutator/operator/alias_removal.rb +11 -81
  34. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
  35. data/lib/evilution/mutator/operator/argument_removal.rb +2 -2
  36. data/lib/evilution/mutator/operator/array_literal.rb +29 -0
  37. data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
  38. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
  39. data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
  40. data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
  41. data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
  42. data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
  43. data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
  44. data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -0
  45. data/lib/evilution/mutator/operator/data_struct_member.rb +1 -1
  46. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +2 -2
  47. data/lib/evilution/mutator/operator/float_literal.rb +54 -19
  48. data/lib/evilution/mutator/operator/freeze_removal.rb +86 -0
  49. data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
  50. data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
  51. data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
  52. data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
  53. data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
  54. data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
  55. data/lib/evilution/mutator/operator/memoization_break.rb +18 -0
  56. data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
  57. data/lib/evilution/mutator/operator/nil_replacement.rb +2 -0
  58. data/lib/evilution/mutator/operator/operator_write_receiver_promotion.rb +38 -0
  59. data/lib/evilution/mutator/operator/range_replacement.rb +2 -0
  60. data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
  61. data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
  62. data/lib/evilution/mutator/operator/rescue_class_widening.rb +54 -0
  63. data/lib/evilution/mutator/operator/round_half_mode_swap.rb +36 -0
  64. data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
  65. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
  66. data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
  67. data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
  68. data/lib/evilution/mutator/operator/splat_operator.rb +16 -24
  69. data/lib/evilution/mutator/operator/string_literal.rb +29 -0
  70. data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
  71. data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
  72. data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
  73. data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
  74. data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
  75. data/lib/evilution/mutator/primitives.rb +24 -2
  76. data/lib/evilution/mutator/receiver_positions.rb +79 -0
  77. data/lib/evilution/mutator/registry.rb +29 -2
  78. data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
  79. data/lib/evilution/parallel/work_queue/channel.rb +58 -5
  80. data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
  81. data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
  82. data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
  83. data/lib/evilution/parallel/work_queue/worker.rb +13 -4
  84. data/lib/evilution/parallel/work_queue.rb +15 -9
  85. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +10 -0
  86. data/lib/evilution/runner/subject_pipeline/target.rb +86 -0
  87. data/lib/evilution/runner/subject_pipeline.rb +20 -51
  88. data/lib/evilution/runner.rb +1 -1
  89. data/lib/evilution/subject.rb +18 -1
  90. data/lib/evilution/version.rb +1 -1
  91. data/lib/evilution.rb +62 -0
  92. 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 first method of the innermost
16
- # enclosing class, module or `class << self`, the way DataStructMember and
17
- # MixinRemoval attribute theirs. An `alias_method` call inside a method is
18
- # left to the generic call operators, and global-variable aliases
19
- # (`alias $new $old`) are not method surface.
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
- collector.declarations.filter_map do |declaration, scope|
48
- declaration if first_method_line(scope) == subject.line_number
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 first_method_line(scope)
53
- finder = FirstMethodFinder.new
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
- # Blocks in the body (`included do ... end`) are searched too: an alias
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.arguments&.arguments
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
- args && args.length >= 1 && positional_only?(args) && node.name != :[]=
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.arguments&.arguments
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 && args.length >= 2 && positional_only?(args) && node.name != :[]= && !member_list?(node)
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
@@ -3,6 +3,8 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::BooleanLiteralReplacement < Evilution::Mutator::Base
6
+ extend Evilution::Mutator::ConstantValueSubjects
7
+
6
8
  def visit_true_node(node)
7
9
  add_mutation(
8
10
  offset: node.location.start_offset,
@@ -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. An attribute or index write
17
- # (`a.b = c`, `a[i] = c`) is a side effect, not a value.
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.arguments ? node.arguments.arguments : []
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.arguments&.arguments
9
- mutate_arguments(node, args) if args && !args.empty?
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
- replacement = case node.value
8
- when 0.0 then "1.0"
9
- when 1.0 then "0.0"
10
- else "0.0"
11
- end
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