Skip to content

Commit

Permalink
Merge pull request #1058 from pgrandjean/issue_1051
Browse files Browse the repository at this point in the history
Add support to extract all variable or type annotations (issue #1051)
  • Loading branch information
joroKr21 authored Apr 27, 2021
2 parents d37ec9e + 6e3b26e commit 4693565
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 34 deletions.
212 changes: 181 additions & 31 deletions core/src/main/scala/shapeless/annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,114 @@ object TypeAnnotations {
implicit def materialize[A, T, Out <: HList]: Aux[A, T, Out] = macro AnnotationMacros.materializeTypeAnnotations[A, T, Out]
}

/**
* Provides all variable annotations for the fields or constructors of case class-like or sum type `T`.
*
* If type `T` is case class-like, this type class inspects its fields and provides their variable annotations. If
* type `T` is a sum type, its constructor types are looked for variable annotations as well.
*
* Type `Out` is an HList having the same number of elements as `T` (number of fields of `T` if `T` is case
* class-like, or number of constructors of `T` if it is a sum type). It is made of `HNil` (no annotations for corresponding
* field or constructor) or `HLists` (list of annotations for corresponding field or constructor).
*
* Method `apply` provides an HList of type `Out` made of `HNil` (corresponding field or constructor not annotated)
* or `HList` (corresponding field or constructor has annotations).
*
* Note that variable annotations must be case class-like for this type class to take them into account.
*
* Example:
* {{{
* case class First(s: String)
* case class Second(i: Int)
*
* case class CC(i: Int, @First("a") @Second(0) s: String)
*
* val ccFirsts = AllAnnotations[CC]
*
* // ccFirsts.Out is HNil :: (First :: Second :: HNil) :: HNil
* // ccFirsts.apply() is
* // HNil :: (First("a") :: Second(0) :: HNil) :: HNil
*
* }}}
*
* This implementation is based on [[shapeless.Annotations]] by Alexandre Archambault.
*
* @tparam T: case class-like or sum type, whose fields or constructors are annotated
*
* @author Patrick Grandjean
*/
trait AllAnnotations[T] extends DepFn0 with Serializable {
type Out <: HList
}

object AllAnnotations {
def apply[T](implicit annotations: AllAnnotations[T]): Aux[T, annotations.Out] = annotations

type Aux[T, Out0 <: HList] = AllAnnotations[T] { type Out = Out0 }

def mkAnnotations[T, Out0 <: HList](annotations: => Out0): Aux[T, Out0] =
new AllAnnotations[T] {
type Out = Out0
def apply(): Out = annotations
}

implicit def materialize[T, Out <: HList]: Aux[T, Out] = macro AnnotationMacros.materializeAllVariableAnnotations[T, Out]
}

/**
* Provides all type annotations for the fields or constructors of case class-like or sum type `T`.
*
* If type `T` is case class-like, this type class inspects its fields and provides their type annotations. If
* type `T` is a sum type, its constructor types are looked for type annotations as well.
*
* Type `Out` is an HList having the same number of elements as `T` (number of fields of `T` if `T` is case
* class-like, or number of constructors of `T` if it is a sum type). It is made of `HNil` (no annotations for corresponding
* field or constructor) or `HLists` (list of annotations for corresponding field or constructor).
*
* Method `apply` provides an HList of type `Out` made of `HNil` (corresponding field or constructor not annotated)
* or `HList` (corresponding field or constructor has annotations).
*
* Note that type annotations must be case class-like for this type class to take them into account.
*
* Example:
* {{{
* case class First(s: String)
* case class Second(i: Int)
*
* case class CC(i: Int, s: String @First("a") @Second(0))
*
* val ccFirsts = AllTypeAnnotations[CC]
*
* // ccFirsts.Out is HNil :: (First :: Second :: HNil) :: HNil
* // ccFirsts.apply() is
* // HNil :: (First("a") :: Second(0) :: HNil) :: HNil
*
* }}}
*
* This implementation is based on [[shapeless.Annotations]] by Alexandre Archambault.
*
* @tparam T: case class-like or sum type, whose fields or constructors are annotated
*
* @author Patrick Grandjean
*/
trait AllTypeAnnotations[T] extends DepFn0 with Serializable {
type Out <: HList
}

object AllTypeAnnotations {
def apply[T](implicit annotations: AllTypeAnnotations[T]): Aux[T, annotations.Out] = annotations

type Aux[T, Out0 <: HList] = AllTypeAnnotations[T] { type Out = Out0 }

def mkAnnotations[T, Out0 <: HList](annotations: => Out0): Aux[T, Out0] =
new AllTypeAnnotations[T] {
type Out = Out0
def apply(): Out = annotations
}

implicit def materialize[T, Out <: HList]: Aux[T, Out] = macro AnnotationMacros.materializeAllTypeAnnotations[T, Out]
}

class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
import c.universe._

Expand Down Expand Up @@ -241,41 +349,27 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
def materializeVariableAnnotations[A: WeakTypeTag, T: WeakTypeTag, Out: WeakTypeTag]: Tree =
materializeAnnotations[A, T, Out](typeAnnotation = false)

def materializeAllVariableAnnotations[T: WeakTypeTag, Out: WeakTypeTag]: Tree =
materializeAllAnnotations[T, Out](typeAnnotation = false)

def materializeTypeAnnotations[A: WeakTypeTag, T: WeakTypeTag, Out: WeakTypeTag]: Tree =
materializeAnnotations[A, T, Out](typeAnnotation = true)

def materializeAllTypeAnnotations[T: WeakTypeTag, Out: WeakTypeTag]: Tree =
materializeAllAnnotations[T, Out](typeAnnotation = true)

def materializeAnnotations[A: WeakTypeTag, T: WeakTypeTag, Out: WeakTypeTag](typeAnnotation: Boolean): Tree = {
val annTpe = weakTypeOf[A]

if (!isProduct(annTpe))
abort(s"$annTpe is not a case class-like type")

val construct0 = construct(annTpe)


val tpe = weakTypeOf[T]

val annTreeOpts =
if (isProduct(tpe)) {
val constructorSyms = tpe
.member(termNames.CONSTRUCTOR)
.asMethod.paramLists.flatten
.map(sym => nameAsString(sym.name) -> sym)
.toMap

fieldsOf(tpe).map { case (name, _) =>
extract(typeAnnotation, constructorSyms(nameAsString(name))).collectFirst {
case ann if ann.tree.tpe =:= annTpe => construct0(ann.tree.children.tail)
}
}
} else if (isCoproduct(tpe))
ctorsOf(tpe).map { cTpe =>
extract(typeAnnotation, cTpe.typeSymbol).collectFirst {
case ann if ann.tree.tpe =:= annTpe => construct0(ann.tree.children.tail)
}
}
else
abort(s"$tpe is not case class like or the root of a sealed family of types")

val annTreeOpts = getAnnotationTreeOptions(tpe, typeAnnotation).map { list =>
list.find(_._1 =:= annTpe).map(_._2)
}

val wrapTpeTrees = annTreeOpts.map {
case Some(annTree) => appliedType(someTpe, annTpe) -> q"_root_.scala.Some($annTree)"
case None => noneTpe -> q"_root_.scala.None"
Expand All @@ -290,14 +384,70 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
else q"_root_.shapeless.Annotations.mkAnnotations[$annTpe, $tpe, $outTpe]($outTree)"
}

def extract(tpe: Boolean, s: Symbol): List[c.universe.Annotation] = {
if (tpe) {
s.typeSignature match {
case a: AnnotatedType => a.annotations
case _ => Nil
def materializeAllAnnotations[T: WeakTypeTag, Out: WeakTypeTag](typeAnnotation: Boolean): Tree = {
val tpe = weakTypeOf[T]
val annTreeOpts = getAnnotationTreeOptions(tpe, typeAnnotation)

val wrapTpeTrees = annTreeOpts.map {
case Nil =>
mkHListTpe(Nil) -> q"(_root_.shapeless.HNil)"
case list =>
mkHListTpe(list.map(_._1)) -> list.foldRight(q"_root_.shapeless.HNil": Tree) {
case ((_, bound), acc) => pq"_root_.shapeless.::($bound, $acc)"
}
}

val outTpe = mkHListTpe(wrapTpeTrees.map { case (aTpe, _) => aTpe })
val outTree = wrapTpeTrees.foldRight(q"_root_.shapeless.HNil": Tree) {
case ((_, bound), acc) =>
pq"_root_.shapeless.::($bound, $acc)"
}

if (typeAnnotation) q"_root_.shapeless.AllTypeAnnotations.mkAnnotations[$tpe, $outTpe]($outTree)"
else q"_root_.shapeless.AllAnnotations.mkAnnotations[$tpe, $outTpe]($outTree)"
}

def getAnnotationTreeOptions(tpe: Type, typeAnnotation: Boolean): List[List[(Type, Tree)]] = {
if (isProduct(tpe)) {
val constructorSyms = tpe
.member(termNames.CONSTRUCTOR)
.asMethod
.paramLists
.flatten
.map(sym => nameAsString(sym.name) -> sym)
.toMap

fieldsOf(tpe).map {
case (name, _) =>
extract(typeAnnotation, constructorSyms(nameAsString(name))).collect {
case ann if isProduct(ann.tree.tpe) =>
val construct1 = construct(ann.tree.tpe)
(ann.tree.tpe, construct1(ann.tree.children.tail))
}
}
} else if (isCoproduct(tpe)) {
ctorsOf(tpe).map { cTpe =>
extract(typeAnnotation, cTpe.typeSymbol).collect {
case ann if isProduct(ann.tree.tpe) =>
val construct1 = construct(ann.tree.tpe)
(ann.tree.tpe, construct1(ann.tree.children.tail))
}
}
} else {
s.annotations
abort(s"$tpe is not case class like or the root of a sealed family of types")
}
}

def extract(tpe: Boolean, s: Symbol): List[c.universe.Annotation] = {
def fromType(t: Type): List[c.universe.Annotation] = t match {
case AnnotatedType(annotations, _) => annotations.reverse
case ClassInfoType(parents, _, _) => parents.flatMap(fromType)
case TypeRef(_, sym, _) if sym.asType.isAliasType => extract(tpe, sym)
case _ => Nil
}

if (tpe) fromType(s.typeSignature)
else s.annotations
}

}
51 changes: 48 additions & 3 deletions core/src/test/scala/shapeless/annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ package shapeless

import scala.annotation.{ Annotation => saAnnotation }
import org.junit.Test
import shapeless.test.illTyped
import shapeless.test.{illTyped, typed}

object AnnotationTestsDefinitions {

case class First() extends saAnnotation
case class Second(i: Int, s: String) extends saAnnotation
case class Third(c: Char) extends saAnnotation

case class Other() extends saAnnotation
case class Last(b: Boolean) extends saAnnotation
Expand All @@ -40,7 +41,11 @@ object AnnotationTestsDefinitions {

sealed trait Base
@First case class BaseI(i: Int) extends Base
@Second(3, "e") case class BaseS(s: String) extends Base
@Second(3, "e") @Third('c') case class BaseS(s: String) extends Base

sealed trait Base2
case class BaseI2(i: Int) extends Base2 @First
case class BaseS2(s: String) extends Base2 @Second(3, "e") @Third('c')

trait Dummy

Expand All @@ -49,6 +54,22 @@ object AnnotationTestsDefinitions {
s: String,
ob: Option[Boolean] @Second(2, "b")
)

case class CC3(
@First i: Int,
s: String,
@Second(2, "b") @Third('c') ob: Option[Boolean]
)

case class CC4(
i: Int @First,
s: String,
ob: Option[Boolean] @Second(2, "b") @Third('c')
)

type PosInt = Int @First
type Email = String @Third('c')
case class User(age: PosInt, email: Email)
}

class AnnotationTests {
Expand Down Expand Up @@ -172,7 +193,31 @@ class AnnotationTests {
def invalidTypeAnnotations: Unit = {
illTyped(" TypeAnnotations[Dummy, CC2] ", "could not find implicit value for parameter annotations: .*")
illTyped(" TypeAnnotations[Dummy, Base] ", "could not find implicit value for parameter annotations: .*")
illTyped(" TypeAnnotations[Second, Dummy] ", "could not find implicit value for parameter annotations: .*")
illTyped(" TypeAnnotations[Second, Dummy] ", "could not find implicit value for parameter annotations: .*")
}

@Test
def allAnnotations: Unit = {
val cc = AllAnnotations[CC3].apply()
typed[(First :: HNil) :: HNil :: (Second :: Third :: HNil) :: HNil](cc)
assert(cc == (First() :: HNil) :: HNil :: (Second(2, "b") :: Third('c') :: HNil) :: HNil)

val st = AllAnnotations[Base].apply()
typed[(First :: HNil) :: (Second :: Third :: HNil) :: HNil](st)
}

@Test
def allTypeAnnotations: Unit = {
val st = AllTypeAnnotations[Base2].apply() // sealed trait
typed[(First :: HNil) :: (Second :: Third :: HNil) :: HNil](st)

val cc = AllTypeAnnotations[CC4].apply() // case class
typed[(First :: HNil) :: HNil :: (Second :: Third :: HNil) :: HNil](cc)
assert(cc == (First() :: HNil) :: HNil :: (Second(2, "b") :: Third('c') :: HNil) :: HNil)

val user = AllTypeAnnotations[User].apply() // type refs
typed[(First :: HNil) :: (Third :: HNil) :: HNil](user)
assert(user == (First() :: HNil) :: (Third('c') :: HNil) :: HNil)
}

}

0 comments on commit 4693565

Please sign in to comment.