ru.makkarpov.scalingua.Macros.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scalingua_sjs1_2.12 Show documentation
Show all versions of scalingua_sjs1_2.12 Show documentation
A simple gettext-like internationalization library for Scala
The newest version!
/******************************************************************************
* 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
import ru.makkarpov.scalingua.plural.Suffix
import ru.makkarpov.scalingua.InsertableIterator._
object Macros {
// All macros variants: (lazy, eager) x (singular, plural) x (interpolation, ctx, non ctx, tagged), 16 total
// Interpolators:
def interpolate[T: c.WeakTypeTag](c: Context)
(args: c.Expr[Any]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
{
import c.universe._
val (msg, argsT) = interpolator(c)(args.map(_.tree))
c.Expr[T](generate[T](c)(None, q"$msg", None, argsT)(Some(lang.tree), outputFormat.tree))
}
def lazyInterpolate[T: c.WeakTypeTag](c: Context)
(args: c.Expr[Any]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
{
import c.universe._
val (msg, argsT) = interpolator(c)(args.map(_.tree))
c.Expr[LValue[T]](generate[T](c)(None, q"$msg", None, argsT)(None, outputFormat.tree))
}
def pluralInterpolate[T: c.WeakTypeTag](c: Context)
(args: c.Expr[Any]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
{
import c.universe._
val (msg, msgP, argsT, nVar) = pluralInterpolator(c)(args.map(_.tree))
c.Expr[T](generate[T](c)(None, q"$msg", Some((q"$msgP", nVar, false)), argsT)(Some(lang.tree), outputFormat.tree))
}
def lazyPluralInterpolate[T: c.WeakTypeTag](c: Context)
(args: c.Expr[Any]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
{
import c.universe._
val (msg, msgP, argsT, nVar) = pluralInterpolator(c)(args.map(_.tree))
c.Expr[LValue[T]](generate[T](c)(None, q"$msg", Some((q"$msgP", nVar, false)), argsT)(None, outputFormat.tree))
}
// Singular:
def singular[T: c.WeakTypeTag](c: Context)
(msg: c.Expr[String], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](generate[T](c)(None, msg.tree, None, args.map(_.tree))(Some(lang.tree), outputFormat.tree))
def lazySingular[T: c.WeakTypeTag](c: Context)
(msg: c.Expr[String], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](generate[T](c)(None, msg.tree, None, args.map(_.tree))(None, outputFormat.tree))
def singularCtx[T: c.WeakTypeTag](c: Context)
(ctx: c.Expr[String], msg: c.Expr[String], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](generate[T](c)(Some(ctx.tree), msg.tree, None, args.map(_.tree))(Some(lang.tree), outputFormat.tree))
def lazySingularCtx[T: c.WeakTypeTag](c: Context)
(ctx: c.Expr[String], msg: c.Expr[String], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](generate[T](c)(Some(ctx.tree), msg.tree, None, args.map(_.tree))(None, outputFormat.tree))
// Plural:
def plural[T: c.WeakTypeTag](c: Context)
(msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](generate[T](c)(None, msg.tree, Some((msgPlural.tree, n.tree, true)), args.map(_.tree))
(Some(lang.tree), outputFormat.tree))
def lazyPlural[T: c.WeakTypeTag](c: Context)
(msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](generate[T](c)(None, msg.tree, Some((msgPlural.tree, n.tree, true)), args.map(_.tree))
(None, outputFormat.tree))
def pluralCtx[T: c.WeakTypeTag](c: Context)
(ctx: c.Expr[String], msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](generate[T](c)(Some(ctx.tree), msg.tree, Some((msgPlural.tree, n.tree, true)), args.map(_.tree))
(Some(lang.tree), outputFormat.tree))
def lazyPluralCtx[T: c.WeakTypeTag](c: Context)
(ctx: c.Expr[String], msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](generate[T](c)(Some(ctx.tree), msg.tree, Some((msgPlural.tree, n.tree, true)), args.map(_.tree))
(None, outputFormat.tree))
// Tagged: just forwards all calls to supplied Language.
// These should be implemented as macros since otherwise they would leak a reference to I18n in compiled code.
def singularTag[T: c.WeakTypeTag](c: Context)
(tag: c.Expr[String], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](tagGenerate(c)(tag.tree, None, args.map(_.tree))(Some(lang.tree), outputFormat.tree))
def lazySingularTag[T: c.WeakTypeTag](c: Context)
(tag: c.Expr[String], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](tagGenerate(c)(tag.tree, None, args.map(_.tree))(None, outputFormat.tree))
def pluralTag[T: c.WeakTypeTag](c: Context)
(tag: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] =
c.Expr[T](tagGenerate(c)(tag.tree, Some(n.tree), args.map(_.tree))(Some(lang.tree), outputFormat.tree))
def lazyPluralTag[T: c.WeakTypeTag](c: Context)
(tag: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*)
(outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] =
c.Expr[LValue[T]](tagGenerate(c)(tag.tree, Some(n.tree), args.map(_.tree))(None, outputFormat.tree))
// Macro internals:
/**
* A generic macro that extracts interpolation string and set of interpolation
* variables from string interpolator invocation.
*
* @param c Macro context
* @param args Arguments of interpolator
* @return (Extracted string, extracted variables)
*/
private def interpolator(c: Context)(args: Seq[c.Tree]): (String, Seq[c.Tree]) = {
import c.universe._
// Extract raw interpolation parts
val parts = c.prefix.tree match {
case Apply(_, List(Apply(_, rawParts))) =>
rawParts.map(stringLiteral(c)(_)).map(processEscapes)
case _ =>
c.abort(c.enclosingPosition, s"failed to match prefix, got ${prettyPrint(c)(c.prefix.tree)}")
}
interpolationString(c)(parts, args)
}
/**
* A macro function that extracts singular and plural strings, arguments and `n` variable from plural interpolation.
*
* E.g.: `I have $n fox${S.ex}` ->
* * Singular string: "I have %(n) fox"
* * Plural string: "I have %(n) foxes"
* * Arguments: <| "n" -> n |>
* * N variable: <| n |>
*
* @param c Macro context
* @param args Interpolation arguments
* @return
*/
private def pluralInterpolator(c: Context)(args: Seq[c.Tree]): (String, String, Seq[c.Tree], c.Tree) = {
import c.universe._
val parts = c.prefix.tree match {
case Apply(_, List(Apply(_, rawParts))) =>
rawParts.map(stringLiteral(c)(_)).map(processEscapes)
case _ =>
c.abort(c.enclosingPosition, s"failed to match prefix, got ${prettyPrint(c)(c.prefix.tree)}")
}
assert(parts.size == args.size + 1)
def nVarHint(expr: c.Tree): Option[c.Tree] = expr match {
case q"$prefix.int2MacroExtension($arg).nVar" => Some(arg)
case q"$prefix.long2MacroExtension($arg).nVar" => Some(arg)
case _ => None
}
// Find a variable that represents plural number and strip `.nVar`s, if any.
val (filteredArgs, nVar) = {
val intVars = args.indices.filter { i =>
val tpe = typecheck(c)(args(i)).tpe
(tpe <:< typeOf[Int]) || (tpe <:< typeOf[Long])
}
val nVars = args.indices.filter(i => nVarHint(args(i)).isDefined)
val chosenN = (intVars, nVars) match {
case (_, Seq(i)) => i
case (_, Seq(_, _*)) => c.abort(c.enclosingPosition, "multiple `.nVar` annotations present")
case (Seq(i), _) => i
case (Seq(), Seq()) => c.abort(c.enclosingPosition, "no integer variable is present - provide at least one as plural number")
case _ => c.abort(c.enclosingPosition, "multiple integer variables present. Annotate one that represents a plural number with `x.nVar`")
}
val fArgs = args.map(x => nVarHint(x).getOrElse(x))
(fArgs, fArgs(chosenN))
}
// Merge parts separated by plural suffix - e.g. "fox"$es"" becomes "fox" and "foxes".
val (partsSingular, partsPlural, finalArgs) = {
val itS = parts.iterator.insertable
val itP = parts.iterator.insertable
val retS = Seq.newBuilder[String]
val retP = Seq.newBuilder[String]
val retA = Seq.newBuilder[c.Tree]
for {
arg <- args
tpe = typecheck(c)(arg).tpe
} if (tpe <:< weakTypeOf[Suffix.Generic]) {
arg match {
case q"$prefix.string2SuffixExtension($sing).&>($plur)" =>
itS.unnext(itS.next() + stringLiteral(c)(sing) + itS.next())
itP.unnext(itP.next() + stringLiteral(c)(plur) + itP.next())
case _ =>
c.abort(c.enclosingPosition, s"expression of type `Suffix.Generic` should have `a &> b` form, got instead `${prettyPrint(c)(arg)}`")
}
} else if (tpe <:< weakTypeOf[Suffix]) {
val rawSuffix =
if (tpe <:< weakTypeOf[Suffix.S]) "s"
else if (tpe <:< weakTypeOf[Suffix.ES]) "es"
else c.abort(c.enclosingPosition, s"unknown suffix type: $tpe")
val suffix =
if (itS.head.nonEmpty && Character.isUpperCase(itS.head.last)) rawSuffix.toUpperCase
else rawSuffix
itS.unnext(itS.next() + itS.next())
itP.unnext(itP.next() + suffix + itP.next())
} else {
retS += itS.next()
retP += itP.next()
retA += arg
}
// One part should remain:
retS += itS.next()
retP += itP.next()
(retS.result(), retP.result(), retA.result())
}
// Build interpolation strings by parts
val (sStr, tArgs) = interpolationString(c)(partsSingular, finalArgs)
// These string are guaranteed to have the same structure, so we can ignore second args:
val (pStr, _) = interpolationString(c)(partsPlural, finalArgs)
(sStr, pStr, tArgs, nVar)
}
/**
* A generic function to generate interpolation results. Other macros do nothing but call it.
*
* @param c Macro context
* @param ctxTree Optional tree with context argument
* @param msgTree Message argument
* @param pluralTree Optional with (plural message, n, insert "n" arg) arguments
* @param argsTree Supplied args as a trees
* @param lang Language tree that if present means instant evaluation
* @param outputFormat Tree representing `OutputFormat[T]` instance
* @return Tree representing an instance of `T` if language was present, or `LValue[T]` if
* language was absent.
*/
private def generate[T: c.WeakTypeTag](c: Context)
(ctxTree: Option[c.Tree], msgTree: c.Tree, pluralTree: Option[(c.Tree, c.Tree, Boolean)], argsTree: Seq[c.Tree])
(lang: Option[c.Tree], outputFormat: c.Tree): c.Tree =
{
import c.universe._
val session = MessageExtractor.setupSession(c)
// Extract literals:
val rawCtx = ctxTree.map(stringLiteral(c))
val ctx = session.setts.mergeContext(rawCtx)
val args = argsTree.map(tupleLiteral(c)(_)) ++ (pluralTree match {
case Some((_, n, true)) => Seq("n" -> n)
case _ => Nil
})
// Strip off introduced escapes if string does not contain interpolation
def unescape(s: String): String =
if (args.isEmpty) StringUtils.interpolate(s) else s
val msg = unescape(stringLiteral(c)(msgTree))
val plural = pluralTree.map { case (s, n, i) => (unescape(stringLiteral(c)(s)), n, i) }
// Call message extractor:
plural match {
case None => session.singular(c)(rawCtx, msg)
case Some((pl, _, _)) => session.plural(c)(rawCtx, msg, pl)
}
// Verify variables consistency:
def verifyVariables(s: String): Unit = {
if (args.isEmpty)
return // no interpolation, nothing to verify
val varsArg = args.map(_._1).toSet
val varsStr = StringUtils.extractVariables(s).toSet
for (v <- (varsArg diff varsStr) ++ (varsStr diff varsArg))
if (varsArg.contains(v))
c.abort(c.enclosingPosition, s"variable `$v` is not present in interpolation string")
else
c.abort(c.enclosingPosition, s"variable `$v` is not present at arguments section")
}
for ((v, xs) <- args.groupBy(_._1) if xs.length > 1)
c.abort(c.enclosingPosition, s"duplicate variable `$v`")
verifyVariables(msg)
for ((pl, _, _) <- plural)
verifyVariables(pl)
/**
* Given a language tree `lng`, creates a tree that will translate given message.
*/
def translate(lng: c.Tree): c.Tree = {
val str = plural match {
case None => q"$lng.singular(..${ctx.toSeq}, $msg)"
case Some((pl, n, _)) => q"$lng.plural(..${ctx.toSeq}, $msg, $pl, $n)"
}
if (args.isEmpty)
q"$outputFormat.convert($str)"
else {
val argsT = processArgs(c)(args, lng)
q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}]($str, ..$argsT)"
}
}
lang match {
case Some(lng) => translate(lng)
case None =>
val name = termName(c)("lng")
q"""
new _root_.ru.makkarpov.scalingua.LValue(
($name: _root_.ru.makkarpov.scalingua.Language) => ${translate(q"$name")}
)
"""
}
}
private def tagGenerate[T: c.WeakTypeTag](c: Context)
(tagTree: c.Tree, pluralTree: Option[c.Tree], argsTree: Seq[c.Tree])
(lang: Option[c.Tree], outputFormat: c.Tree): c.Tree =
{
import c.universe._
def translate(lng: c.Tree): c.Tree = {
val str = pluralTree match {
case None => q"$lng.taggedSingular($tagTree)"
case Some(n) => q"$lng.taggedPlural($tagTree, $n)"
}
if (argsTree.isEmpty) q"$outputFormat.convert($str)"
else q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}]($str, ..$argsTree)"
}
lang match {
case Some(lng) => translate(lng)
case None =>
val name = termName(c)("lng")
q"""
new _root_.ru.makkarpov.scalingua.LValue(
($name: _root_.ru.makkarpov.scalingua.Language) => ${translate(q"$name")}
)
"""
}
}
/**
* Convert name/value pairs to a sequence of tuples and expands specific arguments.
* @param c
* @param args
* @return
*/
private def processArgs(c: Context)(args: Seq[(String, c.Tree)], lang: c.Tree): Seq[c.Tree] = args.map {
case (k, v) =>
import c.universe._
val tpe = typecheck(c)(v).tpe
val xv =
if (tpe <:< weakTypeOf[LValue[_]]) q"$v($lang)"
else v
q"$k -> $xv"
}
/**
* Given the parts of interpolation string and trees of interpolation arguments, this function tries to
* guess final string with variable names like "Hello, %(name)!"
*
* @param c Macro context
* @param parts Interpolation string parts
* @param args Interpolation variables
* @return Final string and trees of arguments to `StringUtils.interpolate` (in format of `a -> b`)
*/
private def interpolationString(c: Context)(parts: Seq[String], args: Seq[c.Tree]): (String, Seq[c.Tree]) = {
import c.universe._
assert(parts.size == args.size + 1)
val inferredNames = args.map {
case Ident(name: TermName) => Some(name.decodedName.toString)
case Select(This(_), name: TermName) => Some(name.decodedName.toString)
case _ => None
}
// Match the %(x) explicit variable name specifications in parts and get final variable names
val filtered: Seq[(String /* part */, String /* arg name */, c.Tree /* value */)] =
for {
idx <- args.indices
argName = inferredNames(idx)
part = parts(idx + 1)
} yield {
if (part.startsWith(StringUtils.VariableStartStr)) {
val pos = part.indexOf(StringUtils.VariableParentheses._2)
val name = part.substring(2, pos)
val filtered = part.substring(pos + 1)
(filtered, name, args(idx))
} else if (part.startsWith(StringUtils.VariableEscapeStr)) {
(StringUtils.VariableStr + part.substring(2), argName.get, args(idx))
} else if (part.startsWith(StringUtils.VariableStr)) {
c.abort(c.enclosingPosition, s"Stray '${StringUtils.VariableStr}' at the beginning of part: it should start "+
s"either with '${StringUtils.VariableEscapeStr}' or '${StringUtils.VariableStartStr}'")
} else {
if (argName.isEmpty)
c.abort(c.enclosingPosition, s"No name is defined for part #$idx (${Compat.prettyPrint(c)(args(idx))})")
(part, argName.get, args(idx))
}
}
(StringUtils.escapeInterpolation(parts.head) + filtered.map {
case (part, name, _) => s"%($name)${StringUtils.escapeInterpolation(part)}"
}.mkString, filtered.map {
case (_, name, value) => q"($name, $value)"
})
}
/**
* Matches string against string literal pattern and return literal string if matched. Currently supported
* literal types:
*
* 1) Plain literals like `"123"`
* 2) Strip margin literals like `""" ... """.stripMargin`
* 3) Strip margin literals with custom margin character like `""" ... """.stripMargin('#')`
*
* @param c Macro context
* @param e Tree to match
* @return Extracted string literal
*/
private def stringLiteral(c: Context)(e: c.Tree): String = {
import c.universe._
def stripMargin(str: Tree, ch: Tree): String = (str, ch) match {
case (Literal(Constant(s: String)), Literal(Constant(c: Char))) => s.stripMargin(c).trim
case (Literal(Constant(s: String)), EmptyTree) => s.stripMargin.trim
case (Literal(Constant(_: 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)}")
}
e match {
case Literal(Constant(s: String)) => s
case q"scala.this.Predef.augmentString($str).stripMargin" => stripMargin(str, EmptyTree) // 2.11
case q"scala.Predef.augmentString($str).stripMargin" => stripMargin(str, EmptyTree) // 2.12
case q"scala.this.Predef.augmentString($str).stripMargin($ch)" => stripMargin(str, ch) // 2.11
case q"scala.Predef.augmentString($str).stripMargin($ch)" => stripMargin(str, ch) // 2.12
case _ =>
c.abort(c.enclosingPosition, s"Expected string literal or multi-line string, got instead ${prettyPrint(c)(e)}")
}
}
/**
* Matches string against tuple `(String, T)` pattern and returns extracted string literal and tuple value.
* Currently supported literal types:
*
* 1) Plain literals like `("1", x)`
* 2) ArrowAssoc literals like `"1" -> x`
*
* @param c Macro context
* @param e Tree to match
* @return Extracted tuple literal parts
*/
private def tupleLiteral(c: Context)(e: c.Tree): (String, c.Tree) = {
import c.universe._
val (a, b) = e match {
case q"scala.Predef.ArrowAssoc[$aType]($ax).->[$bType]($bx)" => (ax, bx) // 2.12
case q"scala.this.Predef.ArrowAssoc[$aType]($ax).->[$bType]($bx)" => (ax, bx) // 2.11
case q"scala.this.Predef.any2ArrowAssoc[$aType]($ax).->[$bType]($bx)" => (ax, bx) // 2.10
case q"($ax, $bx)" => (ax, bx)
case _ =>
c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${prettyPrint(c)(e)}")
}
(stringLiteral(c)(a), b)
}
}