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,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "digest"
5
+ require "fileutils"
6
+ require "json"
7
+ require "sqlite3"
8
+ require "time"
9
+ require "unparser"
10
+
11
+ module Henitai
12
+ # Persists mutant outcomes across runs in a lightweight SQLite database.
13
+ # rubocop:disable Metrics/ClassLength
14
+ class MutantHistoryStore
15
+ RUNS_TABLE_SQL = <<~SQL
16
+ CREATE TABLE IF NOT EXISTS runs (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ version TEXT NOT NULL,
19
+ recorded_at TEXT NOT NULL,
20
+ mutation_score REAL,
21
+ mutation_score_indicator REAL,
22
+ equivalence_uncertainty TEXT,
23
+ total_mutants INTEGER NOT NULL,
24
+ killed_mutants INTEGER NOT NULL,
25
+ survived_mutants INTEGER NOT NULL,
26
+ timeout_mutants INTEGER NOT NULL,
27
+ equivalent_mutants INTEGER NOT NULL
28
+ );
29
+ SQL
30
+
31
+ MUTANTS_TABLE_SQL = <<~SQL
32
+ CREATE TABLE IF NOT EXISTS mutants (
33
+ mutant_id TEXT PRIMARY KEY,
34
+ first_seen_version TEXT NOT NULL,
35
+ first_seen_at TEXT NOT NULL,
36
+ last_seen_version TEXT NOT NULL,
37
+ last_seen_at TEXT NOT NULL,
38
+ current_status TEXT NOT NULL,
39
+ status_history TEXT NOT NULL,
40
+ days_alive INTEGER NOT NULL
41
+ );
42
+ SQL
43
+
44
+ INSERT_RUN_SQL = <<~SQL
45
+ INSERT INTO runs (
46
+ version,
47
+ recorded_at,
48
+ mutation_score,
49
+ mutation_score_indicator,
50
+ equivalence_uncertainty,
51
+ total_mutants,
52
+ killed_mutants,
53
+ survived_mutants,
54
+ timeout_mutants,
55
+ equivalent_mutants
56
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
57
+ SQL
58
+
59
+ UPSERT_MUTANT_SQL = <<~SQL
60
+ INSERT INTO mutants (
61
+ mutant_id,
62
+ first_seen_version,
63
+ first_seen_at,
64
+ last_seen_version,
65
+ last_seen_at,
66
+ current_status,
67
+ status_history,
68
+ days_alive
69
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
70
+ ON CONFLICT(mutant_id) DO UPDATE SET
71
+ last_seen_version = excluded.last_seen_version,
72
+ last_seen_at = excluded.last_seen_at,
73
+ current_status = excluded.current_status,
74
+ status_history = excluded.status_history,
75
+ days_alive = excluded.days_alive
76
+ SQL
77
+
78
+ def initialize(path:)
79
+ @path = path
80
+ end
81
+
82
+ attr_reader :path
83
+
84
+ def record(result, version:, recorded_at: Time.now.utc)
85
+ FileUtils.mkdir_p(File.dirname(path))
86
+
87
+ with_database do |db|
88
+ ensure_schema(db)
89
+ db.transaction do
90
+ insert_run(db, result, version, recorded_at)
91
+ Array(result.mutants).each do |mutant|
92
+ upsert_mutant(db, mutant, version, recorded_at)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def trend_report
99
+ with_database do |db|
100
+ ensure_schema(db)
101
+ {
102
+ generatedAt: Time.now.utc.iso8601,
103
+ runs: load_runs(db),
104
+ mutants: load_mutants(db)
105
+ }
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def with_database
112
+ db = SQLite3::Database.new(path)
113
+ db.results_as_hash = true
114
+ yield db
115
+ ensure
116
+ db&.close
117
+ end
118
+
119
+ def ensure_schema(db)
120
+ db.execute_batch(RUNS_TABLE_SQL)
121
+ db.execute_batch(MUTANTS_TABLE_SQL)
122
+ end
123
+
124
+ def insert_run(db, result, version, recorded_at)
125
+ db.execute(INSERT_RUN_SQL, insert_run_bindings(result, version, recorded_at))
126
+ end
127
+
128
+ def upsert_mutant(db, mutant, version, recorded_at)
129
+ db.execute(
130
+ UPSERT_MUTANT_SQL,
131
+ upsert_mutant_bindings(mutant_history_data(db, mutant, version, recorded_at))
132
+ )
133
+ end
134
+
135
+ def count_mutants(mutants)
136
+ mutants.each_with_object(Hash.new(0)) do |mutant, counts|
137
+ counts[:total] += 1
138
+ counts[mutant.status] += 1
139
+ end
140
+ end
141
+
142
+ def stable_mutant_id(mutant)
143
+ Digest::SHA256.hexdigest(
144
+ [
145
+ mutant.subject.expression,
146
+ mutant.operator,
147
+ mutant.description,
148
+ mutant.location[:file],
149
+ mutant.location[:start_line],
150
+ mutant.location[:end_line],
151
+ mutant.location[:start_col],
152
+ mutant.location[:end_col],
153
+ mutation_signature(mutant)
154
+ ].join("\0")
155
+ )
156
+ end
157
+
158
+ def mutation_signature(mutant)
159
+ Unparser.unparse(mutant.mutated_node)
160
+ rescue StandardError
161
+ mutant.mutated_node.class.name
162
+ end
163
+
164
+ def mutation_history_entry(mutant, version, recorded_at)
165
+ {
166
+ version: version,
167
+ status: mutant.status.to_s,
168
+ recordedAt: recorded_at.iso8601
169
+ }
170
+ end
171
+
172
+ def mutant_history_data(db, mutant, version, recorded_at)
173
+ mutant_id = stable_mutant_id(mutant)
174
+ existing = existing_mutant_row(db, mutant_id)
175
+ history = existing_status_history(existing)
176
+ history << mutation_history_entry(mutant, version, recorded_at)
177
+ first_seen = first_seen_metadata(existing, version, recorded_at)
178
+
179
+ {
180
+ mutant_id: mutant_id,
181
+ first_seen_version: first_seen[:version],
182
+ first_seen_at: first_seen[:at],
183
+ version: version,
184
+ recorded_at: recorded_at,
185
+ mutant: mutant,
186
+ history: history,
187
+ days_alive: days_alive_since(first_seen[:at], recorded_at)
188
+ }
189
+ end
190
+
191
+ def existing_mutant_row(db, mutant_id)
192
+ db.get_first_row(
193
+ "SELECT * FROM mutants WHERE mutant_id = ?",
194
+ mutant_id
195
+ )
196
+ end
197
+
198
+ def existing_status_history(existing)
199
+ return [] unless existing
200
+
201
+ JSON.parse(existing["status_history"], symbolize_names: true)
202
+ end
203
+
204
+ def first_seen_metadata(existing, version, recorded_at)
205
+ {
206
+ version: existing ? existing["first_seen_version"] : version,
207
+ at: existing ? existing["first_seen_at"] : recorded_at.iso8601
208
+ }
209
+ end
210
+
211
+ def days_alive_since(first_seen_at, recorded_at)
212
+ first_seen = Time.iso8601(first_seen_at)
213
+ [(recorded_at.to_date - first_seen.to_date).to_i, 0].max
214
+ end
215
+
216
+ def load_runs(db)
217
+ db.execute("SELECT * FROM runs ORDER BY recorded_at").map do |row|
218
+ {
219
+ version: row["version"],
220
+ recordedAt: row["recorded_at"],
221
+ mutationScore: row["mutation_score"],
222
+ mutationScoreIndicator: row["mutation_score_indicator"],
223
+ equivalenceUncertainty: row["equivalence_uncertainty"],
224
+ totalMutants: row["total_mutants"],
225
+ killedMutants: row["killed_mutants"],
226
+ survivedMutants: row["survived_mutants"],
227
+ timeoutMutants: row["timeout_mutants"],
228
+ equivalentMutants: row["equivalent_mutants"]
229
+ }
230
+ end
231
+ end
232
+
233
+ def load_mutants(db)
234
+ db.execute("SELECT * FROM mutants ORDER BY first_seen_at, mutant_id").map do |row|
235
+ {
236
+ mutantId: row["mutant_id"],
237
+ firstSeenVersion: row["first_seen_version"],
238
+ firstSeenAt: row["first_seen_at"],
239
+ lastSeenVersion: row["last_seen_version"],
240
+ lastSeenAt: row["last_seen_at"],
241
+ currentStatus: row["current_status"],
242
+ daysAlive: row["days_alive"],
243
+ statusHistory: JSON.parse(row["status_history"], symbolize_names: true)
244
+ }
245
+ end
246
+ end
247
+
248
+ def insert_run_bindings(result, version, recorded_at)
249
+ summary = result.scoring_summary
250
+ counts = count_mutants(Array(result.mutants))
251
+ [
252
+ version,
253
+ recorded_at.iso8601,
254
+ summary[:mutation_score],
255
+ summary[:mutation_score_indicator],
256
+ summary[:equivalence_uncertainty],
257
+ counts[:total],
258
+ counts[:killed],
259
+ counts[:survived],
260
+ counts[:timeout],
261
+ counts[:equivalent]
262
+ ]
263
+ end
264
+
265
+ def upsert_mutant_bindings(data)
266
+ [
267
+ data.fetch(:mutant_id),
268
+ data.fetch(:first_seen_version),
269
+ data.fetch(:first_seen_at),
270
+ data.fetch(:version),
271
+ data.fetch(:recorded_at).iso8601,
272
+ data.fetch(:mutant).status.to_s,
273
+ JSON.generate(data.fetch(:history)),
274
+ data.fetch(:days_alive)
275
+ ]
276
+ end
277
+ end
278
+ end
279
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Base class for all mutation operators.
5
+ #
6
+ # An Operator receives an AST node and produces zero or more Mutant objects.
7
+ # Operator names follow the Stryker-compatible naming convention so that the
8
+ # JSON report is compatible with stryker-dashboard filters and HTML reports.
9
+ #
10
+ # Built-in operators (light set):
11
+ # ArithmeticOperator, EqualityOperator, LogicalOperator, BooleanLiteral,
12
+ # ConditionalExpression, StringLiteral, ReturnValue
13
+ #
14
+ # Additional operators (full set):
15
+ # ArrayDeclaration, HashLiteral, RangeLiteral, SafeNavigation,
16
+ # PatternMatch, BlockStatement, MethodExpression, AssignmentExpression
17
+ #
18
+ # Each operator subclass must implement:
19
+ # - .node_types → Array<Symbol> AST node types this operator handles
20
+ # - #mutate(node, subject:) → Array<Mutant>
21
+ class Operator
22
+ LIGHT_SET = %w[
23
+ ArithmeticOperator
24
+ EqualityOperator
25
+ LogicalOperator
26
+ BooleanLiteral
27
+ ConditionalExpression
28
+ StringLiteral
29
+ ReturnValue
30
+ ].freeze
31
+
32
+ FULL_SET = (LIGHT_SET + %w[
33
+ ArrayDeclaration
34
+ HashLiteral
35
+ RangeLiteral
36
+ SafeNavigation
37
+ PatternMatch
38
+ BlockStatement
39
+ MethodExpression
40
+ AssignmentExpression
41
+ ]).freeze
42
+
43
+ # @param set [Symbol] :light or :full
44
+ # @return [Array<Operator>] operator instances for the given set
45
+ def self.for_set(set)
46
+ names = set.to_sym == :full ? FULL_SET : LIGHT_SET
47
+ names.map { |name| Henitai::Operators.const_get(name).new }
48
+ end
49
+
50
+ # Subclasses must declare which AST node types they handle.
51
+ def self.node_types
52
+ raise NotImplementedError, "#{name}.node_types must be defined"
53
+ end
54
+
55
+ # @param node [Parser::AST::Node]
56
+ # @param subject [Subject]
57
+ # @return [Array<Mutant>]
58
+ def mutate(node, subject:)
59
+ raise NotImplementedError, "#{self.class}#mutate must be implemented"
60
+ end
61
+
62
+ # Operator name as used in the Stryker JSON schema.
63
+ def name
64
+ self.class.name.split("::").last
65
+ end
66
+
67
+ private
68
+
69
+ def build_mutant(subject:, original_node:, mutated_node:, description:)
70
+ loc = node_location(original_node)
71
+ Mutant.new(
72
+ subject:,
73
+ operator: name,
74
+ nodes: {
75
+ original: original_node,
76
+ mutated: mutated_node
77
+ },
78
+ description:,
79
+ location: loc
80
+ )
81
+ end
82
+
83
+ def node_location(node)
84
+ exp = node.location.expression
85
+ return {} unless exp
86
+
87
+ {
88
+ file: exp.source_buffer.name,
89
+ start_line: exp.line,
90
+ end_line: exp.last_line,
91
+ start_col: exp.column,
92
+ end_col: exp.last_column
93
+ }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser_current"
4
+
5
+ module Henitai
6
+ module Operators
7
+ # Replaces arithmetic operators with their mutation counterparts.
8
+ class ArithmeticOperator < Henitai::Operator
9
+ NODE_TYPES = [:send].freeze
10
+ MUTATION_MATRIX = {
11
+ :+ => :-,
12
+ :- => :+,
13
+ :* => :/,
14
+ :/ => :*,
15
+ :** => :*,
16
+ :% => :*
17
+ }.freeze
18
+
19
+ def self.node_types
20
+ NODE_TYPES
21
+ end
22
+
23
+ def mutate(node, subject:)
24
+ replacement = MUTATION_MATRIX[node.children[1]]
25
+ return [] unless replacement
26
+
27
+ [
28
+ build_mutant(
29
+ subject:,
30
+ original_node: node,
31
+ mutated_node: mutated_node(node, replacement),
32
+ description: "replaced #{node.children[1]} with #{replacement}"
33
+ )
34
+ ]
35
+ end
36
+
37
+ private
38
+
39
+ def mutated_node(node, replacement)
40
+ receiver = node.children[0]
41
+ arguments = node.children[2..] || []
42
+ Parser::AST::Node.new(node.type, [receiver, replacement, *arguments])
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser_current"
4
+
5
+ module Henitai
6
+ module Operators
7
+ # Removes array elements or replaces empty arrays with a nil element.
8
+ class ArrayDeclaration < Henitai::Operator
9
+ NODE_TYPES = [:array].freeze
10
+
11
+ def self.node_types
12
+ NODE_TYPES
13
+ end
14
+
15
+ def mutate(node, subject:)
16
+ if node.children.empty?
17
+ [empty_array_mutant(subject:, node:)]
18
+ else
19
+ [empty_array_mutant(subject:, node:)] + remove_element_mutants(node, subject:)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def empty_array_mutant(subject:, node:)
26
+ replacement = node.children.empty? ? [Parser::AST::Node.new(:nil, [])] : []
27
+ description = node.children.empty? ? "replaced empty array with [nil]" : "replaced array with empty array"
28
+
29
+ build_mutant(
30
+ subject:,
31
+ original_node: node,
32
+ mutated_node: Parser::AST::Node.new(:array, replacement),
33
+ description:
34
+ )
35
+ end
36
+
37
+ def remove_element_mutants(node, subject:)
38
+ node.children.each_with_index.map do |_element, index|
39
+ children = node.children.dup
40
+ children.delete_at(index)
41
+
42
+ build_mutant(
43
+ subject:,
44
+ original_node: node,
45
+ mutated_node: Parser::AST::Node.new(:array, children),
46
+ description: "removed array element #{index + 1}"
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser_current"
4
+
5
+ module Henitai
6
+ module Operators
7
+ # Mutates compound assignments and reduces ||= to a plain assignment.
8
+ class AssignmentExpression < Henitai::Operator
9
+ NODE_TYPES = %i[op_asgn or_asgn].freeze
10
+ OPERATOR_MAP = {
11
+ :+ => :-,
12
+ :- => :+
13
+ }.freeze
14
+
15
+ def self.node_types
16
+ NODE_TYPES
17
+ end
18
+
19
+ def mutate(node, subject:)
20
+ case node.type
21
+ when :op_asgn
22
+ mutate_compound_assignment(node, subject:)
23
+ when :or_asgn
24
+ # Memoization-style ||= is usually filtered earlier by AridNodeFilter.
25
+ mutate_coalesce_assignment(node, subject:)
26
+ else
27
+ []
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def mutate_compound_assignment(node, subject:)
34
+ left, operator, right = node.children
35
+ replacement = OPERATOR_MAP[operator]
36
+ return [] unless replacement
37
+
38
+ [
39
+ build_mutant(
40
+ subject:,
41
+ original_node: node,
42
+ mutated_node: Parser::AST::Node.new(:op_asgn, [left, replacement, right]),
43
+ description: "replaced #{operator} with #{replacement}"
44
+ )
45
+ ]
46
+ end
47
+
48
+ def mutate_coalesce_assignment(node, subject:)
49
+ left, right = node.children
50
+ mutated_node = assignment_node(left, right)
51
+ return [] unless mutated_node
52
+
53
+ [
54
+ build_mutant(
55
+ subject:,
56
+ original_node: node,
57
+ mutated_node:,
58
+ description: "removed ||="
59
+ )
60
+ ]
61
+ end
62
+
63
+ def assignment_node(left, right)
64
+ case left.type
65
+ when :lvasgn, :ivasgn, :gvasgn, :cvasgn
66
+ Parser::AST::Node.new(left.type, [left.children.first, right])
67
+ when :casgn
68
+ namespace, name = left.children
69
+ Parser::AST::Node.new(:casgn, [namespace, name, right])
70
+ when :send
71
+ assignment_name = left.children[1] == :[] ? :[]= : :"#{left.children[1]}="
72
+ receiver, _method_name, *arguments = left.children
73
+ Parser::AST::Node.new(:send, [receiver, assignment_name, *arguments, right])
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser_current"
4
+
5
+ module Henitai
6
+ module Operators
7
+ # Removes the body of normal block statements.
8
+ class BlockStatement < Henitai::Operator
9
+ NODE_TYPES = [:block].freeze
10
+
11
+ def self.node_types
12
+ NODE_TYPES
13
+ end
14
+
15
+ def mutate(node, subject:)
16
+ return [] if node.children.last.nil?
17
+
18
+ call, args, _body = node.children
19
+
20
+ [
21
+ build_mutant(
22
+ subject:,
23
+ original_node: node,
24
+ mutated_node: Parser::AST::Node.new(:block, [call, args, nil]),
25
+ description: "removed block content"
26
+ )
27
+ ]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser_current"
4
+
5
+ module Henitai
6
+ module Operators
7
+ # Toggles boolean literals and removes unary negation.
8
+ class BooleanLiteral < Henitai::Operator
9
+ NODE_TYPES = %i[true false send].freeze
10
+
11
+ def self.node_types
12
+ NODE_TYPES
13
+ end
14
+
15
+ def mutate(node, subject:)
16
+ # Parser uses :true / :false node types, so the AST symbols are intentional.
17
+ # rubocop:disable Lint/BooleanSymbol
18
+ case node.type
19
+ when :true
20
+ [mutate_true_literal(node, subject:)]
21
+ when :false
22
+ [mutate_false_literal(node, subject:)]
23
+ when :send
24
+ mutate_negation(node, subject:)
25
+ else
26
+ []
27
+ end
28
+ # rubocop:enable Lint/BooleanSymbol
29
+ end
30
+
31
+ private
32
+
33
+ # Parser uses :true / :false node types, so the AST symbols are intentional.
34
+ # rubocop:disable Lint/BooleanSymbol
35
+ def mutate_true_literal(node, subject:)
36
+ build_mutant(
37
+ subject:,
38
+ original_node: node,
39
+ mutated_node: Parser::AST::Node.new(:false, []),
40
+ description: "replaced true with false"
41
+ )
42
+ end
43
+
44
+ def mutate_false_literal(node, subject:)
45
+ build_mutant(
46
+ subject:,
47
+ original_node: node,
48
+ mutated_node: Parser::AST::Node.new(:true, []),
49
+ description: "replaced false with true"
50
+ )
51
+ end
52
+ # rubocop:enable Lint/BooleanSymbol
53
+
54
+ def mutate_negation(node, subject:)
55
+ receiver, method_name, *arguments = node.children
56
+ return [] unless method_name == :! && arguments.empty?
57
+ return [] unless receiver
58
+
59
+ [
60
+ build_mutant(
61
+ subject:,
62
+ original_node: node,
63
+ mutated_node: receiver,
64
+ description: "removed negation"
65
+ )
66
+ ]
67
+ end
68
+ end
69
+ end
70
+ end