From 8053682d4ff0dcff3c1846a1bac9c718c92cc704 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 28 Jan 2014 01:37:11 -0800 Subject: 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. --- test/files/neg/stringinterpolation_macro-neg.check | 131 ++++++++++++++++++--- test/files/neg/stringinterpolation_macro-neg.scala | 42 +++++++ test/files/neg/t7325.check | 8 +- test/files/run/stringinterpolation_macro-run.check | 5 + test/files/run/stringinterpolation_macro-run.scala | 15 +++ 5 files changed, 181 insertions(+), 20 deletions(-) (limited to 'test/files') 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"% 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 % Date: Tue, 4 Feb 2014 19:29:16 -0800 Subject: SI-8092 Refactor f-interp A denshish refactor makes the FormatInterpolator a nice bundle that destructures its input and flattens out the classes to give the code some elbow room. Everything shifts left. The `checkType` method is refolded and renamed `pickAcceptable`. An additional test case captures the leading edge test, that a % should follow a hole, and which is the most basic requirement. --- src/compiler/scala/tools/reflect/FastTrack.scala | 6 +- .../scala/tools/reflect/FormatInterpolator.scala | 313 +++++++++++++++++++++ .../scala/tools/reflect/MacroImplementations.scala | 296 ------------------- test/files/neg/stringinterpolation_macro-neg.check | 5 +- test/files/neg/stringinterpolation_macro-neg.scala | 3 + 5 files changed, 323 insertions(+), 300 deletions(-) create mode 100644 src/compiler/scala/tools/reflect/FormatInterpolator.scala delete mode 100644 src/compiler/scala/tools/reflect/MacroImplementations.scala (limited to 'test/files') diff --git a/src/compiler/scala/tools/reflect/FastTrack.scala b/src/compiler/scala/tools/reflect/FastTrack.scala index bb0bbd79a3..8630ecf69e 100644 --- a/src/compiler/scala/tools/reflect/FastTrack.scala +++ b/src/compiler/scala/tools/reflect/FastTrack.scala @@ -20,8 +20,8 @@ trait FastTrack { private implicit def context2taggers(c0: MacroContext): Taggers { val c: c0.type } = new { val c: c0.type = c0 } with Taggers - private implicit def context2macroimplementations(c0: MacroContext): MacroImplementations { val c: c0.type } = - new { val c: c0.type = c0 } with MacroImplementations + private implicit def context2macroimplementations(c0: MacroContext): FormatInterpolator { val c: c0.type } = + new { val c: c0.type = c0 } with FormatInterpolator private implicit def context2quasiquote(c0: MacroContext): QuasiquoteImpls { val c: c0.type } = new { val c: c0.type = c0 } with QuasiquoteImpls private def makeBlackbox(sym: Symbol)(pf: PartialFunction[Applied, MacroContext => Tree]) = @@ -48,7 +48,7 @@ trait FastTrack { makeBlackbox( materializeWeakTypeTag) { case Applied(_, ttag :: Nil, (u :: _) :: _) => _.materializeTypeTag(u, EmptyTree, ttag.tpe, concrete = false) }, makeBlackbox( materializeTypeTag) { case Applied(_, ttag :: Nil, (u :: _) :: _) => _.materializeTypeTag(u, EmptyTree, ttag.tpe, concrete = true) }, makeBlackbox( ApiUniverseReify) { case Applied(_, ttag :: Nil, (expr :: _) :: _) => c => c.materializeExpr(c.prefix.tree, EmptyTree, expr) }, - makeBlackbox( StringContext_f) { case Applied(Select(Apply(_, ps), _), _, args) => c => c.macro_StringInterpolation_f(ps, args.flatten, c.expandee.pos) }, + makeBlackbox( StringContext_f) { case _ => _.interpolate }, makeBlackbox(ReflectRuntimeCurrentMirror) { case _ => c => currentMirror(c).tree }, makeWhitebox( QuasiquoteClass_api_apply) { case _ => _.expandQuasiquote }, makeWhitebox(QuasiquoteClass_api_unapply) { case _ => _.expandQuasiquote } diff --git a/src/compiler/scala/tools/reflect/FormatInterpolator.scala b/src/compiler/scala/tools/reflect/FormatInterpolator.scala new file mode 100644 index 0000000000..e57a36ea2b --- /dev/null +++ b/src/compiler/scala/tools/reflect/FormatInterpolator.scala @@ -0,0 +1,313 @@ +package scala.tools.reflect + +import scala.reflect.macros.contexts.Context +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 FormatInterpolator { + val c: Context + + import c.universe.{ Match => _, _ } + import definitions._ + + @inline private def truly(body: => Unit): Boolean = { body ; true } + @inline private def falsely(body: => Unit): Boolean = { body ; false } + + private def fail(msg: String) = c.abort(c.enclosingPosition, msg) + + def interpolate: Tree = c.macroApplication match { + case q"$_(..$parts).f(..$args)" => + 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) + } + if (badlyInvoked) c.macroApplication else interpolated(parts, args) + case other => + fail(s"Unexpected application ${showRaw(other)}") + other + } + + /** 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 + * + * 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* + * 7) "...${smth}[%illegalJavaConversion]" => error + * *Legal according to [[http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Formatter.html]] + */ + def interpolated(parts: List[Tree], args: List[Tree]) = { + val fstring = new StringBuilder + val evals = ListBuffer[ValDef]() + val ids = ListBuffer[Ident]() + val argStack = Stack(args: _*) + + // 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 errorLeading(op: Conversion) = op.errorAt(Spec, s"conversions must follow a splice; ${Conversion.literalHelp}") + + def first = n == 0 + // a conversion for the arg is required + if (!first) { + val arg = argStack.pop() + def s_%() = { + fstring append "%s" + defval(arg, AnyTpe) + } + def accept(op: Conversion) = { + if (!op.isLeading) errorLeading(op) + op.accepts(arg) match { + case Some(tpe) => defval(arg, tpe) + case None => + } + } + if (ms.hasNext) { + Conversion(ms.next, part.pos, args.size) 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) { + Conversion(ms.next, part.pos, args.size) 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) => errorLeading(op) + case None => + } + } + fstring append s + } + + parts.zipWithIndex foreach { + case (part, n) => copyPart(part, n) + } + + q"{..$evals; ${fstring.toString}.format(..$ids)}" + } + + val fpat = """%(?:(\d+)\$)?([-#+ 0,(\<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""".r + object SpecifierGroups extends Enumeration { val Spec, Index, Flags, Width, Precision, CC = Value } + + val stdContextTags = new { val tc: c.type = c } with StdContextTags + import stdContextTags._ + val tagOfFormattable = typeTag[Formattable] + + /** A conversion specifier matched by `m` in the string part at `pos`, + * with `argc` arguments to interpolate. + */ + sealed trait Conversion { + def m: Match + def pos: Position + def argc: Int + + 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 isLeading: Boolean = m.start(0) == 0 + def verify: Boolean = goodFlags && goodIndex + def accepts(arg: Tree): Option[Type] + + 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 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") } + } + 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 <= argc) getOrElse true + okRange || hasFlag('<') || falsely { errorAt(Index, "Argument index out of range") } + } + /** Pick the type of an arg to format from among the variants + * supported by a conversion. This is the type of the temporary, + * so failure results in an erroneous assignment to the first variant. + * A more complete message would be nice. + */ + def pickAcceptable(arg: Tree, variants: Type*): Option[Type] = + variants find (arg.tpe <:< _) orElse ( + variants find (c.inferImplicitView(arg, arg.tpe, _) != EmptyTree) + ) orElse Some(variants(0)) + } + object Conversion { + import SpecifierGroups.{ Spec, CC, Width } + def apply(m: Match, p: Position, n: Int): Option[Conversion] = { + def badCC(msg: String) = { + val dk = new ErrorXn(m, p) + 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 GeneralXn(m, p, n) + case 'c' | 'C' => + new CharacterXn(m, p, n) + case 'd' | 'o' | 'x' | 'X' => + new IntegralXn(m, p, n) + case 'e' | 'E' | 'f' | 'g' | 'G' | 'a' | 'A' => + new FloatingPointXn(m, p, n) + case 't' | 'T' => + new DateTimeXn(m, p, n) + case '%' | 'n' => + new LiteralXn(m, p, n) + 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 + } + } + val literalHelp = "use %% for literal %, %n for newline" + } + class GeneralXn(val m: Match, val pos: Position, val argc: Int) extends Conversion { + def accepts(arg: Tree) = cc match { + case 's' | 'S' if hasFlag('#') => pickAcceptable(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 _ => "-<" + } + } + class LiteralXn(val m: Match, val pos: Position, val argc: Int) extends Conversion { + import SpecifierGroups.Width + 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 + } + class CharacterXn(val m: Match, val pos: Position, val argc: Int) extends Conversion { + override def verify = super.verify && noPrecision && only_-("c conversion") + def accepts(arg: Tree) = pickAcceptable(arg, CharTpe, ByteTpe, ShortTpe, IntTpe) + } + class IntegralXn(val m: Match, val pos: Position, val argc: Int) 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 = arg.tpe <:< tagOfBigInt.tpe + 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 + } + if (bad_+) None else pickAcceptable(arg, IntTpe, LongTpe, ByteTpe, ShortTpe, tagOfBigInt.tpe) + } + } + class FloatingPointXn(val m: Match, val pos: Position, val argc: Int) 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) = pickAcceptable(arg, DoubleTpe, FloatTpe, tagOfBigDecimal.tpe) + } + class DateTimeXn(val m: Match, val pos: Position, val argc: Int) extends Conversion { + import SpecifierGroups.CC + 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) = pickAcceptable(arg, LongTpe, tagOfCalendar.tpe, tagOfDate.tpe) + } + class ErrorXn(val m: Match, val pos: Position) extends Conversion { + val argc = 0 + override def verify = false + def accepts(arg: Tree) = None + } +} diff --git a/src/compiler/scala/tools/reflect/MacroImplementations.scala b/src/compiler/scala/tools/reflect/MacroImplementations.scala deleted file mode 100644 index b9b35f3f0f..0000000000 --- a/src/compiler/scala/tools/reflect/MacroImplementations.scala +++ /dev/null @@ -1,296 +0,0 @@ -package scala.tools.reflect - -import scala.reflect.macros.contexts.Context -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.{ Match => _, _ } - import definitions._ - - @inline private def truly(body: => Unit): Boolean = { body ; true } - @inline private def falsely(body: => Unit): Boolean = { body ; false } - - private def fail(msg: String) = c.abort(c.enclosingPosition, msg) - - /** 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 = { - - val fpat = """%(?:(\d+)\$)?([-#+ 0,(\<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""".r - object SpecifierGroups extends Enumeration { val Spec, Index, Flags, Width, Precision, CC = Value } - - // 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] - - 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 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") } - } - 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 - } - 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 - } - } - 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 - } - if (bad_+) None else checkType(arg, IntTpe, LongTpe, ByteTpe, ShortTpe, tagOfBigInt.tpe) - } - } - 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" - } - 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: _*) - - // 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 - } - - 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 00002f5a4b..703846ad62 100644 --- a/test/files/neg/stringinterpolation_macro-neg.check +++ b/test/files/neg/stringinterpolation_macro-neg.check @@ -165,5 +165,8 @@ stringinterpolation_macro-neg.scala:71: error: Date/time conversion must have tw stringinterpolation_macro-neg.scala:72: error: Missing conversion operator in '%10.5'; use %% for literal %, %n for newline f"$s%10.5" ^ +stringinterpolation_macro-neg.scala:75: error: conversions must follow a splice; use %% for literal %, %n for newline + f"${d}random-leading-junk%d" + ^ three warnings found -44 errors found +45 errors found diff --git a/test/files/neg/stringinterpolation_macro-neg.scala b/test/files/neg/stringinterpolation_macro-neg.scala index c5ae708f21..3869d42d66 100644 --- a/test/files/neg/stringinterpolation_macro-neg.scala +++ b/test/files/neg/stringinterpolation_macro-neg.scala @@ -70,4 +70,7 @@ object Test extends App { f"$t%tG" f"$t%t" f"$s%10.5" + + // 8) other brain failures + f"${d}random-leading-junk%d" } -- cgit v1.2.3