henitai 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/assets/schema/henitai.schema.json +123 -0
- data/exe/henitai +6 -0
- data/lib/henitai/arid_node_filter.rb +97 -0
- data/lib/henitai/cli.rb +341 -0
- data/lib/henitai/configuration.rb +132 -0
- data/lib/henitai/configuration_validator.rb +293 -0
- data/lib/henitai/coverage_bootstrapper.rb +75 -0
- data/lib/henitai/coverage_formatter.rb +112 -0
- data/lib/henitai/equivalence_detector.rb +85 -0
- data/lib/henitai/execution_engine.rb +174 -0
- data/lib/henitai/git_diff_analyzer.rb +82 -0
- data/lib/henitai/integration.rb +417 -0
- data/lib/henitai/mutant/activator.rb +234 -0
- data/lib/henitai/mutant.rb +68 -0
- data/lib/henitai/mutant_generator.rb +158 -0
- data/lib/henitai/mutant_history_store.rb +279 -0
- data/lib/henitai/operator.rb +96 -0
- data/lib/henitai/operators/arithmetic_operator.rb +46 -0
- data/lib/henitai/operators/array_declaration.rb +52 -0
- data/lib/henitai/operators/assignment_expression.rb +78 -0
- data/lib/henitai/operators/block_statement.rb +31 -0
- data/lib/henitai/operators/boolean_literal.rb +70 -0
- data/lib/henitai/operators/conditional_expression.rb +184 -0
- data/lib/henitai/operators/equality_operator.rb +41 -0
- data/lib/henitai/operators/hash_literal.rb +66 -0
- data/lib/henitai/operators/logical_operator.rb +84 -0
- data/lib/henitai/operators/method_expression.rb +56 -0
- data/lib/henitai/operators/pattern_match.rb +66 -0
- data/lib/henitai/operators/range_literal.rb +40 -0
- data/lib/henitai/operators/return_value.rb +105 -0
- data/lib/henitai/operators/safe_navigation.rb +34 -0
- data/lib/henitai/operators/string_literal.rb +64 -0
- data/lib/henitai/operators.rb +25 -0
- data/lib/henitai/parser_current.rb +7 -0
- data/lib/henitai/reporter.rb +432 -0
- data/lib/henitai/result.rb +170 -0
- data/lib/henitai/runner.rb +183 -0
- data/lib/henitai/sampling_strategy.rb +33 -0
- data/lib/henitai/scenario_execution_result.rb +71 -0
- data/lib/henitai/source_parser.rb +41 -0
- data/lib/henitai/static_filter.rb +186 -0
- data/lib/henitai/stillborn_filter.rb +34 -0
- data/lib/henitai/subject.rb +71 -0
- data/lib/henitai/subject_resolver.rb +232 -0
- data/lib/henitai/syntax_validator.rb +16 -0
- data/lib/henitai/test_prioritizer.rb +55 -0
- data/lib/henitai/unparse_helper.rb +24 -0
- data/lib/henitai/version.rb +5 -0
- data/lib/henitai/warning_silencer.rb +16 -0
- data/lib/henitai.rb +51 -0
- data/sig/configuration_validator.rbs +29 -0
- data/sig/henitai.rbs +594 -0
- data/sig/unparser.rbs +3 -0
- metadata +153 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Rewrites conditional expressions and loop guards.
|
|
8
|
+
class ConditionalExpression < Henitai::Operator
|
|
9
|
+
NODE_TYPES = %i[if case while until].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
case node.type
|
|
17
|
+
when :if
|
|
18
|
+
mutate_if(node, subject:)
|
|
19
|
+
when :case
|
|
20
|
+
mutate_case(node, subject:)
|
|
21
|
+
when :while, :until
|
|
22
|
+
mutate_loop(node, subject:)
|
|
23
|
+
else
|
|
24
|
+
[]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def mutate_if(node, subject:)
|
|
31
|
+
condition, then_branch, else_branch = node.children
|
|
32
|
+
mutations = condition_variants(node, subject:, condition:)
|
|
33
|
+
|
|
34
|
+
has_else_branch = then_branch && else_branch
|
|
35
|
+
append_removed_else_branch(mutations, node, subject:, then_branch:) if has_else_branch
|
|
36
|
+
append_removed_then_branch(mutations, node, subject:, else_branch:) if then_branch
|
|
37
|
+
|
|
38
|
+
mutations
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mutate_case(node, subject:)
|
|
42
|
+
condition = node.children.first
|
|
43
|
+
when_nodes, else_branch = case_children(node.children.drop(1))
|
|
44
|
+
mutations = condition_variants(node, subject:, condition:)
|
|
45
|
+
|
|
46
|
+
mutations.concat(case_when_mutants(subject:, node:, when_nodes:))
|
|
47
|
+
|
|
48
|
+
if else_branch
|
|
49
|
+
mutations << branch_mutant(
|
|
50
|
+
subject:,
|
|
51
|
+
node:,
|
|
52
|
+
replacement: else_branch,
|
|
53
|
+
description: "kept else branch"
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
mutations
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mutate_loop(node, subject:)
|
|
61
|
+
condition = node.children.first
|
|
62
|
+
condition_variants(node, subject:, condition:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def case_when_mutants(subject:, node:, when_nodes:)
|
|
66
|
+
when_nodes.each_with_index.map do |when_node, index|
|
|
67
|
+
branch_mutant(
|
|
68
|
+
subject:,
|
|
69
|
+
node:,
|
|
70
|
+
replacement: when_node.children.last || nil_node,
|
|
71
|
+
description: "kept when branch #{index + 1}"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def case_children(children)
|
|
77
|
+
return [[], nil] if children.empty?
|
|
78
|
+
|
|
79
|
+
if children.last&.type == :when
|
|
80
|
+
[children, nil]
|
|
81
|
+
else
|
|
82
|
+
[children[0...-1], children.last]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def condition_variants(node, subject:, condition:)
|
|
87
|
+
[
|
|
88
|
+
true_condition_mutant(node, subject:),
|
|
89
|
+
false_condition_mutant(node, subject:),
|
|
90
|
+
negated_condition_mutant(node, subject:, condition:)
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def true_condition_mutant(node, subject:)
|
|
95
|
+
condition_mutant(
|
|
96
|
+
node,
|
|
97
|
+
subject:,
|
|
98
|
+
replacement: true_node,
|
|
99
|
+
description: "replaced condition with true"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def false_condition_mutant(node, subject:)
|
|
104
|
+
condition_mutant(
|
|
105
|
+
node,
|
|
106
|
+
subject:,
|
|
107
|
+
replacement: false_node,
|
|
108
|
+
description: "replaced condition with false"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def negated_condition_mutant(node, subject:, condition:)
|
|
113
|
+
condition_mutant(
|
|
114
|
+
node,
|
|
115
|
+
subject:,
|
|
116
|
+
replacement: negate(condition),
|
|
117
|
+
description: "negated condition"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def condition_mutant(node, subject:, replacement:, description:)
|
|
122
|
+
branch_mutant(
|
|
123
|
+
subject:,
|
|
124
|
+
node:,
|
|
125
|
+
replacement: with_condition(node, replacement),
|
|
126
|
+
description:
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def append_removed_else_branch(mutations, node, subject:, then_branch:)
|
|
131
|
+
mutations << branch_mutant(
|
|
132
|
+
subject:,
|
|
133
|
+
node:,
|
|
134
|
+
replacement: then_branch,
|
|
135
|
+
description: "removed else branch"
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def append_removed_then_branch(mutations, node, subject:, else_branch:)
|
|
140
|
+
mutations << branch_mutant(
|
|
141
|
+
subject:,
|
|
142
|
+
node:,
|
|
143
|
+
replacement: else_branch || nil_node,
|
|
144
|
+
description: "removed then branch"
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def branch_mutant(subject:, node:, replacement:, description:)
|
|
149
|
+
build_mutant(
|
|
150
|
+
subject:,
|
|
151
|
+
original_node: node,
|
|
152
|
+
mutated_node: replacement,
|
|
153
|
+
description:
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def with_condition(node, replacement_condition)
|
|
158
|
+
children = node.children.dup
|
|
159
|
+
children[0] = replacement_condition
|
|
160
|
+
Parser::AST::Node.new(node.type, children)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def negate(node)
|
|
164
|
+
Parser::AST::Node.new(:send, [node, :!])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# rubocop:disable Lint/BooleanSymbol
|
|
168
|
+
def true_node
|
|
169
|
+
# Parser uses :true / :false node types, so the AST symbols are intentional.
|
|
170
|
+
Parser::AST::Node.new(:true, [])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def false_node
|
|
174
|
+
# Parser uses :true / :false node types, so the AST symbols are intentional.
|
|
175
|
+
Parser::AST::Node.new(:false, [])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def nil_node
|
|
179
|
+
Parser::AST::Node.new(:nil, [])
|
|
180
|
+
end
|
|
181
|
+
# rubocop:enable Lint/BooleanSymbol
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Replaces comparison operators with the other comparison operators.
|
|
8
|
+
class EqualityOperator < Henitai::Operator
|
|
9
|
+
NODE_TYPES = [:send].freeze
|
|
10
|
+
OPERATORS = %i[== != < > <= >= <=> eql? equal?].freeze
|
|
11
|
+
|
|
12
|
+
def self.node_types
|
|
13
|
+
NODE_TYPES
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mutate(node, subject:)
|
|
17
|
+
method_name = node.children[1]
|
|
18
|
+
return [] unless OPERATORS.include?(method_name)
|
|
19
|
+
|
|
20
|
+
OPERATORS.each_with_object([]) do |replacement, mutants|
|
|
21
|
+
next if replacement == method_name
|
|
22
|
+
|
|
23
|
+
mutants << build_mutant(
|
|
24
|
+
subject:,
|
|
25
|
+
original_node: node,
|
|
26
|
+
mutated_node: mutated_node(node, replacement),
|
|
27
|
+
description: "replaced #{method_name} with #{replacement}"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def mutated_node(node, replacement)
|
|
35
|
+
receiver = node.children[0]
|
|
36
|
+
arguments = node.children[2..] || []
|
|
37
|
+
Parser::AST::Node.new(:send, [receiver, replacement, *arguments])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Reduces hash literals by emptying them or mutating symbol keys.
|
|
8
|
+
class HashLiteral < Henitai::Operator
|
|
9
|
+
NODE_TYPES = [:hash].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
return [] if node.children.empty?
|
|
17
|
+
|
|
18
|
+
mutants = [empty_hash_mutant(node, subject:)]
|
|
19
|
+
mutants.concat(symbol_key_mutants(node, subject:))
|
|
20
|
+
mutants
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def empty_hash_mutant(node, subject:)
|
|
26
|
+
build_mutant(
|
|
27
|
+
subject:,
|
|
28
|
+
original_node: node,
|
|
29
|
+
mutated_node: Parser::AST::Node.new(:hash, []),
|
|
30
|
+
description: "replaced hash with empty hash"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def symbol_key_mutants(node, subject:)
|
|
35
|
+
node.children.each_with_index.filter_map do |pair, index|
|
|
36
|
+
next unless symbol_key_pair?(pair)
|
|
37
|
+
|
|
38
|
+
build_mutant(
|
|
39
|
+
subject:,
|
|
40
|
+
original_node: node,
|
|
41
|
+
mutated_node: mutated_hash(node, index),
|
|
42
|
+
description: "replaced symbol key with string key"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def symbol_key_pair?(node)
|
|
48
|
+
node.type == :pair && node.children.first&.type == :sym
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mutated_hash(node, pair_index)
|
|
52
|
+
mutated_pairs = node.children.each_with_index.map do |pair, index|
|
|
53
|
+
index == pair_index ? mutated_pair(pair) : pair
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Parser::AST::Node.new(:hash, mutated_pairs)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mutated_pair(pair)
|
|
60
|
+
key, value = pair.children
|
|
61
|
+
mutated_key = Parser::AST::Node.new(:str, [key.children.first.to_s])
|
|
62
|
+
Parser::AST::Node.new(:pair, [mutated_key, value])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Replaces short-circuit logical operators and collapses them to operands.
|
|
8
|
+
class LogicalOperator < Henitai::Operator
|
|
9
|
+
NODE_TYPES = %i[and or].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
case node.type
|
|
17
|
+
when :and
|
|
18
|
+
mutate_and(node, subject:)
|
|
19
|
+
when :or
|
|
20
|
+
mutate_or(node, subject:)
|
|
21
|
+
else
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def mutate_and(node, subject:)
|
|
29
|
+
mutate_binary(node, subject:, replacement_type: :or, from: "&&", to: "||")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mutate_or(node, subject:)
|
|
33
|
+
mutate_binary(node, subject:, replacement_type: :and, from: "||", to: "&&")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mutate_binary(node, subject:, replacement_type:, from:, to:)
|
|
37
|
+
[
|
|
38
|
+
build_replaced_operator_mutant(
|
|
39
|
+
node,
|
|
40
|
+
subject:,
|
|
41
|
+
replacement_type:,
|
|
42
|
+
from:,
|
|
43
|
+
to:
|
|
44
|
+
),
|
|
45
|
+
build_replaced_lhs_mutant(node, subject:, from:),
|
|
46
|
+
build_replaced_rhs_mutant(node, subject:, from:)
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_replaced_operator_mutant(node, subject:, replacement_type:, from:, to:)
|
|
51
|
+
lhs, rhs = node.children
|
|
52
|
+
|
|
53
|
+
build_mutant(
|
|
54
|
+
subject:,
|
|
55
|
+
original_node: node,
|
|
56
|
+
mutated_node: Parser::AST::Node.new(replacement_type, [lhs, rhs]),
|
|
57
|
+
description: "replaced #{from} with #{to}"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_replaced_lhs_mutant(node, subject:, from:)
|
|
62
|
+
lhs, = node.children
|
|
63
|
+
|
|
64
|
+
build_mutant(
|
|
65
|
+
subject:,
|
|
66
|
+
original_node: node,
|
|
67
|
+
mutated_node: lhs,
|
|
68
|
+
description: "replaced #{from} with lhs"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_replaced_rhs_mutant(node, subject:, from:)
|
|
73
|
+
_, rhs = node.children
|
|
74
|
+
|
|
75
|
+
build_mutant(
|
|
76
|
+
subject:,
|
|
77
|
+
original_node: node,
|
|
78
|
+
mutated_node: rhs,
|
|
79
|
+
description: "replaced #{from} with rhs"
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Replaces generic method call results with nil.
|
|
8
|
+
class MethodExpression < Henitai::Operator
|
|
9
|
+
NODE_TYPES = [:send].freeze
|
|
10
|
+
EXCLUDED_METHODS = %i[
|
|
11
|
+
+
|
|
12
|
+
-
|
|
13
|
+
*
|
|
14
|
+
/
|
|
15
|
+
**
|
|
16
|
+
%
|
|
17
|
+
==
|
|
18
|
+
!=
|
|
19
|
+
<
|
|
20
|
+
>
|
|
21
|
+
<=
|
|
22
|
+
>=
|
|
23
|
+
<=>
|
|
24
|
+
eql?
|
|
25
|
+
equal?
|
|
26
|
+
!
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
def self.node_types
|
|
30
|
+
NODE_TYPES
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mutate(node, subject:)
|
|
34
|
+
return [] unless node.type == :send
|
|
35
|
+
|
|
36
|
+
_receiver, method_name, *_arguments = node.children
|
|
37
|
+
return [] if excluded_method?(method_name)
|
|
38
|
+
|
|
39
|
+
[
|
|
40
|
+
build_mutant(
|
|
41
|
+
subject:,
|
|
42
|
+
original_node: node,
|
|
43
|
+
mutated_node: Parser::AST::Node.new(:nil, []),
|
|
44
|
+
description: "replaced method call with nil"
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def excluded_method?(method_name)
|
|
52
|
+
method_name.to_s.end_with?("=") || EXCLUDED_METHODS.include?(method_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Mutates case-match arms and removes pattern guards.
|
|
8
|
+
class PatternMatch < Henitai::Operator
|
|
9
|
+
NODE_TYPES = [:case_match].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
mutants = []
|
|
17
|
+
arm_number = 0
|
|
18
|
+
|
|
19
|
+
node.children.each_with_index do |child, index|
|
|
20
|
+
next unless child&.type == :in_pattern
|
|
21
|
+
|
|
22
|
+
arm_number += 1
|
|
23
|
+
mutants << remove_in_arm(node, subject:, index:, arm_number:)
|
|
24
|
+
|
|
25
|
+
guard = child.children[1]
|
|
26
|
+
next unless guard
|
|
27
|
+
|
|
28
|
+
mutants << remove_guard(node, subject:, index:, arm_number:, child:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
mutants
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def remove_in_arm(node, subject:, index:, arm_number:)
|
|
37
|
+
children = node.children.dup
|
|
38
|
+
children.delete_at(index)
|
|
39
|
+
|
|
40
|
+
build_mutant(
|
|
41
|
+
subject:,
|
|
42
|
+
original_node: node,
|
|
43
|
+
mutated_node: Parser::AST::Node.new(:case_match, children),
|
|
44
|
+
description: "removed in arm #{arm_number}"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove_guard(node, subject:, index:, arm_number:, child:)
|
|
49
|
+
children = node.children.dup
|
|
50
|
+
children[index] = mutated_arm(child)
|
|
51
|
+
|
|
52
|
+
build_mutant(
|
|
53
|
+
subject:,
|
|
54
|
+
original_node: node,
|
|
55
|
+
mutated_node: Parser::AST::Node.new(:case_match, children),
|
|
56
|
+
description: "removed pattern guard #{arm_number}"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mutated_arm(node)
|
|
61
|
+
pattern, _guard, body = node.children
|
|
62
|
+
Parser::AST::Node.new(:in_pattern, [pattern, nil, body])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Flips inclusive and exclusive range operators.
|
|
8
|
+
class RangeLiteral < Henitai::Operator
|
|
9
|
+
NODE_TYPES = %i[irange erange].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
case node.type
|
|
17
|
+
when :irange
|
|
18
|
+
mutate_range(node, subject:, replacement_type: :erange, from: "..", to: "...")
|
|
19
|
+
when :erange
|
|
20
|
+
mutate_range(node, subject:, replacement_type: :irange, from: "...", to: "..")
|
|
21
|
+
else
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def mutate_range(node, subject:, replacement_type:, from:, to:)
|
|
29
|
+
[
|
|
30
|
+
build_mutant(
|
|
31
|
+
subject:,
|
|
32
|
+
original_node: node,
|
|
33
|
+
mutated_node: Parser::AST::Node.new(replacement_type, node.children),
|
|
34
|
+
description: "replaced #{from} with #{to}"
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Replaces return values and implicit final expressions with neutral values.
|
|
8
|
+
class ReturnValue < Henitai::Operator
|
|
9
|
+
# Parser uses :true / :false node types, so the AST symbols are intentional.
|
|
10
|
+
# rubocop:disable Lint/BooleanSymbol
|
|
11
|
+
NODE_TYPES = %i[return send int float str dstr true false if case while until array hash].freeze
|
|
12
|
+
REPLACEMENT_FACTORIES = [
|
|
13
|
+
-> { Parser::AST::Node.new(:nil, []) },
|
|
14
|
+
-> { Parser::AST::Node.new(:int, [0]) },
|
|
15
|
+
-> { Parser::AST::Node.new(:false, []) }
|
|
16
|
+
].freeze
|
|
17
|
+
# rubocop:enable Lint/BooleanSymbol
|
|
18
|
+
|
|
19
|
+
def self.node_types
|
|
20
|
+
NODE_TYPES
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def mutate(node, subject:)
|
|
24
|
+
case node.type
|
|
25
|
+
when :return
|
|
26
|
+
mutate_explicit_return(node, subject:)
|
|
27
|
+
else
|
|
28
|
+
mutate_implicit_return(node, subject:)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def mutate_explicit_return(node, subject:)
|
|
35
|
+
expression = node.children.first
|
|
36
|
+
return [] unless expression
|
|
37
|
+
return [] if expression.type == :nil
|
|
38
|
+
|
|
39
|
+
replacement_nodes(expression).map do |replacement|
|
|
40
|
+
build_mutant(
|
|
41
|
+
subject:,
|
|
42
|
+
original_node: node,
|
|
43
|
+
mutated_node: Parser::AST::Node.new(:return, [replacement]),
|
|
44
|
+
description: "replaced return value with #{replacement_label(replacement)}"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def mutate_implicit_return(node, subject:)
|
|
50
|
+
return [] unless node == final_expression_node(subject&.ast_node)
|
|
51
|
+
|
|
52
|
+
replacement_nodes(node).map do |replacement|
|
|
53
|
+
build_mutant(
|
|
54
|
+
subject:,
|
|
55
|
+
original_node: node,
|
|
56
|
+
mutated_node: replacement,
|
|
57
|
+
description: "replaced final expression with #{replacement_label(replacement)}"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def final_expression_node(method_node)
|
|
63
|
+
return unless method_node
|
|
64
|
+
|
|
65
|
+
body = method_node.children.last
|
|
66
|
+
return body unless body&.type == :begin
|
|
67
|
+
|
|
68
|
+
body.children.rfind { |child| child.is_a?(Parser::AST::Node) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# rubocop:disable Lint/BooleanSymbol
|
|
72
|
+
def replacement_nodes(node)
|
|
73
|
+
nodes = REPLACEMENT_FACTORIES.map(&:call)
|
|
74
|
+
|
|
75
|
+
case node.type
|
|
76
|
+
when :true
|
|
77
|
+
nodes << Parser::AST::Node.new(:false, [])
|
|
78
|
+
when :false
|
|
79
|
+
nodes.delete_if { |replacement| replacement.type == :false }
|
|
80
|
+
nodes << Parser::AST::Node.new(:true, [])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
nodes
|
|
84
|
+
.uniq { |replacement| [replacement.type, replacement.children] }
|
|
85
|
+
.reject { |replacement| replacement == node }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def replacement_label(node)
|
|
89
|
+
case node.type
|
|
90
|
+
when :nil
|
|
91
|
+
"nil"
|
|
92
|
+
when :false
|
|
93
|
+
"false"
|
|
94
|
+
when :true
|
|
95
|
+
"true"
|
|
96
|
+
when :int
|
|
97
|
+
node.children.first.to_s
|
|
98
|
+
else
|
|
99
|
+
node.type.to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# rubocop:enable Lint/BooleanSymbol
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
module Operators
|
|
7
|
+
# Removes the nil guard from safe navigation calls.
|
|
8
|
+
class SafeNavigation < Henitai::Operator
|
|
9
|
+
NODE_TYPES = [:csend].freeze
|
|
10
|
+
|
|
11
|
+
def self.node_types
|
|
12
|
+
NODE_TYPES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutate(node, subject:)
|
|
16
|
+
[
|
|
17
|
+
build_mutant(
|
|
18
|
+
subject:,
|
|
19
|
+
original_node: node,
|
|
20
|
+
mutated_node: mutated_node(node),
|
|
21
|
+
description: "removed nil guard"
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def mutated_node(node)
|
|
29
|
+
receiver, method_name, *arguments = node.children
|
|
30
|
+
Parser::AST::Node.new(:send, [receiver, method_name, *arguments])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|