Ruby Tidbit: __LINE__ and heredocs

Comments

You’re probably familiar with LINE keyword in Ruby. Wherever it’s used, it refers to the line number in the current file. It says so right here in the Ruby docs.

QUICK! What’s the output of the following code?

#!/usr/bin/env ruby
 
class LineDemo
  def self.process(text, lineno)
    # do important processing on text
    puts lineno
  end
end
 
LineDemo.process <<TEXT, __LINE__
Here is some text on which we will do some important processing.
Here, also, is some important text to process.
TEXT
# All done!

The answer is obvious. Of course, it prints 13.

Wait, what?

Yes, it prints 13. Why not 10? Because Ruby does interesting things when parsing heredocs.

When you pass a heredoc to a method as a parameter, Ruby will, in general, stop what it’s doing, read through the file until it encounters the token that terminates the heredoc (TEXT, in our case), and then pick up where it left off.

Sort of.

You see, by passing __LINE__ to our method, we asked Ruby to give us the value of ruby_sourceline (see parse.y in the Ruby source tree). It just so happens that this value is equal to the line that contains the token which ends the heredoc. Note that I added a comment at the end of the script — interestingly, if Ruby encounters the end of the file on the same line that terminates the heredoc, __LINE__ returns 10, as you might have expected, and of course if the heredoc came after the __LINE__ keyword, then the line number wouldn’t be incorrect, either. This only occurs if the heredoc comes before __LINE__.

However, that problematic ordering of parameters is necessary in a very common use case for __LINE__, namely, evals, such as instance_eval:

instance_eval(string [, filename [, lineno]] ) → obj
instance_eval {| | block } → obj
Evaluates a string containing Ruby source code, or the given block, within the context of the receiver (obj). In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj’s instance variables. In the version of instance_eval that takes a String, the optional second and third parameters supply a filename and starting line number that are used when reporting compilation errors.

What’s a Rubyist to do? Trick Ruby’s parser into evaluating __LINE__ before things get all wacky-like. It just so happens that doing some math against __LINE__ will do so, since the computation will need to take place before the method is dispatched. This is why you will commonly see code like this in Rails:

activemodel/lib/active_model/callbacks.rb:

def _define_before_model_callback(klass, callback) #:nodoc:
  klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
    def self.before_#{callback}(*args, &block)
      set_callback(:#{callback}, :before, *args, &block)
    end
  CALLBACK
end

The operation forces the line number to be computed first, and the + 1 tells class_eval that while evaluating, line numbering starts at the line just below the method call, which is the first line of heredoc code. You could call any method against __LINE__, such as __LINE__.to_i, and get the first behavior. It just so happens that in the typical eval use case, we’ll want the addition anyway, so that’s what we’ll normally use.

Tying it all together

To summarize: here’s an example script that demonstrates the different behaviors.

#!/usr/bin/env ruby
 
class LineNumber
  def self.before lineno, param
    puts "line number is #{lineno}"
  end
 
  def self.after param, lineno
    puts "line number is #{lineno}"
  end
end
 
print "before with __LINE__: "
LineNumber.before __LINE__, <<TEXT
This is some text.
As is this.
This, also, is some text.
TEXT
 
print "after with __LINE__: "
LineNumber.after <<TEXT, __LINE__
This is some text.
As is this.
This, also, is some text.
TEXT
 
print "after with __LINE__ + 0: "
LineNumber.after <<TEXT, __LINE__ + 0
This is some text.
As is this.
This, also, is some text.
TEXT
# fin

The output of this script:

before with __LINE__: line number is 14
after with __LINE__: line number is 25
after with __LINE__ + 0: line number is 28

That concludes this look at an interesting and odd little corner of Ruby. Now, the next time you tack on + 1 to an eval’s __LINE__ parameter, you’ll know exactly why you’re doing so.

comments powered by Disqus