ひとつ前の記事では Steep に追加予定の ignore コメント機能を紹介しました。
その記事の中で、ActiveSupport::Concern で提供されている included ブロックのことを 解消の難しい型エラー と紹介しましたが、 ブログ記事にまとめていく最中に解決方法を思いつきました。
そもそもどういう問題だったのかを改めて整理します。
手元のコードでは、concern 系クラスの included ブロックで型エラーが起きていたという話です。 included ブロック内では、 include 先のクラスとして コードが実行されるのですが、Steep はこのブロック内で self が切り替わっていることが判別つかず、 Concern クラスとして判定を進めようとするため、型エラーが発生します。
module Loginable
extend ActiveSupport::Concern
included do
helper_method :foo #=> Type `singleton(::Loginable)` does not have method `helper_method`
end
end
上記の例では singleton(::Loginable)
にメソッドがないと言っていますよね。
前回の記事では、この問題は解消が難しいので、ignore コメントで型エラーを黙らせることにしました。
上の「問題の整理」のところに書いたことにヒントがあります。 「include 先のクラスとして」コードが実行される、「このブロック内で self が切り替わっている」という部分です。 つまり、include 先のクラスの情報を Steep にヒントとして与えてあげれば、型エラーが解消するというわけです。
実際には、次のようなアノテーションコメントを書きます。
module Loginable
extend ActiveSupport::Concern
included do
# @type self: singleton(ActionController::Base)
helper_method :foo
end
end
このコメントを書くことで、Steep に対して「このブロック内では self を ActionController::Base
クラスとして扱うように」と伝えることができます。
Steep には、このように型情報を補足するためのアノテーションコメントが用意されています。 RBS では表現できない、ブロック単位での型情報や変数の型情報などを Ruby のコメントとして記述できます。
Ruby では DSL や宣言的な記述が好まれ、コンテキストの切り替わるブロックがそこかしこにありますが、 現状では RBS だけでこうしたブロック表現をカバーし切るのは難しいため、アノテーションコメントを併用するのがよさそうです。
また、メソッド内、ブロック内の変数の型情報などを補うのも有用です。
前回の記事で #present?
による型の絞り込みができないという話をしましたが、これもアノテーションコメントを使うことで解消できます。
num = [1, 2, 3, nil].sample #=> Integer?
if num.present?
# @type var num: Integer
num.succ #=> #present? では型が絞り込まないが、アノテーションコメントによって Integer と扱われるため、型エラーは発生しない
end
理想としてはツール側の進化で改善してほしいところですが、adhoc な対応として、アノテーションコメントを使って回避する手もあります。 アノテーションコメントを多用するとコードが読みづらくなりますし、どこを ignore して、どこにアノテーションをするとよいのか、というバランスが難しいところですが、 型エラーを解消する手段のひとつとして覚えておくとよいでしょう。
- アノテーションコメントの紹介
- アノテーションコメントを使うと included ブロックの型エラーが解消できること
- 容量用法を守って正しくお使いください