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,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
|