summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSom Snytt <som.snytt@gmail.com>2014-01-28 01:37:11 -0800
committerSom Snytt <som.snytt@gmail.com>2014-01-28 09:42:32 -0800
commit8053682d4ff0dcff3c1846a1bac9c718c92cc704 (patch)
treeafb0b22c79c389997ad37585b2c01603a5f4284b
parentf22ddce265e8622e95f5e9cab4d38168bf2c3bf8 (diff)
downloadscala-8053682d4ff0dcff3c1846a1bac9c718c92cc704.tar.gz
scala-8053682d4ff0dcff3c1846a1bac9c718c92cc704.tar.bz2
scala-8053682d4ff0dcff3c1846a1bac9c718c92cc704.zip
SI-8092 More verify for f-interpolator
Attempt to verify the nooks and crannies of the format string. Allows all syntax in the javadoc, including arg indexes. If the specifier after an arg has an index that doesn't refer to the arg, a warning is issued and the missing `%s` is prepended (just as for a part with a leading `%n`). Other enhancements include detecting that a `Formattable` wasn't supplied to `%#s`. Error messages attempt to be pithy but descriptive.
-rw-r--r--src/compiler/scala/tools/reflect/MacroImplementations.scala399
-rw-r--r--test/files/neg/stringinterpolation_macro-neg.check131
-rw-r--r--test/files/neg/stringinterpolation_macro-neg.scala42
-rw-r--r--test/files/neg/t7325.check8
-rw-r--r--test/files/run/stringinterpolation_macro-run.check5
-rw-r--r--test/files/run/stringinterpolation_macro-run.scala15
6 files changed, 443 insertions, 157 deletions
diff --git a/src/compiler/scala/tools/reflect/MacroImplementations.scala b/src/compiler/scala/tools/reflect/MacroImplementations.scala
index a9ed419b1e..b9b35f3f0f 100644
--- a/src/compiler/scala/tools/reflect/MacroImplementations.scala
+++ b/src/compiler/scala/tools/reflect/MacroImplementations.scala
@@ -1,171 +1,296 @@
package scala.tools.reflect
import scala.reflect.macros.contexts.Context
-import scala.collection.mutable.ListBuffer
-import scala.collection.mutable.Stack
+import scala.collection.mutable.{ ListBuffer, Stack }
import scala.reflect.internal.util.Position
+import scala.PartialFunction.cond
+import scala.util.matching.Regex.Match
+
+import java.util.{ Formatter, Formattable, IllegalFormatException }
abstract class MacroImplementations {
val c: Context
- import c.universe._
+ import c.universe.{ Match => _, _ }
import definitions._
- def macro_StringInterpolation_f(parts: List[Tree], args: List[Tree], origApplyPos: c.universe.Position): Tree = {
- // the parts all have the same position information (as the expression is generated by the compiler)
- // the args have correct position information
-
- // the following conditions can only be violated if invoked directly
- if (parts.length != args.length + 1) {
- if(parts.length == 0)
- c.abort(c.prefix.tree.pos, "too few parts")
- else if(args.length + 1 < parts.length)
- c.abort(if(args.length==0) c.enclosingPosition else args.last.pos,
- "too few arguments for interpolated string")
- else
- c.abort(args(parts.length-1).pos,
- "too many arguments for interpolated string")
- }
+ @inline private def truly(body: => Unit): Boolean = { body ; true }
+ @inline private def falsely(body: => Unit): Boolean = { body ; false }
- val pi = parts.iterator
- val bldr = new java.lang.StringBuilder
- val evals = ListBuffer[ValDef]()
- val ids = ListBuffer[Ident]()
- val argStack = Stack(args : _*)
+ private def fail(msg: String) = c.abort(c.enclosingPosition, msg)
- def defval(value: Tree, tpe: Type): Unit = {
- val freshName = newTermName(c.freshName("arg$"))
- evals += ValDef(Modifiers(), freshName, TypeTree(tpe) setPos value.pos.focus, value) setPos value.pos
- ids += Ident(freshName)
- }
+ /** Every part except the first must begin with a conversion for
+ * the arg that preceded it. If the conversion is missing, "%s"
+ * is inserted.
+ *
+ * In any other position, the only permissible conversions are
+ * the literals (%% and %n) or an index reference (%1$ or %<).
+ *
+ * A conversion specifier has the form:
+ *
+ * [index$][flags][width][.precision]conversion
+ */
+ def macro_StringInterpolation_f(parts: List[Tree], args: List[Tree], origApplyPos: c.universe.Position): Tree = {
- def isFlag(ch: Char): Boolean = {
- ch match {
- case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true
- case _ => false
- }
- }
+ val fpat = """%(?:(\d+)\$)?([-#+ 0,(\<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""".r
+ object SpecifierGroups extends Enumeration { val Spec, Index, Flags, Width, Precision, CC = Value }
- def checkType(arg: Tree, variants: Type*): Option[Type] = {
- variants.find(arg.tpe <:< _).orElse(
- variants.find(c.inferImplicitView(arg, arg.tpe, _) != EmptyTree).orElse(
- Some(variants(0))
- )
+ // None if subtypes none
+ def checkType0(arg: Tree, variants: Type*): Option[Type] =
+ variants find (arg.tpe <:< _)
+ // None if conforming to none
+ def checkType1(arg: Tree, variants: Type*): Option[Type] =
+ checkType0(arg, variants: _*) orElse (
+ variants find (c.inferImplicitView(arg, arg.tpe, _) != EmptyTree)
)
- }
+ // require variants.nonEmpty
+ def checkType(arg: Tree, variants: Type*): Option[Type] =
+ checkType1(arg, variants: _*) orElse Some(variants(0))
val stdContextTags = new { val tc: c.type = c } with StdContextTags
import stdContextTags._
+ val tagOfFormattable = typeTag[Formattable]
+
+ abstract class Conversion(m: Match, pos: Position) {
+ import SpecifierGroups.{ Value => SpecGroup, _ }
+ private def maybeStr(g: SpecGroup) = Option(m group g.id)
+ private def maybeInt(g: SpecGroup) = maybeStr(g) map (_.toInt)
+ val index: Option[Int] = maybeInt(Index)
+ val flags: Option[String] = maybeStr(Flags)
+ val width: Option[Int] = maybeInt(Width)
+ val precision: Option[Int] = maybeStr(Precision) map (_.drop(1).toInt)
+ val op: String = maybeStr(CC) getOrElse ""
+
+ def cc: Char = if ("tT" contains op(0)) op(1) else op(0)
+
+ def indexed: Boolean = index.nonEmpty || hasFlag('<')
+ def isLiteral: Boolean = false
+ def verify: Boolean = goodFlags && goodIndex
+ def accepts(arg: Tree): Option[Type]
- def conversionType(ch: Char, arg: Tree): Option[Type] = {
- ch match {
- case 'b' | 'B' =>
- if(arg.tpe <:< NullTpe) Some(NullTpe) else Some(BooleanTpe)
- case 'h' | 'H' =>
- Some(AnyTpe)
- case 's' | 'S' =>
- Some(AnyTpe)
- case 'c' | 'C' =>
- checkType(arg, CharTpe, ByteTpe, ShortTpe, IntTpe)
- case 'd' | 'o' | 'x' | 'X' =>
- checkType(arg, IntTpe, LongTpe, ByteTpe, ShortTpe, tagOfBigInt.tpe)
- case 'e' | 'E' | 'g' | 'G' | 'f' | 'a' | 'A' =>
- checkType(arg, DoubleTpe, FloatTpe, tagOfBigDecimal.tpe)
- case 't' | 'T' =>
- checkType(arg, LongTpe, tagOfCalendar.tpe, tagOfDate.tpe)
- case _ => None
+ val allFlags = "-#+ 0,(<"
+ def hasFlag(f: Char) = (flags getOrElse "") contains f
+ def hasAnyFlag(fs: String) = fs exists (hasFlag)
+
+ def badFlag(f: Char, msg: String) = {
+ val i = flags map (_.indexOf(f)) filter (_ >= 0) getOrElse 0
+ errorAtOffset(Flags, i, msg)
}
- }
+ def groupPos(g: SpecGroup) = groupPosAt(g, 0)
+ def groupPosAt(g: SpecGroup, i: Int) = pos withPoint (pos.point + m.start(g.id) + i)
+ def errorAt(g: SpecGroup, msg: String) = c.error(groupPos(g), msg)
+ def errorAtOffset(g: SpecGroup, i: Int, msg: String) = c.error(groupPosAt(g, i), msg)
- def copyString(first: Boolean): Unit = {
- val strTree = pi.next()
- val rawStr = strTree match {
- case Literal(Constant(str: String)) => str
- case _ => throw new IllegalArgumentException("internal error: argument parts must be a list of string literals")
+ def noFlags = flags.isEmpty || falsely { errorAt(Flags, "flags not allowed") }
+ def noWidth = width.isEmpty || falsely { errorAt(Width, "width not allowed") }
+ def noPrecision = precision.isEmpty || falsely { errorAt(Precision, "precision not allowed") }
+ def only_-(msg: String) = {
+ val badFlags = (flags getOrElse "") filterNot { case '-' | '<' => true case _ => false }
+ badFlags.isEmpty || falsely { badFlag(badFlags(0), s"Only '-' allowed for $msg") }
}
- val str = StringContext.treatEscapes(rawStr)
- val strLen = str.length
- val strIsEmpty = strLen == 0
- def charAtIndexIs(idx: Int, ch: Char) = idx < strLen && str(idx) == ch
- def isPercent(idx: Int) = charAtIndexIs(idx, '%')
- def isConversion(idx: Int) = isPercent(idx) && !charAtIndexIs(idx + 1, 'n') && !charAtIndexIs(idx + 1, '%')
- var idx = 0
-
- def errorAtIndex(idx: Int, msg: String) = c.error(Position.offset(strTree.pos.source, strTree.pos.point + idx), msg)
- def wrongConversionString(idx: Int) = errorAtIndex(idx, "wrong conversion string")
- def illegalConversionCharacter(idx: Int) = errorAtIndex(idx, "illegal conversion character")
- def nonEscapedPercent(idx: Int) = errorAtIndex(idx,
- "conversions must follow a splice; use %% for literal %, %n for newline")
-
- // STEP 1: handle argument conversion
- // 1) "...${smth}" => okay, equivalent to "...${smth}%s"
- // 2) "...${smth}blahblah" => okay, equivalent to "...${smth}%sblahblah"
- // 3) "...${smth}%" => error
- // 4) "...${smth}%n" => okay, equivalent to "...${smth}%s%n"
- // 5) "...${smth}%%" => okay, equivalent to "...${smth}%s%%"
- // 6) "...${smth}[%legalJavaConversion]" => okay, according to http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Formatter.html
- // 7) "...${smth}[%illegalJavaConversion]" => error
- if (!first) {
- val arg = argStack.pop()
- if (isConversion(0)) {
- // PRE str is not empty and str(0) == '%'
- // argument index parameter is not allowed, thus parse
- // [flags][width][.precision]conversion
- var pos = 1
- while (pos < strLen && isFlag(str charAt pos)) pos += 1
- while (pos < strLen && Character.isDigit(str charAt pos)) pos += 1
- if (pos < strLen && str.charAt(pos) == '.') {
- pos += 1
- while (pos < strLen && Character.isDigit(str charAt pos)) pos += 1
- }
- if (pos < strLen) {
- conversionType(str charAt pos, arg) match {
- case Some(tpe) => defval(arg, tpe)
- case None => illegalConversionCharacter(pos)
- }
- } else {
- wrongConversionString(pos - 1)
+ protected def okFlags: String = allFlags
+ def goodFlags = {
+ val badFlags = flags map (_ filterNot (okFlags contains _))
+ for (bf <- badFlags; f <- bf) badFlag(f, s"Illegal flag '$f'")
+ badFlags.getOrElse("").isEmpty
+ }
+ def goodIndex = {
+ if (index.nonEmpty && hasFlag('<'))
+ c.warning(groupPos(Index), "Argument index ignored if '<' flag is present")
+ val okRange = index map (i => i > 0 && i <= args.size) getOrElse true
+ okRange || hasFlag('<') ||
+ falsely { errorAt(Index, "Argument index out of range") }
+ }
+ }
+ object Conversion {
+ import SpecifierGroups.{ Spec, CC, Width }
+ def apply(m: Match, p: Position): Option[Conversion] = {
+ def badCC(msg: String) = {
+ val dk = new Conversion(m, p) {
+ override def verify = false
+ def accepts(arg: Tree) = None
}
- idx = 1
- } else {
- bldr append "%s"
- defval(arg, AnyTpe)
+ val at = if (dk.op.isEmpty) Spec else CC
+ dk.errorAt(at, msg)
+ }
+ def cv(cc: Char) = cc match {
+ case 'b' | 'B' | 'h' | 'H' | 's' | 'S' =>
+ new Conversion(m, p) with GeneralXn
+ case 'c' | 'C' =>
+ new Conversion(m, p) with CharacterXn
+ case 'd' | 'o' | 'x' | 'X' =>
+ new Conversion(m, p) with IntegralXn
+ case 'e' | 'E' | 'f' | 'g' | 'G' | 'a' | 'A' =>
+ new Conversion(m, p) with FloatingPointXn
+ case 't' | 'T' =>
+ new Conversion(m, p) with DateTimeXn
+ case '%' | 'n' =>
+ new Conversion(m, p) with LiteralXn
+ case _ =>
+ badCC(s"illegal conversion character '$cc'")
+ null
+ }
+ Option(m group CC.id) map (cc => cv(cc(0))) match {
+ case Some(x) => Option(x) filter (_.verify)
+ case None =>
+ badCC(s"Missing conversion operator in '${m.matched}'; $literalHelp")
+ None
}
}
-
- // STEP 2: handle the rest of the text
- // 1) %n tokens are left as is
- // 2) %% tokens are left as is
- // 3) other usages of percents are reported as errors
- if (!strIsEmpty) {
- while (idx < strLen) {
- if (isPercent(idx)) {
- if (isConversion(idx)) nonEscapedPercent(idx)
- else idx += 1 // skip n and % in %n and %%
+ trait GeneralXn extends Conversion {
+ def accepts(arg: Tree) = cc match {
+ case 's' | 'S' if hasFlag('#') => checkType(arg, tagOfFormattable.tpe)
+ case 'b' | 'B' => if (arg.tpe <:< NullTpe) Some(NullTpe) else Some(BooleanTpe)
+ case _ => Some(AnyTpe)
+ }
+ override protected def okFlags = cc match {
+ case 's' | 'S' => "-#<"
+ case _ => "-<"
+ }
+ }
+ trait LiteralXn extends Conversion {
+ override val isLiteral = true
+ override def verify = op match {
+ case "%" => super.verify && noPrecision && truly(width foreach (_ => c.warning(groupPos(Width), "width ignored on literal")))
+ case "n" => noFlags && noWidth && noPrecision
+ }
+ override protected val okFlags = "-"
+ def accepts(arg: Tree) = None
+ }
+ trait CharacterXn extends Conversion {
+ override def verify = super.verify && noPrecision && only_-("c conversion")
+ def accepts(arg: Tree) = checkType(arg, CharTpe, ByteTpe, ShortTpe, IntTpe)
+ }
+ trait IntegralXn extends Conversion {
+ override def verify = {
+ def d_# = (cc == 'd' && hasFlag('#') &&
+ truly { badFlag('#', "# not allowed for d conversion") }
+ )
+ def x_comma = (cc != 'd' && hasFlag(',') &&
+ truly { badFlag(',', "',' only allowed for d conversion of integral types") }
+ )
+ super.verify && noPrecision && !d_# && !x_comma
+ }
+ override def accepts(arg: Tree) = {
+ def isBigInt = checkType0(arg, tagOfBigInt.tpe).nonEmpty
+ val maybeOK = "+ ("
+ def bad_+ = cond(cc) {
+ case 'o' | 'x' | 'X' if hasAnyFlag(maybeOK) && !isBigInt =>
+ maybeOK filter hasFlag foreach (badf =>
+ badFlag(badf, s"only use '$badf' for BigInt conversions to o, x, X"))
+ true
}
- idx += 1
+ if (bad_+) None else checkType(arg, IntTpe, LongTpe, ByteTpe, ShortTpe, tagOfBigInt.tpe)
}
- bldr append (str take idx)
}
+ trait FloatingPointXn extends Conversion {
+ override def verify = super.verify && (cc match {
+ case 'a' | 'A' =>
+ val badFlags = ",(" filter hasFlag
+ noPrecision && badFlags.isEmpty || falsely {
+ badFlags foreach (badf => badFlag(badf, s"'$badf' not allowed for a, A"))
+ }
+ case _ => true
+ })
+ def accepts(arg: Tree) = checkType(arg, DoubleTpe, FloatTpe, tagOfBigDecimal.tpe)
+ }
+ trait DateTimeXn extends Conversion {
+ def hasCC = (op.length == 2 ||
+ falsely { errorAt(CC, "Date/time conversion must have two characters") })
+ def goodCC = ("HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc" contains cc) ||
+ falsely { errorAtOffset(CC, 1, s"'$cc' doesn't seem to be a date or time conversion") }
+ override def verify = super.verify && hasCC && goodCC && noPrecision && only_-("date/time conversions")
+ def accepts(arg: Tree) = checkType(arg, LongTpe, tagOfCalendar.tpe, tagOfDate.tpe)
+ }
+ val literalHelp = "use %% for literal %, %n for newline"
}
-
- copyString(first = true)
- while (pi.hasNext) {
- copyString(first = false)
+ def badlyInvoked = (parts.length != args.length + 1) && truly {
+ def because(s: String) = s"too $s arguments for interpolated string"
+ val (p, msg) =
+ if (parts.length == 0) (c.prefix.tree.pos, "there are no parts")
+ else if (args.length + 1 < parts.length)
+ (if (args.isEmpty) c.enclosingPosition else args.last.pos, because("few"))
+ else (args(parts.length-1).pos, because("many"))
+ c.abort(p, msg)
}
+ def interpolated = {
+ val bldr = new StringBuilder
+ val evals = ListBuffer[ValDef]()
+ val ids = ListBuffer[Ident]()
+ val argStack = Stack(args: _*)
- val fstring = bldr.toString
-// val expr = c.reify(fstring.format((ids.map(id => Expr(id).eval)) : _*))
-// https://issues.scala-lang.org/browse/SI-5824, therefore
- val expr =
- Apply(
- Select(
- Literal(Constant(fstring)),
- newTermName("format")),
- List(ids: _* )
- )
+ // create a tmp val and add it to the ids passed to format
+ def defval(value: Tree, tpe: Type): Unit = {
+ val freshName = TermName(c.freshName("arg$"))
+ evals += ValDef(Modifiers(), freshName, TypeTree(tpe) setPos value.pos.focus, value) setPos value.pos
+ ids += Ident(freshName)
+ }
+ // Append the nth part to the string builder, possibly prepending an omitted %s first.
+ // Sanity-check the % fields in this part.
+ def copyPart(part: Tree, n: Int): Unit = {
+ import SpecifierGroups.{ Spec, Index }
+ val s0 = part match {
+ case Literal(Constant(x: String)) => x
+ case _ => throw new IllegalArgumentException("internal error: argument parts must be a list of string literals")
+ }
+ val s = StringContext.treatEscapes(s0)
+ val ms = fpat findAllMatchIn s
+ def first = n == 0
+ // a conversion for the arg is required
+ if (!first) {
+ val arg = argStack.pop()
+ def s_%(): Unit = {
+ bldr append "%s"
+ defval(arg, AnyTpe)
+ }
+ def accept(op: Conversion) = op.accepts(arg) match {
+ case Some(tpe) => defval(arg, tpe)
+ case None =>
+ }
+ if (ms.hasNext) {
+ val m = ms.next
+ Conversion(m, part.pos) match {
+ case Some(op) if op.isLiteral => s_%()
+ case Some(op) if op.indexed =>
+ if (op.index map (_ == n) getOrElse true) accept(op)
+ else {
+ // either some other arg num, or '<'
+ c.warning(op.groupPos(Index), "Index is not this arg")
+ s_%()
+ }
+ case Some(op) => accept(op)
+ case None =>
+ }
+ } else s_%()
+ }
+ // any remaining conversions must be either literals or indexed
+ while (ms.hasNext) {
+ val m = ms.next
+ Conversion(m, part.pos) match {
+ case Some(op) if first && op.hasFlag('<') => op.badFlag('<', "No last arg")
+ case Some(op) if op.isLiteral || op.indexed => // OK
+ case Some(op) => op.errorAt(Spec, s"conversions must follow a splice; ${Conversion.literalHelp}")
+ case None =>
+ }
+ }
+ bldr append s
+ }
- Block(evals.toList, atPos(origApplyPos.focus)(expr)) setPos origApplyPos.makeTransparent
- }
+ parts.zipWithIndex foreach {
+ case (part, n) => copyPart(part, n)
+ }
+ val fstring = bldr.toString
+ val expr =
+ Apply(
+ Select(
+ Literal(Constant(fstring)),
+ TermName("format")),
+ List(ids: _* )
+ )
+ Block(evals.toList, atPos(origApplyPos.focus)(expr)) setPos origApplyPos.makeTransparent
+ //q"..evals, ${Literal(Constant(fstring))}.format(..$ids)"
+ }
+ if (badlyInvoked) EmptyTree else interpolated
+ }
}
diff --git a/test/files/neg/stringinterpolation_macro-neg.check b/test/files/neg/stringinterpolation_macro-neg.check
index 457f497f2f..00002f5a4b 100644
--- a/test/files/neg/stringinterpolation_macro-neg.check
+++ b/test/files/neg/stringinterpolation_macro-neg.check
@@ -1,61 +1,61 @@
-stringinterpolation_macro-neg.scala:8: error: too few parts
+stringinterpolation_macro-neg.scala:13: error: there are no parts
new StringContext().f()
^
-stringinterpolation_macro-neg.scala:9: error: too few arguments for interpolated string
+stringinterpolation_macro-neg.scala:14: error: too few arguments for interpolated string
new StringContext("", " is ", "%2d years old").f(s)
^
-stringinterpolation_macro-neg.scala:10: error: too many arguments for interpolated string
+stringinterpolation_macro-neg.scala:15: error: too many arguments for interpolated string
new StringContext("", " is ", "%2d years old").f(s, d, d)
^
-stringinterpolation_macro-neg.scala:11: error: too few arguments for interpolated string
+stringinterpolation_macro-neg.scala:16: error: too few arguments for interpolated string
new StringContext("", "").f()
^
-stringinterpolation_macro-neg.scala:14: error: type mismatch;
+stringinterpolation_macro-neg.scala:19: error: type mismatch;
found : String
required: Boolean
f"$s%b"
^
-stringinterpolation_macro-neg.scala:15: error: type mismatch;
+stringinterpolation_macro-neg.scala:20: error: type mismatch;
found : String
required: Char
f"$s%c"
^
-stringinterpolation_macro-neg.scala:16: error: type mismatch;
+stringinterpolation_macro-neg.scala:21: error: type mismatch;
found : Double
required: Char
f"$f%c"
^
-stringinterpolation_macro-neg.scala:17: error: type mismatch;
+stringinterpolation_macro-neg.scala:22: error: type mismatch;
found : String
required: Int
f"$s%x"
^
-stringinterpolation_macro-neg.scala:18: error: type mismatch;
+stringinterpolation_macro-neg.scala:23: error: type mismatch;
found : Boolean
required: Int
f"$b%d"
^
-stringinterpolation_macro-neg.scala:19: error: type mismatch;
+stringinterpolation_macro-neg.scala:24: error: type mismatch;
found : String
required: Int
f"$s%d"
^
-stringinterpolation_macro-neg.scala:20: error: type mismatch;
+stringinterpolation_macro-neg.scala:25: error: type mismatch;
found : Double
required: Int
f"$f%o"
^
-stringinterpolation_macro-neg.scala:21: error: type mismatch;
+stringinterpolation_macro-neg.scala:26: error: type mismatch;
found : String
required: Double
f"$s%e"
^
-stringinterpolation_macro-neg.scala:22: error: type mismatch;
+stringinterpolation_macro-neg.scala:27: error: type mismatch;
found : Boolean
required: Double
f"$b%f"
^
-stringinterpolation_macro-neg.scala:27: error: type mismatch;
+stringinterpolation_macro-neg.scala:32: error: type mismatch;
found : String
required: Int
Note that implicit conversions are not applicable because they are ambiguous:
@@ -64,7 +64,106 @@ Note that implicit conversions are not applicable because they are ambiguous:
are possible conversion functions from String to Int
f"$s%d"
^
-stringinterpolation_macro-neg.scala:30: error: illegal conversion character
+stringinterpolation_macro-neg.scala:35: error: illegal conversion character 'i'
f"$s%i"
^
-15 errors found
+stringinterpolation_macro-neg.scala:38: error: Illegal flag '+'
+ f"$s%+ 0,(s"
+ ^
+stringinterpolation_macro-neg.scala:38: error: Illegal flag ' '
+ f"$s%+ 0,(s"
+ ^
+stringinterpolation_macro-neg.scala:38: error: Illegal flag '0'
+ f"$s%+ 0,(s"
+ ^
+stringinterpolation_macro-neg.scala:38: error: Illegal flag ','
+ f"$s%+ 0,(s"
+ ^
+stringinterpolation_macro-neg.scala:38: error: Illegal flag '('
+ f"$s%+ 0,(s"
+ ^
+stringinterpolation_macro-neg.scala:39: error: Only '-' allowed for c conversion
+ f"$c%#+ 0,(c"
+ ^
+stringinterpolation_macro-neg.scala:40: error: # not allowed for d conversion
+ f"$d%#d"
+ ^
+stringinterpolation_macro-neg.scala:41: error: ',' only allowed for d conversion of integral types
+ f"$d%,x"
+ ^
+stringinterpolation_macro-neg.scala:42: error: only use '+' for BigInt conversions to o, x, X
+ f"$d%+ (x"
+ ^
+stringinterpolation_macro-neg.scala:42: error: only use ' ' for BigInt conversions to o, x, X
+ f"$d%+ (x"
+ ^
+stringinterpolation_macro-neg.scala:42: error: only use '(' for BigInt conversions to o, x, X
+ f"$d%+ (x"
+ ^
+stringinterpolation_macro-neg.scala:43: error: ',' not allowed for a, A
+ f"$f%,(a"
+ ^
+stringinterpolation_macro-neg.scala:43: error: '(' not allowed for a, A
+ f"$f%,(a"
+ ^
+stringinterpolation_macro-neg.scala:44: error: Only '-' allowed for date/time conversions
+ f"$t%#+ 0,(tT"
+ ^
+stringinterpolation_macro-neg.scala:47: error: precision not allowed
+ f"$c%.2c"
+ ^
+stringinterpolation_macro-neg.scala:48: error: precision not allowed
+ f"$d%.2d"
+ ^
+stringinterpolation_macro-neg.scala:49: error: precision not allowed
+ f"%.2%"
+ ^
+stringinterpolation_macro-neg.scala:50: error: precision not allowed
+ f"%.2n"
+ ^
+stringinterpolation_macro-neg.scala:51: error: precision not allowed
+ f"$f%.2a"
+ ^
+stringinterpolation_macro-neg.scala:52: error: precision not allowed
+ f"$t%.2tT"
+ ^
+stringinterpolation_macro-neg.scala:55: error: No last arg
+ f"%<s"
+ ^
+stringinterpolation_macro-neg.scala:56: error: No last arg
+ f"%<c"
+ ^
+stringinterpolation_macro-neg.scala:57: error: No last arg
+ f"%<tT"
+ ^
+stringinterpolation_macro-neg.scala:58: error: Argument index out of range
+ f"${8}%d ${9}%d%3$$d"
+ ^
+stringinterpolation_macro-neg.scala:59: error: Argument index out of range
+ f"${8}%d ${9}%d%0$$d"
+ ^
+stringinterpolation_macro-neg.scala:62: warning: Index is not this arg
+ f"${8}%d ${9}%1$$d"
+ ^
+stringinterpolation_macro-neg.scala:63: warning: Argument index ignored if '<' flag is present
+ f"$s%s $s%s %1$$<s"
+ ^
+stringinterpolation_macro-neg.scala:64: warning: Index is not this arg
+ f"$s%s $s%1$$s"
+ ^
+stringinterpolation_macro-neg.scala:67: error: type mismatch;
+ found : String
+ required: java.util.Formattable
+ f"$s%#s"
+ ^
+stringinterpolation_macro-neg.scala:70: error: 'G' doesn't seem to be a date or time conversion
+ f"$t%tG"
+ ^
+stringinterpolation_macro-neg.scala:71: error: Date/time conversion must have two characters
+ f"$t%t"
+ ^
+stringinterpolation_macro-neg.scala:72: error: Missing conversion operator in '%10.5'; use %% for literal %, %n for newline
+ f"$s%10.5"
+ ^
+three warnings found
+44 errors found
diff --git a/test/files/neg/stringinterpolation_macro-neg.scala b/test/files/neg/stringinterpolation_macro-neg.scala
index ac9d97d678..c5ae708f21 100644
--- a/test/files/neg/stringinterpolation_macro-neg.scala
+++ b/test/files/neg/stringinterpolation_macro-neg.scala
@@ -3,6 +3,11 @@ object Test extends App {
val d = 8
val b = false
val f = 3.14159
+ val c = 'c'
+ val t = new java.util.Date
+ val x = new java.util.Formattable {
+ def formatTo(ff: java.util.Formatter, g: Int, w: Int, p: Int): Unit = ff format "xxx"
+ }
// 1) number of arguments
new StringContext().f()
@@ -28,4 +33,41 @@ object Test extends App {
}
f"$s%i"
+
+ // 3) flag mismatches
+ f"$s%+ 0,(s"
+ f"$c%#+ 0,(c"
+ f"$d%#d"
+ f"$d%,x"
+ f"$d%+ (x"
+ f"$f%,(a"
+ f"$t%#+ 0,(tT"
+
+ // 4) bad precisions
+ f"$c%.2c"
+ f"$d%.2d"
+ f"%.2%"
+ f"%.2n"
+ f"$f%.2a"
+ f"$t%.2tT"
+
+ // 5) bad indexes
+ f"%<s"
+ f"%<c"
+ f"%<tT"
+ f"${8}%d ${9}%d%3$$d"
+ f"${8}%d ${9}%d%0$$d"
+
+ // warnings
+ f"${8}%d ${9}%1$$d"
+ f"$s%s $s%s %1$$<s"
+ f"$s%s $s%1$$s"
+
+ // 6) bad arg types
+ f"$s%#s"
+
+ // 7) misunderstood conversions
+ f"$t%tG"
+ f"$t%t"
+ f"$s%10.5"
}
diff --git a/test/files/neg/t7325.check b/test/files/neg/t7325.check
index d2c40f4df8..61c33f99b1 100644
--- a/test/files/neg/t7325.check
+++ b/test/files/neg/t7325.check
@@ -1,13 +1,13 @@
-t7325.scala:2: error: conversions must follow a splice; use %% for literal %, %n for newline
+t7325.scala:2: error: Missing conversion operator in '%'; use %% for literal %, %n for newline
println(f"%")
^
-t7325.scala:4: error: conversions must follow a splice; use %% for literal %, %n for newline
+t7325.scala:4: error: Missing conversion operator in '%'; use %% for literal %, %n for newline
println(f"%%%")
^
-t7325.scala:6: error: conversions must follow a splice; use %% for literal %, %n for newline
+t7325.scala:6: error: Missing conversion operator in '%'; use %% for literal %, %n for newline
println(f"%%%%%")
^
-t7325.scala:16: error: wrong conversion string
+t7325.scala:16: error: Missing conversion operator in '%'; use %% for literal %, %n for newline
println(f"${0}%")
^
t7325.scala:19: error: conversions must follow a splice; use %% for literal %, %n for newline
diff --git a/test/files/run/stringinterpolation_macro-run.check b/test/files/run/stringinterpolation_macro-run.check
index be62c5780b..ead61e76ac 100644
--- a/test/files/run/stringinterpolation_macro-run.check
+++ b/test/files/run/stringinterpolation_macro-run.check
@@ -46,6 +46,8 @@ S
120
120
120
+ 0X4
+She is 4 feet tall.
120
42
3.400000e+00
@@ -60,3 +62,6 @@ S
05/26/12
05/26/12
05/26/12
+%
+7 7 9
+7 9 9
diff --git a/test/files/run/stringinterpolation_macro-run.scala b/test/files/run/stringinterpolation_macro-run.scala
index 1138cd0860..ff779dd1d3 100644
--- a/test/files/run/stringinterpolation_macro-run.scala
+++ b/test/files/run/stringinterpolation_macro-run.scala
@@ -72,6 +72,14 @@ println(f"${120 : java.lang.Integer}%d")
println(f"${120 : java.lang.Long}%d")
println(f"${BigInt(120)}%d")
println(f"${new java.math.BigInteger("120")}%d")
+println(f"${4}%#10X")
+
+locally {
+ val fff = new java.util.Formattable {
+ def formatTo(f: java.util.Formatter, g: Int, w: Int, p: Int) = f.format("4")
+ }
+ println(f"She is ${fff}%#s feet tall.")
+}
{
implicit val strToShort = (s: String) => java.lang.Short.parseShort(s)
@@ -103,4 +111,11 @@ println(f"${c.getTime.getTime}%TD")
implicit val strToDate = (x: String) => c
println(f"""${"1234"}%TD""")
+
+
+// literals and arg indexes
+println(f"%%")
+println(f"${7}%d %<d ${9}%d")
+println(f"${7}%d %2$$d ${9}%d")
+
}