Feature #19755
closedModule#class_eval and Binding#eval use caller location by default
Description
Background¶
In Ruby we're very reliant on Method#source_location
as well as caller_locations
to locate source code.
However, code generated via Binding#eval
, Module#class_eval
etc defeat this if called without a location:
Foo.class_eval <<~RUBY
def bar
end
RUBY
p Foo.instance_method(:bar).source_location # => ["(eval)", 1]
The overwhelming majority of open source code properly pass a filename
and lineno
, however a small minority doesn't and make locating the code much harder than it needs to be.
Here's some example of anonymous eval uses I fixed in the past:
- https://github.com/ruby/mutex_m/pull/11
- https://github.com/rails/execjs/pull/120
- https://github.com/jnunemaker/httparty/pull/776
- https://github.com/SiftScience/sift-ruby/pull/76
- https://github.com/activemerchant/active_merchant/pull/4675
- https://github.com/rails/thor/pull/807
- https://github.com/dry-rb/dry-initializer/pull/104
- https://github.com/rmosolgo/graphql-ruby/pull/4288
Proposal¶
I suggest that instead of defaulting to "(eval)"
, the optional filename
argument of the eval family of methods should instead default to: "(eval in #{caller_locations(1, 1).first.path})"
and lineno
to caller_locations(1, 1).first.lineno
.
Which can pretty much be monkey patched as this:
module ModuleEval
def class_eval(code, location = "(eval in #{caller_locations(1, 1).first.path})", lineno = caller_locations(1, 1).first.lineno)
super
end
end
Module.prepend(ModuleEval)
module Foo
class_eval <<~RUBY
def foo
end
RUBY
end
p Foo.instance_method(:foo)
before:
#<UnboundMethod: Foo#foo() (eval):1>
after:
#<UnboundMethod: Foo#foo() (eval in /tmp/foo.rb):10>
Of course the lineno
part is likely to not be fully correct, but the reasoning is that it's better than defaulting to 0 or 1.
Another possiblity would be to include the caller lineo
inside the filename
part, and leave the actual lineo default to 1
:
#<UnboundMethod: Foo#foo() (eval at /tmp/foo.rb:10):1>