こんにちは、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.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も参加する予定です。
見かけた際には、お気軽に話しかけてくださると非常に嬉しいです。
アンドパッドは、今年も RubyKaigi 2022 のPlatinumスポンサーとして協賛させていただくことが決まりました!https://t.co/N9QXfGlOmZ
— ANDPAD (アンドパッド)開発部 (@andpad_dev) 2022年7月29日
また、アンドパッドでは一緒に働く仲間を大募集しています。
ご興味を持たれた方はカジュアル面談や情報交換のご連絡をお待ちしております。
engineer.andpad.co.jp