evilution 0.22.5 → 0.22.7

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: 8dfc39a794faace7567c4539876cb9c331cba15f62420625aafc8adf9d3fb7bf
4
- data.tar.gz: e199a8d90e70db722e76fe785968385dc1725297ee6020b31760fdfad5745c70
3
+ metadata.gz: 953b824799c03617e8e41a67f759c5edbd122b63046f8916e19d215021de5871
4
+ data.tar.gz: 38e3498c7cc763e44a5cded169c035b1a48d02c14c95b9ff4ae0800dd63389cd
5
5
  SHA512:
6
- metadata.gz: 32d62ef27f34042244512d5d52082c4a53d82b0f6de91e924faebd8317d5d2d8c57a5fa53b8b5f3cc6bee325723e62dcfbd0b32d9bc8c2b17f368cde027a93b1
7
- data.tar.gz: c4695099eb10bbdd2f69995fa95f3f9dcb8dbb3497532758b9177594fb9e098021080924276cf582c5b951dc95e6af39d1d7e2d1fae404f2e451bf8622fefa39
6
+ metadata.gz: cceb0046285ad1efdd63914af840da66354006d97f1f9da0642dcf649e3628a342c6ed5f3c6776f3b76dee7eb41af833033a4c9c18992eaa6773bf1c5da8ca75
7
+ data.tar.gz: 6928979ecd999f213b87c3460179f4a09008894866e31d5e822f214ab521235e8463a389768e40bff065512fc1e55355d0ca249e739cb571210ccd429d94db82
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.22.7] - 2026-04-13
4
+
5
+ ### Fixed
6
+
7
+ - **Rails 8 models with `enum`: every mutation errors with `ArgumentError`** — re-running the class body via `load`/`require` retriggered Rails 8's `detect_enum_conflict!` because the enum's predicate methods already existed from the first load, so mutations scored 0% on any affected file; now `remove_defined_constants` drops constants defined by the mutated source (via `remove_const` on the parent namespace) before re-loading, so the class body runs on a fresh constant and DSLs with conflict detection (enum, and any future DSL with redefinition guards) see a clean slate (#683)
8
+
9
+ ## [0.22.6] - 2026-04-12
10
+
11
+ ### Fixed
12
+
13
+ - **Zeitwerk re-autoloads original file during mutation load** — Zeitwerk's `const_added` hook re-autoloaded the original source when a module constant was reopened from a temp-dir copy, re-setting `@_included_block` after `clear_concern_state` already removed it; now `pin_autoloaded_constants` resolves all module/class constants from the source via `Object.const_get` before loading, preventing the autoloader from re-triggering (#680)
14
+
3
15
  ## [0.22.5] - 2026-04-12
4
16
 
5
17
  ### Fixed
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "prism"
4
5
  require "tmpdir"
5
6
  require_relative "../integration"
6
7
  require_relative "../temp_dir_tracker"
@@ -87,8 +88,11 @@ class Evilution::Integration::Base
87
88
  File.write(dest, mutation.mutated_source)
88
89
  $LOAD_PATH.unshift(@temp_dir)
89
90
  displace_loaded_feature(mutation.file_path)
91
+ pin_autoloaded_constants(mutation.original_source)
90
92
  clear_concern_state(mutation.file_path)
91
- require(subpath.delete_suffix(".rb"))
93
+ with_redefinition_recovery(mutation.original_source) do
94
+ require(subpath.delete_suffix(".rb"))
95
+ end
92
96
  end
93
97
 
94
98
  def apply_via_load(mutation)
@@ -96,8 +100,24 @@ class Evilution::Integration::Base
96
100
  dest = File.join(@temp_dir, absolute)
97
101
  FileUtils.mkdir_p(File.dirname(dest))
98
102
  File.write(dest, mutation.mutated_source)
103
+ pin_autoloaded_constants(mutation.original_source)
99
104
  clear_concern_state(mutation.file_path)
100
- load(dest)
105
+ with_redefinition_recovery(mutation.original_source) do
106
+ load(dest)
107
+ end
108
+ end
109
+
110
+ def with_redefinition_recovery(original_source)
111
+ yield
112
+ rescue ArgumentError => e
113
+ raise unless redefinition_conflict?(e)
114
+
115
+ remove_defined_constants(original_source)
116
+ yield
117
+ end
118
+
119
+ def redefinition_conflict?(error)
120
+ error.message.include?("already defined")
101
121
  end
102
122
 
103
123
  def restore_original(_mutation)
@@ -112,6 +132,56 @@ class Evilution::Integration::Base
112
132
  @temp_dir = nil
113
133
  end
114
134
 
135
+ def pin_autoloaded_constants(source)
136
+ collect_constant_names(Prism.parse(source).value).each do |name|
137
+ Object.const_get(name) if Object.const_defined?(name, false)
138
+ rescue NameError # :nodoc:
139
+ nil
140
+ end
141
+ end
142
+
143
+ def collect_constant_names(node, nesting = [])
144
+ names = []
145
+ case node
146
+ when Prism::ModuleNode, Prism::ClassNode
147
+ const = node.constant_path.full_name
148
+ qualified = nesting.any? && !const.include?("::") ? "#{nesting.join("::")}::#{const}" : const
149
+ names << qualified
150
+ names.concat(collect_constant_names(node.body, nesting + [const])) if node.body
151
+ when Prism::ProgramNode
152
+ names.concat(collect_constant_names(node.statements, nesting)) if node.statements
153
+ when Prism::StatementsNode
154
+ node.body.each { |child| names.concat(collect_constant_names(child, nesting)) }
155
+ end
156
+ names
157
+ end
158
+
159
+ def remove_defined_constants(source)
160
+ collect_constant_names(Prism.parse(source).value).reverse_each do |name|
161
+ parent_name, _, local_name = name.rpartition("::")
162
+ parent = resolve_loaded_constant_parent(parent_name)
163
+ next unless parent
164
+ next unless parent.const_defined?(local_name, false)
165
+ next if parent.autoload?(local_name)
166
+
167
+ parent.send(:remove_const, local_name.to_sym)
168
+ end
169
+ end
170
+
171
+ def resolve_loaded_constant_parent(parent_name)
172
+ return Object if parent_name.empty?
173
+
174
+ parent_name.split("::").reduce(Object) do |mod, part|
175
+ return nil unless mod.const_defined?(part, false)
176
+ return nil if mod.autoload?(part)
177
+
178
+ resolved = mod.const_get(part, false)
179
+ return nil unless resolved.is_a?(Module)
180
+
181
+ resolved
182
+ end
183
+ end
184
+
115
185
  def clear_concern_state(file_path)
116
186
  return unless defined?(ActiveSupport::Concern)
117
187
 
@@ -135,10 +205,7 @@ class Evilution::Integration::Base
135
205
  end
136
206
 
137
207
  def source_matches?(block_path, absolute, subpath)
138
- return true if block_path == absolute
139
- return true if subpath && block_path.end_with?("/#{subpath}")
140
-
141
- false
208
+ block_path == absolute || (subpath && block_path.end_with?("/#{subpath}"))
142
209
  end
143
210
 
144
211
  def resolve_require_subpath(file_path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.22.5"
4
+ VERSION = "0.22.7"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.5
4
+ version: 0.22.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-12 00:00:00.000000000 Z
11
+ date: 2026-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs