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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +182 -0
  5. data/assets/schema/henitai.schema.json +123 -0
  6. data/exe/henitai +6 -0
  7. data/lib/henitai/arid_node_filter.rb +97 -0
  8. data/lib/henitai/cli.rb +341 -0
  9. data/lib/henitai/configuration.rb +132 -0
  10. data/lib/henitai/configuration_validator.rb +293 -0
  11. data/lib/henitai/coverage_bootstrapper.rb +75 -0
  12. data/lib/henitai/coverage_formatter.rb +112 -0
  13. data/lib/henitai/equivalence_detector.rb +85 -0
  14. data/lib/henitai/execution_engine.rb +174 -0
  15. data/lib/henitai/git_diff_analyzer.rb +82 -0
  16. data/lib/henitai/integration.rb +417 -0
  17. data/lib/henitai/mutant/activator.rb +234 -0
  18. data/lib/henitai/mutant.rb +68 -0
  19. data/lib/henitai/mutant_generator.rb +158 -0
  20. data/lib/henitai/mutant_history_store.rb +279 -0
  21. data/lib/henitai/operator.rb +96 -0
  22. data/lib/henitai/operators/arithmetic_operator.rb +46 -0
  23. data/lib/henitai/operators/array_declaration.rb +52 -0
  24. data/lib/henitai/operators/assignment_expression.rb +78 -0
  25. data/lib/henitai/operators/block_statement.rb +31 -0
  26. data/lib/henitai/operators/boolean_literal.rb +70 -0
  27. data/lib/henitai/operators/conditional_expression.rb +184 -0
  28. data/lib/henitai/operators/equality_operator.rb +41 -0
  29. data/lib/henitai/operators/hash_literal.rb +66 -0
  30. data/lib/henitai/operators/logical_operator.rb +84 -0
  31. data/lib/henitai/operators/method_expression.rb +56 -0
  32. data/lib/henitai/operators/pattern_match.rb +66 -0
  33. data/lib/henitai/operators/range_literal.rb +40 -0
  34. data/lib/henitai/operators/return_value.rb +105 -0
  35. data/lib/henitai/operators/safe_navigation.rb +34 -0
  36. data/lib/henitai/operators/string_literal.rb +64 -0
  37. data/lib/henitai/operators.rb +25 -0
  38. data/lib/henitai/parser_current.rb +7 -0
  39. data/lib/henitai/reporter.rb +432 -0
  40. data/lib/henitai/result.rb +170 -0
  41. data/lib/henitai/runner.rb +183 -0
  42. data/lib/henitai/sampling_strategy.rb +33 -0
  43. data/lib/henitai/scenario_execution_result.rb +71 -0
  44. data/lib/henitai/source_parser.rb +41 -0
  45. data/lib/henitai/static_filter.rb +186 -0
  46. data/lib/henitai/stillborn_filter.rb +34 -0
  47. data/lib/henitai/subject.rb +71 -0
  48. data/lib/henitai/subject_resolver.rb +232 -0
  49. data/lib/henitai/syntax_validator.rb +16 -0
  50. data/lib/henitai/test_prioritizer.rb +55 -0
  51. data/lib/henitai/unparse_helper.rb +24 -0
  52. data/lib/henitai/version.rb +5 -0
  53. data/lib/henitai/warning_silencer.rb +16 -0
  54. data/lib/henitai.rb +51 -0
  55. data/sig/configuration_validator.rbs +29 -0
  56. data/sig/henitai.rbs +594 -0
  57. data/sig/unparser.rbs +3 -0
  58. 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