Various macro engines used in AVSystem Commons library inspect annotations on classes, methods,
parameters and types. For example, GenCodec
materialization inspects annotation
that may adjust how the codec is made.
Because of heavy use of annotations, some common rules, conventions and utilities have been created for annotation processing in all macro engines implemented by AVSystem Commons. They allow the programmer to reduce boilerplate by reusing and grouping annotations.
Mechanisms described below are all implemented in MacroCommons
base trait used in macro implementations.
Table of Contents generated with DocToc
When a MacroCommons
-based macro looks for annotations, they are automatically inherited. What that means
exactly depends on the target symbol of an annotation:
- Annotations applied on a class or trait are automatically inherited by all their subtraits, subclasses
and objects. The only exception is when an annotation extending
NotInheritedFromSealedTypes
is applied on asealed
trait or class. - Annotations applied on a member (
def
,val
,var
) are automatically inherited by all members that implement or override it. - Annotations applied on method parameters (or type parameters) are automatically inherited by corresponding parameters in all implementing and overriding methods.
- Annotations applied on
val
orvar
definitions which correspond to constructor parameters are inherited by these constructor parameters themselves.
You may create subclasses of existing annotations used by macro engines. For example, we may create a subclass
of @name
annotation used by GenCodec
materialization:
import com.avsystem.commons.serialization._
class customName(override val name: String) extends name(name)
Now, when the macro looks for @name
annotation, it will also find @customName
because of their
subtyping relation. However, in order for the macro to be able to properly statically (in compile time)
extract the name
constructor parameter, it must be declared in exactly like in the example above, i.e.
it must be an override val
constructor parameter which overrides the original one.
Note that for such statically inspected annotation parameters you cannot simply pass them as super constructor parameters, e.g. the following will NOT work:
class id extends name("id") // doesn't work
This is because super constructor parameters are not available for macros in compile time. The same effect can however be achieved using annotation aggregates
It is possible to create your own annotations which group - or aggregate - multiple other annotations. This way you can reduce boilerplate associated with annotations.
In order to do that, you must create an annotation class that extends AnnotationAggregate
and
implement its aggregated
method as final def
, using reifyAggregated
macro as its body.
You can then put your aggregated annotations on that method itself, e.g.
import com.avsystem.commons.annotation._
import com.avsystem.commons.serialization._
class id extends AnnotationAggregate {
@name("id")
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
Now, when something is annotated as @id
, macro engines will effectively "explode" this annotation
into all annotations applied on the aggregated
method. This is done recursively which means that
the aggregated
method itself may be annotated with more aggregating annotations.
Having to put aggregated annotations on the aggregated
method may seem strange. It would seem be more natural to
put aggregated annotations on the aggregate class itself, e.g. @name("id") class id extends AnnotationAggregate
(this doesn't work). However, having aggregated annotations on a member of aggregate class allows us to access its
parameters, e.g.
import com.avsystem.commons.annotation._
import com.avsystem.commons.serialization._
class customName(name: String) extends AnnotationAggregate {
@name(name)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
Now, when "exploding" the aggregate, the macro engine will statically replace references to
constructor parameters of the aggregate inside aggregated
annotations. For example, when something
is annotated as @customName("id")
the macro will effectively see @name("id")
.
Scala allows annotating symbols with the same annotation multiple times. Also, annotation may be repeated because of inheritance - the same annotation may be applied on a symbol directly and inherited. In such situations, the order of annotations is well defined:
- When two annotations are applied directly on the same symbol, the first one takes precedence, e.g.
@name("A") @name("B") class C
-@name("A")
has precedence over@name("B")
- Annotations applied directly have precedence over inherited annotations.
- Among inherited annotations, precedence is determined based on linearization order and therefore is analogous to how class/trait members override themselves.
With respect to the order defined above, macro engines may sometimes take only the first annotation of given type or sometimes inspect all of them, in that order. This depends on particular annotation and how it's understood by particular macro engine.
@defaultsToName
is a meta-annotation that you may put on a String
typed constructor parameter
of an annotation. This parameter may then take a dummy default value (e.g. null
) and macro engines
will replace that default value with annotated symbol's original (source) name. For example:
class awesome(@defaultsToName rawName: String = null) extends StaticAnnotation
Now, annotating a class with @awesome
without giving the rawName
argument explicitly:
@awesome class Klass
is equivalent to annotating it as @awesome("Klass")
.
This of course works for all kinds of symbols that can be annotated, not only classes.