ユーザランドでの警告とエラー、パート2

先週は、Scala でユーザランドから警告を出す仕組みの提案である #8820 について書いた。例として ApiMayChange アノテーションを実装した。

package foo
 
import scala.annotation.apiStatus, apiStatus._
 
@apiStatus(
  "should DSL is incubating, and future compatibility is not guaranteed",
  category = Category.ApiMayChange,
  since = "foo-lib 1.0",
  defaultAction = Action.Warning,
)
implicit class ShouldDSL(s: String) {
  def should(o: String): Unit = ()
}

これは始めとしては一応使えるけども、少し冗長だ。もしなんらかの API ステータスが頻繁に使われる場合、ライブラリ作者が独自のステータスアノテーションを定義できると嬉しいと思う。今日はその方法を考える。

その前に少し裏方の解説を必要とする。コンパイラがアノテーションを見る時この情報は AnnotationInfo として渡され、引数は構文木で表される。これによってコールサイトのソースコードはあるが、アノテーションのコードがコンストラクタで何かやったなどの事は分からない。一方、アノテーションクラスにタグ付けされたアノテーションのことは分かる。

ApiMayChange の実装再び

アノテーションのに付けることを前提に作られたアノテーションはメタアノテーションと呼ばれ、これを使うことで apiStatus の継承を行うことができる:

import scala.annotation.{ apiStatus, apiStatusCategory, apiStatusDefaultAction }
import scala.annotation.meta._
 
@apiStatusCategory("api-may-change")
@apiStatusDefaultAction(apiStatus.Action.Warning)
@companionClass @companionMethod
final class apiMayChange(
  message: String,
  since: String = "",
) extends apiStatus(message, since = since)

categorydefaultActionextends apiStatus(....) で渡すのではなく @apiStatusCategory@apiStatusDefaultAction を使って指定する。

一度これを定義してしまうと API のタグ付けは綺麗になる:

@apiMayChange("can DSL is incubating, and future compatibility is not guaranteed")
implicit class CanDSL(s: String) {
  def can(o: String): Unit = ()
}

確認しておくと、この仕組みを使ってライブラリ作者が API をタグ付けしてコンパイラエラーや警告を出すのが目的だ。

scala> "foo" can "say where the road goes?"
       ^
       warning: can DSL is incubating, and future compatibility is not guaranteed

ユーザランドでの警告とエラー

メタアノテーションというテクニックを使って apiStatus を継承して独自のステータスアノテーションを定義して API のタグ付けをすることができた。