henitai 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49d5bc19a4c6d8335797de16d71488d9ffdbb5b85c03b541da1f99a5ba39a009
4
- data.tar.gz: bb6490909c3c87cbe815ed960b649e23d60850ee41bb019097f6a4772de777ab
3
+ metadata.gz: 458245aa72b025224bd6c95c2bcca98ea095dd86a41e93ab8f0671ba56644369
4
+ data.tar.gz: 94a822238b18f41803753a85adae77675f422d2a901cdd89723ea68b14a8842a
5
5
  SHA512:
6
- metadata.gz: 10d2c66aa8f8ca9ecd64a482d4bc73880f1598e3174a9df220201874333eab2d846903560dec5270fed4291b401a58f7a41b0e28fcafd5918fbd631bfb77e70b
7
- data.tar.gz: d2c3859f3c70d28032a7a18b859df7713d6e39ca265004c8be86fe0d15ec450aea2e0bf2a60b147e6d2098c6c1f436829bdef87e6ff68353782790c9419465d6
6
+ metadata.gz: ed70e5425023aa08cd3ecd4ff6cb20dbd6c78b14f1811caaf9b43034e7649cdfeed617398a4fbf200730c1f4e8a36ab1fdfbe727b27379f4ed3b4eeef53be781
7
+ data.tar.gz: 0f2b4a04643f9e1bcd3f188e06d851f34eafbbf65dcafd42b9698678cb0ee540fca70e31207003d81cb4713ee1ca2c78901780e5f474128e20331c150949052a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,48 @@ 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
+
27
+ ## [0.3.1] - 2026-07-13
28
+
29
+ ### Fixed
30
+ - Per-mutant log capture no longer crashes with
31
+ `Encoding::UndefinedConversionError` when the host app sets
32
+ `Encoding.default_internal` (as Rails test environments do) and a child
33
+ process prints multibyte output: the capped log streams are opened in
34
+ binary mode, so pipe chunks are written byte-for-byte. Also keeps a
35
+ multibyte character split across drain chunks (or truncated at the cap)
36
+ from corrupting the file.
37
+
38
+ ### Changed
39
+ - `--since REF` now includes the working tree: tracked files with
40
+ uncommitted changes and untracked files count as changed, matching what
41
+ actually gets tested. Previously only committed changes in `REF..HEAD`
42
+ were considered, so pre-commit runs silently selected nothing.
43
+ - `--since REF` is test-aware: a changed test file selects the source files
44
+ it covers, using the per-test coverage map from the previous run
45
+ (`henitai_per_test.json`). Editing only a test now re-tests the subjects
46
+ that test can kill. First run without a map: no expansion.
47
+ - An empty `--since` run now says why: the terminal summary appends
48
+ `No mutants: no configured source files changed since REF.` instead of
49
+ printing an unexplained all-zero table. `Result` gained a `since`
50
+ attribute to carry the scope.
51
+
10
52
  ## [0.3.0] - 2026-07-13
11
53
 
12
54
  ### Added
@@ -380,7 +422,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
380
422
  - CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
381
423
  - RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
382
424
 
383
- [Unreleased]: https://github.com/martinotten/henitai/compare/v0.3.0...HEAD
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
427
+ [0.3.1]: https://github.com/martinotten/henitai/compare/v0.3.0...v0.3.1
384
428
  [0.3.0]: https://github.com/martinotten/henitai/compare/v0.2.1...v0.3.0
385
429
  [0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
386
430
  [0.2.0]: https://github.com/martinotten/henitai/compare/v0.1.10...v0.2.0
data/README.md CHANGED
@@ -51,7 +51,8 @@ gem install henitai
51
51
  # Run mutation testing on the entire project
52
52
  bundle exec henitai run
53
53
 
54
- # Run only on subjects changed since main (CI-friendly)
54
+ # Run only on subjects changed since main (CI-friendly). Includes uncommitted
55
+ # working-tree changes; changed test files select the sources they cover.
55
56
  bundle exec henitai run --since origin/main
56
57
 
57
58
  # Run on a specific subject pattern
@@ -270,7 +271,17 @@ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/wor
270
271
  - `AssignmentExpression` — mutate compound assignment
271
272
  - `UnaryOperator` — remove unary `-` and `~`
272
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
+
273
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 }`).
274
285
 
275
286
  ## Stryker Dashboard integration
276
287
 
@@ -44,7 +44,7 @@
44
44
  "additionalProperties": false,
45
45
  "properties": {
46
46
  "operators": {
47
- "enum": ["light", "full"]
47
+ "enum": ["light", "full", "hard"]
48
48
  },
49
49
  "timeout": {
50
50
  "type": "number"
@@ -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 }"],
@@ -21,7 +21,7 @@ module Henitai
21
21
  end
22
22
 
23
23
  def add_operator_option(opts, options)
24
- opts.on("--operators SET", "Operator set: light | full") do |set|
24
+ opts.on("--operators SET", "Operator set: light | full | hard") do |set|
25
25
  options[:operators] = set
26
26
  end
27
27
  end
@@ -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
@@ -62,8 +62,11 @@ module Henitai
62
62
 
63
63
  # One capture channel: a pipe whose read end is drained (capped) into the
64
64
  # log file by a dedicated thread. Returns the pieces close needs.
65
+ # Binary mode: readpartial yields ASCII-8BIT chunks, and a text-mode file
66
+ # would transcode them (raising Encoding::UndefinedConversionError on
67
+ # multibyte output when Encoding.default_internal is set, as Rails does).
65
68
  def open_capped_stream(path, cap)
66
- file = File.new(path, "w")
69
+ file = File.new(path, "wb")
67
70
  file.sync = true
68
71
  reader, writer = IO.pipe
69
72
  thread = Thread.new { drain_capped(reader, file, cap) }
@@ -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
- # @param set [Symbol] :light or :full
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 == :full ? FULL_SET : LIGHT_SET
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 by emptying them or mutating symbol keys.
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(symbol_key_mutants(node, subject:))
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
- def symbol_key_mutants(node, subject:)
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 symbol_key_pair?(pair)
42
+ next unless pair.type == :pair
37
43
 
38
44
  build_mutant(
39
45
  subject:,
40
46
  original_node: node,
41
- mutated_node: mutated_hash(node, index),
42
- description: "replaced symbol key with string key"
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 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)
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
- 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])
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
@@ -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"
@@ -28,6 +28,21 @@ module Henitai
28
28
  map.keys.select { |test| covers?(test, mutant) }.sort
29
29
  end
30
30
 
31
+ # The source files the given test file's recorded coverage reaches.
32
+ # Test keys are matched path-insensitively (relative or absolute form);
33
+ # returned source paths are absolute, as recorded.
34
+ #
35
+ # @return [Array<String>] sorted; empty when the test is unknown or the
36
+ # map is unavailable.
37
+ def source_files_covered_by(test_file)
38
+ wanted = File.expand_path(test_file.to_s)
39
+ map.select { |test, _| File.expand_path(test) == wanted }
40
+ .values
41
+ .flat_map(&:keys)
42
+ .uniq
43
+ .sort
44
+ end
45
+
31
46
  # True when the given test file's recorded coverage intersects the
32
47
  # mutant's current line range.
33
48
  def covers?(test, mutant)
@@ -137,10 +137,20 @@ module Henitai
137
137
  format_row("No coverage", count_status(result, :no_coverage)),
138
138
  format_row("Duration", format_duration(result.duration)),
139
139
  reused_verdicts_line(result),
140
- executed_only_score_line(result)
140
+ executed_only_score_line(result),
141
+ empty_since_scope_line(result)
141
142
  ].compact
142
143
  end
143
144
 
145
+ # A zero-mutant summary with every counter at 0 and n/a scores is
146
+ # baffling without its cause; name it when the run was scoped by --since.
147
+ def empty_since_scope_line(result)
148
+ return nil unless result.mutants.empty?
149
+ return nil unless result.respond_to?(:since) && result.since
150
+
151
+ "No mutants: no configured source files changed since #{result.since}."
152
+ end
153
+
144
154
  def reused_verdicts_line(result)
145
155
  reused = reused_mutants(result)
146
156
  return nil if reused.empty?
@@ -16,7 +16,7 @@ module Henitai
16
16
  DEFAULT_THRESHOLDS = { high: 80, low: 60 }.freeze
17
17
 
18
18
  attr_reader :mutants, :started_at, :finished_at, :thresholds, :survivor_stats,
19
- :session_id, :git_sha
19
+ :session_id, :git_sha, :since
20
20
 
21
21
  # @param source_provider [#call] maps a file path to its source string.
22
22
  # Injected so the domain object performs no disk IO; the caller (which
@@ -32,7 +32,8 @@ module Henitai
32
32
  def initialize(mutants:, started_at:, finished_at:, thresholds: nil,
33
33
  partial_rerun: false, survivor_stats: nil,
34
34
  session_id: SecureRandom.uuid, git_sha: nil,
35
- source_provider: ->(_file) { "" }, authoritative: true)
35
+ source_provider: ->(_file) { "" }, authoritative: true,
36
+ since: nil)
36
37
  @mutants = mutants
37
38
  @started_at = started_at
38
39
  @finished_at = finished_at
@@ -43,6 +44,7 @@ module Henitai
43
44
  @git_sha = git_sha
44
45
  @source_provider = source_provider
45
46
  @authoritative = authoritative
47
+ @since = since
46
48
  end
47
49
  # rubocop:enable Metrics/ParameterLists
48
50
 
@@ -168,7 +168,8 @@ module Henitai
168
168
  survivor_stats: survivor_strategy.survivor_stats,
169
169
  git_sha: safe_head_sha,
170
170
  source_provider: source_provider,
171
- authoritative: full_run?
171
+ authoritative: full_run?,
172
+ since: @since
172
173
  )
173
174
  end
174
175
 
@@ -267,12 +268,28 @@ module Henitai
267
268
  def filter_changed(files)
268
269
  return files unless @since
269
270
 
270
- changed_file_set = git_diff_analyzer
271
- .changed_files(from: @since, to: "HEAD")
272
- .map { |path| normalize_path(path) }
271
+ changed_file_set = changed_paths_since.map { |path| normalize_path(path) }
272
+ changed_file_set += covered_sources_for_changed_tests(changed_file_set)
273
273
  files.select { |path| changed_file_set.include?(normalize_path(path)) }
274
274
  end
275
275
 
276
+ # Committed changes since the ref plus the current working tree (tracked
277
+ # dirty and untracked files): the working tree is what gets tested, so it
278
+ # is always part of "changed since REF".
279
+ def changed_paths_since
280
+ git_diff_analyzer.changed_files(from: @since, to: "HEAD") +
281
+ git_diff_analyzer.working_tree_changed_files
282
+ end
283
+
284
+ # Changed test files select the source files they cover, so an edited
285
+ # test re-tests the subjects it can kill. Uses the per-test map from the
286
+ # previous run — Gate 1 runs before this run's bootstrap finishes.
287
+ def covered_sources_for_changed_tests(changed_paths)
288
+ changed_paths
289
+ .flat_map { |path| per_test_coverage.source_files_covered_by(path) }
290
+ .map { |path| normalize_path(path) }
291
+ end
292
+
276
293
  def pattern_subjects
277
294
  Array(@subjects)
278
295
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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]
@@ -718,6 +720,7 @@ module Henitai
718
720
  def initialize: (reports_dir: String, ?coverage_report_reader: CoverageReportReader) -> void
719
721
  def available?: () -> bool
720
722
  def tests_covering: (untyped) -> Array[String]
723
+ def source_files_covered_by: (untyped) -> Array[String]
721
724
  def covers?: (String, untyped) -> bool
722
725
 
723
726
  private
@@ -852,6 +855,7 @@ module Henitai
852
855
  def reused_verdicts_line: (Result) -> String?
853
856
  def reused_mutants: (Result) -> Array[Mutant]
854
857
  def executed_only_score_line: (Result) -> String?
858
+ def empty_since_scope_line: (Result) -> String?
855
859
  def count_status: (Result, Symbol) -> Integer
856
860
  def format_duration: (Float) -> String
857
861
  def format_percent: (Float?) -> String
@@ -977,8 +981,9 @@ module Henitai
977
981
  attr_reader survivor_stats: Hash[Symbol, untyped]?
978
982
  attr_reader session_id: String
979
983
  attr_reader git_sha: String?
984
+ attr_reader since: String?
980
985
 
981
- def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?, ?partial_rerun: bool, ?survivor_stats: Hash[Symbol, untyped]?, ?session_id: String, ?git_sha: String?, ?source_provider: ^(String) -> String, ?authoritative: bool) -> void
986
+ def initialize: (mutants: Array[Mutant], started_at: Time, finished_at: Time, ?thresholds: Hash[Symbol, Integer]?, ?partial_rerun: bool, ?survivor_stats: Hash[Symbol, untyped]?, ?session_id: String, ?git_sha: String?, ?source_provider: ^(String) -> String, ?authoritative: bool, ?since: String?) -> void
982
987
  def partial_rerun?: () -> bool
983
988
  def authoritative?: () -> bool
984
989
  def killed: () -> Integer
@@ -1086,6 +1091,8 @@ module Henitai
1086
1091
  def reject_excluded: (Array[String]) -> Array[String]
1087
1092
  def excluded_source_files: () -> Array[String]
1088
1093
  def filter_changed: (Array[String]) -> Array[String]
1094
+ def changed_paths_since: () -> Array[String]
1095
+ def covered_sources_for_changed_tests: (Array[String]) -> Array[String]
1089
1096
  def pattern_subjects: () -> Array[Subject]
1090
1097
  def unique_subjects: (Array[Subject]) -> Array[Subject]
1091
1098
  def normalize_path: (String) -> String
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.3.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.3.0
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: