Inject a line of code into an existing method at runtime

Viewed 232

At runtime, I would like to inject a line of code at the - # <-- Injection point

class A

  def some_method(a, b = 1, *c, d: 1, **e)
    # <-- Injection point
    
    # Code that does stuff
    puts a
    puts b
    puts c
    puts d
    puts e
  end
end

The reason I need this is so that I can use the Ruby Tracepoint Class to extract some run time information about the method signature.

Specifically the default parameter values in b = 1 and d: 1

I have looked at this question Inspect the default value for optional parameter in ruby's method? where a useful answer exists but want to inject the code that they suggest, dynamically.

I already have existing code that can extracts method signatures from ruby classes.

Example

Given this Class, which is defined with lots of methods using various parameterized signatures.

class A
  def d;                                  end
  def e(a);                               end
  def f(a, b = 1);                        end
  def g(a, b = 1, c = 2);                 end
  def h(*a);                              end
  def i(a, b, *c);                        end
  def j(**a);                             end
  def k(a, *b, **c);                      end
  def l(a, *b, **c, &d);                  end
  def m(a:);                              end
  def n(a:, b: 1);                        end
  def p?;                                 end
  def z(a, b = 1, *c, d: 1, **e);         end
end

I can run the following code to extract signature information. Unfortunately, it fails to resolve the default parameter or named values.

class_definition = MethodSignatures.new(A.new)
class_definition.print

Issues are Red

co

2 Answers

The reason I need this is so that I can use the Ruby Tracepoint Class to extract some run time information about the method signature.

Specifically the default parameter values in b = 1 and d: 1

TracePoint allows you to fetch information without patching the method. You just have to set up a :call hook and then call the method:

TracePoint.trace(:call) do |tp|
  tp.parameters.each do |type, name|
    value = tp.binding.local_variable_get(name)
    puts format('%8s: %s = %s', type, name, value.inspect)
  end
end

A.new.some_method(123)

Output: (omitting the method's output)

     req: a = 123
     opt: b = 1
    rest: c = []
     key: d = 1
 keyrest: e = {}

Prior to Ruby v2.0 one would redefine A#some_method after aliasing the original A#some_method.

class A
  def some_method(a, b, c)
    puts a
    puts b
    puts c
  end
end
A.public_send(:alias_method, :old_some_method, :some_method)
A.define_method(:some_method) do |a,b,c|
  puts "a=#{a}, b=#{b}, c=#{c}"
  old_some_method(a,b,c)
end
A.new.some_method(1,2,3)
a=1, b=2, c=3
1
2
3

A better way, however, is to define the new method A#some_method in a module and then Module#prepend that module to the class A. prepend made its debut in Ruby v2.0.

module M
  def some_method(a,b,c)
    puts "a=#{a}, b=#{b}, c=#{c}"
    super
  end
end
A.prepend M
A.new.some_method(1,2,3)
a=1, b=2, c=3
1
2
3

A.prepend M gives the methods of M priority over those of A.

A.ancestors
  #=> [M, A, Object, Kernel, BasicObject]

shows this to be the case and also explains the role of super in the method M#some_method.

Related