Skip to content

Latest commit

 

History

History
143 lines (105 loc) · 6.66 KB

Annotations.md

File metadata and controls

143 lines (105 loc) · 6.66 KB

Annotation processing

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

Annotation inheritance

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 a sealed 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 or var definitions which correspond to constructor parameters are inherited by these constructor parameters themselves.

Extending existing annotations

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

Aggregating annotations

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").

Annotation order

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

@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.