Last active
February 16, 2016 18:19
-
-
Save bwmcadams/937ca257e093d00efadf to your computer and use it in GitHub Desktop.
ADT Root Type validator, makes sure it's either a trait or abstract class *AND* sealed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package codes.bytes.macros_intro.macros | |
import scala.annotation.{ compileTimeOnly, StaticAnnotation } | |
import scala.language.postfixOps | |
import scala.reflect.macros.whitebox.Context | |
import scala.language.experimental.macros | |
object ADT { | |
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { | |
import c.universe._ | |
import Flag._ | |
// check if it meets the requirements for what can be annotated | |
val inputs = annottees.map(_.tree).toList | |
val result: Tree = { | |
def validateClassDef( | |
cD: c.universe.ClassDef, | |
mods: c.universe.Modifiers, | |
name: c.universe.TypeName, | |
tparams: List[c.universe.TypeDef], | |
impl: c.universe.Template, | |
companion: Option[ModuleDef]): c.universe.Tree = { | |
if (mods.hasFlag(TRAIT)) { | |
if (!mods.hasFlag(SEALED)) { | |
c.error(c.enclosingPosition, s"ADT Root traits (trait $name) must be sealed.") | |
} | |
else { | |
c.info(c.enclosingPosition, s"ADT Root trait $name sanity checks OK.", force = true) | |
} | |
companion match { | |
/** | |
* According to the docs, if you annotate a *class* with a companion, | |
* the class and companion will be sent in (e.g., List(class, object) for | |
* entry Tree. If you annotate an *object* with a companion, only the object | |
* is passed in. | |
* | |
* Using ClassDef match, Scala will refuse to accept returned Tree unless it | |
* includes both companions sent in. For QuasiQuotes it seems to ignore that, | |
* yet the object still works fine after Macro transform. | |
*/ | |
case Some(mD) ⇒ q"$cD; $mD" | |
case None ⇒ cD | |
} | |
} else if (!mods.hasFlag(ABSTRACT)) { | |
c.error(c.enclosingPosition, s"ADT Root classes (class $name) must be abstract.") | |
cD | |
} else if (!mods.hasFlag(SEALED)) { | |
// class that's abstract | |
c.error(c.enclosingPosition, s"ADT Root classes (abstract class $name) must be sealed.") | |
cD | |
} else { | |
c.info(c.enclosingPosition, s"ADT Root class $name sanity checks OK.", force = true) | |
companion match { | |
/** | |
* According to the docs, if you annotate a *class* with a companion, | |
* the class and companion will be sent in (e.g., List(class, object) for | |
* entry Tree. If you annotate an *object* with a companion, only the object | |
* is passed in. | |
* | |
* Using ClassDef match, Scala will refuse to accept returned Tree unless it | |
* includes both companions sent in. For QuasiQuotes it seems to ignore that, | |
* yet the object still works fine after Macro transform. | |
*/ | |
case Some(mD) ⇒ q"$cD; $mD" | |
case None ⇒ cD | |
} | |
} | |
} | |
inputs match { | |
case (cD @ ClassDef(mods, name, tparams, impl)) :: Nil ⇒ | |
validateClassDef(cD, mods, name, tparams, impl, companion = None) | |
// annotated class with companion object. | |
// In the case of an annotated class/object w/ a companion, the companion is passed to | |
// annottees. We *are* assuming the class is one annotated here. | |
case (cD @ ClassDef(mods, name, tparams, impl)) :: (mD: ModuleDef) :: Nil ⇒ | |
validateClassDef(cD, mods, name, tparams, impl, companion = Some(mD)) | |
case (o @ ModuleDef(_, name, _)) :: Nil ⇒ | |
c.error(c.enclosingPosition, s"ADT Roots (object $name) may not be Objects.") | |
o | |
case (d @ DefDef(mods, name, _, _, _, _)) :: Nil ⇒ | |
c.error(c.enclosingPosition, s"ADT Roots (def $name) may not be Methods.") | |
d | |
case (d @ ValDef(mods, name, _, _)) :: Nil ⇒ | |
if (mods.hasFlag(Flag.MUTABLE)) | |
c.error(c.enclosingPosition, s"ADT Roots (var $name) may not be Variables.") | |
else | |
c.error(c.enclosingPosition, s"ADT Roots (val $name) may not be Variables.") | |
d | |
// Not sure what would hit here, I checked and you cannot annotate a package object at all | |
case x :: Nil ⇒ | |
c.error(c.enclosingPosition, s"Invalid ADT Root ($x) [${x.getClass}].") | |
x | |
case Nil ⇒ | |
c.error(c.enclosingPosition, "Cannot validate ADT Root of empty Tree.") | |
// the errors should cause us to stop before this, | |
// but needed to match up our match type | |
reify {}.tree | |
} | |
} | |
c.Expr[Any](result) | |
} | |
} | |
/** | |
* From the Macro Paradise Docs... | |
* | |
* note the @compileTimeOnly annotation. It is not mandatory, but is recommended to avoid confusion. | |
* Macro annotations look like normal annotations to the vanilla Scala compiler, so if you forget | |
* to enable the macro paradise plugin in your build, your annotations will silently fail to expand. | |
* The @compileTimeOnly annotation makes sure that no reference to the underlying definition is | |
* present in the program code after typer, so it will prevent the aforementioned situation | |
* from happening. | |
*/ | |
@compileTimeOnly("Enable Macro Paradise for Expansion of Annotations via Macros.") | |
final class ADT extends StaticAnnotation { | |
def macroTransform(annottees: Any*): Any = macro ADT.impl | |
} | |
// vim: set ts=2 sw=2 sts=2 et: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> :paste | |
// Entering paste mode (ctrl-D to finish) | |
@ADT | |
sealed abstract class TestCompanions { | |
def check = "Test Companion Class" | |
} | |
object TestCompanions { | |
def check = "Test Companion Object" | |
} | |
// Exiting paste mode, now interpreting. | |
<console>:11: ADT Root class TestCompanions sanity checks OK. | |
@ADT | |
^ | |
defined class TestCompanions | |
defined object TestCompanions | |
scala> TestCompanions.check | |
res2: String = Test Companion Object | |
scala> new TestCompanions {}.check | |
res3: String = Test Companion Class |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> import codes.bytes.macros_intro.macros.ADT | |
import codes.bytes.macros_intro.macros.ADT | |
scala> :paste | |
// Entering paste mode (ctrl-D to finish) | |
@ADT | |
trait Foo | |
@ADT | |
object Bar | |
@ADT | |
abstract class Baz | |
@ADT | |
sealed trait Spam | |
@ADT | |
sealed abstract class Eggs | |
// Exiting paste mode, now interpreting. | |
<console>:11: error: ADT Root traits (trait Foo) must be sealed. | |
@ADT | |
^ | |
<console>:14: error: ADT Roots (object Bar) may not be Objects. | |
@ADT | |
^ | |
<console>:17: error: ADT Root classes (abstract class Baz) must be sealed. | |
@ADT | |
^ | |
<console>:20: ADT Root trait Spam sanity checks OK. | |
@ADT | |
^ | |
<console>:23: ADT Root class Eggs sanity checks OK. | |
@ADT | |
scala> @ADT | |
| class NonAbstractUnsealedClass | |
<console>:11: error: ADT Root classes (class NonAbstractUnsealedClass) must be abstract. | |
@ADT |
Thanks to @julianpeeters, who solved the quasiquotes version of combining companions for return.
annotation macros should be whitebox, not blackbox.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fixed to work with companion objects.