Skip to content

Commit

Permalink
Merge pull request #925 from pgrandjean/issue_922
Browse files Browse the repository at this point in the history
Add support for type annotations (issue #922)
  • Loading branch information
milessabin authored Sep 29, 2019
2 parents e4aaa66 + 7a0d0e4 commit 26596c5
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 8 deletions.
92 changes: 84 additions & 8 deletions core/src/main/scala/shapeless/annotation.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-6 Alexandre Archambault
* Copyright (c) 2015-9 Alexandre Archambault
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -114,9 +114,66 @@ object Annotations {
def apply() = annotations
}

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

/**
* Provides the type annotations of type `A` of 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 of type `A`. If
* type `T` is a sum type, its constructor types are looked for type annotations.
*
* 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 `None.type` (no annotation on corresponding
* field or constructor) and `Some[A]` (corresponding field or constructor is annotated).
*
* Method `apply` provides an HList of type `Out` made of `None` (corresponding field or constructor not annotated)
* or `Some(annotation)` (corresponding field or constructor has annotation `annotation`).
*
* 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 CC(i: Int, s: String @First("a"))
*
* val ccFirsts = TypeAnnotations[First, CC]
*
* // ccFirsts.Out is None.type :: Some[First] :: HNil
* // ccFirsts.apply() is
* // None :: Some(First("a")) :: HNil
*
* }}}
*
* This implementation is based on [[shapeless.Annotations]] by Alexandre Archambault.
*
* @tparam A: type annotation type
* @tparam T: case class-like or sum type, whose fields or constructors are annotated
*
* @author Patrick Grandjean
*/
trait TypeAnnotations[A,T] extends DepFn0 with Serializable {
type Out <: HList
}

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

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

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

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

/*
* AnnotationsHelper is defined in scala_2.10 or scala_2.11+ folders.
*/
@macrocompat.bundle
class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
import c.universe._
Expand Down Expand Up @@ -162,7 +219,11 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
}
}

def materializeAnnotations[A: WeakTypeTag, T: WeakTypeTag, Out: WeakTypeTag]: Tree = {
def materializeVariableAnnotations[A: WeakTypeTag, T: WeakTypeTag, Out: WeakTypeTag]: Tree = materializeAnnotations[A, T, Out](false)

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

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

if (!isProduct(annTpe))
Expand All @@ -185,16 +246,19 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
fieldsOf(tpe).map { case (name, _) =>
val paramConstrSym = constructorSyms(name.decodedName.toString)

paramConstrSym.annotations.collectFirst {
val annots = extract(typeAnnotation, paramConstrSym)
annots.collectFirst {
case ann if ann.tree.tpe =:= annTpe => construct0(ann.tree.children.tail)
}
}
} else if (isCoproduct(tpe))
ctorsOf(tpe).map { cTpe =>
cTpe.typeSymbol.annotations.collectFirst {
case ann if ann.tree.tpe =:= annTpe => construct0(ann.tree.children.tail)
}

val annots = extract(typeAnnotation, cTpe.typeSymbol)
annots.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")

Expand All @@ -208,6 +272,18 @@ class AnnotationMacros(val c: whitebox.Context) extends CaseClassMacros {
case ((_, bound), acc) => pq"_root_.shapeless.::($bound, $acc)"
}

q"_root_.shapeless.Annotations.mkAnnotations[$annTpe, $tpe, $outTpe]($outTree)"
if (typeAnnotation) q"_root_.shapeless.TypeAnnotations.mkAnnotations[$annTpe, $tpe, $outTpe]($outTree)"
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
}
} else {
s.annotations
}
}
}
37 changes: 37 additions & 0 deletions core/src/test/scala/shapeless/annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ object AnnotationTestsDefinitions {

trait Dummy

case class CC2(
i: Int @First,
s: String,
ob: Option[Boolean] @Second(2, "b")
)
}

class AnnotationTests {
Expand Down Expand Up @@ -118,4 +123,36 @@ class AnnotationTests {
illTyped(" Annotations[Second, Dummy] ", "could not find implicit value for parameter annotations: .*")
}

@Test
def typeAnnotations: Unit = {
{
val first: Some[First] :: None.type :: None.type :: HNil = TypeAnnotations[First, CC2].apply()
assert(first == Some(First()) :: None :: None :: HNil)

val second: None.type :: None.type :: Some[Second] :: HNil = TypeAnnotations[Second, CC2].apply()
assert(second == None :: None :: Some(Second(2, "b")) :: HNil)

val unused: None.type :: None.type :: None.type :: HNil = TypeAnnotations[Unused, CC2].apply()
assert(unused == None :: None :: None :: HNil)
}

{
val first = TypeAnnotations[First, CC2].apply()
assert(first == Some(First()) :: None :: None :: HNil)

val second = TypeAnnotations[Second, CC2].apply()
assert(second == None :: None :: Some(Second(2, "b")) :: HNil)

val unused = TypeAnnotations[Unused, CC2].apply()
assert(unused == None :: None :: None :: HNil)
}
}

@Test
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: .*")
}

}

0 comments on commit 26596c5

Please sign in to comment.