From 25fc9c302f07da8b1225afb99e949b58503889a5 Mon Sep 17 00:00:00 2001 From: Patrick Grandjean Date: Tue, 14 Dec 2021 19:07:26 -0800 Subject: [PATCH] Implementation of issue #55: Implement type annotations --- .gitignore | 3 + .../shapeless3/deriving/annotation.scala | 304 ++++++++++++++++-- .../shapeless3/deriving/annotation.scala | 106 ++++++ 3 files changed, 382 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 0863164..1a58805 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ local.sbt *.iml *.iws +# Ignore project files for VSCode +.vscode + # Ignore OS X metadata .DS_Store diff --git a/modules/deriving/src/main/scala/shapeless3/deriving/annotation.scala b/modules/deriving/src/main/scala/shapeless3/deriving/annotation.scala index 593b679..77ee1f7 100644 --- a/modules/deriving/src/main/scala/shapeless3/deriving/annotation.scala +++ b/modules/deriving/src/main/scala/shapeless3/deriving/annotation.scala @@ -18,9 +18,10 @@ package shapeless3.deriving import scala.deriving.* import scala.quoted.* - import shapeless3.deriving.internals.* +import scala.annotation.tailrec + /** * Evidence that type `T` has annotation `A`, and provides an instance of the annotation. * @@ -121,9 +122,178 @@ object Annotations { } transparent inline implicit def mkAnnotations[A, T]: Annotations[A, T] = - ${ AnnotationMacros.mkAnnotations[A, T] } + ${ AnnotationMacros.mkVariableAnnotations[A, T] } +} + +/** + * 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]) + * // ccFirsts.apply() is (None, Some(First("a"))) + * + * }}} + * + * 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 Serializable { + type Out <: Tuple + + def apply(): Out +} + +object TypeAnnotations { + def apply[A, T](implicit annotations: TypeAnnotations[A, T]): Aux[A, T, annotations.Out] = annotations + + type Aux[A, T, Out0 <: Tuple] = TypeAnnotations[A, T] { type Out = Out0 } + + def mkAnnotations[A, T, Out0 <: Tuple](annotations: Out0): Aux[A, T, Out0] = + new TypeAnnotations[A, T] { + type Out = Out0 + def apply(): Out = annotations + } + + transparent inline implicit def mkAnnotations[A, T]: TypeAnnotations[A, T] = + ${ AnnotationMacros.mkTypeAnnotations[A, T] } +} +/** + * 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 ((), (First, Second)) + * // ccFirsts.apply() is + * // ((), (First("a"), Second(0))) + * + * }}} + * + * 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 Serializable { + type Out <: Tuple + + def apply(): Out } +object AllAnnotations { + def apply[T](implicit annotations: AllAnnotations[T]): Aux[T, annotations.Out] = annotations + + type Aux[T, Out0 <: Tuple] = AllAnnotations[T] { type Out = Out0 } + + def mkAnnotations[T, Out0 <: Tuple](annotations: Out0): Aux[T, Out0] = + new AllAnnotations[T] { + type Out = Out0 + def apply(): Out = annotations + } + + transparent inline implicit def mkAnnotations[T]: AllAnnotations[T] = + ${ AnnotationMacros.mkAllVariableAnnotations[T] } +} + +/** + * 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 Serializable { + type Out <: Tuple + + def apply(): Out + } + + object AllTypeAnnotations { + def apply[T](implicit annotations: AllTypeAnnotations[T]): Aux[T, annotations.Out] = annotations + + type Aux[T, Out0 <: Tuple] = AllTypeAnnotations[T] { type Out = Out0 } + + def mkAnnotations[T, Out0 <: Tuple](annotations: Out0): Aux[T, Out0] = + new AllTypeAnnotations[T] { + type Out = Out0 + def apply(): Out = annotations + } + + transparent inline implicit def mkAnnotations[T]: AllTypeAnnotations[T] = + ${ AnnotationMacros.mkAllTypeAnnotations[T] } + } + object AnnotationMacros { def mkAnnotation[A: Type, T: Type](using Quotes): Expr[Annotation[A, T]] = { import quotes.reflect._ @@ -145,45 +315,117 @@ object AnnotationMacros { } } - def mkAnnotations[A: Type, T: Type](using q: Quotes): Expr[Annotations[A, T]] = { - import quotes.reflect._ + def mkVariableAnnotations[A: Type, T: Type](using Quotes) = mkAnnotations[A, T, Annotations](ofExprVariableAnnotations[A, T](_)) + + def mkTypeAnnotations[A: Type, T: Type](using Quotes) = mkAnnotations[A, T, TypeAnnotations](ofExprTypeAnnotations[A, T](_)) + + def mkAnnotations[A: Type, T: Type, AS[A, T]: Type](mk: Seq[Expr[Any]] => Expr[AS[A, T]])(using q: Quotes): Expr[AS[A, T]] = + import q.reflect._ + + val tpe = TypeRepr.of[AS[A, T]] <:< TypeRepr.of[TypeAnnotations[A, T]] + // println(s"tpe = ${tpe}") val annotTpe = TypeRepr.of[A] val annotFlags = annotTpe.typeSymbol.flags if (annotFlags.is(Flags.Abstract) || annotFlags.is(Flags.Trait)) { report.throwError(s"Bad annotation type ${annotTpe.show} is abstract") } else { - val r = new ReflectionUtils(q) - import r._ - - def mkAnnotations(annotTrees: Seq[Expr[Any]]): Expr[Annotations[A, T]] = - Expr.ofTupleFromSeq(annotTrees) match { - case '{ $t: tup } => '{ Annotations.mkAnnotations[A, T, tup & Tuple]($t) } - } - - def findAnnotation[A: Type](annoteeSym: Symbol): Expr[Option[A]] = - // TODO try to use `getAnnotation` for performance - annoteeSym.annotations.find(_.tpe <:< TypeRepr.of[A]) match { + val annotations = extractAnnotations[T](tpe) + // println(s"extractAnnotations = \n\t${annotations.mkString("\n\t")}") + val exprs = annotations.map { child => + child.find(_.tpe <:< TypeRepr.of[A]) match { case Some(tree) => '{ Some(${tree.asExprOf[A]}) } case None => '{ None } } + } - val annoteeTpe = TypeRepr.of[T] - annoteeTpe.classSymbol match { - case Some(annoteeCls) if annoteeCls.flags.is(Flags.Case) => - val valueParams = annoteeCls.primaryConstructor.paramSymss - .find(_.headOption.fold(false)( _.isTerm)).getOrElse(Nil) - mkAnnotations(valueParams.map { vparam => findAnnotation[A](vparam) }) - case Some(annoteeCls) => - Mirror(annoteeTpe) match { - case Some(rm) => - mkAnnotations(rm.MirroredElemTypes.map { child => findAnnotation[A](child.typeSymbol) }) - case None => - report.throwError(s"No Annotations for sum type ${annoteeTpe.show} with no Mirror") - } - case None => - report.throwError(s"No Annotations for non-class ${annoteeTpe.show}") + mk(exprs) + } + + def mkAllVariableAnnotations[T: Type](using Quotes) = mkAllAnnotations[T, AllAnnotations](ofExprAllVariableAnnotations) + + def mkAllTypeAnnotations[T: Type](using Quotes) = mkAllAnnotations[T, AllTypeAnnotations](ofExprAllTypeAnnotations) + + def mkAllAnnotations[T: Type, AS[T]: Type](mk: Seq[Expr[Any]] => Expr[AS[T]])(using q: Quotes): Expr[AS[T]] = + import q.reflect._ + + val tpe = TypeRepr.of[AS[T]] <:< TypeRepr.of[AllTypeAnnotations[T]] + // println(s"tpe = ${tpe}") + + val annotations = extractAnnotations[T](tpe) + // println(s"annotations = \n\t${annotations.mkString("\n\t")}") + val exprs = annotations.map { anns => + Expr.ofTupleFromSeq(anns.map(_.asExpr)) + } + + mk(exprs) + + def extractAnnotations[T: Type](tpe: Boolean)(using q: Quotes): Seq[List[q.reflect.Term]] = + import q.reflect._ + + val r = new ReflectionUtils(q) + import r._ + + def getAnnotations(tree: Tree, acc: List[Term] = Nil, depth: Int = 0): List[Term] = + // println(s"${depth}: ${tree.show(using Printer.TreeStructure)}") + if (tpe) { + tree match { + case classDef: ClassDef => classDef.parents.flatMap(getAnnotations(_, acc, depth + 1)) + case valDef: ValDef => getAnnotations(valDef.tpt, acc, depth + 1) + case typeId: TypeIdent => getAnnotationsFromType(typeId.tpe, acc, depth) + case inferred: Inferred => getAnnotationsFromType(inferred.tpe, acc, depth) + case annotated: Annotated => getAnnotations(annotated.arg, annotated.annotation :: acc, depth + 1) + case _ => acc + } + } else { + tree.symbol.annotations.reverse + } + + @tailrec + def getAnnotationsFromType(typeRepr: TypeRepr, acc: List[Term] = Nil, depth: Int = 0): List[Term] = + // println(s"${depth}: typeRepr = ${typeRepr}") + typeRepr match { + case annotatedType: AnnotatedType => getAnnotationsFromType(annotatedType.underlying, annotatedType.annotation :: acc, depth + 1) + case typeRef: TypeRef if typeRef.typeSymbol.isAliasType => getAnnotationsFromType(typeRef.translucentSuperType, acc, depth + 1) + case _ => acc } + + val annoteeTpe = TypeRepr.of[T] + annoteeTpe.classSymbol match { + case Some(annoteeCls) if annoteeCls.flags.is(Flags.Case) => + val valueParams = annoteeCls.primaryConstructor + .paramSymss + .find(_.headOption.fold(false)( _.isTerm)) + .getOrElse(Nil) + valueParams.map { vparam => getAnnotations(vparam.tree) } + case Some(annoteeCls) => + Mirror(annoteeTpe) match { + case Some(rm) => + rm.MirroredElemTypes.map { child => getAnnotations(child.typeSymbol.tree) } + case None => + report.throwError(s"No Annotations for sum type ${annoteeTpe.show} with no Mirror") + } + case None => + report.throwError(s"No Annotations for non-class ${annoteeTpe.show}") + } + + def ofExprVariableAnnotations[A: Type, T: Type](annotTrees: Seq[Expr[Any]])(using q: Quotes): Expr[Annotations[A, T]] = + Expr.ofTupleFromSeq(annotTrees) match { + case '{ $t: tup } => '{ Annotations.mkAnnotations[A, T, tup & Tuple]($t) } + } + + def ofExprTypeAnnotations[A: Type, T: Type](annotTrees: Seq[Expr[Any]])(using q: Quotes): Expr[TypeAnnotations[A, T]] = + Expr.ofTupleFromSeq(annotTrees) match { + case '{ $t: tup } => '{ TypeAnnotations.mkAnnotations[A, T, tup & Tuple]($t) } + } + + def ofExprAllVariableAnnotations[T: Type](annotTrees: Seq[Expr[Any]])(using q: Quotes): Expr[AllAnnotations[T]] = + Expr.ofTupleFromSeq(annotTrees) match { + case '{ $t: tup } => '{ AllAnnotations.mkAnnotations[T, tup & Tuple]($t) } + } + + def ofExprAllTypeAnnotations[T: Type](annotTrees: Seq[Expr[Any]])(using q: Quotes): Expr[AllTypeAnnotations[T]] = + Expr.ofTupleFromSeq(annotTrees) match { + case '{ $t: tup } => '{ AllTypeAnnotations.mkAnnotations[T, tup & Tuple]($t) } } - } } diff --git a/modules/deriving/src/test/scala/shapeless3/deriving/annotation.scala b/modules/deriving/src/test/scala/shapeless3/deriving/annotation.scala index d1e06de..fc6e7d0 100644 --- a/modules/deriving/src/test/scala/shapeless3/deriving/annotation.scala +++ b/modules/deriving/src/test/scala/shapeless3/deriving/annotation.scala @@ -24,6 +24,7 @@ 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 @@ -44,6 +45,34 @@ object AnnotationTestsDefinitions { trait Abstract1 abstract class Abstract2 + + 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 + + case class CC2( + i: Int @First, + 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 { @@ -120,4 +149,81 @@ class AnnotationTests { illTyped(" Annotations[Abstract2, Base] ", ".*no implicit argument.*") illTyped(" Annotations[Second, Abstract1] ", ".*no implicit argument.*") } + + @Test + def typeAnnotations: Unit = { + { + val first: (Some[First], None.type, None.type) = TypeAnnotations[First, CC4].apply() + assert(first == (Some(First()), None, None)) + + val second: (None.type, None.type, Some[Second]) = TypeAnnotations[Second, CC2].apply() + assert(second == (None, None, Some(Second(2, "b")))) + + val unused: (None.type, None.type, None.type) = TypeAnnotations[Unused, CC2].apply() + assert(unused == (None, None, None)) + + val firstSum: (Some[First], None.type) = TypeAnnotations[First, Base2].apply() + assert(firstSum == (Some(First()), None)) + + val secondSum: (None.type, Some[Second]) = TypeAnnotations[Second, Base2].apply() + assert(secondSum == (None, Some(Second(3, "e")))) + } + + { + val first = TypeAnnotations[First, CC2].apply() + assert(first == (Some(First()), None, None)) + + val second = TypeAnnotations[Second, CC2].apply() + assert(second == (None, None, Some(Second(2, "b")))) + + val unused = TypeAnnotations[Unused, CC2].apply() + assert(unused == (None, None, None)) + + val firstSum = TypeAnnotations[First, Base2].apply() + assert(firstSum == (Some(First()), None)) + + val secondSum = TypeAnnotations[Second, Base2].apply() + assert(secondSum == (None, Some(Second(3, "e")))) + } + } + + @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: .*") + } + + @Test + def allAnnotations: Unit = { + type T1First = Tuple1[First] + val first: T1First = Tuple1(First()) + + val cc: (T1First, EmptyTuple.type, (Second, Third)) = AllAnnotations[CC3].apply() + assert(cc == (first, EmptyTuple, (Second(2, "b"), Third('c')))) + + type T1Second = Tuple1[Second] + val second: T1Second = Tuple1(Second(3, "e")) + + val st: (T1First, T1Second) = AllAnnotations[Base].apply() + assert(st == (first, second)) + } + + @Test + def allTypeAnnotations: Unit = { + type T1First = Tuple1[First] + val first: T1First = Tuple1(First()) + + val st: (T1First, (Second, Third)) = AllTypeAnnotations[Base2].apply() // sealed trait + assert(cc == (first, (Second(3, "e"), Third('c')))) + + val cc: (T1First, EmptyTuple.type, (Second, Third)) = AllTypeAnnotations[CC4].apply() // case class + assert(cc == (first, EmptyTuple, (Second(2, "b"), Third('c')))) + + type T1Third = Tuple1[Third] + val third: T1Third = Tuple1(Third('c')) + + val user: (T1First, T1Third) = AllTypeAnnotations[User].apply() // type refs + assert(user == (first, third)) + } }