diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala index 5ce5d67a..bc9ee190 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala @@ -94,4 +94,14 @@ private[ducktape] object ErrorMessage { "Case config's path should always end with an `.at` segment" val side: Side = Side.Source } + + case object LoopingTransformerDetected extends ErrorMessage { + val side: Side = Side.Dest + + def render(using Quotes): String = + "Detected usage of `_.to[B]`, `_.fallibleTo[B]`, `_.into[B].transform()` or `_.into[B].fallible.transform()` in a given Transformer definition which results in a self-looping Transformer. Please use `Transformer.define[A, B]` or `Transformer.define[A, B].fallible` (for some types A and B) to create Transformer definitions" + + val span: Span | None.type = None + } + } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala index 47b60fe2..98d9f1b3 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala @@ -1,8 +1,11 @@ package io.github.arainko.ducktape.internal +import io.github.arainko.ducktape.internal.Plan.{ Derived, UserDefined } +import io.github.arainko.ducktape.internal.Summoner.UserDefined.{ FallibleTransformer, TotalTransformer } import io.github.arainko.ducktape.internal.* import scala.quoted.* +import scala.util.boundary private[ducktape] object Planner { import Structure.* @@ -28,7 +31,7 @@ private[ducktape] object Planner { planProductFunctionTransformation(source, dest) case UserDefinedTransformation(transformer) => - Plan.UserDefined(source, dest, transformer) + verifyNotSelfReferential(Plan.UserDefined(source, dest, transformer)) case (source, dest) if source.tpe.repr <:< dest.tpe.repr => Plan.Upcast(source, dest) @@ -70,7 +73,7 @@ private[ducktape] object Planner { Plan.BetweenUnwrappedWrapped(source, dest) case DerivedTransformation(transformer) => - Plan.Derived(source, dest, transformer) + verifyNotSelfReferential(Plan.Derived(source, dest, transformer)) case (source, dest) => Plan.Error( @@ -157,13 +160,13 @@ private[ducktape] object Planner { def unapply[F <: Fallible](structs: (Structure, Structure))(using Quotes, Depth, TransformationSite, Summoner[F]) = { val (src, dest) = structs - def summonTransformer = + def summonTransformer(using Quotes) = (src.tpe -> dest.tpe) match { case '[src] -> '[dest] => Summoner[F].summonUserDefined[src, dest] } // if current depth is lower or equal to 1 then that means we're most likely referring to ourselves - summon[TransformationSite] match { + TransformationSite.current match { case TransformationSite.Definition if Depth.current <= 1 => None case TransformationSite.Definition => summonTransformer case TransformationSite.Transformation => summonTransformer @@ -180,4 +183,31 @@ private[ducktape] object Planner { } } } + + private def verifyNotSelfReferential( + plan: Plan.Derived[Fallible] | Plan.UserDefined[Fallible] + )(using TransformationSite, Depth, Quotes): Plan.Error | plan.type = { + import quotes.reflect.* + + val transformerExpr = plan match + case UserDefined(source, dest, Summoner.UserDefined.TotalTransformer(t)) => t + case UserDefined(source, dest, Summoner.UserDefined.FallibleTransformer(t)) => t + case Derived(source, dest, Summoner.Derived.TotalTransformer(t)) => t + case Derived(source, dest, Summoner.Derived.FallibleTransformer(t)) => t + + val transformerSymbol = transformerExpr.asTerm.symbol + + TransformationSite.current match + case TransformationSite.Transformation if Depth.current == 1 => + boundary[Plan.Error | plan.type]: + var owner = Symbol.spliceOwner + while (!owner.isNoSymbol) { + if owner == transformerSymbol then + boundary.break(Plan.Error.from(plan, ErrorMessage.LoopingTransformerDetected, None)) + owner = owner.maybeOwner + } + plan + case _ => plan + + } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala index 0329041e..cb4a04d0 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala @@ -8,6 +8,8 @@ private[ducktape] enum TransformationSite { } private[ducktape] object TransformationSite { + def current(using ts: TransformationSite): TransformationSite = ts + def fromStringExpr(value: Expr["transformation" | "definition"])(using Quotes): TransformationSite = { import quotes.reflect.* diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue165Suite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue165Suite.scala new file mode 100644 index 00000000..41bd1235 --- /dev/null +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue165Suite.scala @@ -0,0 +1,57 @@ +package io.github.arainko.ducktape.issues + +import io.github.arainko.ducktape.* + +class Issue165Suite extends DucktapeSuite { + test("rejects _.to in given Transformer definitions") { + assertFailsToCompileWith { + """ + case class A(a: Int) + case class B(b: Int) + given Transformer[A, B] = _.to[B] + """ + }( + "Detected usage of `_.to[B]`, `_.fallibleTo[B]`, `_.into[B].transform()` or `_.into[B].fallible.transform()` in a given Transformer definition which results in a self-looping Transformer. Please use `Transformer.define[A, B]` or `Transformer.define[A, B].fallible` (for some types A and B) to create Transformer definitions @ B" + ) + } + + test("rejects _.into.transform() in given Transformer definitions") { + assertFailsToCompileWith { + """ + case class A(a: Int) + case class B(b: Int) + given Transformer[A, B] = _.into[B].transform() + """ + }( + "Detected usage of `_.to[B]`, `_.fallibleTo[B]`, `_.into[B].transform()` or `_.into[B].fallible.transform()` in a given Transformer definition which results in a self-looping Transformer. Please use `Transformer.define[A, B]` or `Transformer.define[A, B].fallible` (for some types A and B) to create Transformer definitions @ B" + ) + } + + test("rejects _.falibleTo in given Trasformer.Fallible definitions") { + assertFailsToCompileWith { + """ + given Mode.FailFast.Option with {} + + case class A(a: Int) + case class B(b: Int) + given Transformer.Fallible[Option, A, B] = _.fallibleTo[B] + """ + }( + "Detected usage of `_.to[B]`, `_.fallibleTo[B]`, `_.into[B].transform()` or `_.into[B].fallible.transform()` in a given Transformer definition which results in a self-looping Transformer. Please use `Transformer.define[A, B]` or `Transformer.define[A, B].fallible` (for some types A and B) to create Transformer definitions @ B" + ) + } + + test("rejects _.into.falible in given Trasformer.Fallible definitions") { + assertFailsToCompileWith { + """ + given Mode.FailFast.Option with {} + + case class A(a: Int) + case class B(b: Int) + given Transformer.Fallible[Option, A, B] = _.into[B].fallible.transform() + """ + }( + "Detected usage of `_.to[B]`, `_.fallibleTo[B]`, `_.into[B].transform()` or `_.into[B].fallible.transform()` in a given Transformer definition which results in a self-looping Transformer. Please use `Transformer.define[A, B]` or `Transformer.define[A, B].fallible` (for some types A and B) to create Transformer definitions @ B" + ) + } +}