1

Given a custom defined annotation class GreaterThan(m: Double) extends scala.annotation.StaticAnnotation and a case class case class Person(name: String, @GreaterThan(0) age: Double), how can make sure that the uses of the annotation GreaterThanare used on doubles but not strings, obviously GreaterThan does not make sense to use on the name parameter.

Is there a way to emit a warning if the user uses an annotation in the wrong place( that I define), or like an error ?

Thanks

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
outlaw
  • 241
  • 4
  • 16

1 Answers1

1

Macro annotations transform untyped trees, so you should use c.typecheck

import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

class GreaterThan(m: Double) extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro GreaterThanMacro.impl
}
object GreaterThanMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    annottees match {
      case q"$_ val $_: $tpt = $_" :: tail =>
        if (!(c.typecheck(tq"$tpt", mode = c.TYPEmode).tpe <:< typeOf[Double]))
          c.abort(c.enclosingPosition, s"$tpt is not Double")
 
        q"..$tail"
    }
  }
}
case class Person(name: String, @GreaterThan(0) age: Double) // compiles

  // doesn't compile: String is not Double
// case class Person(@GreaterThan(0) name: String, age: Double)

Besides c.abort there are c.error, c.warning.

Please beware of limitations for type checking in macro annotations (when a type is defined in the same scope where annotation is expanded)

type Dbl = Double // outer scope

{
  case class Person(name: String, @GreaterThan(0) age: Dbl) // compiles
}
type Dbl = Double // the same scope

  // doesn't compile: scala.reflect.macros.TypecheckException: not found: type Dbl
// case class Person(name: String, @GreaterThan(0) age: Dbl)

Scala whitebox macro how to check if class fields are of type of a case class

Sometimes implicit macros (macro-generated type classes) can be an alternative to macro annotations.

Maybe you could also try the library https://github.com/fthomas/refined/

import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Positive

case class Person(name: String, age: Double Refined Positive)
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Can I access the Refined types information (`Refined Positive`) using reflection? – outlaw Feb 21 '23 at 21:58
  • @outlaw I hope by reflection you mean compile-time reflection (macros). In such case yes I guess you do (although this can depend on what you mean by information). `Double Refined Positive` is `Refined[Double, Positive]` so just the type of a field. If you mean runtime reflection then because of type erasure `Refined[Double, Positive]` is just `Refined[_, _]` so you'll need `TypeTag`s to restore type arguments. – Dmytro Mitin Feb 21 '23 at 22:20