Rubyでrescueしたらグローバルなオブジェクトを破壊してたよって話

こんにちは、ydahです。
2021年の12月から株式会社アンドパッドでソフトウェアエンジニアとしてANDPAD検査の開発に携わっています。

昨年、息子が生まれてから夜更かしすることがなくなり、早朝に起きては軽くジョギングをしてから、OSSプログラミングにいそしむのが朝のルーティンになった今日このごろです。

さて本稿では、Rubyの例外処理を眺めていたらrescueがグローバルなオブジェクトを破壊するケースがあったんですよという話と、その対策について話したいと思います。

発生していたケースについて

突然ではありますが以下のコードをご覧ください。 この中にグローバルなオブジェクトを壊してしまうrescueがいます。

# 1
begin
  raise 'foo'
rescue ArgumentError
end

# 2
begin
  raise 'foo'
rescue => ArgumentError
end

# 3
begin
  raise 'foo'
rescue ArgumentError => e
end

正解はクリックで展開 正解は2番です。以下がグローバルなオブジェクトを破壊してしまっています。

begin
  raise 'foo'
rescue => ArgumentError
end

何故そうなるのか

それはrescue => XXXの挙動によるものです。

rescue => XXX
例外処理で例外結果を変数 XXX に代入します。
Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.3 リファレンスマニュアル)

問題のコードではrescueの例外結果でArgumentErrorを上書きしてしまっています。
つまり、ArgumentError の挙動が変わってしまっています

尚、このコードをruby/debugで動かしてみると、以下のように警告が表示されます。

rdbg rescue.rb
[1, 4] in rescue.rb
     1| begin
=>   2|   raise 'foo'
     3| rescue => ArgumentError
     4| end
=>#0    <main> at rescue.rb:2
(rdbg) n    # next command
rescue.rb:3: warning: already initialized constant ArgumentError

↓ ruby/debug で検知している警告であると誤解を与えそうとの指摘があったため追記しました。(2022-8-25 12:45)

この警告はRuby本体側の警告であり、以下でも確認することができます。

ruby -w -e '
begin
  raise 'foo'
rescue => ArgumentError
end
'
-e:4: warning: already initialized constant ArgumentError

対策について

レビューで頑張って弾くというのは最悪手だと思っています。
上述の問題では該当箇所だけ切り出していたので、まだ問題のケースであるか否かはわかるかもしれませんが、実際のコードレビューで気付けるかというと非常に怪しいです。

なので、機械的にこの問題となっているコードを検知するためには、静的解析ツールを使えばいい。
Rubyの静的解析ツールといえば、そう RuboCop です。 github.com

該当するcop(=ルール)は存在しないことはわかっていたので、前述の早朝のOSSプログラミング時間を使って以下のパッチを送りました。 github.com

最初はCustom copでも良いのではないかとも思いましたが、汎用的なルールとして有用そうであったので、RuboCopにcopを追加するパッチを送ることにしました。

尚、現在このパッチはマージされており、RuboCop 1.31でリリースされています。 ドキュメントについてはこちらをご覧ください。

Lint/ConstantOverwrittenInRescue を有効化するには

RuboCopのバージョンが1.31未満の場合

何はともあれバージョンをまず上げます。

bundle exec update --conservative rubocop

RuboCopのバージョンが1.31以上の場合

RuboCop(Extensionも含む)はメジャーバージョンアップまでは、pendingの状態として新規のcopが追加されます。
今回のLint/ConstantOverwrittenInRescueについても同様に、デフォルトではpendingとなっています。

RuboCop//Docs - Lint/ConstantOverwrittenInRescue

そのため別途、.rubocop.ymlに設定が必要です。
Enabling Pending Cops in Bulkの設定をするか、Lint/ConstantOverwrittenInRescueを個別に有効化する必要があります。

AllCops:
  NewCops: enable

# or

Lint/ConstantOverwrittenInRescue:
  Enabled: true

また、このcopはautocorrectionを実装しているので、以下を実行すると全ての違反を自動修正することができます。

bundle exec rubocop -a --only Lint/ConstantOverwrittenInRescue

さいごに

今回はrescueしたらグローバルなオブジェクトを破壊するケースと、対策について紹介しました。
ご紹介した対策で皆様のプロジェクトでも同様に検知することができますので、是非ご活用いただければ幸いです。

アンドパッドは、今年も RubyKaigi 2022 のPlatinumスポンサーとして協賛させていただくことが決まりました!

ブースも出展予定で、本稿の執筆を担当したydahも参加する予定です。
見かけた際には、お気軽に話しかけてくださると非常に嬉しいです。

また、アンドパッドでは一緒に働く仲間を大募集しています。
ご興味を持たれた方はカジュアル面談や情報交換のご連絡をお待ちしております。 engineer.andpad.co.jp