Using __LINE__ in dynamic methods

I’ve been digging into active_support to see how config_accessor was working. This is the code I found:

      def config_accessor(*names)
        options = names.extract_options!

        names.each do |name|
          reader, line = "def #{name}; config.#{name}; end", __LINE__
          writer, line = "def #{name}=(value); config.#{name} = value; end", __LINE__

          singleton_class.class_eval reader, __FILE__, line
          singleton_class.class_eval writer, __FILE__, line
          class_eval reader, __FILE__, line unless options[:instance_reader] == false
          class_eval writer, __FILE__, line unless options[:instance_writer] == false
        end
      end

What it is doing is fairly straight forward. It is creating setters and getters for attributes of a config object. The setter definition being held in the variable writer and the getter in reader.

The thing that really piqued my interest, was the construction of reader, writer and line. In particular, what line was doing.

After a little reading around I learnt that line is used to ensure the exception backtrace points to the place where the method is being defined. So for reader, the back trace will point to the place where the method was defined, and not to where class_eval was called.

To help me get my head around this, I wrote this:

module LinePlay

  def self.raise_error_here
    method, line = "def self.raise_here; raise; end", __LINE__
    make_into_method(method, line)
  end

  def self.raise_error_there
    method, line = "def self.raise_there; raise; end", __LINE__
    make_into_method(method)
  end

  def self.make_into_method(method, line = nil)
    if line
      module_eval method, __FILE__, line
    else
      module_eval method
    end
  end

  def self.rescue_error(method)
    begin
      send(method)
    rescue => e
      puts "\nOutput for #{method}"
      puts e.backtrace
    end
  end

  raise_error_here
  raise_error_there

end

LinePlay.rescue_error(:raise_here)
LinePlay.rescue_error(:raise_there)

This dynamically creates two methods via module_eval: LinePlay.raise_here, and LinePlay.raise_there. I called module_eval within make_into_method, to separate the definition method from the eval method. It then calls the two dynamically defined methods, rescues the exceptions and outputs the backtraces.

This is the output:

Output for raise_here
line_play.rb:4:in `raise_here'
line_play.rb:23:in `rescue_error'
line_play.rb:35:in `<main>'

Output for raise_there
(eval):1:in `raise_there'
line_play.rb:23:in `rescue_error'
line_play.rb:36:in `<main>'

For raise_here, the line number where that method is defined, is passed to module_eval. If you look at the resulting backtrace, this shows line 4 as the source of the error. That is, the line where the method causing the error, was defined.

For raise_there, __FILE__ and line number are not passed to module_eval. In this case the backtrace points to where class_eval was called and not where it was defined.

So using this technique generates much more useful error reporting. A neat trick I think.

However, have you spotted the error in the original code? The variable line for reader is overwritten on the next line when writer is assigned. That means backtraces for both reader and writer will point at the writer definition. This error has since been corrected. See this commit.

This entry was posted in Ruby. Bookmark the permalink.