diff --git a/.gitignore b/.gitignore index 9e31eead..5f6153ae 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,10 @@ project/plugins/project/ .scala_dependencies .idea +.vscode/ + +# Metals +.metals/ .bloop/ +project/metals.sbt +metals.sbt diff --git a/compiler/src/main/java/play/japi/twirl/compiler/TwirlCompiler.java b/compiler/src/main/java/play/japi/twirl/compiler/TwirlCompiler.java index 9db8ecf9..43dce4c9 100644 --- a/compiler/src/main/java/play/japi/twirl/compiler/TwirlCompiler.java +++ b/compiler/src/main/java/play/japi/twirl/compiler/TwirlCompiler.java @@ -62,6 +62,7 @@ public static Optional compile( List constructorAnnotations, Codec codec, boolean inclusiveDot) { + String scalaVersion = play.twirl.compiler.BuildInfo$.MODULE$.scalaVersion(); Seq scalaAdditionalImports = JavaConverters$.MODULE$ .asScalaBufferConverter(new ArrayList(additionalImports)) @@ -75,6 +76,7 @@ public static Optional compile( sourceDirectory, generatedDirectory, formatterType, + scala.Option.apply(scalaVersion), scalaAdditionalImports, scalaConstructorAnnotations, codec, diff --git a/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala b/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala index ccac8c90..c9c3969e 100644 --- a/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala +++ b/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala @@ -162,6 +162,17 @@ case class GeneratedSourceVirtual(path: String) extends AbstractGeneratedSource object TwirlCompiler { + // For constants that depend on Scala 2 or 3 mode. + private[compiler] class ScalaCompat(emitScala3Sources: Boolean) { + val varargSplicesSyntax: String = + if (emitScala3Sources) "*" else ": _*" + } + + private[compiler] object ScalaCompat { + def apply(scalaVersion: Option[String]): ScalaCompat = + new ScalaCompat(scalaVersion.exists(_.startsWith("3."))) + } + def defaultImports(scalaVersion: String) = { val implicits = if (scalaVersion.startsWith("3.")) { Seq( @@ -199,7 +210,30 @@ object TwirlCompiler { constructorAnnotations: collection.Seq[String] = Nil, codec: Codec = TwirlIO.defaultCodec, inclusiveDot: Boolean = false - ) = { + ): Option[File] = + compile( + source, + sourceDirectory, + generatedDirectory, + formatterType, + None, + additionalImports, + constructorAnnotations, + codec, + inclusiveDot + ) + + def compile( + source: File, + sourceDirectory: File, + generatedDirectory: File, + formatterType: String, + scalaVersion: Option[String], + additionalImports: collection.Seq[String], + constructorAnnotations: collection.Seq[String], + codec: Codec, + inclusiveDot: Boolean + ): Option[File] = { val resultType = formatterType + ".Appendable" val (templateName, generatedSource) = generatedFile(source, codec, sourceDirectory, generatedDirectory, inclusiveDot) @@ -211,6 +245,7 @@ object TwirlCompiler { relativePath(source), resultType, formatterType, + scalaVersion, additionalImports, constructorAnnotations, inclusiveDot @@ -232,7 +267,33 @@ object TwirlCompiler { constructorAnnotations: collection.Seq[String] = Nil, codec: Codec = TwirlIO.defaultCodec, inclusiveDot: Boolean = false - ) = { + ): GeneratedSourceVirtual = + compileVirtual( + content, + source, + sourceDirectory, + resultType, + formatterType, + None, + additionalImports, + constructorAnnotations, + codec, + inclusiveDot + ) + + def compileVirtual( + content: String, + source: File, + sourceDirectory: File, + resultType: String, + formatterType: String, + scalaVersion: Option[String], + additionalImports: collection.Seq[String], + constructorAnnotations: collection.Seq[String], + codec: Codec, + inclusiveDot: Boolean + ): GeneratedSourceVirtual = { + val (templateName, generatedSource) = generatedFileVirtual(source, sourceDirectory, inclusiveDot) val generated = parseAndGenerateCode( templateName, @@ -241,6 +302,7 @@ object TwirlCompiler { relativePath(source), resultType, formatterType, + scalaVersion, additionalImports, constructorAnnotations, inclusiveDot @@ -262,7 +324,31 @@ object TwirlCompiler { additionalImports: collection.Seq[String], constructorAnnotations: collection.Seq[String], inclusiveDot: Boolean - ) = { + ): String = parseAndGenerateCode( + templateName, + content, + codec, + relativePath, + resultType, + formatterType, + None, + additionalImports, + constructorAnnotations, + inclusiveDot + ) + + private def parseAndGenerateCode( + templateName: Array[String], + content: Array[Byte], + codec: Codec, + relativePath: String, + resultType: String, + formatterType: String, + scalaVersion: Option[String], + additionalImports: collection.Seq[String], + constructorAnnotations: collection.Seq[String], + inclusiveDot: Boolean + ): String = { val templateParser = new TwirlParser(inclusiveDot) templateParser.parse(new String(content, codec.charSet)) match { case templateParser.Success(parsed: Template, rest) if rest.atEnd() => { @@ -274,6 +360,7 @@ object TwirlCompiler { parsed, resultType, formatterType, + ScalaCompat(scalaVersion), additionalImports, constructorAnnotations ) @@ -448,8 +535,29 @@ object TwirlCompiler { formatterType: String, additionalImports: collection.Seq[String], constructorAnnotations: collection.Seq[String] + ): collection.Seq[Any] = generateCode( + packageName, + name, + root, + resultType, + formatterType, + ScalaCompat(None), + additionalImports, + constructorAnnotations + ) + + private def generateCode( + packageName: String, + name: String, + root: Template, + resultType: String, + formatterType: String, + scalaCompat: ScalaCompat, + additionalImports: collection.Seq[String], + constructorAnnotations: collection.Seq[String] ): collection.Seq[Any] = { - val (renderCall, f, templateType) = TemplateAsFunctionCompiler.getFunctionMapping(root.params.str, resultType) + val (renderCall, f, templateType) = + TemplateAsFunctionCompiler.getFunctionMapping(root.params.str, resultType, scalaCompat) // Get the imports that we need to include, filtering out empty imports val imports: Seq[Any] = Seq(additionalImports.map(i => Seq("import ", i, "\n")), formatImports(root.topImports)) @@ -509,9 +617,42 @@ package """ :+ packageName :+ """ formatterType: String, additionalImports: collection.Seq[String], constructorAnnotations: collection.Seq[String] + ): String = generateFinalTemplate( + relativePath, + contents, + packageName, + name, + root, + resultType, + formatterType, + ScalaCompat(None), + additionalImports, + constructorAnnotations + ) + + private def generateFinalTemplate( + relativePath: String, + contents: Array[Byte], + packageName: String, + name: String, + root: Template, + resultType: String, + formatterType: String, + scalaCompat: ScalaCompat, + additionalImports: collection.Seq[String], + constructorAnnotations: collection.Seq[String] ): String = { val generated = - generateCode(packageName, name, root, resultType, formatterType, additionalImports, constructorAnnotations) + generateCode( + packageName, + name, + root, + resultType, + formatterType, + scalaCompat, + additionalImports, + constructorAnnotations + ) Source.finalSource(relativePath, contents, generated, Hash(contents, additionalImports)) } @@ -531,7 +672,17 @@ package """ :+ packageName :+ """ } } - def getFunctionMapping(signature: String, returnType: String): (String, String, String) = { + def getFunctionMapping( + signature: String, + returnType: String, + ): (String, String, String) = + getFunctionMapping(signature, returnType, ScalaCompat(None)) + + private[compiler] def getFunctionMapping( + signature: String, + returnType: String, + sc: ScalaCompat + ): (String, String, String) = { val params: List[List[Term.Param]] = try { @@ -573,7 +724,7 @@ package """ :+ packageName :+ """ .map { p => p.name.toString + Option(p.decltpe.get.toString) .filter(_.endsWith("*")) - .map(_ => ".toIndexedSeq:_*") + .map(_ => s".toIndexedSeq${sc.varargSplicesSyntax}") .getOrElse("") } .mkString(",") + ")" @@ -601,7 +752,7 @@ package """ :+ packageName :+ """ .map { p => p.name.toString + Option(p.decltpe.get.toString) .filter(_.endsWith("*")) - .map(_ => ".toIndexedSeq:_*") + .map(_ => s".toIndexedSeq${sc.varargSplicesSyntax}") .getOrElse("") } .mkString(",") + ")" diff --git a/compiler/src/test/scala-2/play/twirl/compiler/test/Helper.scala b/compiler/src/test/scala-2/play/twirl/compiler/test/Helper.scala index 6448bf17..e94f9da2 100644 --- a/compiler/src/test/scala-2/play/twirl/compiler/test/Helper.scala +++ b/compiler/src/test/scala-2/play/twirl/compiler/test/Helper.scala @@ -6,11 +6,12 @@ package play.twirl.compiler package test import java.io._ +import play.twirl.parser.TwirlIO object Helper { case class CompilationError(message: String, line: Int, column: Int) extends RuntimeException(message) - class CompilerHelper(sourceDir: File, generatedDir: File, generatedClasses: File) { + class CompilerHelper(sourceDir: File, val generatedDir: File, generatedClasses: File) { import java.net._ import scala.collection.mutable import scala.reflect.internal.util.Position @@ -99,7 +100,11 @@ object Helper { sourceDir, generatedDir, "play.twirl.api.HtmlFormat", - additionalImports = TwirlCompiler.defaultImports(scalaVersion) ++ additionalImports + Option(scalaVersion), + additionalImports = TwirlCompiler.defaultImports(scalaVersion) ++ additionalImports, + constructorAnnotations = Nil, + codec = TwirlIO.defaultCodec, + inclusiveDot = false ) val mapper = GeneratedSource(generated) diff --git a/compiler/src/test/scala-3/play/twirl/compiler/test/Helper.scala b/compiler/src/test/scala-3/play/twirl/compiler/test/Helper.scala index b53fe10b..c586d253 100644 --- a/compiler/src/test/scala-3/play/twirl/compiler/test/Helper.scala +++ b/compiler/src/test/scala-3/play/twirl/compiler/test/Helper.scala @@ -20,11 +20,12 @@ import dotty.tools.io.PlainDirectory import dotty.tools.io.Directory import dotty.tools.io.ClassPath import scala.jdk.CollectionConverters._ +import play.twirl.parser.TwirlIO object Helper { case class CompilationError(message: String, line: Int, column: Int) extends RuntimeException(message) - class CompilerHelper(sourceDir: File, generatedDir: File, generatedClasses: File) { + class CompilerHelper(sourceDir: File, val generatedDir: File, generatedClasses: File) { import java.net._ import scala.collection.mutable @@ -60,14 +61,22 @@ object Helper { ): CompiledTemplate[T] = { val scalaVersion = play.twirl.compiler.BuildInfo.scalaVersion val templateFile = new File(sourceDir, templateName) - val Some(generated) = twirlCompiler.compile( + val generatedOpt: Option[File] = twirlCompiler.compile( templateFile, sourceDir, generatedDir, "play.twirl.api.HtmlFormat", - additionalImports = TwirlCompiler.defaultImports(scalaVersion) ++ additionalImports + Option(scalaVersion), + additionalImports = TwirlCompiler.defaultImports(scalaVersion) ++ additionalImports, + constructorAnnotations = Nil, + codec = TwirlIO.defaultCodec, + inclusiveDot = false ) + val generated = generatedOpt.getOrElse { + throw new FileNotFoundException(s"Could not find generated file for $templateName") + } + val mapper = GeneratedSource(generated) val compilerArgs = Array( @@ -94,7 +103,10 @@ object Helper { class TestDriver(outDir: Path, compilerArgs: Array[String], path: Path) extends Driver { def compile(): Reporter = { - val Some((toCompile, rootCtx)) = setup(compilerArgs :+ path.toAbsolutePath.toString, initCtx.fresh) + val setupOpt = setup(compilerArgs :+ path.toAbsolutePath.toString, initCtx.fresh) + val (toCompile, rootCtx) = setupOpt.getOrElse { + throw new Exception("Failed to initialize compiler") + } val silentReporter = new ConsoleReporter.AbstractConsoleReporter { def printMessage(msg: String): Unit = { diff --git a/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala b/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala index 075af74c..909936d1 100644 --- a/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala +++ b/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala @@ -10,6 +10,8 @@ import play.twirl.api.Html import play.twirl.parser.TwirlIO import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec +import TwirlCompiler.ScalaCompat +import scala.io.Source class CompilerSpec extends AnyWordSpec with Matchers { @@ -159,6 +161,12 @@ class CompilerSpec extends AnyWordSpec with Matchers { .static(Array(List(1, 2, 3), List(4, 5, 6))) .toString .trim + + val compat = ScalaCompat(Option(BuildInfo.scalaVersion)) + val generatedFile = helper.generatedDir.toPath.resolve("html/varArgsExistential.template.scala").toFile + val generatedText = Source.fromFile(generatedFile).getLines().mkString("\n") + + generatedText must include(s"list.toIndexedSeq${compat.varargSplicesSyntax}") text must be("123456") } @@ -329,4 +337,24 @@ class CompilerSpec extends AnyWordSpec with Matchers { ) hello.static(args).toString.trim must be("the-key => the-value") } + + "ScalaCompat" should { + + val cases = List( + None -> ": _*", + Some("2.12.18") -> ": _*", + Some("2.13.12") -> ": _*", + Some("3.3.1") -> "*" + ) + + "produce correct varargs splice syntax" in { + + cases.foreach { case (version, expected) => + ScalaCompat(version).varargSplicesSyntax must be(expected) + } + + } + + } + } diff --git a/sbt-twirl/src/main/scala/play/twirl/sbt/SbtTwirl.scala b/sbt-twirl/src/main/scala/play/twirl/sbt/SbtTwirl.scala index 2f341cc5..e3728a1f 100644 --- a/sbt-twirl/src/main/scala/play/twirl/sbt/SbtTwirl.scala +++ b/sbt-twirl/src/main/scala/play/twirl/sbt/SbtTwirl.scala @@ -117,7 +117,8 @@ object SbtTwirl extends AutoPlugin { (compileTemplates / includeFilter).value, (compileTemplates / excludeFilter).value, Codec(sourceEncoding.value), - streams.value.log + streams.value.log, + scalaVersion.value ) } diff --git a/sbt-twirl/src/main/scala/play/twirl/sbt/TemplateCompiler.scala b/sbt-twirl/src/main/scala/play/twirl/sbt/TemplateCompiler.scala index beae4d51..4dfb9ed6 100644 --- a/sbt-twirl/src/main/scala/play/twirl/sbt/TemplateCompiler.scala +++ b/sbt-twirl/src/main/scala/play/twirl/sbt/TemplateCompiler.scala @@ -21,6 +21,30 @@ object TemplateCompiler { excludeFilter: FileFilter, codec: Codec, log: Logger + ): Seq[File] = compile( + sourceDirectories, + targetDirectory, + templateFormats, + templateImports, + constructorAnnotations, + includeFilter, + excludeFilter, + codec, + log, + "2.13.x" // using a dummy scala version (not starting with "3." to generate Scala 2 code by default) + ) + + def compile( + sourceDirectories: Seq[File], + targetDirectory: File, + templateFormats: Map[String, String], + templateImports: Seq[String], + constructorAnnotations: Seq[String], + includeFilter: FileFilter, + excludeFilter: FileFilter, + codec: Codec, + log: Logger, + scalaVersion: String ): Seq[File] = { try { syncGenerated(targetDirectory, codec) @@ -32,6 +56,7 @@ object TemplateCompiler { sourceDirectory, targetDirectory, format, + Some(scalaVersion), imports, constructorAnnotations, codec,