[Sorbet] requires_ancestorでmoduleが特定のクラスを継承したクラスにincludeされることを保証する
特定のクラスを継承したクラスで使われることを期待したメソッドを切り出したようなmoduleを作ることは良くあります。
RubyにSorbetで型を当てている場合は、requires_ancestorという機能を使うことでエラーを回避できる場合があるため紹介します。
問題
例えば、以下のようなコードがあったとします。(Concernsを意識したコードにしてみました。)
class ApplicationController
def base_method
end
end
module Countable
def count
base_method # -> Method base_method does not exist on Countable
end
end
class FooController < ApplicationController
include Countable
end
FooControllerがApplicationControllerを継承していて、Countableをincludeしています。
module Countableの内部ではApplicationController内のbase_methodに依存しています。
しかし、Sorbetはそのmoduleをincludeする側のクラスの情報は知らないため、base_methodを呼び出している部分でエラーになってしまいます。
解決方法
requires_ancestorという機能を使い、moduleをincludeするクラスが特定のクラスを継承していることを保証します。
moduleにrequires_ancestor { xxx }
でApplicationControllerを指定することで、Countable内でbase_methodを呼び出すことができます。
class ApplicationController
def base_method
end
end
module Countable
extend T::Helpers
requires_ancestor { ApplicationController }
def count
base_method # No error
end
end
class FooController < ApplicationController
include Countable
end
以下のリンクで動作を確認できます。
Sorbet Playground
補足事項
requires_ancestorは実験的機能なので、CLIで--enable-experimental-requires-ancestor
オプションを指定する必要があります。
ファイルに書く場合はsorbet/config
に一行加えます。
Sorbet Playgroundでは、URLに?arg=--enable-experimental-requires-ancestor
を加えることで有効にできます。
(左のエディタではうまく反映されずにエラーの波線が出てしまいますが、右のエリアではエラーが消えていることがわかります。)
よく使う場面と対応方法
最後に、requires_ancestorを良く使う場面と対応方法をメモしておきます。
- ModelやControllerに対するConcerns
requires_ancestor { ApplicationController }
requires_ancestor { ApplicationRecord }
- raiseが解決できないファイル
requires_ancestor { Kernel }
Discussion