From b48c312d4dcabf82fd10e02bd586be9d9cd4e1da Mon Sep 17 00:00:00 2001 From: Roland Reckel Date: Fri, 4 Aug 2023 10:33:36 +0200 Subject: [PATCH 1/2] Add a Scala3Renderer --- .../Scala3CaseClassRenderer.scala | 66 +++++++++++++++++ .../Scala3CaseObjectRenderer.scala | 71 +++++++++++++++++++ .../scala/sbtbuildinfo/Scala3Renderer.scala | 29 ++++++++ 3 files changed, 166 insertions(+) create mode 100644 src/main/scala/sbtbuildinfo/Scala3CaseClassRenderer.scala create mode 100644 src/main/scala/sbtbuildinfo/Scala3CaseObjectRenderer.scala create mode 100644 src/main/scala/sbtbuildinfo/Scala3Renderer.scala diff --git a/src/main/scala/sbtbuildinfo/Scala3CaseClassRenderer.scala b/src/main/scala/sbtbuildinfo/Scala3CaseClassRenderer.scala new file mode 100644 index 0000000..1d92875 --- /dev/null +++ b/src/main/scala/sbtbuildinfo/Scala3CaseClassRenderer.scala @@ -0,0 +1,66 @@ +package sbtbuildinfo + +case class Scala3CaseClassRenderer(options: Seq[BuildInfoOption], pkg: String, obj: String) extends Scala3Renderer { + override def fileType = BuildInfoType.Source + override def extension = "scala" + + val traitNames = options.collect{case BuildInfoOption.Traits(ts @ _*) => ts}.flatten + val objTraits = if (traitNames.isEmpty) "" else "extends " ++ traitNames.mkString(" with ") + + // It is safe to add `import scala.Predef` even though we need to keep `-Ywarn-unused-import` in mind + // because we always generate code that has a reference to `String`. If the "base" generated code were to be + // changed and no longer contain a reference to `String`, we would need to remove `import scala.Predef` and + // fully qualify every reference. Note it is NOT safe to use `import scala._` because of the possibility of + // the project using `-Ywarn-unused-import` because we do not always generated references that are part of + // `scala` such as `scala.Option`. + val importScalaPredef = options.contains(BuildInfoOption.ImportScalaPredef) + def header = List( + "// $COVERAGE-OFF$", + s"package $pkg", + "" + ) + val imports = if (importScalaPredef) List( + "import scala.Predef.*", + "" + ) else Nil + val generateComment = List( + s"/** This file was generated by sbt-buildinfo. */" + ) + + def footer = List("// $COVERAGE-ON$") + + override def renderKeys(buildInfoResults: Seq[BuildInfoResult]) = + header ++ imports ++ generateComment ++ + caseClassDefinitionBegin ++ + buildInfoResults.flatMap(caseClassParameter).mkString(",\n").split("\n") ++ + caseClassDefinitionEnd ++ + toMapLines(buildInfoResults) ++ + toJsonLines ++ + caseClassEnd ++ + List("") ++ + caseObjectLine(buildInfoResults) ++ + footer + + private def caseClassDefinitionBegin = List( + withPkgPriv(s"case class $obj(") + ) + + private def caseClassParameter(r: BuildInfoResult): Seq[String] = { + val typeDecl = getType(r.typeExpr) getOrElse "scala.Any" + + List( + s" ${r.identifier}: $typeDecl" + ) + } + + private def caseClassDefinitionEnd = List(s") $objTraits {", "") + private def caseClassEnd = List("}") + + private def caseObjectLine(buildInfoResults: Seq[BuildInfoResult]) = List( + withPkgPriv(s"case object $obj {"), + s" def apply(): $obj = new $obj(${buildInfoResults.map(r => s"\n ${r.identifier} = ${quote(r.value)}").mkString(",")})", + s" val get = apply()", + s" val value = apply()", + s"}" + ) +} diff --git a/src/main/scala/sbtbuildinfo/Scala3CaseObjectRenderer.scala b/src/main/scala/sbtbuildinfo/Scala3CaseObjectRenderer.scala new file mode 100644 index 0000000..870f692 --- /dev/null +++ b/src/main/scala/sbtbuildinfo/Scala3CaseObjectRenderer.scala @@ -0,0 +1,71 @@ +package sbtbuildinfo + +case class Scala3CaseObjectRenderer(options: Seq[BuildInfoOption], pkg: String, obj: String) extends Scala3Renderer { + + override def fileType = BuildInfoType.Source + override def extension = "scala" + val traitNames = options.collect{case BuildInfoOption.Traits(ts @ _*) => ts}.flatten + val objTraits = if (traitNames.isEmpty) "" else " extends " ++ traitNames.mkString(" with ") + val constantValue = options.contains(BuildInfoOption.ConstantValue) + + // It is safe to add `import scala.Predef` even though we need to keep `-Ywarn-unused-import` in mind + // because we always generate code that has a reference to `String`. If the "base" generated code were to be + // changed and no longer contain a reference to `String`, we would need to remove `import scala.Predef` and + // fully qualify every reference. Note it is NOT safe to use `import scala._` because of the possibility of + // the project using `-Ywarn-unused-import` because we do not always generated references that are part of + // `scala` such as `scala.Option`. + val importScalaPredef = options.contains(BuildInfoOption.ImportScalaPredef) + def header = List( + "// $COVERAGE-OFF$", + s"package $pkg", + "" + ) + val imports = if (importScalaPredef) List( + "import scala.Predef.*", + "" + ) else Nil + val objectHeader = List( + s"/** This object was generated by sbt-buildinfo. */", + withPkgPriv(s"case object $obj$objTraits {") + ) + + def footer = List("}", "// $COVERAGE-ON$") + + override def renderKeys(buildInfoResults: Seq[BuildInfoResult]) = + header ++ imports ++ objectHeader ++ + buildInfoResults.flatMap(line) ++ + Seq(toStringLines(buildInfoResults)) ++ + toMapLines(buildInfoResults) ++ + toJsonLines ++ + footer + + private val constantTypes = Set("scala.Int", "scala.Long", "scala.Double", "scala.Boolean", "scala.Symbol", "String") + + private def line(result: BuildInfoResult): Seq[String] = { + import result._ + val (typeDecl, modifier) = + getType(result.typeExpr) match { + case Some(tp) if !constantValue || !constantTypes(tp) => + (s": $tp", "") + case _ if constantValue => + ("", "final ") + case _ => + ("", "") + } + List( + s" /** The value is ${quote(value)}. */", + s" ${modifier}val $identifier$typeDecl = ${quote(value)}" + ) + } + + def toStringLines(results: Seq[BuildInfoResult]): String = { + val idents = results.map(_.identifier) + val fmt = idents.map("%s: %%s" format _).mkString(", ") + val vars = idents.mkString(", ") + s""" override val toString: String = { + | "$fmt".format( + | $vars + | ) + | }""".stripMargin + } +} diff --git a/src/main/scala/sbtbuildinfo/Scala3Renderer.scala b/src/main/scala/sbtbuildinfo/Scala3Renderer.scala new file mode 100644 index 0000000..fc2fd9a --- /dev/null +++ b/src/main/scala/sbtbuildinfo/Scala3Renderer.scala @@ -0,0 +1,29 @@ +package sbtbuildinfo + +abstract class Scala3Renderer extends ScalaRenderer { + + override protected def toJsonLines: Seq[String] = + if (options contains BuildInfoOption.ToJson) + List( + """| private def quote(x: scala.Any): String = "\"" + x + "\"" + | private def toJsonValue[T <: Matchable](value: T): String = { + | value match { + | case elem: scala.collection.Seq[? <: Matchable] => elem.map(toJsonValue).mkString("[", ",", "]") + | case elem: scala.Option[? <: Matchable] => elem.map(toJsonValue).getOrElse("null") + | case elem: scala.collection.Map[?, ? <: Matchable] => elem.map { + | case (k, v) => toJsonValue(k.toString) + ":" + toJsonValue(v) + | }.mkString("{", ", ", "}") + | case d: scala.Double => d.toString + | case f: scala.Float => f.toString + | case l: scala.Long => l.toString + | case i: scala.Int => i.toString + | case s: scala.Short => s.toString + | case bool: scala.Boolean => bool.toString + | case str: String => quote(str) + | case other => quote(other.toString) + | } + | } + | + | val toJson: String = toJsonValue(toMap)""".stripMargin) + else Nil +} From abf290c8daf611716ec616b74e41c026c1b15fd0 Mon Sep 17 00:00:00 2001 From: Roland Reckel Date: Fri, 4 Aug 2023 13:40:37 +0200 Subject: [PATCH 2/2] Default to scala3Renderer if scalaVersion is 3 --- src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala b/src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala index c9baa33..98a209d 100644 --- a/src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala +++ b/src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala @@ -110,6 +110,6 @@ object BuildInfoPlugin extends AutoPlugin { buildInfoKeys := Seq(name, version, scalaVersion, sbtVersion), buildInfoBuildNumber := buildNumberTask(baseDirectory.value, 1), buildInfoOptions := Seq(), - buildInfoRenderFactory := ScalaCaseObjectRenderer.apply + buildInfoRenderFactory := (if(scalaVersion.value.startsWith("3")) Scala3CaseObjectRenderer.apply else ScalaCaseObjectRenderer.apply) ) }