Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to extract all variable or type annotations (issue #1051) #1058

Merged
merged 5 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 181 additions & 26 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,10 +384,71 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
else q"_root_.shapeless.Annotations.mkAnnotations[$annTpe, $tpe, $outTpe]($outTree)"
}

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 {
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] = {
if (tpe) {
s.typeSignature match {
case a: AnnotatedType => a.annotations
case a: AnnotatedType => a.annotations.reverse
case c: ClassInfoType =>
val parents = c.parents
parents.flatMap {
case a: AnnotatedType => a.annotations.reverse
case _ => Nil
}
case t: TypeRef => extract(tpe, t.sym)
pgrandjean marked this conversation as resolved.
Show resolved Hide resolved
case _ => Nil
}
} else {
Expand Down
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)
pgrandjean marked this conversation as resolved.
Show resolved Hide resolved
}

}