search term:

user-land compiler warnings in Scala

As a library author, I’ve been wanting to tag methods in Scala that can trigger custom warnings or compiler errors. Why would I want to intentionally cause a compiler error? One potential use case is displaying a migration message for a removed API.

For example, if you try to use <<= in sbt 1.3.8 you’d get the following error on load:

/tmp/hello/build.sbt:13: error: `<<=` operator is removed. Use `key := { x.value }` or `key ~= (old => { newValue })`.
See http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html
    foo <<= test,
        ^
[error] sbt.compiler.EvalException: Type error in expression
[error] Use 'last' for the full log.
Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore?

It’s good that it’s doable, but using a macro for this is too pompous. According to Yoshida-san, you can do this in Haskell just by putting Whoops in the type signature:

-- | This function is being removed and is no longer usable.
-- Use 'Data.IntMap.Strict.insertWith'
insertWith' :: Whoops "Data.IntMap.insertWith' is gone. Use Data.IntMap.Strict.insertWith."
            => (a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWith' _ _ _ _ = undefined

configurable warnings

In March 2019, I sent a pull request #7790 to scala/scala proposing @compileTimeError annotation. The pull request evolved into @restricted annotation and configurable warning option -Wconf. The idea was that @restricted can tag methods with labels, and -Wconf would be able to escalate the tag to either a warning or an error like -Wconfig apiMayChange:foo.*:error.

Unfortunately #7790 got shot down as we were approaching Scala 2.13.0, but -Wconfig was resurrected during the summer by Lukas Rytz (@lrytz) as a general-purpose filter #8373 that can configure any warnings by the category, message content, source, origin, or the deprecation since field. Using this the library users will be able to toggle deprecation messages from certain version as error etc. #8373 is merged and will be part of Scala 2.13.2.

ApiMayChange annotation

As an example of denoting a “status” of API, Lightbend’s Akka library has a few interesting ones. For example ApiMayChange denotes that the tagged APIs are exempt from the normal binary compatibility guarantees, basically a beta feature that might evolve in the future.

This would be an interesting tag for any long-supported libraries. One interesting aspect of this annotation is that it’s purely a social convention. Meaning that the compiler will not print any warnings if you call the “may change” API.

apiStatus annotation (proposal)

-Wconfig is useful, but currently the only tool given to library authors are @deprecated annotation to trigger a warning without resorting to a macro. A week ago, I sent #8820 to scala/scala proposing the idea of @apiStatus that enables user-land compiler warnings and errors.

Here are some examples. Let’s say we want to make <<= method an error.

import scala.annotation.apiStatus, apiStatus._

@apiStatus(
  "method <<= is removed; use := syntax instead",
  category = Category.ForRemoval,
  since = "foo-lib 1.0",
  defaultAction = Action.Error,
)
def <<=(): Unit = ???

Here how it would look if someone calls this method:

example.scala:26: error: method <<= is removed; use := syntax instead (foo-lib 1.0)
  <<=()
  ^

So the custom compiler message works.

implementing ApiMayChange

Let’s try implementing ApiMayChange annotation.

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.Silent,
)
implicit class ShouldDSL(s: String) {
  def should(o: String): Unit = ()
}

Following Akka, I chose the default action to be Action.Silent so it won’t display a warning. Here’s where -Wconf can shine. Using -Wconf:cat=api-may-change&origin=foo\..*:warning option, the user can enable “api-may-change” category just for foo.* package.

example.scala:28: warning: should DSL is incubating, and future compatibility is not guaranteed (foo-lib 1.0)
  "bar" should "something"
  ^

If you want to make it a warning by default you can also change it to defaultAction = Action.Warning.

user-land warnings and errors

The category field is just a String so you can use your imagination on what kind of useful tagging you can do to denote your classes and methods. (This should also make it easy to backport to older Scala for cross building).

In general, what do you think about the idea of user-land warnings and errors? Please let us know by hitting +1/-1 or commenting on #8820.