Techioz Blog

「srb tc」が RSpec テストの「expect」メソッドと「eq」メソッドを見つけられないのはなぜですか?

概要

私は実験的なオープンソース プロジェクト (ruby_crystal_codemod) で Sorbet を試しています。ネストされたテスト プロジェクト内の一部の RSpec テストで型チェックを機能させる方法がわかりません。 srb tc を実行すると、次のような型チェック エラーが表示されます。

spec/src/example_class_annotated_spec.rb:6: Method it does not exist on T.class_of(<root>) https://srb.help/7003
     6 |  it 'should add @foo and @bar' do
     7 |    instance = ExampleClass.new(2, 3, 4)
     8 |    expect(instance.add).to eq 5
     9 |  end

spec/src/example_class_annotated_spec.rb:8: Method expect does not exist on T.class_of(<root>) https://srb.help/7003
     8 |    expect(instance.add).to eq 5
            ^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/67cd17f5168252fdec1ad04839b31fdda8bc6155/rbi/core/kernel.rbi#L2662: Did you mean: Kernel#exec?
    2662 |  def exec(*args); end
            ^^^^^^^^^^^^^^^

spec/src/example_class_annotated_spec.rb:8: Method eq does not exist on T.class_of(<root>) https://srb.help/7003
     8 |    expect(instance.add).to eq 5
                                    ^^^^

# etc.

これは、GitHub 上のネストされたプロジェクトのソース ディレクトリです。

次のコマンドを実行して型エラーを再現できるはずです。

cd /tmp
git clone https://github.com/DocSpring/ruby_crystal_codemod.git
cd ruby_crystal_codemod
git checkout sorbet-rspec-type-checking-error
cd spec/fixtures/rspec_project/
bundle install
bundle exec srb tc

次のタイプ エラーが表示されるはずです。

spec/src/example_class_annotated_spec.rb:6: Method it does not exist on T.class_of(<root>) https://srb.help/7003
     6 |  it 'should add @foo and @bar' do
     7 |    instance = ExampleClass.new(2, 3, 4)
     8 |    expect(instance.add).to eq 5
     9 |  end

# etc.

spec/fixtures/rspec_project/sorbet/rbi/gems/rspec-core.rbi などの RBI ファイルに問題があるのでしょうか?

解決策

生成された RBI ファイルには何も問題はありません。

RSpec は DSL に大きく依存します。ブロックを受け入れる RSpec.describe のようなメソッドを呼び出すと、RSpec は特定のバインディングを使用して特定のスコープでそのブロックを実行し、RSpec テスト DSL を呼び出すことができるようになります。

Sorbet が RSpec メソッドの知識を得るには、この動作を認識する必要があります。メソッドによって受け取られたコード ブロックが実行されるバインディングについて Sorbet にヒントを与える方法はいくつかあります。これについては、Sorbet のドキュメントで読むことができます。

あなたが言及した RBI ファイルを見ると、この情報は含まれていません。 gem の自動 RBI ジェネレーターは、通常、gem 内にどの定数 (クラス、モジュール) とどのメソッドが存在するかを確立することに限定されます。これには、これらのメソッドまたはその引数の署名は含まれません。

特定のブロックのバインドに関するヒントがない場合、Sorbet は、ブロックがブロックの外部コンテキストにバインドされていると想定します。たとえば、Ruby のトップレベルで RSpec.describe を使用する場合、それが Sorbet が description に渡されるブロックに対して想定するコンテキストです。 Ruby のグローバル スコープには eq の定義がないため、型チェックは失敗します。

これを解決するには、さまざまなレベルの努力と報酬を伴いながら、できることはたくさんあります。

1 と 3 は大した作業ではないと思うので、2 番目のオプションの進め方について詳しく説明します。

次のような rspec の shim ファイルを sorbet/rbi/shims/rspec.rbi に作成します。

# typed: strict

module RSpec
end

このファイルでは、RSpec メソッドに渡されるブロックのバインドについて何も想定しないようにするために必要な署名を追加します。たとえば、説明から始めましょう。自動生成された RBI にアクセスすると、次の宣言があることがわかります。

  def self.describe(*args, &example_group_block); end

これを独自の RBI ファイルにコピーし、独自の署名を先頭に追加しましょう。

# typed: strict

module RSpec
  sig do
    params(
      args: T.untyped,
      example_group_block: T.proc.bind(T.untyped).void
    ).void
  end
  def self.describe(*args, &example_group_block); end
end

これで、説明するために渡したブロックが T.untyped にバインドされていることを Sorbet に伝えました。このタイプはエスケープ ハッチとして機能し、あらゆるメソッド呼び出しを許可するため、Sorbet は内部のメソッドが欠落していることについて文句を言いません。タイプチェッカーが完全に無視できるようになるまで、異なる RSpec クラスの他のメソッドに対してもこの操作を繰り返す必要がある場合があります。

これにより、仕様ファイルで # typed: true を使用できるようになります。これは完璧ではなく、実際にはパッチにすぎませんが、少なくともいくつかの非常に基本的な型チェック機能を提供します。