Ruby 3 ゴッチャ

このセクションでは、Ruby 3 サポートの作業中に見つけた、微妙なバグや理解しにくいテストの失敗につながるいくつかの問題を記録します。日常的にRubyのコードを書いているGitLabの貢献者には、これらのイシューに慣れることをお勧めします。

Ruby 3言語と標準ライブラリの変更点の完全なリストはRuby Changesをご覧ください。

Hash#each 一貫してラムダに2要素の配列を返すようになりました。

次のスニペットを見てください:

def foo(a, b)
  p [a, b]
end

def bar(a, b = 2)
  p [a, b]
end

foo_lambda = method(:foo).to_proc
bar_lambda = method(:bar).to_proc

{ a: 1 }.each(&foo_lambda)
{ a: 1 }.each(&bar_lambda)

Ruby 2.7では、このプログラムの出力から、ハッシュ・エントリをラムダに渡す際の振る舞いが、必要な引数の数によって異なることがわかります:

# Ruby 2.7
{ a: 1 }.each(&foo_lambda) # prints [:a, 1]
{ a: 1 }.each(&bar_lambda) # prints [[:a, 1], 2]

Ruby 3ではこの挙動は一貫しており、ハッシュエントリは常に単一の[key, value] 配列として返されます:

# Ruby 3.0
{ a: 1 }.each(&foo_lambda) # `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
{ a: 1 }.each(&bar_lambda) # prints [[:a, 1], 2]

2.7と3.0の両方で動作するコードを書くには、以下のオプションを検討してください:

  • ラムダ本体は常にブロックとして渡します:{ a: 1 }.each { |a, b| p [a, b] }.
  • ラムダの引数を分解する:{ a: 1 }.each(&->((a, b)) { p [a, b] }).

常に明示的にブロックを渡すことを推奨し、2つの必須引数をブロック引数として使用することを推奨します。

詳しくはRuby issue 12706 を参照してください。

Symbol#to_proc ラムダと一貫性のあるシグネチャメタデータを返します。

Rubyでよく使われるイディオムは、&:<symbol> の省略記法を使ってProc オブジェクトを取得し、それを高階関数に渡すというものです:

[1, 2, 3].each(&:to_s)

Ruby は&:<symbol>Symbol#to_proc にデシュガーします。最初の引数としてメソッド_受信者_(ここではInteger )を、残りの引数としてすべてのメソッド_引数_(ここでは none)を指定して呼び出すことができます。

これはRuby 2.7でもRuby 3でも同じです。Ruby 3が分岐するのは、このProc オブジェクトをキャプチャして呼び出しシグネチャを検査する場合です。これは、DSLを書いたり、メタプログラミングを使ったりするときによく行われます:

p = :foo.to_proc # This usually happens via a conversion through `&:foo`

# Ruby 2.7: prints [[:rest]] (-1)
# Ruby 3.0: prints [[:req], [:rest]] (-2)
puts "#{p.parameters} (#{p.arity})"

Ruby 2.7では、このProc オブジェクトの必須パラメータは0個、オプションパラメータは1個と報告されていますが、Ruby 3では必須パラメータは1個、オプションパラメータは1個と報告されています。Ruby 2.7は正しくありません。Proc オブジェクトが表すメソッドのレシーバであり、レシーバなしでメソッドを呼び出すことはできないため、第一引数は常に渡さなければなりません。

Ruby 3では修正されています。Proc オブジェクトのアリティやパラメータリストをテストするコードが壊れる可能性があるため、更新する必要があります。

詳しくはRuby issue 16260 を参照してください。

OpenStruct はフィールドを遅延評価しません。

Ruby 3では、OpenStruct の実装が一部書き換えられ、動作が変更されました。Ruby 2.7では、OpenStruct メソッドが最初にアクセスされたときにメソッドを遅延的に定義して OpenStructいました。OpenStruct Ruby 3.0では、これらのメソッドはイニシャライザでeagerlyに定義 OpenStructされるため、これらのメソッドをOpenStruct 継承 OpenStructしたりオーバーライドしたりOpenStruct するクラスが壊れる可能性が OpenStructあります。

このOpenStruct ような理由で OpenStructメソッドを継承するのはやめましょうOpenStructOpenStruct新しいコードを書くときは、Struct

RegexpRange のインスタンスは凍結されます。

Ruby 3 ではRegexpRange のインスタンスは生成時に自動的にフリーズされるので、明示的にフリーズさせる必要はなくなりました。

これには微妙な副作用があります:これらの型のメソッド呼び出しをスタブするテストはエラーで失敗するようになりました:

# Ruby 2.7: works
# Ruby 3.0: error: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)

影響を受けるテストは、フリーズしたオブジェクトのメソッド呼び出しをスタブしないように書き直します。上の例は、次のように書き換えることができます:

# Works with any Ruby version
allow(subject).to receive(:function_returning_range).and_return(1..42)

Ruby 3.0.2 でテーブルテストが失敗します。

Ruby 3.0.2には、テーブルの値が整数値で構成されている場合にテーブルテストに失敗する既知のバグがあります。その理由はイシュー337614 に書かれています。この問題はRubyで修正されており、Ruby 3.0.3で修正される予定です。

この問題は、パッチが適用されていないRuby 3.0.2を実行しているユーザーにのみ影響します。手動またはasdf のようなツールを使ってRubyをインストールした場合、この問題が発生する可能性があります。gitlab-development-kit (GDK) のユーザーもこの問題の影響を受けます。

ビルドイメージにはこのバグにアドレスしたパッチセットが含まれているため、影響はありません。

メソッドがスタブ化されている場合、DeprecationToolkitで非推奨が検出されません。

Ruby 2で非推奨となり、Ruby 3で削除された機能を使用する際に、deprecation_toolkit 。Ruby 2からRuby 3への移行時によく発生するイシューは、Ruby 3.0の位置引数とキーワード引数の分離に関するものです。

残念ながら、作成者がテストの中でこのようなメソッドをスタブしていた場合、非推奨化を検出することはできません。deprecation_toolkitKernel#warn が警告を発するという事実に依存しているため、この呼び出しをスタブアウトすると警告の呼び出しが効果的に削除されます。つまり、deprecation_toolkit が非推奨の警告を目にすることはありません。実装をスタブアウトすることで、この警告は削除され、私たちはこの警告を拾うことはないので、ビルドはグリーンです。

より詳しい背景はイシュー364099を参照してください。

irb でのテストとrails console

もう一つの落とし穴は、irb/rails c でテストすると非推奨の警告が表示されなくなることです。Ruby 2.7.x のirb には非推奨の警告が表示されなくなるバグがあるからです。

コードを書くときやコードレビューを行うときには、f({k: v}) という形式のメソッド呼び出しに特に注意してください。これはRuby 2ではfHash またはキーワード引数を取る場合に有効ですが、Ruby 3ではfHashを取る場合のみ有効とみなされます。Ruby 3に準拠するためには、f がキーワード引数を取る場合、これを次のいずれかの呼び出しに変更する必要があります:

  • f(**{k: v})
  • f(k: v)

RSpecwith 引数マッチャが省略記法 Hash 構文で失敗します。

Ruby 3ではキーワード引数(“kwargs”)がファーストクラスの概念であるため、キーワード引数は内部Hash インスタンスに変換されなくなりました。このため、RSpecメソッドの引数マッチャは、レシーバがkwargsの代わりに位置オプションのハッシュを取ると失敗します:

def m(options={}); end
expect(subject).to receive(:m).with(a: 42)

Ruby 3では、この期待は以下のエラーで失敗します:

  Failure/Error:

     #<subject> received :m with unexpected arguments
       expected: ({:a=>42})
            got: ({:a=>42})

これは、RSpec が kwargs 引数 matcher を使用しているにもかかわらず、メソッドがハッシュを受け取るために発生します。Ruby 2では、a: 42 が最初にハッシュに変換され、RSpecがハッシュ引数マッチャーを使うので、うまくいきます。

回避策としては、オプションのハッシュを受け取るメソッドがわかっている場合は、省略記法を使わず、代わりに実際のHash

# Note the braces around the key-value pair.
expect(subject).to receive(:m).with({ a: 42 })

詳しくはRSpec の公式イシューレポートを参照してください。