diff --git a/build.sbt b/build.sbt index bd62360e..e9db37b2 100644 --- a/build.sbt +++ b/build.sbt @@ -85,6 +85,25 @@ lazy val sharedSettings = List[Setting[_]]( skip.in(publish) := true disablePlugins(MimaPlugin) +lazy val pprint = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .in(file("metaconfig-pprint")) + .settings( + sharedSettings, + moduleName := "metaconfig-pprint", + libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.3.0", + libraryDependencies ++= { + if (scalaVersion.value.startsWith("2.")) + List( + "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.scala-lang" % "scala-compiler" % scalaVersion.value + ) + else Nil + } + ) + .nativeSettings( + crossScalaVersions -= scala3 + ) + lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .in(file("metaconfig-core")) .settings( @@ -92,8 +111,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) moduleName := "metaconfig-core", libraryDependencies ++= List( "org.typelevel" %%% "paiges-core" % "0.4.2", - "org.scala-lang.modules" %%% "scala-collection-compat" % "2.5.0", - "com.lihaoyi" %%% "pprint" % "0.7.1" + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.5.0" ) ) .settings( @@ -115,6 +133,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .nativeSettings( crossScalaVersions -= scala3 ) + .dependsOn(pprint) lazy val coreJVM = core.jvm lazy val coreJS = core.js diff --git a/metaconfig-core/shared/src/main/scala-2/metaconfig/internal/Macros.scala b/metaconfig-core/shared/src/main/scala-2/metaconfig/internal/Macros.scala index 0511e662..ec9af84a 100644 --- a/metaconfig-core/shared/src/main/scala-2/metaconfig/internal/Macros.scala +++ b/metaconfig-core/shared/src/main/scala-2/metaconfig/internal/Macros.scala @@ -262,7 +262,7 @@ class Macros(val c: blackbox.Context) { } val tprint = c.internal.typeRef( NoPrefix, - weakTypeOf[pprint.TPrint[_]].typeSymbol, + weakTypeOf[metaconfig.pprint.TPrint[_]].typeSymbol, paramTpe :: Nil ) val tpeString = c.inferImplicitValue(tprint) diff --git a/metaconfig-core/shared/src/main/scala-3/metaconfig/generic/package.scala b/metaconfig-core/shared/src/main/scala-3/metaconfig/generic/package.scala index 7be0b5e5..ef5d1bca 100644 --- a/metaconfig-core/shared/src/main/scala-3/metaconfig/generic/package.scala +++ b/metaconfig-core/shared/src/main/scala-3/metaconfig/generic/package.scala @@ -272,7 +272,7 @@ private[generic] def deriveSurfaceImpl[T: Type](using q: Quotes) = val tpeString = fieldType.asType match case '[t] => - val renderer = Expr.summon[pprint.TPrint[t]].get + val renderer = Expr.summon[metaconfig.pprint.TPrint[t]].get '{ $renderer.render.render } val fieldExpr = '{ diff --git a/metaconfig-pprint/shared/src/main/scala-2/metaconfig/pprint/TPrintImpl.scala b/metaconfig-pprint/shared/src/main/scala-2/metaconfig/pprint/TPrintImpl.scala new file mode 100644 index 00000000..ec88fd1a --- /dev/null +++ b/metaconfig-pprint/shared/src/main/scala-2/metaconfig/pprint/TPrintImpl.scala @@ -0,0 +1,276 @@ +package metaconfig.pprint +import language.experimental.macros +import reflect.macros.blackbox.Context + +trait TPrintLowPri { + implicit def default[T]: TPrint[T] = macro TPrintLowPri.typePrintImpl[T] +} +object TPrintLowPri { + sealed trait WrapType + object WrapType { + case object NoWrap extends WrapType + case object Infix extends WrapType + case object Tuple extends WrapType + } + def typePrintImpl[T: c.WeakTypeTag](c: Context): c.Expr[TPrint[T]] = { + // Used to provide "empty string" values in quasiquotes + + import c.universe._ + val tpe = weakTypeOf[T] + val rendered = typePrintImplRec(c)(tpe, rightMost = true).render + val res = c.Expr[TPrint[T]]( + q"_root_.metaconfig.pprint.TPrint.recolor(_root_.fansi.Str($rendered))" + ) + res + } + + val functionTypes: Set[String] = + Range.inclusive(0, 22).map(i => s"scala.Function$i").toSet + val tupleTypes: Set[String] = + Range.inclusive(0, 22).map(i => s"scala.Tuple$i").toSet + + def typePrintImplRec[T]( + c: Context + )(tpe: c.Type, rightMost: Boolean): fansi.Str = { + typePrintImplRec0(c)(tpe, rightMost)._1 + } + def typePrintImplRec0[T]( + c: Context + )(tpe: c.Type, rightMost: Boolean): (fansi.Str, WrapType) = { + import c.universe._ + def printSymString(s: Symbol) = + if (s.name.decodedName.toString.startsWith("_$")) "_" + else s.name.decodedName.toString.stripSuffix(".type") + + def literalColor(s: fansi.Str): fansi.Str = fansi.Color.Green(s) + def printSym(s: Symbol): fansi.Str = literalColor(printSymString(s)) + + def printSymFull(s: Symbol): fansi.Str = { + if (lookup(s)) printSym(s) + else printSymFull(s.owner) ++ "." ++ printSym(s) + } + + /** + * Looks up a symbol in the enclosing scope and returns + * whether it exists in scope by the same name + */ + def lookup(s: Symbol) = { + val cas = c.asInstanceOf[reflect.macros.runtime.Context] + val g = cas.global + val gName = s.name.asInstanceOf[g.Name] + val lookedUps = + for (n <- Stream(gName.toTermName, gName.toTypeName)) yield { + cas.callsiteTyper.context + .lookupSymbol(n, _ => true) + .symbol + } + + if (!s.isType) { + // Try to de-reference `val` references + lookedUps.exists(x => x == s || x.tpe.termSymbol == s.asTerm) + } else { + // Try to resolve aliases for types + lookedUps.exists(x => + x == s || x.tpe.typeSymbol == s.asInstanceOf[g.Symbol].tpe.typeSymbol + ) + } + } + + def prefixFor(pre: Type, sym: Symbol): fansi.Str = { + // Depending on what the prefix is, you may use `#`, `.` + // or even need to wrap the prefix in parentheses + val sep = pre match { + case x if x.toString.endsWith(".type") => + typePrintImplRec(c)(pre, false) ++ "." + case x: TypeRef => literalColor(typePrintImplRec(c)(pre, true)) ++ "#" + case x: SingleType => + literalColor(typePrintImplRec(c)(pre, false)) ++ "." + case x: ThisType => literalColor(typePrintImplRec(c)(pre, false)) ++ "." + case x => fansi.Str("(") ++ typePrintImplRec(c)(pre, true) ++ ")#" + } + + val prefix = if (!lookup(sym)) sep else fansi.Str("") + prefix ++ printSym(sym) + } + + def printArgSyms(args: List[Symbol]): fansi.Str = { + def added = + args + .map { x => + val TypeBounds(lo, hi) = x.info + printSym(x) ++ printBounds(lo, hi) + } + .reduceLeft[fansi.Str]((l, r) => l ++ ", " ++ r) + + if (args == Nil) fansi.Str("") else fansi.Str("[") ++ added ++ "]" + } + def printArgs(args: List[Type]): fansi.Str = { + def added = + args + .map(typePrintImplRec(c)(_, true)) + .reduceLeft[fansi.Str]((l, r) => l ++ ", " ++ r) + + if (args == Nil) fansi.Str("") else fansi.Str("[") ++ added ++ "]" + } + + def printBounds(lo: Type, hi: Type) = { + val loTree = + if (lo =:= typeOf[Nothing]) fansi.Str("") + else fansi.Str(" >: ") ++ typePrintImplRec(c)(lo, true) + val hiTree = + if (hi =:= typeOf[Any]) fansi.Str("") + else fansi.Str(" <: ") ++ typePrintImplRec(c)(hi, true) + loTree ++ hiTree + } + + def showRefinement(quantified: List[Symbol]) = { + def stmts = + for { + t <- quantified + suffix <- t.info match { + case PolyType(typeParams, resultType) => + val paramTree = printArgSyms( + t.asInstanceOf[TypeSymbol].typeParams + ) + val resultBounds = + if (resultType =:= typeOf[Any]) fansi.Str("") + else fansi.Str(" <: ") ++ typePrintImplRec(c)(resultType, true) + + Some(paramTree ++ resultBounds) + case TypeBounds(lo, hi) + if t.toString.contains("$") && lo =:= typeOf[Nothing] && hi =:= typeOf[ + Any + ] => + None + case TypeBounds(lo, hi) => + Some(printBounds(lo, hi)) + } + } yield { + if (t.toString.endsWith(".type")) { + val TypeBounds(lo, hi) = t.info + val RefinedType(parents, defs) = hi + val filtered = internal.refinedType( + parents.filter(x => !(x =:= typeOf[scala.Singleton])), + defs + ) + + fansi.Str("val ") ++ literalColor( + t.name.toString.stripSuffix(".type") + ) ++ + ": " ++ typePrintImplRec(c)(filtered, true) + } else { + fansi.Str("type ") ++ printSym(t) ++ suffix + } + } + if (stmts.length == 0) None + else Some(stmts.reduceLeft((l, r) => l + "; " + r)) + } + + tpe match { + case TypeBounds(lo, hi) => + val res = printBounds(lo, hi) + (fansi.Str("_") ++ res, WrapType.NoWrap) + case ThisType(sym) => + ( + printSymFull(sym) + (if (sym.isPackage || sym.isModuleClass) "" + else ".this.type"), + WrapType.NoWrap + ) + + case SingleType(NoPrefix, sym) => + (printSym(sym) ++ (if (rightMost) ".type" else ""), WrapType.NoWrap) + case SingleType(pre, sym) => + ( + prefixFor(pre, sym) ++ (if (rightMost) ".type" else ""), + WrapType.NoWrap + ) + // Special-case operator two-parameter types as infix + case TypeRef(pre, sym, List(left, right)) + if lookup(sym) && sym.name.encodedName.toString != sym.name.decodedName.toString => + ( + typePrintImplRec(c)(left, true) ++ " " ++ printSym(sym) ++ " " ++ typePrintImplRec( + c + )(right, true), + WrapType.Infix + ) + + case TypeRef(pre, sym, args) if functionTypes.contains(sym.fullName) => + args match { + case Seq(r) => + ( + fansi.Str("() => ") ++ typePrintImplRec(c)(r, true), + WrapType.Infix + ) + + case many => + val (left, leftWrap) = typePrintImplRec0(c)(many.head, true) + + if (many.size == 2 && leftWrap == WrapType.NoWrap) { + ( + left ++ " => " ++ typePrintImplRec(c)(many(1), true), + WrapType.Infix + ) + } else + ( + fansi.Str("(") ++ + fansi.Str.join( + (left +: many.init.tail.map(typePrintImplRec(c)(_, true))), + sep = ", " + ) ++ + ") => " ++ typePrintImplRec(c)(many.last, true), + WrapType.Infix + ) + } + case TypeRef(pre, sym, args) if tupleTypes.contains(sym.fullName) => + ( + fansi.Str("(") ++ + fansi.Str + .join(args.map(typePrintImplRec(c)(_, true)), sep = ", ") ++ + ")", + WrapType.Tuple + ) + + case TypeRef(NoPrefix, sym, args) => + (printSym(sym) ++ printArgs(args), WrapType.NoWrap) + case TypeRef(pre, sym, args) => + if (sym.fullName == "scala.") + ( + fansi.Str("=> ") ++ typePrintImplRec(c)(args(0), true), + WrapType.Infix + ) + else (prefixFor(pre, sym) ++ printArgs(args), WrapType.NoWrap) + case et @ ExistentialType(quantified, underlying) => + ( + showRefinement(quantified) match { + case None => typePrintImplRec(c)(underlying, true) + case Some(block) => + typePrintImplRec(c)(underlying, true) ++ " forSome { " ++ block ++ " }" + }, + WrapType.NoWrap + ) + case AnnotatedType(annots, tp) => + val mapped = annots + .map(x => " @" + typePrintImplRec(c)(x.tpe, true)) + .reduceLeft((x, y) => x + y) + + ( + typePrintImplRec(c)(tp, true) + mapped, + WrapType.NoWrap + ) + case RefinedType(parents, defs) => + val pre = + if (parents.forall(_ =:= typeOf[AnyRef])) "" + else + parents + .map(typePrintImplRec(c)(_, true)) + .reduceLeft[fansi.Str]((l, r) => l ++ " with " ++ r) + ( + pre + (if (defs.isEmpty) "" else "{" ++ defs.mkString(";") ++ "}"), + WrapType.NoWrap + ) + case ConstantType(value) => + (fansi.Str(value.value.toString()), WrapType.NoWrap) + } + } + +} diff --git a/metaconfig-pprint/shared/src/main/scala-3/metaconfig/pprint/TPrintImpl.scala b/metaconfig-pprint/shared/src/main/scala-3/metaconfig/pprint/TPrintImpl.scala new file mode 100644 index 00000000..f7ba18f9 --- /dev/null +++ b/metaconfig-pprint/shared/src/main/scala-3/metaconfig/pprint/TPrintImpl.scala @@ -0,0 +1,131 @@ +package metaconfig.pprint + +trait TPrintLowPri{ + inline given default[T]: TPrint[T] = ${ TPrintLowPri.typePrintImpl[T] } +} + +object TPrintLowPri{ + + import scala.quoted._ + + sealed trait WrapType + object WrapType{ + case object NoWrap extends WrapType + case object Infix extends WrapType + case object Tuple extends WrapType + } + + val functionTypes = Range.inclusive(0, 22).map(i => s"scala.Function$i").toSet + val tupleTypes = Range.inclusive(0, 22).map(i => s"scala.Tuple$i").toSet + + def typePrintImpl[T](using Quotes, Type[T]): Expr[TPrint[T]] = { + + import quotes.reflect._ + import util._ + + def literalColor(s: fansi.Str): fansi.Str = { + fansi.Color.Green(s) + } + + def printSymString(s: String) = + if (s.toString.startsWith("_$")) "_" + else s.toString.stripSuffix(".type") + + def printBounds(lo: TypeRepr, hi: TypeRepr): fansi.Str = { + val loTree = + if (lo =:= TypeRepr.of[Nothing]) None else Some(fansi.Str(" >: ") ++ rec(lo) ) + val hiTree = + if (hi =:= TypeRepr.of[Any]) None else Some(fansi.Str(" <: ") ++ rec(hi) ) + val underscore = fansi.Str("_") + loTree.orElse(hiTree).map(underscore ++ _).getOrElse(underscore) + } + + def printSym(s: String): fansi.Str = literalColor(fansi.Str(s)) + + //TODO: We don't currently use this method + def prefixFor(pre: TypeTree, sym: String): fansi.Str = { + // Depending on what the prefix is, you may use `#`, `.` + // or even need to wrap the prefix in parentheses + val sep = pre match { + case x if x.toString.endsWith(".type") => + rec(pre.tpe) ++ "." + } + sep ++ printSym(sym) + } + + + def printArgs(args: List[TypeRepr]): fansi.Str = { + fansi.Str("[") ++ printArgs0(args) ++ "]" + } + + def printArgs0(args: List[TypeRepr]): fansi.Str = { + val added = fansi.Str.join( + args.map { + case TypeBounds(lo, hi) => + printBounds(lo, hi) + case tpe: TypeRepr => + rec(tpe, false) + }, + sep = ", " + ) + added + } + + + object RefinedType { + def unapply(tpe: TypeRepr): Option[(TypeRepr, List[(String, TypeRepr)])] = tpe match { + case Refinement(p, i, b) => + unapply(p).map { + case (pp, bs) => (pp, (i -> b) :: bs) + }.orElse(Some((p, (i -> b) :: Nil))) + case _ => None + } + } + + def rec(tpe: TypeRepr, end: Boolean = false): fansi.Str = rec0(tpe)._1 + def rec0(tpe: TypeRepr, end: Boolean = false): (fansi.Str, WrapType) = tpe match { + case TypeRef(NoPrefix(), sym) => + (printSym(sym), WrapType.NoWrap) + // TODO: Add prefix handling back in once it works! + case TypeRef(_, sym) => + (printSym(sym), WrapType.NoWrap) + case AppliedType(tpe, args) => + if (functionTypes.contains(tpe.typeSymbol.fullName)) { + ( + if(args.size == 1 ) fansi.Str("() => ") ++ rec(args.last) + else{ + val (left, wrap) = rec0(args(0)) + if(args.size == 2 && wrap == WrapType.NoWrap){ + left ++ fansi.Str(" => ") ++ rec(args.last) + } + else fansi.Str("(") ++ printArgs0(args.dropRight(1)) ++ fansi.Str(") => ") ++ rec(args.last) + + }, + WrapType.Infix + ) + + } else if (tupleTypes.contains(tpe.typeSymbol.fullName)) + (fansi.Str("(") ++ printArgs0(args) ++ fansi.Str(")"), WrapType.Tuple) + else (printSym(tpe.typeSymbol.name) ++ printArgs(args), WrapType.NoWrap) + case RefinedType(tpe, refinements) => + val pre = rec(tpe) + lazy val defs = fansi.Str.join( + refinements.collect { + case (name, tpe: TypeRepr) => + fansi.Str("type " + name + " = ") ++ rec(tpe) + case (name, TypeBounds(lo, hi)) => + fansi.Str("type " + name) ++ printBounds(lo, hi) ++ rec(tpe) + }, + sep = "; " + ) + (pre ++ (if(refinements.isEmpty) fansi.Str("") else fansi.Str("{") ++ defs ++ "}"), WrapType.NoWrap) + case AnnotatedType(parent, annot) => + (rec(parent, end), WrapType.NoWrap) + case _ => + (fansi.Str(Type.show[T]), WrapType.NoWrap) + } + val value: fansi.Str = rec(TypeRepr.of[T]) + + '{TPrint.recolor(fansi.Str(${Expr(value.render)}))} + } +} \ No newline at end of file diff --git a/metaconfig-pprint/shared/src/main/scala/metaconfig/pprint/TPrint.scala b/metaconfig-pprint/shared/src/main/scala/metaconfig/pprint/TPrint.scala new file mode 100644 index 00000000..bbe0999d --- /dev/null +++ b/metaconfig-pprint/shared/src/main/scala/metaconfig/pprint/TPrint.scala @@ -0,0 +1,42 @@ +package metaconfig.pprint + +/** + * Summoning an implicit `TPrint[T]` provides a pretty-printed + * string representation of the type `T`, much better than is + * provided by the default `Type#toString`. In particular + * + * - More forms are properly supported and printed + * - Prefixed Types are printed un-qualified, according to + * what's currently in scope + */ +trait TPrint[T] { + def render(implicit tpc: TPrintColors): fansi.Str + +} + +object TPrint extends TPrintLowPri { + def recolor[T](s: fansi.Str): TPrint[T] = { + new TPrint[T] { + def render(implicit tpc: TPrintColors) = { + val colors = s.getColors + val updatedColors = colors.map { c => + if (c == fansi.Color.Green.applyMask) tpc.typeColor.applyMask else 0L + } + fansi.Str.fromArrays(s.getChars, updatedColors) + } + } + } + def implicitly[T](implicit t: TPrint[T]): TPrint[T] = t + implicit val NothingTPrint: TPrint[Nothing] = + recolor[Nothing](fansi.Color.Green("Nothing")) + +} + +case class TPrintColors(typeColor: fansi.Attrs) + +object TPrintColors { + implicit object BlackWhite extends TPrintColors(fansi.Attrs()) + object Colors extends TPrintColors(fansi.Color.Green) { + implicit val Colored: TPrintColors = this + } +} diff --git a/metaconfig-tests/shared/src/test/scala/metaconfig/DeriveSurfaceSuite.scala b/metaconfig-tests/shared/src/test/scala/metaconfig/DeriveSurfaceSuite.scala index 683f5db0..00690620 100644 --- a/metaconfig-tests/shared/src/test/scala/metaconfig/DeriveSurfaceSuite.scala +++ b/metaconfig-tests/shared/src/test/scala/metaconfig/DeriveSurfaceSuite.scala @@ -3,7 +3,7 @@ package metaconfig import java.io.File import metaconfig.generic.Settings import metaconfig.generic.Surface -import pprint.TPrintColors +import metaconfig.pprint.TPrintColors class DeriveSurfaceSuite extends munit.FunSuite { @@ -76,7 +76,7 @@ class DeriveSurfaceSuite extends munit.FunSuite { case class CustomTypePrinting(a: Int, b: Option[Int], c: List[String]) test("tprint") { - import pprint.TPrint + import metaconfig.pprint.TPrint implicit val intPrint = new TPrint[Int] { def render(implicit tpc: TPrintColors): fansi.Str = fansi.Str("number") }