David Geismar
David Geismar

Reputation: 3422

Stack Level too deep with method_added ruby

I have created a module to hook methods before a method call in a class :

module Hooks

def self.included(base)
 base.send :extend, ClassMethods
end


module ClassMethods
  # everytime we add a method to the class we check if we must redifine it
  def method_added(method)
    if @hooker_before.present? && @methods_to_hook_before.include?(method)
      hooked_method = instance_method(@hooker_before)
      @methods_to_hook_before.each do |method_name|
        begin
          method_to_hook = instance_method(method_name)
        rescue NameError => e
          return
        end
        define_method(method_name) do |*args, &block|
          hooked_method.bind(self).call
          method_to_hook.bind(self).(*args, &block) ## your old code in the method of the class
        end
      end
     end
   end

  def before(*methods_to_hooks, hookers)
   @methods_to_hook_before = methods_to_hooks
   @hooker_before = hookers[:call]
  end
 end
end

I have included the module in one of my class :

require_relative 'hooks'

class Block
  include Indentation
  include Hooks
  attr_accessor :file, :indent
  before :generate, call: :indent
  # after  :generate, call: :write_end

  def initialize(file, indent=nil)
    self.file = file
    self.indent = indent
  end

  def generate
    yield
  end
end

this Block class is parent to another class that is implementing its own version of the generate method and that is actually implemented.

When my code is running method_added is actually called with method :generate as argument in some kind of infinite loop. I can't figure out why method_added is caught in this infinite loop. Do you know what's wrong with this code ? Here's a link to the full code : link to code on github

Upvotes: 1

Views: 157

Answers (1)

Tom Lord
Tom Lord

Reputation: 28305

You've caused an infinite recursion because you're calling define_method inside method_added. The stack trace (which you haven't provided unfortunately) should show this.

A slightly ugly workaround to resolve this could be to explicitly set a variable (e.g. @_adding_a_method) and use it as a guard clause for method_added:

module ClassMethods
  def method_added(method)
    return if @_adding_a_method

    if @hooker_before.present? && @methods_to_hook_before.include?(method)
      # ...

      @_adding_a_method = true
      define_method(method_name) do |*args, &block|
        # ...
      end
      @_adding_a_method = false

      # ...
    end
  end
end

However, taking a step back, I'm not really sure what this module is trying to achieve. Couldn't you just achieve this with Module#prepend instead of this meta-programming?

This code reminds me of what you might find in an old Ruby 1.8/1.9 tutorial on advanced meta-programming techniques; Module#prepend makes such workarounds redundant for the most part.

Upvotes: 6

Related Questions