アプリケーションをRuby3にあげるときにやること


今年のクリスマスにRubyのメジャーバージョンが代わり、ついにRuby3.0.0が世に出ましたね。
Ruby2系でこの言語に触れた方は多いのではないでしょうか。（その一人です）

初のメジャーアップデートということで、仕事で関わっているアプリケーションや自分が管理しているgemを、ためしにRuby3にあげてみよう・・・と試みたところでハマった点について書いていこうと思います。

さあ、レッツ `rbenv global 3.0.0`！

## TL;DR

直前・直後のバージョンでCIを回すことが大切ですが、いくつか注意点があります。

- Ruby2.7.2以降では、デフォルトで警告が表示されない
- テスト環境に環境変数 `RUBYOPT='-W:deprecated'` をつけておこう
- バージョンをまたいだマトリックスビルドをする場合、テストヘルパーに `Warning[:deprecated] = true` が便利

おまけとして、実際の対応で遭遇した現象の調査結果と、具体的な対処法も書いておきます。

## 直前バージョンが警告を出さない
Ruby3へバージョンを上げる前に行うことは、Ruby2.7にまず引き上げた上で、表示されるDeprecation Warningを対応していくことが基本です。
が、現時点で最新のRuby2.7.2では、**Deprecation Warningは出てきません。**

2.7.1までは普通にkeyword argumentをはじめとした警告が出ていたので、gemの更新でいい感じに収まったのか、はたまた廃止時期が伸びたのか。

・・・と、勘違いしそうですが、試しにRuby3に挙げてみると、警告が出なかっただけでしっかり死ぬことがわかります。（ええ、わかりました）
すでに[こちらのエントリ](https://qiita.com/pink_bangbi/items/2901e9ba9d7fb6ecb670)などで指摘がされていますが（感謝・・！）、deprecation warningはデフォルトで抑制されています。

公式はこちら。
https://www.ruby-lang.org/ja/news/2020/10/02/ruby-2-7-2-released/
https://bugs.ruby-lang.org/issues/16345

いきなり死んだのでﾌｧｯ?!ってなりました（語彙）

## Deprecation Warningを表示する方法

というわけで、まずアプリケーションをRuby2.7.2に引き上げた上で警告を表示させておきましょう。

### 通常のアプリケーション

環境変数で設定できます。
とはいえ開発環境や本番環境で常に出てくるのはさすがにわずらわしいので（とくにRails6.1未満では大量にログが出ます）、テスト環境だけ入れておくのがいいと思います。

たとえばRSpecだとして、以下のようにやるのがいいでしょう。

```bash
$ RUBYOPT='-W:deprecated' bundle exec rspec
```

circleciなどの設定ファイルや、ローカルの `.env` に書いておくことで、CIを回すたびに警告を確認することができます。

もしテストがかかっていないRakeタスクなどがあった場合も、この環境変数を設定して開発環境で回してみるという小技が使えそうです。

### 複数バージョンをテストしている場合

このRUBYOPTですが、2.6.6以前のRubyでテストする場合に問題になります。

```bash
$ rbenv local 2.6.6
$ RUBYOPT='-W:deprecated' bundle exec rspec
Traceback (most recent call last):
/Users/xxxxx/.rbenv/versions/2.6.6/bin/ruby: invalid option -:  (-h will show valid options) (RuntimeError)
```

以前のバージョンまではデフォルトで警告オンだったため、**そんなオプションねえよ！！** という怒られだと思われます。(逆に `no-deprecated` というオプションはある）

過去も含めた複数バージョンへのサポートを提供しているgemにおいて、CI上でのマトリックスビルドが走る際にこれが問題になります。

```yaml
# .travis.ymlでよくある形
language: ruby
env:
  global:
    - RUBYOPT='-W:deprecated'
rvm:
  - 2.7.2
  - 2.6.6 # 異常終了
  - 2.5.8 # 異常終了
```

バージョンごとに設定を出し分けたいときは、テストヘルパーなどにバージョンごとの設定を書いておくのがいいかと思います。

```ruby
# spec/spec_helper.rb
if RUBY_VERSION >= '2.7.2'
  # NOTE: https://bugs.ruby-lang.org/issues/17000
  Warning[:deprecated] = true
end
```

個人で運営しているOSSなどは、これを入れることで対応しました。

・・というか他にやり方が思いつかなかった。
こんな方法あるよ、といういい知恵をご存知の方、教えていただけると幸いです。

## Ruby3へあげるときにやること

ここから下は当たり前の手順でしかないので、わかる方は飛ばしてくださいませ。（一応タイトルにオチをつけたい）

### Ruby2.7.2で警告をなくす

上で書いた手順で警告がでないことを確認します。
注意点としては、自分たちのコード修正が必要な部分にくわえて、gem由来の警告があるというところです。

観測範囲でいうとキーワード引数の受け渡しがうまく行ってないパターンが多そうです。というか圧倒的にソレですね。

#### キーワード引数の警告が出たら

典型的には以下のようなメッセージが出るかと思います。

```bash

/Users/xxxxx/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/hoge-1.0.0/lib/hoge.rb:38: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/xxxxx/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/hoge-1.0.0/lib/hoge.rb:76: warning: The called method `foo' is defined here
```

最初の行は呼び出し側を、次の行は呼び出されているメソッド定義の行を指しています。
どちらかが原因で、キーワード引数の型（？）がマッチしてないことが考えられます。

##### 切り分け

アプリケーション側の対応でなおるものは、主に以下のパターンです。

- 両方の警告がアプリケーションコードの場合は、自分たちのコードを見直すことになります。
- また、最初の警告（呼び出し側）だけがアプリケーションコードの場合は、そのコードをなおすことで対応することができる場合があります。（できないパターンもある）

その逆（呼び出される側）がアプリケーションで、呼び出し側が依存gem・・ということはないと思われるのでこの２通りかなと思っています。

これに当てはまらないものはgem側の問題ということになります。
上の例は両方とも`hoge`というgemの内部を指しているように見えるので、そういう場合はgemの問題であることがわかります。
これは依存するgem間で起こることもありえます。（例えば、graphql-rubyのAPIをたたくbatch-loaderでこのケースを目撃しました）

#### アプリケーション由来の警告をなおす

これはふつうに頑張って直します。ついでにキーワード引数とハッシュを無遠慮に混ぜた反省もします。

多いのは、メソッドのデリゲーションで失敗しているパターンや、キーワード引数をとるメソッドにハッシュを渡している（その逆）パターンの印象です。

粗雑すぎる例ですが、たとえば

```ruby
class Book
  def initialize(title:, author:)# キーワードを取るメソッド
    # ..snip
  end
end

params = { title: 'Romeo and Juliet', author: 'William Shakespeare' }
Book.new(params)   # ハッシュ渡しはArgumentError
Book.new(**params) # => OK
```

とか、

```ruby
class User  < ApplicationRecord
  def self.import(*args, **options)
    # ...snip
  end
end

class UserImportTask
  def self.import(*args) # 別途キーワード引数（**options）を分離して引き受ける必要がある
    User.import(*args)   # ここでキーワード引数（**options）も委譲する必要がある
  end
end

UserImportTask.import(@records, validate: true)  # キーワード引数の受け渡し方がRuby3で変わり、optionsに渡らなくなる
```

ようするにキーワード引数渡しなのか、ハッシュオブジェクト渡し（ポジショナル引数）なのかを明確に区別する必要があります。
詳細な仕様や、パターンごとの修正方法については、公式に詳しくまとまっています。

https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

#### gem由来の警告に対処する

Ruby2.7.1系まで警告が出ていたこともあり、gem側がすでに対応しているケースが結構あります。
まずは `bundle update foo` で警告状態のgemを最新バージョンにあげることが必要です。

代表例として、例えばRailsは6.1以上にする必要がありますね。

その上で警告が止まらない場合はいくつかの打ち手があります。

##### 1. gem自体は対応しているが、依存関係により最新にあげられない場合

まずコンフリクトしているgemを洗い出します。
手っ取り早いのは、Gemfileに明示的にバージョン指定をしてしまうことです。

```ruby
gem 'foo', '~>2.0'
```

コンフリクトがでたら、fooのバージョンアップをブロックしているgemがわかりますね。
そのgemを外してbundle installして無事通過する・・ということになれば、そいつがビンゴです。

その際はブロッカー側のgemについてバージョンをあげるか、外すのか・・という感じで、再度この1-4を検討することになります。

##### 2. gem自体が対応しているが、まだバージョンが切り替わってない場合

一旦待つしかなさそうですが、もしテストを回すだけ回してみたいということであれば、最新マスターから引いてしまうこともできますね。

```ruby
gem 'foo', git: 'https://github.com/hoge/foo.git'
``` 

##### 3. gemは未対応だが、すでに対応するPRが出ている場合

応援スタンプを押したりコメントしたりして、メンテナーにマージしてもらう後押しをするというのが吉です。
が、同じくいったんCIだけ見ておきたい・・ということなら、PRがかかっているブランチから引っ張ることが可能です。（＊自己責任で）

```ruby
gem 'foo', git: 'https://github.com/a_contributor/foo.git', branch: 'ruby-3'
``` 
それでも動かなければ、「このPRだとXXというケースが足りない」というポイントを、再現ケースを添えて指摘してあげましょう。
うまく協力し合えると、自分たちのアプリケーションのケースに対応してもらうことができる可能性が高まります。

##### 4. gemが未対応で、対応するPRがない場合

おめでとうございます。コントリビュートナントカですね :tada:
イシューも出てないようなら立てておき勇者を待つか、もし可能ならPRを出していきましょう。

手前味噌ですが、昨年のアドベントで[こんなのを書いたので](https://qiita.com/fursich/items/20b35d564e62fa0530b5)ご参考になればﾄﾞｿﾞ

・・・と、自分でいっておいてなにもやらないのもアレなので、今回ひっかかったgemに[Ruby3対応のためのPR](https://github.com/pry/pry-stack_explorer/pull/55)を奉納しておきました。（無事マージ済み）

## そしてRuby3へ

ここまで全て対応できたらあとはRubyをあげるだけですね。

GemfileのRubyバージョン指定を `3.0.0` にします。
ローカル開発なら`rbenv local 3.0.0`、Dockerfileならベースイメージを`FROM: ruby-3.0.0` とかやってみて、`bundle install`。

ここでおそらく未対応のgemだったり、バージョンコンフリクトの関係で落ちることがありますが、その時は落ち着いて前チャプターの1-5を繰り返します。
もしいけそうなら、そのままテストを回してみましょう :gem:

たとえばCircleCIで`cimg/ruby:3.0`とかやってキレイに通るようなら、勝利はすぐそこですね。

## Ruby3.0.0対応状況（観測範囲）

試験的に、開発現場のアプリケーションをオールグリーンになるまで通してみました。まだ開発歴1年少しの若いプロダクトでしたが、思ったより結構落ちた。

ついでに自作OSSなどを対応させる作業をしたので、調査したgemをあわせて列挙しておきます。（2020/12/30現在）

* rails
  - Rail6.1.0以上で対応。おびただしい数の警告が消えていて感謝しかないやつ〜。
* shrine
  - 最新masterブランチで対応（まだバージョンは切られてない）
* redis-namespace
  - 最新masterブランチで対応（まだバージョンは切られてない）
* rspec
  - 3系初期バージョンだと落ちるが、3.10.0では概ね対応。下のrspec-mockで一部に非対応機能が残っている
* rspec-mock
  - 一部のテストケースが落ちる（後述）。現時点は未対応
* binding_of_caller
   - 1.0.0で対応済み。下記gemの依存によりアップデートを押さえられていた
* pry-stack-explorer
   - ~~未対応（binding_of_callerのバージョンを制限している）~~
   - (20/12/31追記）対応PRがマージされたのでmasterブランチで通る
* batch-loader
  - [こちらに立っているPR](https://github.com/exAspArk/batch-loader/pull/71)で、（手元のアプリケーションは）動きました
* caxlsx
  - おなじく[こちらに立っているPR](https://github.com/exAspArk/batch-loader/pull/71)で、一応動いた

#### ※ rspec-mockについて

RSpecで`and_call_original`をつかったモックを利用している場合、RSpecが死ぬケースがあります。

元のメソッドがキーワード引数を利用している場合に引っかかるようです。

```ruby
class User < ApplicationRecord; end

it  'calls #update with params' do
  expect(user).to receive(:update).with(name: 'Taro').and_call_original # キーワード引数を元のメソッドに委譲することに失敗する

  user.update(name: 'Taro')
end
```

このケースだと、「`and_call_original` いらんやん」で解決するのですが、これを利用しているgemが大量に踏んでいて困りました。

Ruby3.0対応のCIを通すPRを出したいが、これのせいでCIすら通せない・・

ざっとみた感じ、[こちらのPR](https://github.com/rspec/rspec-mocks/pull/1324)や[こちらのPR](https://github.com/rspec/rspec-mocks/pull/1385)が立っていてメンテナ中心に改修が進んでいるが未マージです。
調べた中ではもっとも難航している雰囲気なので、腕に覚えのある皆々様においては、いいねスタンプやテストケースの提供など、ぜひご協力お願いしたいところです🤲
（とくに回し者ではない）

## （最後に）なぜバージョンをあげるのか？

そこに3.0があるから・・

いや、個人的には３つくらい「いいこと」があるとおもっています。

#### 開発現場への貢献
Ruby3への移行により高速化が見込め、ユーザーの利便性は向上すると思われます。メソッドが便利になったり、その気になれば型も使えたりして、開発体験もよくなるでしょう。（おもな変更点は[この神記事](https://zenn.dev/jnchito/articles/24e0bd7fd1045d)をご参照あれ）
仮に予習程度の実験をするだけでも、バージョンアップのためのブロッカーを把握できるため、本格的な移行作業に向けて役に立ちますね！

ここで開発するアプリケーションに起因する問題が見つかれば、今後の開発の中で徐々にでも改善を施していけます。
とくに歴史の長いアプリケーションにおいてこの「段差」はそれなりに大きい可能性があるので、プロダクトが今後の進化に取り残されることを防ぐために、問題となるポイントを把握しておくことは意味がありそうです。

#### 個人としての技術が高まる
Ruby2時代に始まったプロダクトは多く、Ruby3へのメジャーアップデートは当面あちこちで起こりそうです。
アップデートに伴う気づきや、得られた知見などは、他の開発現場でも重宝されそう・・・な気がします（ゲス顔で）。きっと。知らんけど。
色々なイシューを掘ることで、Rubyや周辺gemの仕様にも詳しくなれるだけではなく、OSSにコントリビュートするチャンスもふえますね。

#### Rubyコミュニティーへの貢献
Ruby3へのマイグレーションプロセスはかなり配慮されているものの、やはり進化の過程として、マイナーバージョンとは異なる破壊的変更は存在します。
Ruby2.7発表から1年がたったものの、世の中の多くのgemの対応が完了するまでには思ったより時間がかかりそうです。結構ひっかかってびっくりした。
（特に個人OSSなどはメンテナーのキャパシティの問題もありますよね）

型ファイルの定義などの貢献をしていくにしても、まず世の中のgemがRuby3に追いついていくことが最初の一歩です。
問題をみつけて早めにイシューやPRを立てたり、内容的によさそうなものに応援のスタンプを押すなどで、移行を促すことは、お世話になってるRubyコミュニティへの貢献になりそうです。

というか、三番目が一番いいたかった。

というわけで、2021年は新たな気持ちで`rbenv global 3.0.0！`やってきましょう :bamboo: