Ruby 2.7.0 で導入された Arguments Forwarding

Ruby 2.7.0 で導入された Arguments Forwarding について、RuboCop でスタイル部署の新たな Cop として開発中なので、Cop 開発の過程で得ている知見やらを進捗を記しておきます。

Arguments Forwarding とは

def foo(...) といった形で引数への構文拡張がされています。以下のコードを動かしてみるとイメージがつくかもしれません。

% cat arguments_forwarding.rb
def foo(...)
  bar(...)
end

def bar(str)
  puts "hello, #{str}"
end

foo('world') # => hello, world

... でイメージがつくかもしれませんが、サンプルではひとつの引数のみですが、実際は複数の引数をそのままフォワードします。

効能

pocke さんによる RuboCop への Feature Request に詳しいです。

github.com

キーワード引数の分離への警告 (Ruby 3.0 ではエラー) の回避の側面を提案に含めているのが興味深かったです。実際に検知できるケースがアプリケーションレイヤーでどの程度あるかはわかりませんが、Ruby 3.0 へのマイグレーターのひとつになるかと思い着手することにしました。

構文上の注意

Arguments Forwarding を使ったメソッド定義では引数の括弧は省略できません。

def foo ...
end

以下のような構文エラーになります。

% ruby -vc foo.rb
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin17]
foo.rb:1: warning: ... at EOL, should be parenthesized?
foo.rb:1: syntax error, unexpected ..., expecting ';' or '\n'
def foo ...

つまり以下のようなコードを Arguments Forwarding に置き換える場合は括弧が必須になります。

# Before
def foo *args, **kwargs, &block
end

# After
def foo(...)
end

一方でフォワーディング先のメソッド呼び出しは括弧を省略できます。

def foo(...)
  bar ...
end

ただし以下のような警告が発生するため括弧をつけることが好ましいでしょう。 ただし以下のような警告が発生します。追記した内容を踏まえて括弧をつけることを一考するとよいでしょう。

% ruby foo.rb
foo.rb:2: warning: ... at EOL, should be parenthesized?

2020年1月15日追記

こちらについて pocke さんから括弧の有無で意味合いが変わるというフィードバックをいただく。

Parser gem でも試して見たところ以下のように結果が変わりました。pocke さん、いつもありがとうございます!

% ruby-parse -e 'def foo(...); bar ...; end'
(def :foo
  (forward-args)
  (erange
    (send nil :bar) nil))

% ruby-parse -e 'def foo(...); bar(...); end'
(def :foo
  (forward-args)
  (send nil :bar
    (forwarded-args)))

(追記ここまで)

また想像にかたくないですが、メソッド定義が Arguments Forwarding でない場合に、フォワード先のみ ... にしていると構文エラーです。

def foo(*args, **kwargs, &block)
  bar(...)
end

def bar(*args, **kwargs, &block)
end

foo('world', num: 42)

実行した内容は以下です。

% ruby foo.rb
foo.rb:2: unexpected ...

Style/ArgumentsForwarding cop

以下で開発中の Ruby 2.7 以上を対象とした現在開発中の新しい Cop です。

github.com

pocke さんのイシューにあるとおり以下のケースを Arguments Forwarding にするとブロックまで引き渡されるという振る舞いの破壊が入ります。

# We can replace the parameter with `...`, but it will change the behavior.
# Because `...` forwards block also.
def foo(*args)
  bar(*args)
end

そのためこのケースは Strict オプションとして設定できるようにして、デフォルトでは Safe な振る舞いにするため無効にしています。

オプションを含めてここまでの説明について概ね実装ができています。何が不足してるかというとメソッド定義の (Arguments Forwarding ではない) 通常の引数がメソッド内で使われているケースで誤検知がおきます。

例えば以下のようなケースです。

def foo(*args, **kwargs, &block)
  bar(*args, **kwargs, &block)
  args.first.do_something
end

式展開で使われているケースや、以下のようなコーナーケースもアウトです。

# Array リテラルでもちいたケース
def foo(*args, **kwargs, &block)
  bar(*args, **kwargs, &block)
  var = [args]
  do_something(var)
end

# Hash リテラルでもちいたケース
def foo(*args, **kwargs, &block)
  bar(*args, **kwargs, &block)
  h = {key: args} # key: value 両方とも
  do_something(h)
end

以下のようにフォワーディングだけしているケースを検出するのであれば容易ですが、フォワーディングの前後でデコレーションあるいはフィルタリング処理をいれているような実践ケースが検知できなくなります。

def foo(*args)
  bar(*args)
end

このあたりをどのように実装するかというのが現状で、variable_table あたりを使ったなにか良い方法があるのか、地道に例外ケースを潰すのかといった調査と設計をしているが現状のステータスです。


おわりに - OSS の開発、メンテナンスについて

このような OSS 開発への活動を支援していただけるスポンサーを募集しています。

github.com

実際のところ GitHub Sponsors は想像していた以上に OSS 活動のモチベーション継続の一要因になっています。これを通じて RailsConf 2019 (at ミネアポリス) にて DHH が講演した OSS の社会構図が変わるきっかけになるといいなと思っています。

RailsアプリケーションのRuby 3.0への展望

パッチ会や地域 Ruby コミュニティなどで集めた知見を元に、勤務先の永和システムマネジメントなんかで度々話している表題についてテキスト化しておく。

TL;DR

  • Ruby 2.8.0 の開発が始まっているが、それは 2020 年のどこかで Ruby 3.0 になるらしい
  • Ruby 3.0 ではキーワード引数 (以下 kwargs) の分離という破壊的変更があり、Ruby 2.7 系は事実上の移行パスバージョン的な位置付けになるだろう
  • 2020年1月8日の現時点では、Ruby 2.7 の kwargs の分離警告について対応された安定版の Rails はなく、周辺 Gem も WIP なので OSS エコシステムに参加していくと良い

2.8.0 (tentative; to be 3.0.0) development has started

2019年の ruby/ruby での matz のコミットです。

commit 537a1cd5a97a8c5e93b64851abaab42812506f66
Author: Yukihiro "Matz" Matsumoto <matz@ruby.or.jp>
Date:   Thu Dec 26 10:55:58 2019 +0900

    2.8.0 (tentative; to be 3.0.0) development has started.
---
 include/ruby/version.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/include/ruby/version.h b/include/ruby/version.h
index 64c5c614e0..25a961566b 100644
--- a/include/ruby/version.h
+++ b/include/ruby/version.h
@@ -31,7 +31,7 @@

 /* API version */
 #define RUBY_API_VERSION_MAJOR 2
-#define RUBY_API_VERSION_MINOR 7
+#define RUBY_API_VERSION_MINOR 8
 #define RUBY_API_VERSION_TEENY 0
 #define RUBY_API_VERSION_CODE (RUBY_API_VERSION_MAJOR*10000+RUBY_API_VERSION_MINOR*100+RUBY_API_VERSION_TEENY)


commit 3a0471faa0d383392ba05b3a6409b973b7b009d1
Author: matz <matz@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date:   Tue Dec 25 13:45:17 2018 +0000

    version.h (RUBY_VERSION): 2.7.0 development has started.

2.8.0 (tentative; to be 3.0.0) development has started. とのことで、暫定的に 2.8.0 というナンバリングで開発が開発されているものの、これが 3.0.0 になるようです。

kwargs の分離

公式エントリに Separation of positional and keyword arguments in Ruby 3.0 として記されているとおり、Ruby 3.0 での破壊的変更として kwargs の分離がされる。それに向けて Ruby 2.7 では以下のような非推奨警告がされる。

.../publicsuffix-ruby/lib/public_suffix/list.rb:51: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
.../publicsuffix-ruby/lib/public_suffix/list.rb:69: warning: The called method `parse' is defined here```

Ruby 2.8.0-dev では警告ではなく ArgumentError が起きるように本対応が入っている。

github.com

例えば上記警告はこのようなエラーになる。

Error:
AcceptanceTest#test_ignore_private:
ArgumentError: wrong number of arguments (given 2, expected 1)
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/lib/public_suffix/list.rb:69:in `parse'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/lib/public_suffix/list.rb:51:in `default'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/lib/public_suffix.rb:69:in `parse'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/lib/public_suffix.rb:143:in `domain'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/test/acceptance_test.rb:105:in `block in test_ignore_private'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/test/acceptance_test.rb:101:in `each'
    /Users/koic/src/github.com/weppos/publicsuffix-ruby/test/acceptance_test.rb:101:in `test_ignore_private'

このようなエラーが起きている Gem をメンテナーやアーリーアダプターが対応してまわっているのが、現在のエコシステムのステータス。

このエラー例は ruby-head (Ruby 2.8) のテストが落ちていた addressable という Gem から依存されている publicsuffix-ruby という Gem でのこと。依存 Gem が対応していないと芋づる式にアプリケーションでもエラーが起きることになる。当然パッチがあたったあと手に届くため rubygems.org でパッケージ公開される必要がある。

すでに対応するための PR が出ていたので以下のイシューで、リリースへのトスを上げていた。

github.com

Rails での kwargs 分離への対応

Ruby の仕事で Web を書く場合に使われているだろう Rails フレームワークの話。

Rails での kwargs 分離への対応について、kamipo さんや amatsuda さんなどが Rails の master ブランチや Rails からの依存 Gem に対して進めてくれている。注意点として master ブランチはまだリリースプランの公開されていない開発版の Rails 6.1 を指している。これはローンチしている一般的なサービスで github オプションを指定しての本番投入がされているものではないと思う。

ということで巷の Rails アプリケーションがもちいている最新の安定版は十中八九 Rails 6.0 となるだろう。Rails 6.0 は 6-0-stable というブランチでメンテナンスがされており、master からそちらにバックポートがされる必要がある。当然 master との乖離があるためバックポートがどうなるかという話があるが、以下の PR がマージされていくつかのコンポーネントでのバックポートが進んでいる。

github.com

将来的にバックポートが完遂されれば Ruby 2.7 での警告が消えて、Rails 6.0 系についても Ruby 3.0 へのサポートがされていく未来が見えてくる。A Light in the Black.

一方で Rails 5.2 系へのバックポートはされないようで、例えば Oracle enhanced adapter でも Rails 5.2 ではサポートされないことを yahonda さんより回答されている。

github.com

つまり公式として Rails 5.2 系では Ruby 3.0 はサポートされないことが意味される。

Rails 5.1 系はすでにセキュリティメンテナンスもされていないので、そもそも対象とならない。

guides.rubyonrails.org

私の考える RubyRails のアップグレード

いずれにせよ Rails 5.2 以下を使っているようであれば、なにはともあれ Rails 6.0 までアップグレードをするのが先決。

Ruby に関しては最新の安定版 Ruby である Ruby 2.6 が Ruby 2 系における最後の警告なし互換バージョンになるだろう。Rails フレームワークが落ち着くまでは、それで運用することになりそう (Ruby 2.7 で導入された ruby -W:no-deprecated を使うという手もあるが、警告に蓋をするのは本筋ではないだろう) 。

その裏でローカル開発では Ruby 2.7 を使って kwargs 分離の非推奨警告を対応してまわることになると思う (当然 CI はランタイムに合わせた Ruby のバージョンになる) 。対応方法は、公式エントリ など見て各自がんばってほしい。個人的には delegate :type_to_sql, to: :conn のような delegate を介した kwargs の分離への警告は、メソッド定義と delegate 宣言の行が組み合わせとして示されるので、delegate の呼び出し場所を直す必要がある場合に手間取ったりした。

Ruby 2.7 で kwargs の分離の警告が消えたら Ruby 2.8 つまり Ruby 3.0 へのアップグレード準備の進捗はまずまずなのではないかと推測する。Ruby 1.8 から Ruby 1.9 にアップグレードするとき文字列のエンコーディングまわりで対応を要した記憶があるが、Ruby 1.9 に上げておくと Ruby 2.0 へのジャンプはスムースだった。いちおうその経験を裏打ちとして Ruby 2.7 は事実上の移行バージョンになると思う。

今日からやれること

そうは言ってもまだ Gem のエコシステムの方の対応が WIP だったりするので、仕事で使っている OSS への対応をするのが現状だと思う。例えば Gemfile.lock に記されている Gem など見てまわるのは初手にできるかもしれない。

このエントリのまとめは kamipo さんのツイートに集約される。

amatsuda さんと Rails / OSS パッチ会というのを開催しているので、そういった場なども活用されたい。

blog.agile.esm.co.jp

RuboCop 0.79.0 リリース解説

RuboCop 0.79.0 がリリースされたので、概要をざっくり記しておきます。

github.com

今回は RoboCop 1.0 に向けた新たな Enable オプションへの新たな値 pending が導入されたのと、Ruby 2.7 の初期サポートが含まれたリリースです。

ちなみにレビュー以外での自分の関わりは、11のバグフィックスうち7つを手がけていました。

New features

#7296: Recognize console and binding.console (rails/web-console) calls in Lint/Debuggers. ([@gsamokovarov][])

Lint/Debuggers cop で web-console での consoleデバッグ用のメソッドとして検知するようにしています。ただ、TTY ライブラリで使われるような名前なので、そのあたりの衝突が懸念されていたところリリースされていたものでした。実際のところは Rails で衝突しているようでリバートされるかもしれません。

github.com

#7567: Introduce new pending status for new cops. ([@Darhazer], [@pirj])

RuboCop 1.0 に向けて、.rubocop.yml の Enable オプションに、新たな値 pending が導入されました。 初期リリースされた新たな cop でままある誤検知への対策で、リリース初期の cop は pending というステータスではじまりデフォルトでは実行されない流れになります。

たしか Enable: false だけだと無効化の意味合いが曖昧ということで新たに追加されたステータスです。

Bug fixes

#7193: Prevent Style/PercentLiteralDelimiters from changing %i literals that contain escaped delimiters. ([@buehmann][])

Style/PercentLiteralDelimiters での問題を直しています。これなぜか自分の環境では再現できかったんですよね。

#7590: Fix an error for Layout/SpaceBeforeBlockBraces when using with EnforcedStyle: line_count_based of Style/BlockDelimiters cop. ([@koic][])

Style/BlockDelimiters cop の EnforcedStyleline_count_based の際にエラーになる問題を直しました。これは Layout/SpaceBeforeBlockBraces cop 中で Style/BlockDelimiters cop の設定を参照している部分のコードに対してテストが足りていないことで、見落としていたバグでした。

#7569: Make Style/YodaCondition accept __FILE__ == $0. ([@koic][])

__FILE__ == $0 はいずれも事実上の読み取り専用ということで、Style/YodaCondition cop で検知しなくても良いだろうと受け入れるようにしました。

#7576: Fix an error for Gemspec/OrderedDependencies when using a local variable in an argument of dependent gem. ([@koic][])

gemspec で以下のように add_dependency をループ処理内に書くなどしたときにエラーになるのを防ぎました。

Gem::Specification.new do |s|
  %w[foo bar].each { |dep| s.add_dependency dep }
  s.add_dependency 'baz'
end

そもそもループ処理などではなく、一律でフラットに書くという cop がレビュー中で提案されています。

#7595: Make Style/NumericPredicate aware of ignored methods when specifying ignored methods. ([@koic][])

Style/NumericPredicate cop で IgnoredMethods オプションが効いていなかった問題を直しています。

#7607: Fix Style/FrozenStringLiteralComment infinite loop when magic comments are newline-separated. ([@pirj][])

# frozen_string_literal: true でループするようなバグがあったようです。自分の環境だと再現がうまくできなかったけれど何だったのだろう。

#7602: Ensure proper handling of Ruby 2.7 syntax. ([@drenmi][])

Ruby 2.7 の初期サポートが入りました。まだバグがあるようで、新たな問題があればフィードバックをください。

github.com

#7620: Fix a false positive for Migration/DepartmentName when a disable comment contains a plain comment. ([@koic][])

# rubocop:disable Style/SafeNavigation # support Ruby < 2.3.0 のように、disable コメントに続いてコメントが書かれているようなケースでの偽陽性を直しました。

#7616: Fix an incorrect autocorrect for Style/MultilineWhenThen for when statement with then is an array or a hash. ([@koic][])

Style/MultilineWhenThen cop で以下のような auto-correct による syntax の破壊を直しました。

% cat example.rb
case condition
when foo then {
    key: 'value'
  }
end

% ruby -c example.rb
Syntax OK

% bundle exec rubocop -a --only Style/MultilineWhenThen
(snip)

% cat example.rb
case condition
when foo {
    key: 'value'
  }
end

% ruby -c example.rb
example.rb:3: syntax error, unexpected ':', expecting '}'
    key: 'value'
example.rb:4: syntax error, unexpected '}', expecting end-of-input

#7628: Fix an incorrect autocorrect for Layout/MultilineBlockLayout removing trailing comma with single argument. ([@pawptart][])

以下のようなケースで auto-correct によりブロック引数のコードが破壊される問題が直されています。

# Before
[[1, 2, 3], [3, 4, 5]].each do |a,
  |
  p [a]
end
# After
[[1, 2, 3], [3, 4, 5]].each do |a|
  p [a]
end

#7627: Fix a false negative for Migration/DepartmentName when there is space around : (e.g. # rubocop : disable). ([@koic][])

disable コメントは、rubocop : disable のように : の前後にスペースを入れても有効だったものが検出できなかった問題を直しました。この振る舞い知らなかった。

Changes

#7287: Style/FrozenStringLiteralComment is now considered unsafe. ([@buehmann][])

fstring のマジックコメントを書くとコードとしての互換性がなくなっている可能性があるため、unsafe にマークされました。


多くのユーザーにとってはバグフィックスがメインになると思うので、アップデートしてみてください。

AirPods Pro が届いた

今月のパッチ会のときに yahonda さんから AirPods Pro の体験が良いと聞いたので注文していた AirPods Pro が届いた。

Apple AirPods Pro

Apple AirPods Pro

  • 発売日: 2019/10/30
  • メディア: エレクトロニクス

実際のところアクティブノイズキャンセリングの効果が素晴らしくて、イヤホンの装着によって「ふっ」と音楽を聞く体勢に入る無音の世界に切り替わって、雑音のある現実世界とノイズキャンセリングされた音楽の世界との往復ができるという体験は素晴らしかった。

このアクティブノイズキャンセリングは、雑音のない世界に入って集中力を高めたプログラミングをするのにも良いと思うので、BGM を愛用するプログラマーにおすすめだと思う。

Ruby 2.7 リリースパーティー

Ruby 2.7 リリースパーティーに行った。会場は Speee さん。

松田さんの MC で Ruby コミッターによる Ruby 2.7 制作秘話から始まり、そのままパーティーへという流れだった。

個人的には Ruby 3 に向けた型について、ruby/ruby-signature で集められている型定義ファイル rbs (Steep が対応) と sorbet/sorbet-typed で集められている型定義ファイル rbi (Sorbet が対応) で行われていることが重複している点として気になっていたので、パーティーで Matz や soutaro さんにその辺りを聞きたいと思っていて聞いたりした。いちおう型チームの中では rbs に寄せていくということで話ができているらしく、Sorbet の方でも rbi 以外に rbs のサポートもしていくのではといった感じだった。なので型定義は rbs が公式であり、処理系として Steep と Sorbet を選択していくというのが現在と近未来になりそう。その他、Ruby 4.0 のコンセプトである『Smarter, Faster』について話を聞いたりして、今後の Ruby への気持ちを高めることができる良い会でした。

またこのパーティーでは hasumikin のお燗番という豪華な熱燗提供でした。ごちそうさまでした。楽しかったです。

RuboCop Performance 1.5.2, RuboCop Rails 2.4.1, RuboCop Minitest 0.5.1 をリリースした

RuboCop Performance 1.5.2, RuboCop Rails 2.4.1, RuboCop Minitest 0.5.1 をまとめてリリースした。

github.com

github.com

github.com

これらの master ブランチにあるバグフィックスについて、年またぎで溜めていてもなのでクリスマスということでリリースしておいた。新機能は含んでいないバグフィックスが主なので同系のバージョンを使っているようであれば (いるようでなくても) bundle update しておくと良いと思います。

特に深刻な問題がなければ自分がメインメンテナーをしている RuboCop extension gems としては、年内のリリース納めです。メリークリスマス。

RubyGems 3.1のローカル非推奨警告を消す

RubyGems 3.1 がリリースされたのでローカル環境の RubyGems をアップデートした。

% gem update --system
% gem -v
3.1.2

アップデート後、gem i <gemname> や gem の差分を見る gemdiff などを使った時に以下のような警告の川が流れるようになった。

実際 Gem::Specification#rubyforge_project= が使われている gemspec に関してかなりの量が自分のローカルの場合は表示されていたので、上記は抜粋となる (古めのリポジトリを対象としているので、それも影響しているかもしれない) 。

Gem::Specification#rubyforge_project= called from /Users/koic/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/specifications/letter_opener-1.7.0.gemspec:16.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /Users/koic/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/specifications/letter_opener-1.6.0.gemspec:16.

警告を消すひとつとしては gem pristine <gemname> すれば良い。

% gem pristine letter_opener

(snip)
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /Users/koic/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/specifications/letter_opener-1.7.0.gemspec:16.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /Users/koic/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/specifications/letter_opener-1.6.0.gemspec:16.
Restoring gems to pristine condition...
    exists /Users/koic/src/github.com/ryanb/letter_opener
Restored letter_opener-1.6.0
    exists /Users/koic/src/github.com/ryanb/letter_opener
Restored letter_opener-1.7.0

今回量が多かったので以下のようなワンライナーをバックグラウンドで実行しておいた。

% ruby -e 'Gem::Specification.map(&:name).uniq.each {|spec_name| puts "* gem pristine #{spec_name}"; `gem pristine #{spec_name}`}'

このワークアラウンドだと警告の出ていない gem についても gem pristine を実行することになるが、年末の大掃除ということでまとめて適用しておいた。bundle pristine で対処できるかなと思ったらそうではなかったために書いたワークアラウンドなので、もっとうまいやり方があると思う。

(2019.12.20追記)

hsbt さんからコメントをもらう (いつもありがとうございます!) 。upstream でまだ非推奨 API が使われているようであればパッチを送ると良さそう。