Skip to content

Commit

Permalink
[Issue#165] reject calls to _.to and _.into.transform (and their fall…
Browse files Browse the repository at this point in the history
…ible counterparts) in given Transformer definitions (#167)

Fixes #165
  • Loading branch information
arainko authored May 19, 2024
2 parents f88ea25 + a2b707a commit e726035
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

}
Original file line number Diff line number Diff line change
@@ -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.*
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
)
}
}

0 comments on commit e726035

Please sign in to comment.