Class: Evilution::Integration::Loading::RedefinitionRecovery

Inherits:
Object
  • Object
show all
Defined in:
lib/evilution/integration/loading/redefinition_recovery.rb

Overview

Some DSLs (Rails 8 enum, define_method guards) raise ArgumentError on re-declaration. On such a conflict we strip constants declared in the source and retry the load once against a fresh namespace.

A second class of idempotency violation comes from gem-internal registries (dry-monads ‘register_mixin`, Rails plugins, etc.) which raise when called a second time in the same process. For these we swallow the error: the class body executed up to the raise point — method defs preceding the registry call are already in place — and any state change blocked by the guard was intentional duplicate-prevention from the gem’s side. Mutations that target a def after such a class-body call would not be applied; emit a one-shot warning so that mode is visible.

Constant Summary collapse

IDEMPOTENCY_PATTERNS =
[
  "already registered",
  "already initialized",
  "already exists"
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(constant_names: Evilution::AST::ConstantNames.new) ⇒ RedefinitionRecovery

Returns a new instance of RedefinitionRecovery.



25
26
27
# File 'lib/evilution/integration/loading/redefinition_recovery.rb', line 25

def initialize(constant_names: Evilution::AST::ConstantNames.new)
  @constant_names = constant_names
end

Instance Method Details

#call(source, &block) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/evilution/integration/loading/redefinition_recovery.rb', line 29

def call(source, &block)
  block.call
rescue ArgumentError => e
  if redefinition_conflict?(e)
    remove_defined_constants(source)
    block.call
  elsif idempotency_violation?(e)
    warn_once_for(e)
    nil
  else
    raise
  end
rescue TypeError => e
  raise unless superclass_mismatch?(e)

  # `class X < Struct.new(...)` (or Data.define / Class.new in the same
  # position) returns a fresh anonymous parent class on every call, so the
  # recorded superclass of the existing X differs from the re-eval's. Ruby
  # raises TypeError. Simply swallowing would leave the *original* class
  # in place and silently report the mutation as survived — a false
  # negative. Instead, strip the constants this source declares and retry
  # exactly once, mirroring the ArgumentError 'already defined' path. If
  # the retry still mismatches (genuine inheritance conflict the mutation
  # cannot resolve), propagate so the mutation reports :error rather than
  # being silently miscounted.
  remove_defined_constants(source)
  block.call
end