【Ruby3.1】error_highlight gem について

# はじめに

Ruby3.1で導入された error_highlight について軽く調査したので記事にまとめました。

https://github.com/ruby/error_highlight

- Ruby3.1で導入されたバックトレース中の詳細なエラー位置を表示してくれる組み込みの gem
- デフォルトで有効
    - `--disable-error_highlight` コマンドラインオプションを指定することで無効化できる
- その他注意点
    - 全角文字が含まれると表示されない
    - 1行の文字が長すぎるとうまくハイライトできない
    - irb上では動作しない

## 例

Ruby3.0 までのエラー出力

```ruby:before
$ ruby -e '1.time {}'
-e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError)
Did you mean?  times
```

Ruby3.1 以降のエラー出力

```ruby:after
$ ruby -e '1.time'
-e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError)

1.time
 ^^^^^
Did you mean?  times
```

`^` でエラーの位置が強調されています！！

## error_highlight の API `ErrorHighlight.spot` を利用する

`ErrorHighlight` クラスの `spot` が利用できます。
（この API は実験的なものであり、将来変更される可能性があります。）

```ruby:sample.rb
class Dummy
  def test(_dummy_arg)
    node = RubyVM::AbstractSyntaxTree.of(caller_locations.first, keep_script_lines: true)
    ErrorHighlight.spot(node)
  end
end

pp Dummy.new.test(42)
```

以下のように、 `spot` に渡した node の場所の情報が取得できます。

```ruby
$ ruby sample.rb
{:first_lineno=>8,
 :first_column=>12,
 :last_lineno=>8,
 :last_column=>17,
 :snippet=>"pp Dummy.new.test(42)\n",
 :script_lines=>
  ["class Dummy\n",
   "  def test(_dummy_arg)\n",
   "    node = RubyVM::AbstractSyntaxTree.of(caller_locations.first, keep_script_lines: true)\n",
   "    ErrorHighlight.spot(node)\n",
   "  end\n",
   "end\n",
   "\n",
   "pp Dummy.new.test(42)\n"]}
```

上記の例では、8行目の12から17列目まで。
この情報を元に、以下のように表示しています。

```
pp Dummy.new.test(42) # <- Line 8
#           ^^^^^       <- Column 12--17
```

## エラーのフォーマットを変更する

`ErrorHighlight.formatter` を上書きすることで、エラーのフォーマットが変更できます。
`spot` の結果を受けとるメソッド `message_for` を定義したオブジェクトを作成します。

```ruby:formatter_sample.rb
formatter = Object.new
def formatter.message_for(spot)
  marker = " " * spot[:first_column] + "^" + "~" * (spot[:last_column] - spot[:first_column] - 1)

  "\n\n#{ spot[:snippet] }#{ marker }"
end

ErrorHighlight.formatter = formatter

1.time {}
```

```
$ ruby formatter_example.rb 
formatter_example.rb:10:in `<main>': undefined method `time' for 1:Integer (NoMethodError)

1.time {}
 ^~~~~
Did you mean?  times
```

出力が変化しました！

## 実装

`error_highlight` gem は以下のようなファイル構成になっています。

- error_highlight.rb
- error_highlight/
    - base.rb
    - core_ext.rb
    - formatter.rb
    - version.rb

`base.rb` に以下のような分岐があり、node の種類ごとに処理を行っています。

```ruby
      case @node.type

      when :CALL, :QCALL
        case @point_type
        when :name
          spot_call_for_name
        when :args
          spot_call_for_args
        end
```

`core_ext.rb` で `prepend` により `TypeError` と `ArgumentError` のメソッド上書きすることで、この機能を提供しているらしいです。

```ruby
    TypeError.prepend(CoreExt)
    ArgumentError.prepend(CoreExt)
```

## 参考文献

- [Ruby 3.1.0 リリース](https://www.ruby-lang.org/ja/news/2021/12/25/ruby-3-1-0-released/)
- [サンプルコードでわかる！Ruby 3.1の主な新機能と変更点 - Qiita](https://qiita.com/jnchito/items/bcd9b7f59bf4b30ea5b3#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%82%A4%E3%83%A9%E3%82%A4%E3%83%88error_highlight)
- [Ruby 3.1: error_highlight gemが追加された（翻訳）｜TechRacho by BPS株式会社](https://techracho.bpsinc.jp/hachi8833/2022_02_25/115817)
- [【RubyKaigi発表予告】error_highlight: user-friendly error diagnostics - クックパッド開発者ブログ](https://techlife.cookpad.com/entry/2022/09/01/104312)
- [Ruby 3.1はエラー表示をちょっと親切にします - クックパッド開発者ブログ](https://techlife.cookpad.com/entry/2021/12/07/103547)

## おまけ

Qiita株式会社では Ruby3 新機能の社内勉強会を開催しており、この記事もその一環で記載しました！
Devトークも募集しておりますので、社内勉強会についてや Ruby3 新機能についてお話ししましょう！

https://jobs.qiita.com/employers/qiita-inc/dev_talks/529