Skip to content

Commit

Permalink
[Issue#142] config linting (#159)
Browse files Browse the repository at this point in the history
Fixes #142
  • Loading branch information
arainko authored May 19, 2024
2 parents e726035 + 0766b19 commit cdd96dc
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 71 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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-RC1" % Test
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import scala.collection.mutable.Builder
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ private[ducktape] object Backend {
Logger.info("Config", configs)
Logger.info("Reconfigured plan", reconfiguredPlan)

reconfiguredPlan.warnings
.groupBy(_.span)
.foreach { (span, warnings) =>
val messages = ConfigWarning.renderAll(warnings)
messages.foreach(report.warning(_, span.toPosition))
}

reconfiguredPlan.result.refine match {
case Left(errors) =>
val ogErrors =
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down Expand Up @@ -124,12 +125,14 @@ 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)
}
}

Expand All @@ -138,6 +141,7 @@ private[ducktape] object Plan {
final case class Reconfigured[+F <: Fallible](
errors: List[Plan.Error],
successes: List[(Path, Side)],
warnings: List[ConfigWarning],
result: Plan[Plan.Error, F]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)]): 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],
Expand Down Expand Up @@ -89,19 +88,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[ConfigWarning]:
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[ConfigWarning]) = {
config match {
case cfg: (Configuration.Instruction.Static[F] | Configuration.Instruction.Dynamic[F]) =>
staticOrDynamic(cfg, current, parent)
Expand Down Expand Up @@ -135,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)]) = {
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]) = {
instruction match {
case static: Configuration.Instruction.Static[F] =>
current.configureIfValid(static, static.config)
Expand All @@ -156,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)]): Plan[Plan.Error, F] =
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Plan.Error, F] =
plan match {
case plan: Upcast => plan

Expand Down Expand Up @@ -201,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)]): Plan[Error, F] = {
)(using Quotes, Accumulator[Plan.Error], Accumulator[(Path, Side)], Accumulator[ConfigWarning]): Plan[Error, F] = {

enum IsAnythingModified {
case Yes, No
Expand Down Expand Up @@ -250,16 +250,27 @@ 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[ConfigWarning]
) = {
def isReplaceableBy(update: Configuration[F])(using Quotes) =
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 => ConfigWarning(plan.span, instruction.span, path))
}
Plan.Configured.from(currentPlan, config, instruction.side)
Plan.Configured.from(currentPlan, config, instruction)
else
Accumulator.append {
Plan.Error.from(
Expand All @@ -276,4 +287,15 @@ private[ducktape] object PlanConfigurer {
}
}

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
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
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]] = {

@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
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
}
val errors = recurse(plan :: Nil, Nil)
}

def run[F <: Fallible](plan: Plan[Plan.Error, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = {
// 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]])
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading

0 comments on commit cdd96dc

Please sign in to comment.