henitai 0.3.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +19 -1
- data/README.md +10 -0
- data/assets/schema/henitai.schema.json +1 -1
- data/lib/henitai/cli/operator_command.rb +2 -1
- data/lib/henitai/cli/run_options.rb +1 -1
- data/lib/henitai/configuration_validator.rb +1 -1
- data/lib/henitai/operator.rb +12 -2
- data/lib/henitai/operators/hash_key_type.rb +50 -0
- data/lib/henitai/operators/hash_literal.rb +19 -20
- data/lib/henitai/operators.rb +1 -0
- data/lib/henitai/version.rb +1 -1
- data/sig/henitai.rbs +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 458245aa72b025224bd6c95c2bcca98ea095dd86a41e93ab8f0671ba56644369
|
|
4
|
+
data.tar.gz: 94a822238b18f41803753a85adae77675f422d2a901cdd89723ea68b14a8842a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed70e5425023aa08cd3ecd4ff6cb20dbd6c78b14f1811caaf9b43034e7649cdfeed617398a4fbf200730c1f4e8a36ab1fdfbe727b27379f4ed3b4eeef53be781
|
|
7
|
+
data.tar.gz: 0f2b4a04643f9e1bcd3f188e06d851f34eafbbf65dcafd42b9698678cb0ee540fca70e31207003d81cb4713ee1ca2c78901780e5f474128e20331c150949052a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hard` operator set (`mutation.operators: hard` / `--operators hard`), a
|
|
14
|
+
strict superset of `full` for usually-unkillable mutations (ADR-12):
|
|
15
|
+
currently `EqualityIdentityOperator` and the new `HashKeyType`
|
|
16
|
+
(`{ a: 1 }` -> `{ "a" => 1 }`) — framework key normalization (e.g.
|
|
17
|
+
ActiveRecord `order`/`where`) makes key-type mutants frequently equivalent
|
|
18
|
+
- `HashLiteral` gains per-pair removal (`{ a: 1, b: 2 }` -> `{ b: 2 }`);
|
|
19
|
+
single-pair hashes and double-splat entries are skipped
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `full` now means "usually killable": the symbol-key -> string-key mutation
|
|
23
|
+
moved from `HashLiteral` into the new hard-set `HashKeyType`, and
|
|
24
|
+
`EqualityIdentityOperator` moved from `full` to `hard` — `full` runs emit
|
|
25
|
+
fewer, higher-signal mutants
|
|
26
|
+
|
|
10
27
|
## [0.3.1] - 2026-07-13
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -405,7 +422,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
405
422
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
406
423
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
407
424
|
|
|
408
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.
|
|
425
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.4.0...HEAD
|
|
426
|
+
[0.4.0]: https://github.com/martinotten/henitai/compare/v0.3.1...v0.4.0
|
|
409
427
|
[0.3.1]: https://github.com/martinotten/henitai/compare/v0.3.0...v0.3.1
|
|
410
428
|
[0.3.0]: https://github.com/martinotten/henitai/compare/v0.2.1...v0.3.0
|
|
411
429
|
[0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
|
data/README.md
CHANGED
|
@@ -271,7 +271,17 @@ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/wor
|
|
|
271
271
|
- `AssignmentExpression` — mutate compound assignment
|
|
272
272
|
- `UnaryOperator` — remove unary `-` and `~`
|
|
273
273
|
- `UpdateOperator` — swap compound assignments (`+=`↔`-=`, `*=`↔`/=`, `||=`↔`&&=`)
|
|
274
|
+
|
|
275
|
+
**Hard** — adds usually-unkillable mutations on top of full, for hunting the
|
|
276
|
+
last survivors (see [ADR-12](docs/architecture/adr/ADR-12-hard-operator-set.md)):
|
|
277
|
+
|
|
274
278
|
- `EqualityIdentityOperator` — `==` ↔ `eql?`/`equal?` (hardest equality pairing to kill; see [ADR-10](docs/architecture/adr/ADR-10-split-equality-identity-mutations.md))
|
|
279
|
+
- `HashKeyType` — `{ a: 1 }` → `{ "a" => 1 }` (frameworks that normalize key
|
|
280
|
+
types, e.g. ActiveRecord `order`/`where`, make these mutants equivalent at
|
|
281
|
+
many call sites; disable per site with `# henitai:disable HashKeyType`)
|
|
282
|
+
|
|
283
|
+
`HashLiteral` (full set) empties the hash and removes one pair at a time
|
|
284
|
+
(`{ a: 1, b: 2 }` → `{}` / `{ b: 2 }`).
|
|
275
285
|
|
|
276
286
|
## Stryker Dashboard integration
|
|
277
287
|
|
|
@@ -14,7 +14,8 @@ module Henitai
|
|
|
14
14
|
"StringLiteral" => ["String literals", '"foo" -> ""'],
|
|
15
15
|
"ReturnValue" => ["Return expressions", "return x -> return nil"],
|
|
16
16
|
"ArrayDeclaration" => ["Array literals", "[1, 2] -> []"],
|
|
17
|
-
"HashLiteral" => ["Hash literals", "{ a: 1 } -> {}"],
|
|
17
|
+
"HashLiteral" => ["Hash literals", "{ a: 1, b: 2 } -> {} / { b: 2 }"],
|
|
18
|
+
"HashKeyType" => ["Hash key types", '{ a: 1 } -> { "a" => 1 }'],
|
|
18
19
|
"RangeLiteral" => ["Range literals", "1..5 -> 1...5"],
|
|
19
20
|
"SafeNavigation" => ["Safe navigation", "user&.name -> user.name"],
|
|
20
21
|
"PatternMatch" => ["Pattern matching", "in { x: Integer } -> in { x: String }"],
|
|
@@ -33,7 +33,7 @@ module Henitai
|
|
|
33
33
|
VALID_THRESHOLDS_KEYS = %i[high low].freeze
|
|
34
34
|
VALID_DASHBOARD_KEYS = %i[project base_url].freeze
|
|
35
35
|
VALID_INTEGRATION_KEYS = %i[name].freeze
|
|
36
|
-
VALID_OPERATORS = %i[light full].freeze
|
|
36
|
+
VALID_OPERATORS = %i[light full hard].freeze
|
|
37
37
|
VALIDATION_STEPS = %i[
|
|
38
38
|
validate_top_level_keys
|
|
39
39
|
validate_integration
|
data/lib/henitai/operator.rb
CHANGED
|
@@ -44,13 +44,23 @@ module Henitai
|
|
|
44
44
|
AssignmentExpression
|
|
45
45
|
UnaryOperator
|
|
46
46
|
UpdateOperator
|
|
47
|
+
]).freeze
|
|
48
|
+
|
|
49
|
+
# Usually-unkillable operators on top of full: mutations whose survival
|
|
50
|
+
# rarely indicates a test gap (framework key normalization, the
|
|
51
|
+
# ==/eql?/equal? pairing). Opt in when hunting the last survivors
|
|
52
|
+
# (ADR-12).
|
|
53
|
+
HARD_SET = (FULL_SET + %w[
|
|
47
54
|
EqualityIdentityOperator
|
|
55
|
+
HashKeyType
|
|
48
56
|
]).freeze
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
SETS = { light: LIGHT_SET, full: FULL_SET, hard: HARD_SET }.freeze
|
|
59
|
+
|
|
60
|
+
# @param set [Symbol] :light, :full or :hard
|
|
51
61
|
# @return [Array<Operator>] operator instances for the given set
|
|
52
62
|
def self.for_set(set)
|
|
53
|
-
names = set.to_sym
|
|
63
|
+
names = SETS.fetch(set.to_sym, LIGHT_SET)
|
|
54
64
|
names.map { |name| Henitai::Operators.const_get(name).new }
|
|
55
65
|
end
|
|
56
66
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
module Operators
|
|
5
|
+
# Mutates symbol hash keys into string keys, one pair at a time
|
|
6
|
+
# (`{ a: 1 }` -> `{ "a" => 1 }`). Symbol/string key confusion is a real
|
|
7
|
+
# defect class, but frameworks that normalize keys (e.g. ActiveRecord's
|
|
8
|
+
# `order`/`where`) make these mutants frequently unkillable — hence the
|
|
9
|
+
# hard set, not full (ADR-12).
|
|
10
|
+
class HashKeyType < Henitai::Operator
|
|
11
|
+
NODE_TYPES = [:hash].freeze
|
|
12
|
+
|
|
13
|
+
def self.node_types
|
|
14
|
+
NODE_TYPES
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mutate(node, subject:)
|
|
18
|
+
node.children.each_with_index.filter_map do |pair, index|
|
|
19
|
+
next unless symbol_key_pair?(pair)
|
|
20
|
+
|
|
21
|
+
build_mutant(
|
|
22
|
+
subject:,
|
|
23
|
+
original_node: node,
|
|
24
|
+
mutated_node: mutated_hash(node, index),
|
|
25
|
+
description: "replaced symbol key with string key"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def symbol_key_pair?(node)
|
|
33
|
+
node.type == :pair && node.children.first&.type == :sym
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mutated_hash(node, index)
|
|
37
|
+
pairs = node.children.each_with_index.map do |pair, pair_index|
|
|
38
|
+
pair_index == index ? stringified_pair(pair) : pair
|
|
39
|
+
end
|
|
40
|
+
Parser::AST::Node.new(:hash, pairs)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stringified_pair(pair)
|
|
44
|
+
key, value = pair.children
|
|
45
|
+
string_key = Parser::AST::Node.new(:str, [key.children.first.to_s])
|
|
46
|
+
Parser::AST::Node.new(:pair, [string_key, value])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -4,7 +4,10 @@ require_relative "../parser_current"
|
|
|
4
4
|
|
|
5
5
|
module Henitai
|
|
6
6
|
module Operators
|
|
7
|
-
# Reduces hash literals
|
|
7
|
+
# Reduces hash literals: empties the whole hash and removes one pair at a
|
|
8
|
+
# time. Symbol-key -> string-key mutation lives in {HashKeyType} (hard
|
|
9
|
+
# set) because framework key normalization makes it frequently unkillable
|
|
10
|
+
# (ADR-12).
|
|
8
11
|
class HashLiteral < Henitai::Operator
|
|
9
12
|
NODE_TYPES = [:hash].freeze
|
|
10
13
|
|
|
@@ -16,7 +19,7 @@ module Henitai
|
|
|
16
19
|
return [] if node.children.empty?
|
|
17
20
|
|
|
18
21
|
mutants = [empty_hash_mutant(node, subject:)]
|
|
19
|
-
mutants.concat(
|
|
22
|
+
mutants.concat(pair_removal_mutants(node, subject:))
|
|
20
23
|
mutants
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -31,35 +34,31 @@ module Henitai
|
|
|
31
34
|
)
|
|
32
35
|
end
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
# Removing the only entry would duplicate the empty-hash mutant.
|
|
38
|
+
def pair_removal_mutants(node, subject:)
|
|
39
|
+
return [] if node.children.size < 2
|
|
40
|
+
|
|
35
41
|
node.children.each_with_index.filter_map do |pair, index|
|
|
36
|
-
next unless
|
|
42
|
+
next unless pair.type == :pair
|
|
37
43
|
|
|
38
44
|
build_mutant(
|
|
39
45
|
subject:,
|
|
40
46
|
original_node: node,
|
|
41
|
-
mutated_node:
|
|
42
|
-
description: "
|
|
47
|
+
mutated_node: hash_without_pair(node, index),
|
|
48
|
+
description: "removed hash pair #{pair_key_label(pair)}"
|
|
43
49
|
)
|
|
44
50
|
end
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
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)
|
|
53
|
+
def hash_without_pair(node, pair_index)
|
|
54
|
+
remaining = node.children.reject.with_index { |_pair, index| index == pair_index }
|
|
55
|
+
Parser::AST::Node.new(:hash, remaining)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
# Pair keys are always AST nodes (sym/str/…); their first child is the
|
|
59
|
+
# literal value used purely as a human-readable label.
|
|
60
|
+
def pair_key_label(pair)
|
|
61
|
+
pair.children.first.children.first
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
64
|
end
|
data/lib/henitai/operators.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Henitai
|
|
|
16
16
|
autoload :ReturnValue, "henitai/operators/return_value"
|
|
17
17
|
autoload :ArrayDeclaration, "henitai/operators/array_declaration"
|
|
18
18
|
autoload :HashLiteral, "henitai/operators/hash_literal"
|
|
19
|
+
autoload :HashKeyType, "henitai/operators/hash_key_type"
|
|
19
20
|
autoload :RangeLiteral, "henitai/operators/range_literal"
|
|
20
21
|
autoload :SafeNavigation, "henitai/operators/safe_navigation"
|
|
21
22
|
autoload :PatternMatch, "henitai/operators/pattern_match"
|
data/lib/henitai/version.rb
CHANGED
data/sig/henitai.rbs
CHANGED
|
@@ -353,6 +353,8 @@ module Henitai
|
|
|
353
353
|
class Operator
|
|
354
354
|
LIGHT_SET: Array[String]
|
|
355
355
|
FULL_SET: Array[String]
|
|
356
|
+
HARD_SET: Array[String]
|
|
357
|
+
SETS: Hash[Symbol, Array[String]]
|
|
356
358
|
|
|
357
359
|
def self.for_set: (Symbol) -> Array[untyped]
|
|
358
360
|
def self.node_types: () -> Array[Symbol]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: henitai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Otten
|
|
@@ -166,6 +166,7 @@ files:
|
|
|
166
166
|
- lib/henitai/operators/conditional_expression.rb
|
|
167
167
|
- lib/henitai/operators/equality_identity_operator.rb
|
|
168
168
|
- lib/henitai/operators/equality_operator.rb
|
|
169
|
+
- lib/henitai/operators/hash_key_type.rb
|
|
169
170
|
- lib/henitai/operators/hash_literal.rb
|
|
170
171
|
- lib/henitai/operators/logical_operator.rb
|
|
171
172
|
- lib/henitai/operators/method_chain_unwrap.rb
|
|
@@ -223,7 +224,7 @@ metadata:
|
|
|
223
224
|
changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
|
|
224
225
|
documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
|
|
225
226
|
homepage_uri: https://github.com/martinotten/henitai
|
|
226
|
-
source_code_uri: https://github.com/martinotten/henitai/tree/v0.
|
|
227
|
+
source_code_uri: https://github.com/martinotten/henitai/tree/v0.4.0
|
|
227
228
|
rubygems_mfa_required: 'true'
|
|
228
229
|
rdoc_options: []
|
|
229
230
|
require_paths:
|