Class: Henitai::Mutant::Activator

Inherits:
Object
  • Object
show all
Includes:
ConstantRedefinitionFilter
Defined in:
lib/henitai/mutant/activator.rb,
sig/henitai.rbs

Overview

Activates a mutant inside the forked child process.

Defined Under Namespace

Modules: ConstantRedefinitionFilter

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ConstantRedefinitionFilter

#warn

Constructor Details

#initializeActivator

Returns a new instance of Activator.



767
# File 'sig/henitai.rbs', line 767

def initialize: () -> void

Class Method Details

.activate!(mutant) ⇒ Symbol?

Parameters:

  • (Object)

Returns:

  • (Symbol, nil)


26
27
28
# File 'lib/henitai/mutant/activator.rb', line 26

def self.activate!(mutant)
  new.activate!(mutant)
end

.activation_source_for(mutant) ⇒ String?

Returns the define_method source string for mutant without actually evaluating it. Used to pre-compute activation recipes. Returns nil if the source cannot be computed (e.g. unsupported AST node).

Parameters:

Returns:

  • (String, nil)


33
34
35
36
37
# File 'lib/henitai/mutant/activator.rb', line 33

def self.activation_source_for(mutant)
  new.send(:method_source, mutant)
rescue StandardError
  nil
end

Instance Method Details

#activate!(mutant) ⇒ Symbol?

Parameters:

  • (Object)

Returns:

  • (Symbol, nil)


39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/henitai/mutant/activator.rb', line 39

def activate!(mutant)
  subject = mutant.subject
  raise ArgumentError, "Cannot activate wildcard subjects" if subject.method_name.nil?

  source = mutant.precomputed_activation_source || method_source(mutant)
  target = target_for(subject)
  Henitai::WarningSilencer.silence do
    target.class_eval(source, __FILE__, __LINE__ + 1)
    nil
  end
rescue Unparser::UnsupportedNodeError, SyntaxError
  :compile_error
end

#body_source(mutant) ⇒ String

Parameters:

Returns:

  • (String)


84
85
86
87
88
89
90
91
92
# File 'lib/henitai/mutant/activator.rb', line 84

def body_source(mutant)
  subject_node = mutant.subject.ast_node
  return compile_safe_unparse(mutant.mutated_node) unless subject_node

  body = method_body(subject_node)
  return compile_safe_unparse(Parser::AST::Node.new(:nil, [])) unless body

  body_source_for_mutant(body, mutant)
end

#compile_safe_unparse(node) ⇒ String

Parameters:

  • (Object)

Returns:

  • (String)


211
212
213
214
215
# File 'lib/henitai/mutant/activator.rb', line 211

def compile_safe_unparse(node)
  Unparser.unparse(node)
rescue StandardError => e
  raise Unparser::UnsupportedNodeError, e.message
end

#load_source_file(subject) ⇒ void

This method returns an undefined value.

Parameters:



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/henitai/mutant/activator.rb', line 173

def load_source_file(subject)
  source_file = subject.source_file || source_file_from_ast(subject)
  return unless source_file && File.file?(source_file)

  Thread.current[:henitai_filter_const_warnings] = true
  load(source_file)
  loaded_feature = File.expand_path(source_file)
  $LOADED_FEATURES << loaded_feature unless $LOADED_FEATURES.include?(loaded_feature)
ensure
  Thread.current[:henitai_filter_const_warnings] = false
end

#load_target(subject) ⇒ Object

Parameters:

Returns:

  • (Object)


166
167
168
169
170
171
# File 'lib/henitai/mutant/activator.rb', line 166

def load_target(subject)
  Object.const_get(subject.namespace.delete_prefix("::"))
rescue NameError
  load_source_file(subject)
  Object.const_get(subject.namespace.delete_prefix("::"))
end

#method_body(subject_node) ⇒ Object

Parameters:

  • (Object)

Returns:

  • (Object)


113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/henitai/mutant/activator.rb', line 113

def method_body(subject_node)
  case subject_node.type
  when :def
    subject_node.children[2]
  when :defs
    subject_node.children[3]
  when :block
    block_body(subject_node)
  else
    subject_node
  end
end

#method_source(mutant) ⇒ String

The trailing module_function re-copy handles module_function subjects: module_function copies the (private) instance method onto the module's singleton, and define_method only replaces the instance side — without the re-copy, callers of Module.method keep running the original code and every mutant in such modules falsely survives. The guard checks the pre-injection shape (private instance + singleton method on a non-Class module) so a class-level method that merely shares an instance method's name is never clobbered.

Parameters:

Returns:

  • (String)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/henitai/mutant/activator.rb', line 68

def method_source(mutant)
  method_name = mutant.subject.method_name
  parameters = parameter_source(mutant)
  replacement = body_source(mutant)

  <<~RUBY
    __henitai_module_function__ = !is_a?(Class) &&
      private_method_defined?(:#{method_name}) &&
      singleton_class.method_defined?(:#{method_name})
    define_method(:#{method_name}) do |#{parameters}|
      #{replacement}
    end
    module_function :#{method_name} if __henitai_module_function__
  RUBY
end

#node_location_signatureArray[untyped]?

Parameters:

  • (Object)

Returns:

  • (Array[untyped], nil)


783
# File 'sig/henitai.rbs', line 783

def node_location_signature: (untyped) -> Array[untyped]?

#parameter_source(mutant) ⇒ String

Parameters:

Returns:

  • (String)


126
127
128
# File 'lib/henitai/mutant/activator.rb', line 126

def parameter_source(mutant)
  ParameterSource.new.build(mutant.subject.ast_node)
end

#replace_childObject

Parameters:

  • (Object)
  • (Object)
  • (Object)

Returns:

  • (Object)


780
# File 'sig/henitai.rbs', line 780

def replace_child: (untyped, untyped, untyped) -> untyped

#replace_nodeObject

Parameters:

  • (Object)
  • (Object)
  • (Object)

Returns:

  • (Object)


778
# File 'sig/henitai.rbs', line 778

def replace_node: (untyped, untyped, untyped) -> untyped

#same_node?Boolean

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Boolean)


779
# File 'sig/henitai.rbs', line 779

def same_node?: (untyped, untyped) -> bool

#source_file_from_ast(subject) ⇒ String?

Parameters:

Returns:

  • (String, nil)


185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/henitai/mutant/activator.rb', line 185

def source_file_from_ast(subject)
  ast_node = subject.ast_node
  return unless ast_node

  location = ast_node.location
  return unless location

  expression = location.expression
  return unless expression

  expression.source_buffer.name
end

#target_for(subject) ⇒ Object

Parameters:

Returns:

  • (Object)


55
56
57
58
# File 'lib/henitai/mutant/activator.rb', line 55

def target_for(subject)
  target = load_target(subject)
  subject.method_type == :class ? target.singleton_class : target
end