diff --git a/build.sbt b/build.sbt index 44f99c7..21e4b3f 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,7 @@ val common = Seq( version := "0.2", crossPaths := true, - scalaVersion := "2.11.7", + scalaVersion := "2.10.4", crossScalaVersions := Seq("2.10.4", "2.11.7"), scalacOptions ++= Seq( "-Xfatal-warnings", "-feature", "-deprecation" ), @@ -62,7 +62,17 @@ lazy val scalingua = project libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", "org.scalatest" %% "scalatest" % "2.2.6" % Test - ) + ), + + libraryDependencies ++= { + CrossVersion.binaryScalaVersion(scalaVersion.value) match { + case "2.10" => Seq( + compilerPlugin("org.scalamacros" % "paradise" % "2.0.0" cross CrossVersion.full), + "org.scalamacros" %% "quasiquotes" % "2.0.0" + ) + case _ => Nil + } + } ) .dependsOn(core) diff --git a/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala b/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala index d0212df..e5baa2b 100644 --- a/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala +++ b/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala @@ -16,128 +16,7 @@ package ru.makkarpov.scalingua -import ru.makkarpov.scalingua.extract.MessageExtractor - object Compat { type Context = scala.reflect.macros.Context - - def showCode(c: Context)(e: c.Tree): String = c.universe.show(e) - - def tupleLiteral[T](c: Context)(e: c.Expr[(String, T)]): (String, c.Expr[T]) = { - import c.universe._ - - val (aTree, bTree) = e.tree match { - // matches AST for a -> b - case Apply(TypeApply( - Select( - Apply(TypeApply( - // scala.Predef.arrowAssoc (no extractor for type names. so match them manually) - Select(Select(This(tnScala: TypeName), tnPredef: Name), tnArrAss: TermName), List(TypeTree()) - ), List(a)), - // $minus$greater - tnMg: TermName - ), List(TypeTree())), List(b) - ) if (tnScala.decodedName.toString == "scala") && (tnPredef.decodedName.toString == "Predef") && - (tnArrAss.decodedName.toString == "any2ArrowAssoc") && (tnMg.decodedName.toString == "->") => (a, b) - - // matches AST for (a, b) - case Apply( - TypeApply( - Select(Select(Ident(tnScala), tnTuple), tnApply), - List(TypeTree(), TypeTree()) - ), List(a, b) - ) if (tnScala.decodedName.toString == "scala") && (tnTuple.decodedName.toString == "Tuple2") && - (tnApply.decodedName.toString == "apply") => (a, b) - - case _ => c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${showCode(c)(e.tree)}") - } - - (aTree match { - case Literal(Constant(x: String)) => x - case _ => c.abort(c.enclosingPosition, s"Expected string literal as first entry of tuple, got ${showCode(c)(aTree)} instead") - }, c.Expr[T](bTree)) - } - - def generateSingular[T: c.WeakTypeTag] - (c: Context) - (ctx: Option[String], str: String, args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - MessageExtractor.singular(c)(ctx, str) - - import c.universe._ - - val strLit = c.literal(str) - val tr = ctx match { - case Some(s) => - val ctxLit = c.literal(s) - reify { lang.splice.singular(ctxLit.splice, strLit.splice) } - case None => reify { lang.splice.singular(strLit.splice) } - } - - generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] - } - - def generatePlural[T: c.WeakTypeTag] - (c: Context) - (ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - MessageExtractor.plural(c)(ctx, str, strPlural) - - import c.universe._ - - val strLit = c.literal(str) - val strPluralLit = c.literal(strPlural) - - val tr = ctx match { - case Some(s) => - val ctxLit = c.literal(s) - reify { lang.splice.plural(ctxLit.splice, strLit.splice, strPluralLit.splice, n.splice) } - - case None => reify { lang.splice.plural(strLit.splice, strPluralLit.splice, n.splice) } - } - - generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] - } - - private def generateInterpolation[T: c.WeakTypeTag] - (c: Context) - (str: c.Expr[String], args: Map[String, c.Tree], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - import c.universe._ - - if (args.nonEmpty) { - val argList = flipSeq(c)(args.map { - case (name, value) => - val nlit = c.literal(name) - val vlit = c.Expr[Any](value) - - reify { nlit.splice -> vlit.splice } - }.toSeq) - - reify { - StringUtils.interpolate[T]( - str.splice, - argList.splice:_* - )(outputFormat.splice) - } - } else { - reify { - outputFormat.splice.convert(str.splice) - } - } - } - - private def flipSeq[T](c: Context)(t: Seq[c.Expr[T]]): c.Expr[Seq[T]] = { - import c.universe._ - - c.Expr[Seq[T]](Apply( - Select(reify(Seq).tree, newTermName("apply")), - t.map(_.tree).toList - )) - } + def prettyPrint(c: Context)(e: c.Tree): String = c.universe.show(e) } diff --git a/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala b/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala index d38b012..da113d0 100644 --- a/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala +++ b/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala @@ -22,75 +22,5 @@ import scala.reflect.macros.whitebox object Compat { type Context = whitebox.Context - - // Everything becomes better with quasiquotes. - - def showCode(c: Context)(e: c.Tree): String = c.universe.showCode(e) - - def tupleLiteral[T](c: Context)(e: c.Expr[(String, T)]): (String, c.Expr[T]) = { - import c.universe._ - - val (aTree, bTree) = e.tree match { - case q"scala.this.Predef.ArrowAssoc[$_]($a).->[$_]($b)" => (a, b) - case q"($a, $b)" => (a, b) - case _ => - c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${c.universe.showCode(e.tree)}") - } - - val keyLiteral = aTree match { - case Literal(Constant(x: String)) => x - case _ => c.abort(c.enclosingPosition, s"Expected string literal as first entry of tuple, got ${c.universe.showCode(e.tree)} instead") - } - - (keyLiteral, c.Expr[T](bTree)) - } - - def generateSingular[T: c.WeakTypeTag] - (c: Context) - (ctx: Option[String], str: String, args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - MessageExtractor.singular(c)(ctx, str) - - import c.universe._ - - val tr = c.Expr[String](ctx match { - case Some(s) => q"${lang.tree}.singular($s, $str)" - case None => q"${lang.tree}.singular($str)" - }) - - generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] - } - - def generatePlural[T: c.WeakTypeTag] - (c: Context) - (ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - MessageExtractor.plural(c)(ctx, str, strPlural) - - import c.universe._ - - val tr = c.Expr[String](ctx match { - case Some(s) => q"${lang.tree}.plural($s, $str, $strPlural, ${n.tree})" - case None => q"${lang.tree}.plural($str, $strPlural, ${n.tree})" - }) - - generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] - } - - private def generateInterpolation[T: c.WeakTypeTag] - (c: Context) - (str: c.Expr[String], args: Map[String, c.Tree], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = - { - - import c.universe._ - - c.Expr[T](if (args.nonEmpty) { - val argList = args.map { case (k, v) => q"$k -> $v" } - q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}](${str.tree}, ..$argList)(${outputFormat.tree})" - } else q"${outputFormat.tree}.convert(${str.tree})") - } + def prettyPrint(c: Context)(e: c.Tree): String = c.universe.showCode(e) } diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala new file mode 100644 index 0000000..6a8d97a --- /dev/null +++ b/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala @@ -0,0 +1,112 @@ +/****************************************************************************** + * Copyright © 2016 Maxim Karpov * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ******************************************************************************/ + +package ru.makkarpov.scalingua + +import Compat._ +import ru.makkarpov.scalingua.extract.MessageExtractor + +protected object MacroUtils { + def stringLiteral[T](c: Context)(e: c.Tree): String = { + import c.universe._ + + e match { + case Literal(Constant(s: String)) => s + + case q"scala.this.Predef.augmentString($str).stripMargin" => + str match { + case Literal(Constant(s: String)) => s.stripMargin.trim + case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") + } + + case q"scala.this.Predef.augmentString($str).stripMargin($ch)" => + (str, ch) match { + case (Literal(Constant(s: String)), Literal(Constant(c: Char))) => s.stripMargin(c).trim + case (Literal(Constant(s: String)), _) => + c.abort(c.enclosingPosition, s"Expected character literal, got instead ${prettyPrint(c)(ch)}") + case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") + } + + case _ => + c.abort(c.enclosingPosition, s"Expected string literal or multi-line string, got instead ${prettyPrint(c)(e)}") + } + } + + def tupleLiteral[T](c: Context)(e: c.Expr[(String, T)]): (String, c.Expr[T]) = { + import c.universe._ + + val (aTree, bTree) = e.tree match { + case q"scala.this.Predef.ArrowAssoc[$aType]($a).->[$bType]($b)" => (a, b) // 2.11 + case q"scala.this.Predef.any2ArrowAssoc[$aType]($a).->[$bType]($b)" => (a, b) // 2.10 + case q"($a, $b)" => (a, b) + case _ => + c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${prettyPrint(c)(e.tree)}") + } + + val keyLiteral = aTree match { + case Literal(Constant(x: String)) => x + case _ => c.abort(c.enclosingPosition, s"Expected string literal as first entry of tuple, got ${prettyPrint(c)(e.tree)} instead") + } + + (keyLiteral, c.Expr[T](bTree)) + } + + def generateSingular[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { + MessageExtractor.singular(c)(ctx, str) + + import c.universe._ + + val tr = c.Expr[String](ctx match { + case Some(s) => q"${lang.tree}.singular($s, $str)" + case None => q"${lang.tree}.singular($str)" + }) + + generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] + } + + def generatePlural[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { + MessageExtractor.plural(c)(ctx, str, strPlural) + + import c.universe._ + + val tr = c.Expr[String](ctx match { + case Some(s) => q"${lang.tree}.plural($s, $str, $strPlural, ${n.tree})" + case None => q"${lang.tree}.plural($str, $strPlural, ${n.tree})" + }) + + generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] + } + + private def generateInterpolation[T: c.WeakTypeTag] + (c: Context) + (str: c.Expr[String], args: Map[String, c.Tree], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { + import c.universe._ + + c.Expr[T](if (args.nonEmpty) { + val argList = args.map { case (k, v) => q"$k -> $v" } + q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}](${str.tree}, ..$argList)(${outputFormat.tree})" + } else q"${outputFormat.tree}.convert(${str.tree})") + } +} diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala index 995d2e8..98520e0 100644 --- a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala +++ b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala @@ -17,6 +17,7 @@ package ru.makkarpov.scalingua import Compat._ +import MacroUtils._ /** * An entry point for all macros which may be useful if you want to define custom functions (like your own `Utils.t` @@ -40,10 +41,7 @@ object Macros { val parts = c.prefix.tree match { case Apply(_, List(Apply(_, rawParts))) => - rawParts map { - case Literal(Constant(s: String)) => StringContext.treatEscapes(s) - } - + rawParts.map(stringLiteral(c)(_)).map(StringContext.treatEscapes) case _ => c.abort(c.enclosingPosition, "Failed to detect application context") } @@ -70,7 +68,7 @@ object Macros { (filtered, name, args(idx)) } else { if (argName.isEmpty) - c.abort(c.enclosingPosition, s"No name is defined for part #$idx (${Compat.showCode(c)(args(idx).tree)})") + c.abort(c.enclosingPosition, s"No name is defined for part #$idx (${Compat.prettyPrint(c)(args(idx).tree)})") (part, argName.get, args(idx)) } @@ -85,7 +83,7 @@ object Macros { val tr = filtered.groupBy(_._2).mapValues(_.head._3.tree) - Compat.generateSingular[T](c)(None, msgid, tr)(lang, outputFormat).asInstanceOf[c.Expr[T]] + generateSingular[T](c)(None, msgid, tr)(lang, outputFormat).asInstanceOf[c.Expr[T]] } /** @@ -100,7 +98,7 @@ object Macros { val (msgid, vars) = verifyVariables(c)(msg, args, None) - Compat.generateSingular[T](c)(None, msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] + generateSingular[T](c)(None, msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } /** @@ -113,10 +111,10 @@ object Macros { (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { - val ctxStr = stringLiteral(c)(ctx) + val ctxStr = stringLiteral(c)(ctx.tree) val (msgid, vars) = verifyVariables(c)(msg, args, None) - Compat.generateSingular[T](c)(Some(ctxStr), msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] + generateSingular[T](c)(Some(ctxStr), msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } /** @@ -132,7 +130,7 @@ object Macros { val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) val (msgidPlural, _) = verifyVariables(c)(msgPlural, args, Some(n)) - Compat.generatePlural[T](c)(None, msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] + generatePlural[T](c)(None, msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } /** @@ -145,11 +143,11 @@ object Macros { (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { - val ctxStr = stringLiteral(c)(ctx) + val ctxStr = stringLiteral(c)(ctx.tree) val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) val (msgidPlural, _) = verifyVariables(c)(msgPlural, args, Some(n)) - Compat.generatePlural[T](c)(Some(ctxStr), msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] + generatePlural[T](c)(Some(ctxStr), msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -158,7 +156,7 @@ object Macros { (c: Context) (msg: c.Expr[String], args: Seq[c.Expr[(String, Any)]], n: Option[c.Expr[Long]]): (String, Map[String, c.Tree]) = { - val msgStr = stringLiteral(c)(msg) + val msgStr = stringLiteral(c)(msg.tree) val vars = StringUtils.extractVariables(msgStr) var exprs = args.map(tupleLiteral(c)(_)) @@ -182,13 +180,4 @@ object Macros { (msgStr, Map(exprs:_*).mapValues(_.tree).asInstanceOf[Map[String, c.Tree]]) } - - private def stringLiteral(c: Context)(s: c.Expr[String]): String = { - import c.universe._ - - s.tree match { - case Literal(Constant(s: String)) => s - case _ => c.abort(c.enclosingPosition, s"Expected string literal, got ${Compat.showCode(c)(s.tree)}") - } - } } diff --git a/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala index f35d3da..a814da0 100644 --- a/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala +++ b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala @@ -114,4 +114,32 @@ class MacroTest extends FlatSpec with Matchers { """ val s = "1"; pc(s, "%(n)", "%(n)", 1) """ shouldNot compile """ val s = "%(n)"; pc("1", s, "%(n)", 1) """ shouldNot compile } + + it should "handle multiline strings" in { + t"""1 +2 +3""" shouldBe "{s:1\n2\n3}" + + t"""1 + |2""" shouldBe "{s:1\n |2}" + + t("""1 +2 +3""") shouldBe "{s:1\n2\n3}" + + + t("""1 + |2 + |3""".stripMargin) shouldBe "{s:1\n2\n3}" + + + t("""1 + #2 + #3 + """.stripMargin('#')) shouldBe "{s:1\n2\n3}" + + t("""1 + |2 + """.stripMargin('#')) shouldBe "{s:1\n |2}" + } }