Class: Evilution::Integration::Loading::BodyCallNeutralizer

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

Overview

Strip non-idempotent class/module-body side-effect calls from a mutated source before re-eval. Such calls (e.g. dry-monads ‘register_mixin`, plugin registries) raise on second invocation because they assume single-eval semantics. The first invocation already ran in the parent process during preload — the child fork inherits the resulting state, so re-running them is wasted work that aborts the eval before the mutated method takes effect.

Strategy: walk Prism tree, find CallNodes that sit directly under a class or module body (not inside a def). Calls on a small allowlist of patterns known to be idempotent (‘include`, `attr_*`, visibility modifiers, etc.) are preserved; everything else is replaced byte-for-byte with `nil`.

Constant Summary collapse

IDEMPOTENT_CALLS =
%i[
  include extend prepend using
  attr_reader attr_writer attr_accessor
  private public protected module_function private_class_method public_class_method
  alias_method
  define_method define_singleton_method
  delegate
  require require_relative autoload
].to_set.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.preloaded_featuresObject

Snapshot of ‘$LOADED_FEATURES` captured at parent preload-end (or lazily initialised on first access). Forks inherit this set via copy-on-write, so worker processes see the same membership the parent saw when it finished its preload phase. Frozen so in-place mutation cannot silently change neutralization semantics or force child forks to copy the page.



36
37
38
# File 'lib/evilution/integration/loading/body_call_neutralizer.rb', line 36

def preloaded_features
  @preloaded_features ||= $LOADED_FEATURES.to_set.freeze
end

Class Method Details

.reset_preload_snapshot!Object



40
41
42
# File 'lib/evilution/integration/loading/body_call_neutralizer.rb', line 40

def reset_preload_snapshot!
  @preloaded_features = nil
end

Instance Method Details

#call(source, file_path: nil) ⇒ Object

‘file_path` (optional) lets callers gate neutralization on whether the target file was actually preloaded into the parent. The neutralizer’s premise — “this body has already run once, re-running it would double- register” — only holds when the parent loaded the file. Lazy-loaded plugin files (e.g. roda’s ‘lib/roda/plugins/typecast_params.rb`, which the gem only requires when the user opts in via `plugin :typecast_params`) are first-loaded inside the child fork, so neutralizing their DSL calls strips method definitions that subsequent sibling statements (alias, etc.) depend on, producing cascading NameError. Callers that don’t pass a path get the legacy always-neutralize behavior.



55
56
57
58
59
60
61
62
63
64
65
# File 'lib/evilution/integration/loading/body_call_neutralizer.rb', line 55

def call(source, file_path: nil)
  return source if file_path && !preloaded?(file_path)

  result = Prism.parse(source)
  return source if result.failure?

  edits = collect_edits(result.value)
  return source if edits.empty?

  apply_edits(source, edits)
end