Scala 3 Macros: How to Read Annotations :: Jun 25, 2023
This post will examine how to read class & field `StaticAnnotation`s from a Scala 3 Macro.
Types of Annotations
Scala has a few different types of annotations. The base class of all annotations is Annotation, but developers should extend one of the subtypes. StaticAnnotations are persisted to the class file, and can be read from macros. ConstantAnnotation extends `StaticAnnotation` and requires all parameters to be compile-time constants. Finally, MacroAnnotation is an experimental annotation that can transform & create new definitions.
Reading Annotations on a Class
Assume we have the following `SqlName` StaticAnnotation, applied to a class:
class SqlName(val sqlName: String) extends StaticAnnotation
@SqlName("app_user")
case class AppUser(
id: Long,
firstName: Option[String],
lastName: String
)
We can read the `sqlName` value with the following macro:
import scala.compiletime.*
import scala.quoted.*
inline def sqlNameFor[T]: Option[String] = ${ sqlNameForImpl[T] }
private def sqlNameForImpl[T: Type](using Quotes): Expr[Option[String]] =
import quotes.reflect.*
val annot = TypeRepr.of[SqlName]
TypeRepr
.of[T]
.typeSymbol
.annotations
.collectFirst:
case term if term.tpe =:= annot => term.asExprOf[SqlName]
match
case Some(expr) => '{ Some($expr.sqlName) }
case None => '{ None }
In a different file:
@main def main: Unit =
println(sqlNameFor[AppUser]) // Some(app_user)
Reading Annotations on a Field
Let's take the same `SqlName` annotation as above, and apply it on a field:
class SqlName(val sqlName: String) extends StaticAnnotation
case class AppUser(
id: Long,
firstName: Option[String],
@SqlName("last_name") lastName: String
)
We can read the `sqlName` value with the following macro:
inline def sqlFieldNamesFor[T]: Vector[(String, String)] = ${
sqlFieldNamesForImpl[T]
}
private def sqlFieldNamesForImpl[T: Type](using
q: Quotes // must be named!!
): Expr[Vector[(String, String)]] =
import quotes.reflect.*
val annot = TypeRepr.of[SqlName].typeSymbol
val tuples: Seq[Expr[(String, String)]] = TypeRepr
.of[T]
.typeSymbol
.primaryConstructor
.paramSymss
.flatten
.collect:
case sym if sym.hasAnnotation(annot) =>
val fieldNameExpr = Expr(sym.name.asInstanceOf[String])
val annotExpr = sym.getAnnotation(annot).get.asExprOf[SqlName]
'{ ($fieldNameExpr, $annotExpr.sqlName) }
val seq: Expr[Seq[(String, String)]] = Expr.ofSeq(tuples)
'{ $seq.toVector }
And in a different file,
@main def hello: Unit =
println(sqlFieldNamesFor[AppUser]) // Vector((lastName,last_name))
It's weird that the compiler requires the given Quotes to be named, and the error message is pretty cryptic. I've filed a Scala 3 bug: https://github.com/lampepfl/dotty/issues/18059