From 97928308f2f54d4fab7cf05a5165d7cc1006d611 Mon Sep 17 00:00:00 2001 From: Corey Woodfield Date: Tue, 18 Jul 2023 23:56:21 -0600 Subject: [PATCH 1/3] Update scala 3 macro to support a hierarchy of sealed traits --- .../enumeratum/values/ValueEnumSpec.scala | 16 ++++++++++++++++ .../scala-3/enumeratum/ValueEnumMacros.scala | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala index 7f7ff968..3cd7ecc8 100644 --- a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala +++ b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala @@ -135,6 +135,22 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers { """ should (compile) } + it("should compile when there is a hierarchy of sealed traits") { + """ + sealed abstract class Top(val value: Int) extends IntEnumEntry + sealed trait Middle extends Top + + case object Top extends IntEnum[Top] { + case object One extends Top(1) + case object Two extends Top(2) + case object Three extends Top(3) with Middle + case object Four extends Top(4) with Middle + + val values = findValues + } + """ should compile + } + it("should fail to compile when there are non literal values") { """ sealed abstract class ContentTypeRepeated(val value: Long, name: String) extends LongEnumEntry diff --git a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala index eab04a0b..8f3d3153 100644 --- a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala +++ b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala @@ -109,7 +109,8 @@ object ValueEnumMacros { tpe: Type[A], valueTpe: Type[ValueType] )(using cls: ClassTag[ValueType]): Expr[IndexedSeq[A]] = { - type TakeHead[Head <: A & Singleton, Tail <: Tuple] = Head *: Tail + type SingletonHead[Head <: A & Singleton, Tail <: Tuple] = Head *: Tail + type OtherHead[Head <: A, Tail <: Tuple] = Head *: Tail type SumOf[X <: A, T <: Tuple] = Mirror.SumOf[X] { type MirroredElemTypes = T @@ -186,7 +187,7 @@ In SBT settings: values: Map[TypeRepr, ValueType] )(using tupleTpe: Type[T]): Either[String, Expr[List[A]]] = tupleTpe match { - case '[TakeHead[h, tail]] => { + case '[SingletonHead[h, tail]] => { val htpr = TypeRepr.of[h] (for { @@ -224,6 +225,19 @@ In SBT settings: } } + case '[OtherHead[h, tail]] => + Expr.summon[Mirror.SumOf[h]] match { + case Some(sum) => + sum.asTerm.tpe.asType match { + case '[SumOf[a, t]] => collect[Tuple.Concat[t, tail]](instances, values) + + case _ => Left(s"Invalid `Mirror.SumOf[${TypeRepr.of[h].show}]") + } + + case None => + Left(s"Missing `Mirror.SumOf[${TypeRepr.of[h].show}]`") + } + case '[EmptyTuple] => { val allowAlias = repr <:< TypeRepr.of[AllowAlias] From 7b8f1030011825c3f915cb617eed0d951691ac36 Mon Sep 17 00:00:00 2001 From: Jaden Peterson Date: Thu, 20 Jul 2023 11:33:32 -0400 Subject: [PATCH 2/3] Updated the Scala 3 macro to support entries with type parameters --- .../scala/enumeratum/values/ValueEnumSpec.scala | 15 +++++++++++++++ .../main/scala-3/enumeratum/ValueEnumMacros.scala | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala index 7f7ff968..798dc7c5 100644 --- a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala +++ b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala @@ -152,6 +152,21 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers { } """ shouldNot compile } + + it("should compile when entries accept type parameters") { + """ + sealed abstract class ExampleEnumEntry[Suffix](override val value: String) extends StringEnumEntry { + def toString(suffix: Suffix): String = value + suffix.toString + } + + object ExampleEnum extends StringEnum[ExampleEnumEntry[?]] { + case object Entry1 extends ExampleEnumEntry[Int]("Entry1") + case object Entry2 extends ExampleEnumEntry[String]("Entry2") + + override def values: IndexedSeq[ExampleEnumEntry[?]] = findValues + } + """ should compile + } } describe("trying to use with improper types") { diff --git a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala index eab04a0b..89a8cca9 100644 --- a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala +++ b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala @@ -195,8 +195,8 @@ In SBT settings: case ClassDef(_, _, spr, _, rhs) => { val fromCtor = spr .collectFirst { - case Apply(Select(New(id), _), args) if id.tpe <:< repr => - args + case Apply(Select(New(id), _), args) if id.tpe <:< repr => args + case Apply(TypeApply(Select(New(id), _), _), args) if id.tpe <:< repr => args } .flatMap(_.lift(valueParamIndex).collect { case ConstVal(const) => const From 0496db666eeb55e01fc1e3e6af1f5314989d99fb Mon Sep 17 00:00:00 2001 From: Corey Woodfield Date: Thu, 20 Jul 2023 14:08:43 -0600 Subject: [PATCH 3/3] Make findValues less strict about types so higher-kinded types work Some code of the form `extends StringEnum[Entry[?]]` that worked in scala 2 no longer works in scala 3, as existential types were removed. The simplest way to get the types to work is to change the `?` to `Any`. But then `findValues` can't process all the enum entries because they don't extend `Entry[Any]`, they extend `Entry[X]` for some type `X`. The macro was previously quite strict about types. It would get the `MirroredElemTypes` from a `Mirror.SumOf[A]`, and then while processing the types, it would check each element to make sure it extended `A`. But the elements are from the mirror, which just has the things that extend `A`, so of course they extend `A`. It would also check while looking for the constructor to pull the value from that the constructor type extended `A`. I updated this check to unwrap both types if either was an `AppliedType`. I.e. instead of checking that `Entry[Int] <:< Entry[Any]` it checks that `Entry <:< Entry`. This is a slight change from the scala 2 macro behavior, but it allows higher kinded type parameters to be used in enums without needing existential types. --- .../values/ValueEnumSpecCompat.scala | 40 +++++++++++++++++++ .../values/ValueEnumSpecCompat.scala | 40 +++++++++++++++++++ .../enumeratum/values/ValueEnumSpec.scala | 8 +++- .../scala-3/enumeratum/ValueEnumMacros.scala | 34 +++++++++++----- 4 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 enumeratum-core/src/test/scala-2/enumeratum/values/ValueEnumSpecCompat.scala create mode 100644 enumeratum-core/src/test/scala-3/enumeratum/values/ValueEnumSpecCompat.scala diff --git a/enumeratum-core/src/test/scala-2/enumeratum/values/ValueEnumSpecCompat.scala b/enumeratum-core/src/test/scala-2/enumeratum/values/ValueEnumSpecCompat.scala new file mode 100644 index 00000000..76148c69 --- /dev/null +++ b/enumeratum-core/src/test/scala-2/enumeratum/values/ValueEnumSpecCompat.scala @@ -0,0 +1,40 @@ +package enumeratum.values + +private[values] trait ValueEnumSpecCompat { this: ValueEnumSpec => + // recursively referential types can't be defined inside a describe block + abstract class AbstractEnumEntry[A, Comp <: AbstractEnumEntryCompanion[A, ?]]( + override val value: String + ) extends StringEnumEntry + abstract class AbstractEnumEntryCompanion[A, Entry <: AbstractEnumEntry[A, ?]](entry: Entry) { + def thing: A + } + + def scalaCompat = describe("Scala2 higher-kinded types") { + it("should work with higher-kinded type parameters") { + // In scala 2, `extends StringEnum[Entry[?]]` was allowed, and worked fine with findValues. + // In scala 3, `extends StringEnum[Entry[?]]` is not allowed (it fails with "unreducible application of + // higher-kinded type Entry to wildcard arguments"). The closest thing is `extends StringEnum[Entry[Any]]`, + // which would not have worked with findValues in scala 2, but has been made to work in scala 3 as the + // existential types needed to resolve the type validly have been removed. + """ + abstract class AbstractEnum[ + Entry[A] <: AbstractEnumEntry[A, Companion[A]], + Companion[A] <: AbstractEnumEntryCompanion[A, Entry[A]] + ] extends StringEnum[Entry[?]] + + sealed abstract class Enum[A](value: String) extends AbstractEnumEntry[A, EnumCompanion[A]](value) + sealed abstract class EnumCompanion[A](entry: Enum[A]) extends AbstractEnumEntryCompanion[A, Enum[A]](entry) + + object Enum extends AbstractEnum[Enum, EnumCompanion] { + case class One(thing: Int) extends EnumCompanion[Int](One) + case object One extends Enum[Int]("One") + + case class Two(thing: String) extends EnumCompanion[String](Two) + case object Two extends Enum[String]("Two") + + override def values: IndexedSeq[Enum[?]] = findValues + } + """ should compile + } + } +} diff --git a/enumeratum-core/src/test/scala-3/enumeratum/values/ValueEnumSpecCompat.scala b/enumeratum-core/src/test/scala-3/enumeratum/values/ValueEnumSpecCompat.scala new file mode 100644 index 00000000..e3d1198d --- /dev/null +++ b/enumeratum-core/src/test/scala-3/enumeratum/values/ValueEnumSpecCompat.scala @@ -0,0 +1,40 @@ +package enumeratum.values + +private[values] trait ValueEnumSpecCompat { this: ValueEnumSpec => + // recursively referential types can't be defined inside a describe block + abstract class AbstractEnumEntry[A, Comp <: AbstractEnumEntryCompanion[A, ?]]( + override val value: String + ) extends StringEnumEntry + abstract class AbstractEnumEntryCompanion[A, Entry <: AbstractEnumEntry[A, ?]](entry: Entry) { + def thing: A + } + + def scalaCompat = describe("Scala3 higher-kinded types") { + it("should work with higher-kinded type parameters") { + // In scala 2, `extends StringEnum[Entry[?]]` was allowed, and worked fine with findValues. + // In scala 3, `extends StringEnum[Entry[?]]` is not allowed (it fails with "unreducible application of + // higher-kinded type Entry to wildcard arguments"). The closest thing is `extends StringEnum[Entry[Any]]`, + // which would not have worked with findValues in scala 2, but has been made to work in scala 3 as the + // existential types needed to resolve the type validly have been removed. + """ + abstract class AbstractEnum[ + Entry[A] <: AbstractEnumEntry[A, Companion[A]], + Companion[A] <: AbstractEnumEntryCompanion[A, Entry[A]] + ] extends StringEnum[Entry[Any]] + + sealed abstract class Enum[A](value: String) extends AbstractEnumEntry[A, EnumCompanion[A]](value) + sealed abstract class EnumCompanion[A](entry: Enum[A]) extends AbstractEnumEntryCompanion[A, Enum[A]](entry) + + object Enum extends AbstractEnum[Enum, EnumCompanion] { + case class One(thing: Int) extends EnumCompanion[Int](One) + case object One extends Enum[Int]("One") + + case class Two(thing: String) extends EnumCompanion[String](Two) + case object Two extends Enum[String]("Two") + + override def values: IndexedSeq[Enum[Any]] = findValues + } + """ should compile + } + } +} diff --git a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala index ecfabbb9..f5b44865 100644 --- a/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala +++ b/enumeratum-core/src/test/scala/enumeratum/values/ValueEnumSpec.scala @@ -7,7 +7,11 @@ import org.scalatest.matchers.should.Matchers * * Copyright 2016 */ -class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers { +class ValueEnumSpec + extends AnyFunSpec + with Matchers + with ValueEnumHelpers + with ValueEnumSpecCompat { describe("basic sanity check") { it("should have the proper values") { @@ -240,4 +244,6 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers { } } } + + scalaCompat } diff --git a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala index 4ab3b219..8a51e0fd 100644 --- a/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala +++ b/macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala @@ -109,8 +109,8 @@ object ValueEnumMacros { tpe: Type[A], valueTpe: Type[ValueType] )(using cls: ClassTag[ValueType]): Expr[IndexedSeq[A]] = { - type SingletonHead[Head <: A & Singleton, Tail <: Tuple] = Head *: Tail - type OtherHead[Head <: A, Tail <: Tuple] = Head *: Tail + type SingletonHead[Head <: Singleton, Tail <: Tuple] = Head *: Tail + type OtherHead[Head, Tail <: Tuple] = Head *: Tail type SumOf[X <: A, T <: Tuple] = Mirror.SumOf[X] { type MirroredElemTypes = T @@ -181,6 +181,14 @@ In SBT settings: } } + object CorrectType { + def unwrap(tpe: TypeRepr) = tpe match { + case AppliedType(tpe, _) => tpe + case _ => tpe + } + def unapply(tree: TypeTree): Boolean = unwrap(tree.tpe) <:< unwrap(repr) + } + @annotation.tailrec def collect[T <: Tuple]( instances: List[Expr[A]], @@ -193,17 +201,23 @@ In SBT settings: (for { vof <- Expr.summon[ValueOf[h]] constValue <- htpr.typeSymbol.tree match { - case ClassDef(_, _, spr, _, rhs) => { - val fromCtor = spr - .collectFirst { - case Apply(Select(New(id), _), args) if id.tpe <:< repr => args - case Apply(TypeApply(Select(New(id), _), _), args) if id.tpe <:< repr => args + case classDef @ ClassDef(_, _, spr, _, rhs) => { + val treeAcc = new TreeAccumulator[Option[List[Term]]] { + def foldTree(value: Option[List[Term]], tree: Tree)( + owner: Symbol + ): Option[List[Term]] = value.orElse { + tree match { + case Apply(Select(New(CorrectType()), _), args) => Some(args) + case Apply(TypeApply(Select(New(CorrectType()), _), _), args) => Some(args) + case _ => foldOverTree(None, tree)(owner) + } } + } + treeAcc + .foldTrees(None, spr)(classDef.symbol) .flatMap(_.lift(valueParamIndex).collect { case ConstVal(const) => const }) - - fromCtor .orElse(rhs.collectFirst { case ConstVal(v) => v }) .flatMap { const => cls.unapply(const.value) @@ -214,7 +228,7 @@ In SBT settings: case _ => Option.empty[ValueType] } - } yield Tuple3(TypeRepr.of[h], '{ ${ vof }.value: A }, constValue)) match { + } yield Tuple3(TypeRepr.of[h], '{ ${ vof }.value.asInstanceOf[A] }, constValue)) match { case Some((tpr, instance, value)) => collect[tail](instance :: instances, values + (tpr -> value))