🌊

Ruby 3.3.0 で発生しているブロック内で匿名引数を参照したときのバグについて

2024/01/15に公開

Ruby 3.3.0 では以下のようにメソッドの匿名引数をブロック内で他のメソッドにフォワードしようとするとシンタックスエラーになるバグが発生しています。

def hoge(*)
  proc do
    # error: anonymous rest parameter is also used within block (SyntaxError)
    p(*)
  end
end

hoge(1)

これは以下のチケットでの修正が原因となっています。

経緯

[Feature #19370] Anonymous parameters for blocks? は以下のようにブロックの引数で匿名引数を受け取り、それを別のフォワードする場合にエラーになるが、実装可能であればサポートを追加してほしい、という旨のチケットです。

# data in form [request method, URL, params]:
[
  [:get, 'https://google.com', {q: 'Ruby'}, {'User-Argent': 'Google-Chrome'}],
  [:post, 'https://gist.github.com', 'body'],
  # ...
].each { |method, *|
  # ブロックの匿名引数を他のメソッドをフォワードできるようにしたい
  # 現状はエラーになる
  request(method.to_s.upcase, *)
}

と、いうのが元々のこのチケットの趣旨だったのですが、以下のようにメソッドの匿名引数をブロック内で別のメソッドにフォワードした場合に混乱する可能性があるという話にシフトしました。

def test(*)
  # ...

  # p(*) の * は実際には test(*) で受け取った引数がフォワードされる
  # なのでこのコードを実行すると p(2) が出力される
  proc { |*| p(*) }.call(1)
end

test(2)
# => 2

これが意図しない挙動につながるということでこのチケットでは以下のように対応する事になりました。

def m(*)
  # この書き方は許容しない
  # ブロックの引数で * を受け取り、ブロックの中で * を参照する場合はシンタックスエラーにする
  ->(*) { p(*) }    # SyntaxError
  ->(x, *) { p(*) } # SyntaxError

  # この書き方は許容する
  # ブロックの引数で * を受け取らない場合はブロックの中で * が参照できる
  ->(x) { p(*) }    #=> 1
  proc {|x| p(*) }  #=> 1
end

m(1).call(2)

Ruby 3.3.0 ではこの対応を含んでリリースされたんですがこの挙動にバグがあり 許容する想定の下2つのコードもエラーになるバグ が含まれてしまっています。

def m(*)
  # Ruby 3.3.0 ではこれもエラーになってしまう
  ->(x) { p(*) }    #=> 1
  proc {|x| p(*) }  #=> 1

  # ブロックの引数がない場合もエラー
  proc { p(*) }
end

m(1).call(2)

この問題は Bug #20090: Anonymous arguments are now syntax errors in unambiguous cases などで報告されているんですが既に開発版では対応済みです。
クリティカルなバグだと思うのでそのうち Ruby 3.3.1 が出ると思うんですが、対応前にアップデートするのは難しそうですねえ…。
事前に(自分で)動作確認できていれば防げたと思うのでちょっと悔しい。
この対応をされたのが Ruby 3.3.0 リリースの2日前とかなのでなかなか気づくのは厳しかった気がする。
と、言う感じで Ruby 3.3.0 にアップデートしようとしている方は気をつけましょう。

GitHubで編集を提案

Discussion