I’ve been digging into active_support to see how config_accessor was working. This is the code I found:
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:
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:
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.