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 7f7ff968..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") { @@ -135,6 +139,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 @@ -152,6 +172,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") { @@ -209,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 eab04a0b..8a51e0fd 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 <: Singleton, Tail <: Tuple] = Head *: Tail + type OtherHead[Head, Tail <: Tuple] = Head *: Tail type SumOf[X <: A, T <: Tuple] = Mirror.SumOf[X] { type MirroredElemTypes = T @@ -180,29 +181,43 @@ 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]], 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 { 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 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) @@ -213,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)) @@ -224,6 +239,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]