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 に詳しいです。
キーワード引数の分離への警告 (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 です。
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 Sponsors は想像していた以上に OSS 活動のモチベーション継続の一要因になっています。これを通じて RailsConf 2019 (at ミネアポリス) にて DHH が講演した OSS の社会構図が変わるきっかけになるといいなと思っています。