pockestrap

Programmer's memo

Style/EvalWithLocation Copを作った

github.com

github.com

rubocop-jpのissuesにあげたものを、実装してみました。

このCopが対象とする問題

eval1のメソッドには、コードの位置情報を渡すことが出来ます。 この位置情報は主に例外が上がった際のバックトレースの表示に役立ちます。

https://docs.ruby-lang.org/ja/latest/method/Kernel/m/eval.html

fname = 'file name!'
lnum = 100
eval "raise", binding, fname, lnum
# file name!:100:in `<main>': unhandled exception
#  from test.rb:3:in `eval'
#  from test.rb:3:in `<main>'

このように、evalメソッドにはファイル名と行番号を渡すことができます。 これには任意の値を渡すことが可能ですが、evalに直接Stringリテラルを渡す場合、常識的に考えてevalの呼び出し元のファイル名と、そのStringリテラルがある行の番号を渡すべきでしょう。

# test.rb
eval "raise", binding, 'test.rb', 2
# test.rb:2:in `<main>': unhandled exception
#  from test.rb:2:in `eval'
#  from test.rb:2:in `<main>'

これで正しいファイル名がバックトレースに表示されるようになりました。ですが、このままではファイルをリネームしたり、行を足したりしただけでこのバックトレースが壊れてしまいます。

この問題を避けるために、__FILE____LINE__を使用することができます。 これらは疑似変数と呼ばれる特殊な変数です。__FILE__は現在のソースファイル名、__LINE__はそれが書かれた位置の行番号が入っています。

これらを使用すると、先程のコードは次のように書くことが可能です。

# test.rb
eval "raise", binding, __FILE__, __LINE__
# test.rb:2:in `<main>': unhandled exception
#  from test.rb:2:in `eval'
#  from test.rb:2:in `<main>'

なお、ヒアドキュメントを使う場合や、引数の途中で改行を行う場合には注意が必要です。

# ヒアドキュメントを使う場合はコードが __LINE__ を書いた次の行から始まるため、__LINE__ + 1 する必要がある。
eval <<~RUBY, binding, __FILE__, __LINE__ + 1
  def foo
    do_something
  end
RUBY

# evalの引数を改行する場合はコードが __LINE__ を書いた行より前に来るので、__LINE__ - 1 する必要がある。
eval "def foo; do_something; end", binding,
     __FILE__, __LINE__ - 1

このCopはevalメソッドの引数の位置情報が正しく渡されているかを検査します。

このCopは常に守る必要があるの?

いいえ、常に守る必要はありません。 ですが、次に上げるいくつかの例外ケースをのぞいては、守っていたほうが良い効果を得られるでしょう。

eval内のコードで例外が発生しない場合

例えば、以下のコードでは例外が発生しません2

eval <<~RUBY
  def foo
    1
  end
RUBY

例外が発生しないため、ファイル名を足す必要性はそこまで高くありません。 ただし、このようなケースでもファイル名を足しておくことにより、pryなどでメソッドの定義位置を探すことができるようになるメリットがあります。

# test.rb
eval <<~RUBY
  def foo
    1
  end
RUBY
eval <<~RUBY, binding, __FILE__, __LINE__
  def bar
    1
  end
RUBY
[1] pry(main)> require_relative 'test.rb'
=> true
[2] pry(main)> $ foo
Error: Cannot open "(eval)" for reading.
[3] pry(main)> $ bar

From: /tmp/tmp.tOBiT2XfMg/test.rb @ line 7:
Owner: Object
Visibility: private
Number of lines: 5

eval <<~RUBY, binding, __FILE__, __LINE__
  def bar
    1
  end
RUBY

コードを文字列リテラルとして直接渡すわけではない場合

動的にRubyのコードを生成してevalメソッドに渡す場合は、__FILE__などを渡す必要性は薄いでしょう。

def define_cool_method(something)
  code = generate_some_ruby_code(something)
  eval code
end

define_cool_method(42)

このようなケースではevalの行にコードがないため、__FILE____LINE__を指定してもあまり意味がありません。

また、このケースに関してはこのCopは警告を出さないようになっています。

このCopはいつから使えるの?

RuboCopの次のリリースから使うことが出来ます。しばらくお待ちください。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#pseudo


  1. evalの他、class_eval, module_eval, instance_evalの各メソッドのこと

  2. いや、例えばclassがfreezeされているとか考えるとダメだな