In Ruby, why should class_eval and eval with a heredoc string get __FILE__ and __LINE__ + 1, and what do backtraces show otherwise?
answer
- strings have no source file
- (eval at FILE:LINE) default
- heredoc body starts next line
- Style/EvalWithLocation
- 3.0: binding stops supplying location
basics
~20 sString code has no file of its own. The file and line arguments set what backtraces, SyntaxError messages and FILE report; without them Ruby 4.0 labels the code (eval at caller.rb:N) and counts lines from 1 inside the string.
solid answer
~40 s`class_eval`, `module_eval`, `instance_eval` and `eval` take an optional file name and starting line for string code. Ruby uses them in backtraces, in `SyntaxError` messages and for `__FILE__` and `__LINE__` inside the string. Passing `__FILE__, __LINE__ + 1` with a heredoc makes each frame point at the real line of the template, because the heredoc body starts on the line after the call. Without them, Ruby 4.0 reports the file as `(eval at lib/money.rb:3)`, the call site, and the line as an offset inside the string, so you must count lines by hand. Since Ruby 3.0, `eval(code, binding)` no longer borrows the binding's location either. RuboCop's `Style/EvalWithLocation`, enabled by default, flags string-literal evals missing these arguments and checks the offset.
code
ruby · 10 lines# lib/money.rb
class Money
%w[cents dollars].each do |unit|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{unit}_or_zero # def cents_or_zero
@#{unit} || 0 # @cents || 0
end # end
RUBY
end
endgo deeper
Recall the idiom class_eval <<~RUBY, FILE, LINE + 1 and that it makes errors in generated methods point at the real file.
Explain why the offset is + 1 for a heredoc, what an (eval at ...) frame means, and that the arguments label run-time frames too.
Enforce location arguments with Style/EvalWithLocation, add expanded-form comments on interpolated defs, and read an unlabelled eval frame back to its source.
Make location arguments and expansion comments part of the definition of done for any code generator, since unreadable backtraces cost incident time.
## Why string code needs a location Code loaded from a file carries its file name and line numbers into every backtrace frame. Code that arrives as a **string** has no file. `Kernel#eval`, `Module#class_eval` / `module_eval` and `BasicObject#instance_eval` therefore accept two optional arguments after the code — for `eval`, after the binding — that say which file and starting line the string should pretend to come from: - `klass.class_eval(code, filename, lineno)` - `obj.instance_eval(code, filename, lineno)` - `eval(code, binding, filename, lineno)` Those values show up in three places: 1. **Backtraces** of exceptions raised by methods the string defined. 2. **`SyntaxError` messages** when the string does not parse. 3. The values of **`__FILE__` and `__LINE__`** inside the evaluated code. ## Why __LINE__ + 1 The common idiom is a heredoc: ```ruby class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{unit}_or_zero @#{unit} || 0 end RUBY ``` `__LINE__` is the line of the `class_eval` call itself. The heredoc's first line of code sits one line below, so `__LINE__ + 1` makes line 1 of the string map onto its real line in the file. A frame inside `cents_or_zero` then points at the `@#{unit} || 0` line of the template, which is exactly where you want your editor to jump. ## What you get without them In Ruby 4.0, string code evaluated without a file name is labelled with the **call site**: the file becomes `(eval at lib/money.rb:3)`, meaning "evaluated by the call on line 3 of lib/money.rb", and line numbers count from 1 inside the string. A frame such as `(eval at lib/money.rb:3):2:in 'Money#cents_or_zero'` is decodable, but you have to open the file, find the call and count down by hand, and several evals from one loop share the same label. | Call | File shown in the backtrace | Line shown | |---|---|---| | `class_eval(code)` | `(eval at lib/money.rb:3)` | line inside the string | | `class_eval(code, __FILE__, __LINE__ + 1)` | `lib/money.rb` | real line in the file | | `eval(code, b)` | `(eval at ...)` naming the eval call | line inside the string | | `eval(code, b, __FILE__, __LINE__)` | the file passed | the line passed | The third row is a version detail: before Ruby 3.0, `eval(code, binding)` reported the binding's file and line, which pointed at the place the binding was created rather than at the evaluated text. Ruby 3.0 stopped borrowing the binding's location; you pass the location explicitly or accept the eval label. ## Enforcing it RuboCop's **`Style/EvalWithLocation`** cop, enabled by default, checks `eval`, `class_eval`, `module_eval` and `instance_eval` calls whose code is a string literal. It reports calls with no file and line, a wrong file (anything other than `__FILE__`), or a wrong offset (for a heredoc, anything other than `__LINE__ + 1`), and it can autocorrect them. It does not check a string held in a variable, and for `eval` it also asks for a binding. A second cop, `Style/DocumentDynamicEvalDefinition` (pending by default), asks for a comment showing what an interpolated definition expands to. ## Practical rules - Always pass `__FILE__, __LINE__ + 1` with a heredoc, and `__FILE__, __LINE__` when the code string starts on the same line as the call. - Keep one method per heredoc when you can, so a line number leads to one method. - Add a comment next to interpolated lines showing the expanded form, such as `# def cents_or_zero`, so a reader grepping for the method name finds it. - Prefer a block when you do not need a string at all; block code carries its own file and line automatically. ## Mistakes to avoid - Passing `__LINE__` with a heredoc, which shifts every reported line up by one. - Believing the file and line arguments only affect syntax errors; they also label run-time backtraces of the defined methods. - Expecting `eval(code, binding)` to report the binding's file on current Ruby.
- Which RuboCop cop enforces the location arguments, and what does it not catch?`Style/EvalWithLocation`, enabled by default. It checks `eval`, `class_eval`, `module_eval` and `instance_eval` when the code is a string literal, flags a missing or wrong file and line offset, and can autocorrect. It does not inspect code passed in a variable, and for a bare `eval` it also asks for a binding.
- Do the file and line arguments affect only syntax errors?No. They set the location for everything compiled from the string, so methods defined there report that file and line in run-time backtraces too, and `__FILE__` and `__LINE__` inside the string return them.
saying these in an interview costs you the question
- Passing __LINE__ rather than __LINE__ + 1 with a heredoc is fine
- File and line arguments only matter for SyntaxError messages
- eval with a binding reports the binding's file in Ruby 4.0
- Style/EvalWithLocation also checks code held in a variable
- Unlabelled eval frames show the real file and line anyway