From e49654f76970f961efa6e4a0ac30d5c9f60bf664 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 13 Apr 2024 02:26:22 +0200 Subject: [PATCH 1/7] config warnings POC --- .../ducktape/internal/Accumulator.scala | 13 +++-- .../arainko/ducktape/internal/Backend.scala | 4 ++ .../arainko/ducktape/internal/Plan.scala | 12 +++-- .../ducktape/internal/PlanConfigurer.scala | 39 +++++++++++---- .../ducktape/internal/PlanRefiner.scala | 39 ++++----------- .../ducktape/internal/PlanTraverser.scala | 48 +++++++++++++++++++ .../arainko/ducktape/internal/Warning.scala | 3 ++ 7 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala create mode 100644 ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala index 49c44bc9..53c44232 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala @@ -6,16 +6,21 @@ private[ducktape] opaque type Accumulator[A] = Builder[A, List[A]] private[ducktape] object Accumulator { - inline def use[A]: [B] => (f: Accumulator[A] ?=> B) => (List[A], B) = - [B] => + def use[A]: [B <: Tuple] => (f: Accumulator[A] ?=> B) => List[A] *: B = + [B <: Tuple] => (f: Accumulator[A] ?=> B) => { val builder = List.newBuilder[A] val result = f(using builder) - builder.result() -> result + builder.result() *: result } - inline def append[A](value: A)(using acc: Accumulator[A]): A = { + def append[A](value: A)(using acc: Accumulator[A]): A = { acc.addOne(value) value } + + def appendAll[A, B <: Iterable[A]](values: B)(using acc: Accumulator[A]): B = { + acc.addAll(values) + values + } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala index df680ff7..cd719a3d 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala @@ -17,6 +17,10 @@ private[ducktape] object Backend { Logger.info("Config", configs) Logger.info("Reconfigured plan", reconfiguredPlan) + reconfiguredPlan.warnings.foreach { warning => + report.warning(warning.message, warning.span.toPosition) + } + reconfiguredPlan.result.refine match { case Left(errors) => val ogErrors = diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala index 57425767..4c065201 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala @@ -51,7 +51,8 @@ private[ducktape] object Plan { case class Configured[+F <: Fallible] private ( source: Structure, dest: Structure, - config: Configuration[F] + config: Configuration[F], + span: Span ) extends Plan[Nothing, F] case class BetweenProductFunction[+E <: Plan.Error, +F <: Fallible]( @@ -124,12 +125,12 @@ private[ducktape] object Plan { } object Configured { - def from[F <: Fallible](plan: Plan[Plan.Error, F], conf: Configuration[F], side: Side)(using Quotes): Plan.Configured[F] = + def from[F <: Fallible](plan: Plan[Plan.Error, F], conf: Configuration[F], instruction: Configuration.Instruction[F])(using Quotes): Plan.Configured[F] = (plan.source.tpe, plan.dest.tpe, conf.tpe) match { case ('[src], '[dest], '[confTpe]) => - val source = if side.isDest then Structure.Lazy.of[confTpe](plan.source.path) else plan.source - val dest = if side.isSource then Structure.Lazy.of[confTpe](plan.dest.path) else plan.dest - Plan.Configured(source, dest, conf) + val source = if instruction.side.isDest then Structure.Lazy.of[confTpe](plan.source.path) else plan.source + val dest = if instruction.side.isSource then Structure.Lazy.of[confTpe](plan.dest.path) else plan.dest + Plan.Configured(source, dest, conf, instruction.span) } } @@ -138,6 +139,7 @@ private[ducktape] object Plan { final case class Reconfigured[+F <: Fallible]( errors: List[Plan.Error], successes: List[(Path, Side)], + warnings: List[Warning], result: Plan[Plan.Error, F] ) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index d8c7394e..bb0a7380 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -15,7 +15,7 @@ private[ducktape] object PlanConfigurer { def configureSingle( plan: Plan[Plan.Error, F], config: Configuration.Instruction[F] - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)]): Plan[Plan.Error, F] = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Plan.Error, F] = { def recurse( current: Plan[Plan.Error, F], @@ -89,19 +89,20 @@ private[ducktape] object PlanConfigurer { else recurse(plan, config.path.segments.toList, None) } - val (errors, (successes, reconfiguredPlan)) = + val (errors, successes, warnings, reconfiguredPlan) = Accumulator.use[Plan.Error]: Accumulator.use[(Path, Side)]: - configs.foldLeft(plan)(configureSingle) + Accumulator.use[Warning]: + configs.foldLeft(plan)(configureSingle) *: EmptyTuple - Plan.Reconfigured(errors, successes, reconfiguredPlan) + Plan.Reconfigured(errors, successes, warnings, reconfiguredPlan) } private def configurePlan[F <: Fallible]( config: Configuration.Instruction[F], current: Plan[Error, F], parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)]) = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]) = { config match { case cfg: (Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic[F]) => staticOrDynamic(cfg, current, parent) @@ -135,7 +136,7 @@ private[ducktape] object PlanConfigurer { instruction: Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic[F], current: Plan[Plan.Error, F], parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)]) = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]) = { instruction match { case static: Configuration.Instruction.Static[F] => current.configureIfValid(static, static.config) @@ -156,7 +157,7 @@ private[ducktape] object PlanConfigurer { plan: Plan[Plan.Error, F], modifier: Configuration.Instruction.Regional, parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)]): Plan[Plan.Error, F] = + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Plan.Error, F] = plan match { case plan: Upcast => plan @@ -201,7 +202,7 @@ private[ducktape] object PlanConfigurer { private def bulk[F <: Fallible]( current: Plan[Plan.Error, F], instruction: Configuration.Instruction.Bulk - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)]): Plan[Error, F] = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Error, F] = { enum IsAnythingModified { case Yes, No @@ -250,7 +251,12 @@ private[ducktape] object PlanConfigurer { private def configureIfValid( instruction: Configuration.Instruction[F], config: Configuration[F] - )(using quotes: Quotes, errors: Accumulator[Plan.Error], successes: Accumulator[(Path, Side)]) = { + )(using + quotes: Quotes, + errors: Accumulator[Plan.Error], + successes: Accumulator[(Path, Side)], + warnings: Accumulator[Warning] + ) = { def isReplaceableBy(update: Configuration[F])(using Quotes) = update.tpe.repr <:< currentPlan.destPath.currentTpe.repr @@ -259,7 +265,8 @@ private[ducktape] object PlanConfigurer { if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side else currentPlan.sourcePath -> instruction.side } - Plan.Configured.from(currentPlan, config, instruction.side) + Accumulator.appendAll(configuredCollector.run(currentPlan, Nil).map(plan => Warning(plan.span, "Config overriden"))) + Plan.Configured.from(currentPlan, config, instruction) else Accumulator.append { Plan.Error.from( @@ -276,4 +283,16 @@ private[ducktape] object PlanConfigurer { } } + private val configuredCollector = + new PlanTraverser[List[Plan.Configured[Fallible]]] { + protected def foldOver( + plan: Plan[Error, Fallible], + accumulator: List[Plan.Configured[Fallible]] + ): List[Plan.Configured[Fallible]] = + plan match { + case configured: Plan.Configured[Fallible] => configured :: accumulator + case other => accumulator + } + } + } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala index 0e958168..a86430fd 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala @@ -1,36 +1,17 @@ package io.github.arainko.ducktape.internal -import scala.annotation.tailrec - private[ducktape] object PlanRefiner { - def run[F <: Fallible](plan: Plan[Plan.Error, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = { + private val errorCollector = + new PlanTraverser[List[Plan.Error]] { + protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: List[Plan.Error]): List[Plan.Error] = + plan match { + case error: Plan.Error => error :: accumulator + case other => accumulator + } + } - @tailrec - def recurse(stack: List[Plan[Plan.Error, F]], errors: List[Plan.Error]): List[Plan.Error] = - stack match { - case head :: next => - head match { - case plan: Plan.Upcast => recurse(next, errors) - case Plan.BetweenProducts(_, _, fieldPlans) => - recurse(fieldPlans.values.toList ::: next, errors) - case Plan.BetweenCoproducts(_, _, casePlans) => - recurse(casePlans.toList ::: next, errors) - case Plan.BetweenProductFunction(_, _, argPlans) => - recurse(argPlans.values.toList ::: next, errors) - case Plan.BetweenOptions(_, _, plan) => recurse(plan :: next, errors) - case Plan.BetweenNonOptionOption(_, _, plan) => recurse(plan :: next, errors) - case Plan.BetweenCollections(_, _, plan) => recurse(plan :: next, errors) - case plan: Plan.BetweenSingletons => recurse(next, errors) - case plan: Plan.UserDefined[F] => recurse(next, errors) - case plan: Plan.Derived[F] => recurse(next, errors) - case plan: Plan.Configured[F] => recurse(next, errors) - case plan: Plan.BetweenWrappedUnwrapped => recurse(next, errors) - case plan: Plan.BetweenUnwrappedWrapped => recurse(next, errors) - case error: Plan.Error => recurse(next, error :: errors) - } - case Nil => errors - } - val errors = recurse(plan :: Nil, Nil) + def run[F <: Fallible](plan: Plan[Plan.Error, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = { + val errors = errorCollector.run(plan, Nil) // if no errors were accumulated that means there are no Plan.Error nodes which means we operate on a Plan[Nothing] NonEmptyList.fromList(errors).toLeft(plan.asInstanceOf[Plan[Nothing, F]]) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala new file mode 100644 index 00000000..06d998db --- /dev/null +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala @@ -0,0 +1,48 @@ +package io.github.arainko.ducktape.internal + +import scala.annotation.tailrec + +private[ducktape] trait PlanTraverser[A] { + final def run(plan: Plan[Plan.Error, Fallible], initial: A): A = { + @tailrec + def recurse(stack: List[Plan[Plan.Error, Fallible]], accumulator: A): A = + stack match { + case head :: next => + head match { + case plan: Plan.Upcast => + recurse(next, foldOver(plan, accumulator)) + case plan @ Plan.BetweenProducts(_, _, fieldPlans) => + recurse(fieldPlans.values.toList ::: next, foldOver(plan, accumulator)) + case plan @ Plan.BetweenCoproducts(_, _, casePlans) => + recurse(casePlans.toList ::: next, foldOver(plan, accumulator)) + case plan @ Plan.BetweenProductFunction(_, _, argPlans) => + recurse(argPlans.values.toList ::: next, foldOver(plan, accumulator)) + case p @ Plan.BetweenOptions(_, _, plan) => + recurse(plan :: next, foldOver(p, accumulator)) + case p @ Plan.BetweenNonOptionOption(_, _, plan) => + recurse(plan :: next, foldOver(p, accumulator)) + case p @ Plan.BetweenCollections(_, _, plan) => + recurse(plan :: next, foldOver(p, accumulator)) + case plan: Plan.BetweenSingletons => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.UserDefined[Fallible] => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.Derived[Fallible] => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.Configured[Fallible] => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.BetweenWrappedUnwrapped => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.BetweenUnwrappedWrapped => + recurse(next, foldOver(plan, accumulator)) + case plan: Plan.Error => + recurse(next, foldOver(plan, accumulator)) + } + case Nil => accumulator + } + + recurse(plan :: Nil, initial) + } + + protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: A): A +} \ No newline at end of file diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala new file mode 100644 index 00000000..992c40a2 --- /dev/null +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala @@ -0,0 +1,3 @@ +package io.github.arainko.ducktape.internal + +private[ducktape] final case class Warning(span: Span, message: String) From 97864fc42230ac3ff991039e4a902031679e9b5a Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sun, 14 Apr 2024 19:01:37 +0200 Subject: [PATCH 2/7] add source of the config that overrides --- .../arainko/ducktape/internal/Backend.scala | 9 +++-- .../ducktape/internal/PlanConfigurer.scala | 34 +++++++++++-------- .../ducktape/internal/PlanRefiner.scala | 20 +++++------ .../arainko/ducktape/internal/Warning.scala | 12 ++++++- .../ducktape/total/AppliedBuilderSuite.scala | 14 ++++++-- 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala index cd719a3d..449bf1cc 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala @@ -17,9 +17,12 @@ private[ducktape] object Backend { Logger.info("Config", configs) Logger.info("Reconfigured plan", reconfiguredPlan) - reconfiguredPlan.warnings.foreach { warning => - report.warning(warning.message, warning.span.toPosition) - } + reconfiguredPlan.warnings + .groupBy(_.span) + .foreach { (span, warnings) => + val rendered = warnings.map(_.render).mkString(System.lineSeparator) + report.warning(rendered, span.toPosition) + } reconfiguredPlan.result.refine match { case Left(errors) => diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index bb0a7380..d94ca302 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -261,11 +261,16 @@ private[ducktape] object PlanConfigurer { update.tpe.repr <:< currentPlan.destPath.currentTpe.repr if isReplaceableBy(config) then - Accumulator.append { - if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side - else currentPlan.sourcePath -> instruction.side + val (path, _) = + Accumulator.append { + if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side + else currentPlan.sourcePath -> instruction.side + } + Accumulator.appendAll { + ConfiguredCollector + .run(currentPlan, Nil) + .map(plan => Warning(plan.span, instruction.span, path)) } - Accumulator.appendAll(configuredCollector.run(currentPlan, Nil).map(plan => Warning(plan.span, "Config overriden"))) Plan.Configured.from(currentPlan, config, instruction) else Accumulator.append { @@ -283,16 +288,15 @@ private[ducktape] object PlanConfigurer { } } - private val configuredCollector = - new PlanTraverser[List[Plan.Configured[Fallible]]] { - protected def foldOver( - plan: Plan[Error, Fallible], - accumulator: List[Plan.Configured[Fallible]] - ): List[Plan.Configured[Fallible]] = - plan match { - case configured: Plan.Configured[Fallible] => configured :: accumulator - case other => accumulator - } - } + private object ConfiguredCollector extends PlanTraverser[List[Plan.Configured[Fallible]]] { + protected def foldOver( + plan: Plan[Error, Fallible], + accumulator: List[Plan.Configured[Fallible]] + ): List[Plan.Configured[Fallible]] = + plan match { + case configured: Plan.Configured[Fallible] => configured :: accumulator + case other => accumulator + } + } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala index a86430fd..aeac92c1 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanRefiner.scala @@ -1,18 +1,18 @@ package io.github.arainko.ducktape.internal private[ducktape] object PlanRefiner { - private val errorCollector = - new PlanTraverser[List[Plan.Error]] { - protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: List[Plan.Error]): List[Plan.Error] = - plan match { - case error: Plan.Error => error :: accumulator - case other => accumulator - } - } + private object ErrorCollector extends PlanTraverser[List[Plan.Error]] { + protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: List[Plan.Error]): List[Plan.Error] = + plan match { + case error: Plan.Error => error :: accumulator + case other => accumulator + } + } def run[F <: Fallible](plan: Plan[Plan.Error, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = { - val errors = errorCollector.run(plan, Nil) // if no errors were accumulated that means there are no Plan.Error nodes which means we operate on a Plan[Nothing] - NonEmptyList.fromList(errors).toLeft(plan.asInstanceOf[Plan[Nothing, F]]) + NonEmptyList + .fromList(ErrorCollector.run(plan, Nil)) + .toLeft(plan.asInstanceOf[Plan[Nothing, F]]) } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala index 992c40a2..852842a2 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala @@ -1,3 +1,13 @@ package io.github.arainko.ducktape.internal -private[ducktape] final case class Warning(span: Span, message: String) +import scala.quoted.Quotes +import io.github.arainko.ducktape.internal.Path.Segment + +private[ducktape] final case class Warning(span: Span, overriderSpan: Span, path: Path) { + def render(using Quotes): String = { + val pos = overriderSpan.toPosition + val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" + + s"Config for ${path.render} overriden by $codeAndLocation" + } +} diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala index 636e3714..82deb379 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala @@ -152,9 +152,9 @@ class AppliedBuilderSuite extends DucktapeSuite { }: @nowarn("msg=unused") test("The last applied field config is the picked one") { - final case class FieldSource(additionalArg: String) + final case class FieldSource(additionalArg: String, str: String) - val fieldSource = FieldSource("str-sourced") + val fieldSource = FieldSource("str-sourced", "str2") val expected = TestClassWithAdditionalString(1, "str", "str-computed") @@ -164,14 +164,24 @@ class AppliedBuilderSuite extends DucktapeSuite { .into[TestClassWithAdditionalString] .transform( Field.const(_.additionalArg, "FIRST"), + + Field.renamed(_.additionalArg, _.str), + + Field.allMatching(fieldSource), Field.allMatching(fieldSource), + Field.computed(_.additionalArg, _.str + "-computed") ) + assertEquals(actual, expected) } + + + + test("When configs are applied to the same field repeateadly a warning is emitted".ignore) { assertFailsToCompileWith { """ From 8c47a26eae08bfe4d3cf52da4f6116b73198a576 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Mon, 15 Apr 2024 18:43:43 +0200 Subject: [PATCH 3/7] format warnings in a nice way --- .../ducktape/internal/Accumulator.scala | 19 +++++++++++++++++++ .../arainko/ducktape/internal/Backend.scala | 18 ++++++++++++++++-- .../internal/FallibilityRefiner.scala | 2 +- .../internal/FalliblePlanInterpreter.scala | 2 +- .../ducktape/internal/NonEmptyList.scala | 1 + .../ducktape/internal/PlanInterpreter.scala | 2 +- .../ducktape/total/AppliedBuilderSuite.scala | 1 + 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala index 53c44232..7d4da671 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala @@ -1,11 +1,30 @@ package io.github.arainko.ducktape.internal import scala.collection.mutable.Builder +import scala.compiletime.* private[ducktape] opaque type Accumulator[A] = Builder[A, List[A]] private[ducktape] object Accumulator { + // type Expand[T <: Tuple, Ret <: Tuple] = + // T match { + // case EmptyTuple => Ret + // case h *: t => Accumulator[h] => Expand[t, List[h] *: Ret] + // } + + // inline def expand[T <: Tuple]: Expand[T, EmptyTuple] = + // inline erasedValue[T] match { + // case _: EmptyTuple => ??? + // case _: (h *: t) => ??? + // } + + // val cosn: Accumulator[Int] => Accumulator[String] => Accumulator[Double] => (List[Double], List[String], List[Int]) = + // expand[(Int, String, Double)] + + // val cos2: Accumulator[Int] => Accumulator[String] => Accumulator[Double] => (List[Double], List[String], List[Int]) = + // accInt => accStr => accDouble => (accDouble.result(), accStr.result(), accInt.result()) + def use[A]: [B <: Tuple] => (f: Accumulator[A] ?=> B) => List[A] *: B = [B <: Tuple] => (f: Accumulator[A] ?=> B) => { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala index 449bf1cc..fd1c80a7 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala @@ -20,8 +20,22 @@ private[ducktape] object Backend { reconfiguredPlan.warnings .groupBy(_.span) .foreach { (span, warnings) => - val rendered = warnings.map(_.render).mkString(System.lineSeparator) - report.warning(rendered, span.toPosition) + val messages = + warnings + .groupBy(_.overriderSpan) + .map { (overriderSpan, warnings) => + val pos = overriderSpan.toPosition + val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" + + if warnings.size > 1 then s""" + |Configs for: + |${warnings.map(w => " * " + w.path.render).mkString(System.lineSeparator)} + |overriden by $codeAndLocation + |""".stripMargin + else warnings.map(_.render).mkString + } + + messages.foreach(report.warning(_, span.toPosition)) } reconfiguredPlan.result.refine match { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala index 48e63d45..c9d813bc 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala @@ -28,7 +28,7 @@ private[ducktape] object FallibilityRefiner { case Summoner.Derived.TotalTransformer(value) => () case Summoner.Derived.FallibleTransformer(value) => boundary.break(None) - case Configured(source, dest, config) => + case Configured(source, dest, config, _) => config match case Configuration.Const(value, tpe) => () case Configuration.CaseComputed(tpe, function) => () diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala index 30dda7dc..2cc1dc09 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala @@ -29,7 +29,7 @@ private[ducktape] object FalliblePlanInterpreter { plan match { case Plan.Upcast(_, _) => Value.Unwrapped(value) - case Plan.Configured(_, _, config) => + case Plan.Configured(_, _, config, _) => config match case cfg @ Configuration.Const(_, _) => Value.Unwrapped(PlanInterpreter.evaluateConfig(cfg, value)) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala index bdf587e3..b568346f 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala @@ -18,6 +18,7 @@ private[ducktape] object NonEmptyList { private[ducktape] given [A: Debug]: Debug[NonEmptyList[A]] = Debug.collection[A, List] + extension [A](self: NonEmptyList[A]) { export toList.{ foldLeft, reduceLeft, head, tail, exists, filter, collect, toVector } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala index 3ced90cc..86c056f1 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala @@ -19,7 +19,7 @@ private[ducktape] object PlanInterpreter { plan match { case Plan.Upcast(_, _) => value - case Plan.Configured(_, _, config) => + case Plan.Configured(_, _, config, _) => evaluateConfig(config, value) case Plan.BetweenProducts(sourceTpe, destTpe, fieldPlans) => diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala index 82deb379..bb78edf3 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala @@ -177,6 +177,7 @@ class AppliedBuilderSuite extends DucktapeSuite { assertEquals(actual, expected) } + From c76cce57fb20be2fc1a237ebb4feea9cef66a380 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Fri, 19 Apr 2024 20:10:39 +0200 Subject: [PATCH 4/7] tweak the warning message --- .../scala/io/github/arainko/ducktape/internal/Backend.scala | 6 ++---- .../scala/io/github/arainko/ducktape/internal/Warning.scala | 2 +- .../github/arainko/ducktape/total/AppliedBuilderSuite.scala | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala index fd1c80a7..0886b3b3 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala @@ -27,11 +27,9 @@ private[ducktape] object Backend { val pos = overriderSpan.toPosition val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" - if warnings.size > 1 then s""" - |Configs for: + if warnings.size > 1 then s"""Configs for: |${warnings.map(w => " * " + w.path.render).mkString(System.lineSeparator)} - |overriden by $codeAndLocation - |""".stripMargin + |are being overriden by $codeAndLocation""".stripMargin else warnings.map(_.render).mkString } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala index 852842a2..94722c7b 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala @@ -8,6 +8,6 @@ private[ducktape] final case class Warning(span: Span, overriderSpan: Span, path val pos = overriderSpan.toPosition val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" - s"Config for ${path.render} overriden by $codeAndLocation" + s"Config for ${path.render} is being overriden by $codeAndLocation" } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala index bb78edf3..2e93ce87 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala @@ -174,6 +174,10 @@ class AppliedBuilderSuite extends DucktapeSuite { Field.computed(_.additionalArg, _.str + "-computed") ) + + + + assertEquals(actual, expected) } From 60483e28ce24fd2c7e6cfac51d026d888d9e81d7 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sun, 19 May 2024 20:42:46 +0200 Subject: [PATCH 5/7] finalize config lints and add tests --- build.sbt | 1 + .../ducktape/internal/Accumulator.scala | 20 -------- .../arainko/ducktape/internal/Backend.scala | 14 +----- .../ducktape/internal/ConfigWarning.scala | 27 ++++++++++ .../ducktape/internal/NonEmptyList.scala | 1 - .../arainko/ducktape/internal/Plan.scala | 6 ++- .../ducktape/internal/PlanConfigurer.scala | 17 +++---- .../ducktape/internal/PlanTraverser.scala | 2 +- .../arainko/ducktape/internal/Span.scala | 2 + .../arainko/ducktape/internal/Warning.scala | 13 ----- .../arainko/ducktape/DucktapeSuite.scala | 5 ++ .../ducktape/total/AppliedBuilderSuite.scala | 49 +++++++++++-------- .../total/AppliedViaBuilderSuite.scala | 6 +-- 13 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigWarning.scala delete mode 100644 ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala diff --git a/build.sbt b/build.sbt index df50b71f..26871f2a 100644 --- a/build.sbt +++ b/build.sbt @@ -48,6 +48,7 @@ lazy val ducktape = .settings( scalacOptions ++= List("-deprecation", "-Wunused:all"), Test / scalacOptions --= List("-deprecation"), + Test / scalacOptions ++= List("-Werror", "-Wconf:cat=deprecation:s"), libraryDependencies += "org.scalameta" %%% "munit" % "1.0.0-M11" % Test ) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala index 7d4da671..7f2f30fa 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Accumulator.scala @@ -1,30 +1,10 @@ package io.github.arainko.ducktape.internal import scala.collection.mutable.Builder -import scala.compiletime.* private[ducktape] opaque type Accumulator[A] = Builder[A, List[A]] private[ducktape] object Accumulator { - - // type Expand[T <: Tuple, Ret <: Tuple] = - // T match { - // case EmptyTuple => Ret - // case h *: t => Accumulator[h] => Expand[t, List[h] *: Ret] - // } - - // inline def expand[T <: Tuple]: Expand[T, EmptyTuple] = - // inline erasedValue[T] match { - // case _: EmptyTuple => ??? - // case _: (h *: t) => ??? - // } - - // val cosn: Accumulator[Int] => Accumulator[String] => Accumulator[Double] => (List[Double], List[String], List[Int]) = - // expand[(Int, String, Double)] - - // val cos2: Accumulator[Int] => Accumulator[String] => Accumulator[Double] => (List[Double], List[String], List[Int]) = - // accInt => accStr => accDouble => (accDouble.result(), accStr.result(), accInt.result()) - def use[A]: [B <: Tuple] => (f: Accumulator[A] ?=> B) => List[A] *: B = [B <: Tuple] => (f: Accumulator[A] ?=> B) => { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala index 0886b3b3..54789ff2 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Backend.scala @@ -20,19 +20,7 @@ private[ducktape] object Backend { reconfiguredPlan.warnings .groupBy(_.span) .foreach { (span, warnings) => - val messages = - warnings - .groupBy(_.overriderSpan) - .map { (overriderSpan, warnings) => - val pos = overriderSpan.toPosition - val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" - - if warnings.size > 1 then s"""Configs for: - |${warnings.map(w => " * " + w.path.render).mkString(System.lineSeparator)} - |are being overriden by $codeAndLocation""".stripMargin - else warnings.map(_.render).mkString - } - + val messages = ConfigWarning.renderAll(warnings) messages.foreach(report.warning(_, span.toPosition)) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigWarning.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigWarning.scala new file mode 100644 index 00000000..ed751424 --- /dev/null +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigWarning.scala @@ -0,0 +1,27 @@ +package io.github.arainko.ducktape.internal + +import scala.quoted.Quotes + +private[ducktape] final case class ConfigWarning(span: Span, overriderSpan: Span, path: Path) { + def render(using Quotes): String = { + val pos = overriderSpan.withEnd(_ - 1).toPosition + val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" + + s"Config for ${path.render} is being overriden by $codeAndLocation" + } +} + +private[ducktape] object ConfigWarning { + def renderAll(warnings: List[ConfigWarning])(using Quotes) = + warnings + .groupBy(_.overriderSpan) + .map { (overriderSpan, warnings) => + val pos = overriderSpan.withEnd(_ - 1).toPosition + val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" + + if warnings.size > 1 then s"""Configs for: + |${warnings.map(warning => " * " + warning.path.render).mkString(System.lineSeparator)} + |are being overriden by $codeAndLocation""".stripMargin + else warnings.map(_.render).mkString + } +} diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala index b568346f..bdf587e3 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala @@ -18,7 +18,6 @@ private[ducktape] object NonEmptyList { private[ducktape] given [A: Debug]: Debug[NonEmptyList[A]] = Debug.collection[A, List] - extension [A](self: NonEmptyList[A]) { export toList.{ foldLeft, reduceLeft, head, tail, exists, filter, collect, toVector } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala index 4c065201..42fe2b2f 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala @@ -125,7 +125,9 @@ private[ducktape] object Plan { } object Configured { - def from[F <: Fallible](plan: Plan[Plan.Error, F], conf: Configuration[F], instruction: Configuration.Instruction[F])(using Quotes): Plan.Configured[F] = + def from[F <: Fallible](plan: Plan[Plan.Error, F], conf: Configuration[F], instruction: Configuration.Instruction[F])(using + Quotes + ): Plan.Configured[F] = (plan.source.tpe, plan.dest.tpe, conf.tpe) match { case ('[src], '[dest], '[confTpe]) => val source = if instruction.side.isDest then Structure.Lazy.of[confTpe](plan.source.path) else plan.source @@ -139,7 +141,7 @@ private[ducktape] object Plan { final case class Reconfigured[+F <: Fallible]( errors: List[Plan.Error], successes: List[(Path, Side)], - warnings: List[Warning], + warnings: List[ConfigWarning], result: Plan[Plan.Error, F] ) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index d94ca302..9e27fad0 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -2,7 +2,6 @@ package io.github.arainko.ducktape.internal import io.github.arainko.ducktape.internal.Configuration.Instruction -import scala.collection.mutable.Builder import scala.quoted.* private[ducktape] object PlanConfigurer { @@ -15,7 +14,7 @@ private[ducktape] object PlanConfigurer { def configureSingle( plan: Plan[Plan.Error, F], config: Configuration.Instruction[F] - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Plan.Error, F] = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Plan.Error, F] = { def recurse( current: Plan[Plan.Error, F], @@ -92,7 +91,7 @@ private[ducktape] object PlanConfigurer { val (errors, successes, warnings, reconfiguredPlan) = Accumulator.use[Plan.Error]: Accumulator.use[(Path, Side)]: - Accumulator.use[Warning]: + Accumulator.use[ConfigWarning]: configs.foldLeft(plan)(configureSingle) *: EmptyTuple Plan.Reconfigured(errors, successes, warnings, reconfiguredPlan) @@ -102,7 +101,7 @@ private[ducktape] object PlanConfigurer { config: Configuration.Instruction[F], current: Plan[Error, F], parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]) = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]) = { config match { case cfg: (Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic[F]) => staticOrDynamic(cfg, current, parent) @@ -136,7 +135,7 @@ private[ducktape] object PlanConfigurer { instruction: Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic[F], current: Plan[Plan.Error, F], parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]) = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]) = { instruction match { case static: Configuration.Instruction.Static[F] => current.configureIfValid(static, static.config) @@ -157,7 +156,7 @@ private[ducktape] object PlanConfigurer { plan: Plan[Plan.Error, F], modifier: Configuration.Instruction.Regional, parent: Plan[Plan.Error, F] | None.type - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Plan.Error, F] = + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Plan.Error, F] = plan match { case plan: Upcast => plan @@ -202,7 +201,7 @@ private[ducktape] object PlanConfigurer { private def bulk[F <: Fallible]( current: Plan[Plan.Error, F], instruction: Configuration.Instruction.Bulk - )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[Warning]): Plan[Error, F] = { + )(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Error, F] = { enum IsAnythingModified { case Yes, No @@ -255,7 +254,7 @@ private[ducktape] object PlanConfigurer { quotes: Quotes, errors: Accumulator[Plan.Error], successes: Accumulator[(Path, Side)], - warnings: Accumulator[Warning] + warnings: Accumulator[ConfigWarning] ) = { def isReplaceableBy(update: Configuration[F])(using Quotes) = update.tpe.repr <:< currentPlan.destPath.currentTpe.repr @@ -269,7 +268,7 @@ private[ducktape] object PlanConfigurer { Accumulator.appendAll { ConfiguredCollector .run(currentPlan, Nil) - .map(plan => Warning(plan.span, instruction.span, path)) + .map(plan => ConfigWarning(plan.span, instruction.span, path)) } Plan.Configured.from(currentPlan, config, instruction) else diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala index 06d998db..7fa133fd 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanTraverser.scala @@ -45,4 +45,4 @@ private[ducktape] trait PlanTraverser[A] { } protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: A): A -} \ No newline at end of file +} diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Span.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Span.scala index af0b495d..c25b3947 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Span.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Span.scala @@ -7,6 +7,8 @@ private[ducktape] final case class Span(start: Int, end: Int) derives Debug { import quotes.reflect.* Position(SourceFile.current, start, end) } + + def withEnd(f: Int => Int): Span = copy(end = f(end)) } private[ducktape] object Span { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala deleted file mode 100644 index 94722c7b..00000000 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Warning.scala +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.arainko.ducktape.internal - -import scala.quoted.Quotes -import io.github.arainko.ducktape.internal.Path.Segment - -private[ducktape] final case class Warning(span: Span, overriderSpan: Span, path: Path) { - def render(using Quotes): String = { - val pos = overriderSpan.toPosition - val codeAndLocation = s"${pos.sourceCode.mkString} @ ${pos.sourceFile.name}:${pos.endLine + 1}:${pos.endColumn + 1}" - - s"Config for ${path.render} is being overriden by $codeAndLocation" - } -} diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala index d1b64054..debcb060 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala @@ -16,6 +16,11 @@ trait DucktapeSuite extends FunSuite { assertEquals(errors, expected.toSet, "Error did not contain expected value") } + transparent inline def assertFailsToCompileContains(inline code: String)(head: String, tail: String*)(using Location) = { + val errors = compiletime.testing.typeCheckErrors(code).map(_.message).toSet + (head :: tail.toList).foreach(expected => errors.contains(expected)) + } + extension [A](inline self: A) { inline def code: A = internal.CodePrinter.code(self) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala index 2e93ce87..c9792fb5 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedBuilderSuite.scala @@ -156,39 +156,46 @@ class AppliedBuilderSuite extends DucktapeSuite { val fieldSource = FieldSource("str-sourced", "str2") - val expected = TestClassWithAdditionalString(1, "str", "str-computed") + val expected = TestClassWithAdditionalString(1, "str2", "str-computed") - // @nowarn("msg=Field 'additionalArg' is configured multiple times") + @nowarn("msg=Config[s]? for") val actual = testClass .into[TestClassWithAdditionalString] .transform( Field.const(_.additionalArg, "FIRST"), - - Field.renamed(_.additionalArg, _.str), - Field.allMatching(fieldSource), - Field.allMatching(fieldSource), - Field.computed(_.additionalArg, _.str + "-computed") ) - - - - - - assertEquals(actual, expected) } - - + test("overriding multiple fields multiple times is reported appropriately") { + final case class FieldSource(additionalArg: String, str: String) + val fieldSource = FieldSource("str-sourced", "str2") + val expected = TestClassWithAdditionalString(1, "str2", "str-computed") - test("When configs are applied to the same field repeateadly a warning is emitted".ignore) { - assertFailsToCompileWith { + assertFailsToCompileContains { + """ + testClass + .into[TestClassWithAdditionalString] + .transform( + Field.allMatching(fieldSource), + Field.allMatching(fieldSource), + ) + """ + }("""Configs for: + * TestClassWithAdditionalString.str + * TestClassWithAdditionalString.additionalArg +are being overriden by Field.allMatching(fieldSource)""") + }: @nowarn("msg=unused") + + test("When configs are applied to the same field repeateadly a warning is emitted") { + + assertFailsToCompileContains { """ testClass .into[TestClassWithAdditionalString] @@ -197,7 +204,7 @@ class AppliedBuilderSuite extends DucktapeSuite { Field.renamed(_.additionalArg, _.str), ) """ - }("Field 'additionalArg' is configured multiple times") + }("Config for TestClassWithAdditionalString.additionalArg is being overriden by Field.renamed(_.additionalArg, _.str)") } test("Case.const properly applies the constant for that subtype") { @@ -285,9 +292,9 @@ class AppliedBuilderSuite extends DucktapeSuite { assertEquals(actual, expected) } - test("When a Case is configured multiple times a warning is emitted".ignore) { + test("When a Case is configured multiple times a warning is emitted") { - assertFailsToCompileWith { + assertFailsToCompileContains { """ LessCases.Case1 .into[MoreCases] @@ -296,7 +303,7 @@ class AppliedBuilderSuite extends DucktapeSuite { Case.const[LessCases.Case3.type](MoreCases.Case3) ) """ - }("Case 'io.github.arainko.ducktape.total.builder.AppliedBuilderSuite.LessCases.Case3' is configured multiple times") + }("Config for LessCases.at[io.github.arainko.ducktape.total.AppliedBuilderSuite.LessCases.Case3.type] is being overriden by") } test("derive a transformer for case classes with default values if configured") { diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedViaBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedViaBuilderSuite.scala index 86f175ac..0c618ec3 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedViaBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/AppliedViaBuilderSuite.scala @@ -47,11 +47,11 @@ class AppliedViaBuilderSuite extends DucktapeSuite { assertEquals(actual, expected) } - test("When an Arg is configured multiple times a warning is emitted".ignore) { + test("When an Arg is configured multiple times a warning is emitted") { @nowarn("msg=unused local definition") def method(str: String, int: Int, additionalArg: String) = TestClassWithAdditionalString(int, str, additionalArg) - assertFailsToCompileWith { + assertFailsToCompileContains { """ testClass .intoVia(method) @@ -60,7 +60,7 @@ class AppliedViaBuilderSuite extends DucktapeSuite { Arg.renamed(_.additionalArg, _.str) ) """ - }("Arg 'additionalArg' is configured multiple times") + }("Config for TestClassWithAdditionalString.additionalArg is being overriden by") } test("Builder reports a missing argument") { From 5fe92199642f168a9388deced52a2f72e107f125 Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Thu, 23 May 2024 14:10:56 +0000 Subject: [PATCH 6/7] Update munit to 1.0.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7229106d..4d4abf99 100644 --- a/build.sbt +++ b/build.sbt @@ -49,7 +49,7 @@ lazy val ducktape = scalacOptions ++= List("-deprecation", "-Wunused:all"), Test / scalacOptions --= List("-deprecation"), Test / scalacOptions ++= List("-Werror", "-Wconf:cat=deprecation:s"), - libraryDependencies += "org.scalameta" %%% "munit" % "1.0.0-RC1" % Test + libraryDependencies += "org.scalameta" %%% "munit" % "1.0.0" % Test ) lazy val docs = From a0b58d26744effe64317f07aff7d82d88947e460 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Mon, 27 May 2024 22:13:25 +0200 Subject: [PATCH 7/7] add 'Mode#locally' --- ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala index 5f93f8d7..8248c8b4 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala @@ -19,6 +19,10 @@ sealed trait Mode[F[+x]] { } object Mode { + extension [F[+x], M <: Mode[F]](self: M) { + inline def locally[A](inline f: M ?=> A): A = f(using self) + } + trait Accumulating[F[+x]] extends Mode[F] { def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] }