From 5adbe411d8e31f9709d15eacb9a0095e30346aba Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 14 May 2019 14:52:27 +0200 Subject: [PATCH 01/36] Add f-interpolator --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 5 +- .../dotty/internal/StringContext.scala | 790 +++++++++++++++++- .../scala/internal/quoted/Matcher.scala | 2 +- tests/run-macros/f-interpolator-tests.scala | 42 + .../f-interpolator/NegativeTests.scala | 232 +++++ .../stringinterpolation_macro-neg.check | 172 ++++ .../stringinterpolation_macro-neg.scala | 101 +++ .../stringinterpolation_macro-pos.check | 58 ++ .../stringinterpolation_macro-pos.scala | 105 +++ 9 files changed, 1491 insertions(+), 16 deletions(-) create mode 100755 tests/run-macros/f-interpolator-tests.scala create mode 100644 tests/run-macros/f-interpolator/NegativeTests.scala create mode 100755 tests/run-macros/stringinterpolation_macro-neg.check create mode 100644 tests/run-macros/stringinterpolation_macro-neg.scala create mode 100644 tests/run-macros/stringinterpolation_macro-pos.check create mode 100644 tests/run-macros/stringinterpolation_macro-pos.scala diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 28f11a24de21..3d996a57693b 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1184,10 +1184,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { val baseType = tree.tpe.baseType(defn.QuotedExprClass) val argType = if (baseType != NoType) baseType.argTypesHi.head - else { - assert(ctx.reporter.hasErrors) - defn.NothingType - } + else defn.NothingType ref(defn.InternalQuoted_exprSplice).appliedToType(argType).appliedTo(tree) } def unapply(tree: Tree)(implicit ctx: Context): Option[Tree] = tree match { diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 04279c602c4c..bc64e427d02f 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -1,22 +1,790 @@ package dotty.internal import scala.quoted._ +import scala.quoted.matching._ +import scala.tasty.Reflection +import scala.language.implicitConversions +import scala.quoted.Exprs.LiftedExpr +import reflect._ object StringContext { /** Implemetation of scala.StringContext.f used in Dotty while the standard library is still not bootstrapped */ inline def f(sc: => scala.StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } +} + +/** This object implements the f interpolator macro. + * + * This kind of interpolator lets the user prepend f to any string literal to create formatted strings. + * Every variable used in the string literal should have a format, like "%d" for integers, "%2.2f", etc. + * For example, f"$name%s is $height%2.2f meters tall") will return "James is 1.90 meters tall" + */ + object fImpl{ + + /** This trait defines a tool to report errors/warnings that do not depend on Position. */ + trait Reporter{ + + /** Reports error/warning of size 1 linked with a part of the StringContext. + * + * @param message the message to report as error/warning + * @param index the index of the part inside the list of parts of the StringContext + * @param offset the index in the part String where the error is + * @return an error/warning depending on the function + */ + def partError(message : String, index : Int, offset : Int) : Unit + def partWarning(message : String, index : Int, offset : Int) : Unit + + /** Reports error/warning linked with an argument to format. + * + * @param message the message to report as error/warning + * @param index the index of the argument inside the list of arguments of the format function + * @return an error/warning depending on the function + */ + def argError(message : String, index : Int) : Unit + def argWarning(message : String, index : Int) : Unit - private def fImpl(sc: Expr[StringContext], args: Expr[Seq[Any]]): Expr[String] = { - // TODO implement f interpolation checks and generate optimal code - // See https://github.com/alemannosara/f-interpolators-in-Dotty-macros - '{ - // Purely runtime implementation of the f interpolation without any checks - val parts = $sc.parts.toList - assert(parts.nonEmpty, "StringContext should have non empty parts") - val parts2 = parts.head :: parts.tail.map(x => if (x.startsWith("%s")) x else "%s" + x) - parts2.mkString.format($args: _*) + /** Reports error linked with the list of arguments or the StringContext. + * + * @param message the message to report in the error + * @return an error + */ + def strCtxError(message : String) : Unit + def argsError(message : String) : Unit + + /** Claims whether an error or a warning has been reported + * + * @return true if an error/warning has been reported, false + */ + def hasReported() : Boolean + + /** Stores the old value of the reported and reset it to false */ + def resetReported() : Unit + + /** Restores the value of the reported boolean that has been reset */ + def restoreReported() : Unit } - } -} + /** Retrieves a String from an Expr containing it + * + * @param expression the Expr containing the String + * @return the String contained in the given Expr + * @throws an Exception if the given Expr does not contain a String + */ + private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { + case Const(string : String) => string + case _ => throw new Exception("Expected statically known part list") + } + + /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String + * + * @param strCtxExpr the Expr containing the StringContext + * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext + * @throws an Exception if the given Expr does not correspond to a StringContext + */ + private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + import reflect._ + println(strCtxExpr.unseal.show) + println(strCtxExpr.unseal.showExtractors) + println() + strCtxExpr match { + case '{ StringContext($partsExpr: _*) } => + val ExprSeq(parts) = partsExpr + parts.toList + // case '{ new StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case _ => throw new Exception("Expected statically known String Context") + } + } + + /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments + * + * @param argsExpr the Expr containing the list of arguments + * @return a list of Expr containing arguments + * @throws an Exception if the given Expr does not contain a list of arguments + */ + private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + import reflect._ + argsExpr.unseal.underlyingArgument match { + case Typed(Repeated(args, _), _) => args.map(_.seal) + case tree => throw new Exception("Expected statically known argument list") + } + } + + /** Lifts a StringContext from StringContext to Expr of StringContext + * + * @return a Liftable StringContext by redefining the toExpr function + */ + private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { + def toExpr(strCtx: StringContext): Expr[StringContext] = { + implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { + override def toExpr(list: List[String]): Expr[List[String]] = list match { + case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} + case Nil => '{Nil} + } + } + '{StringContext(${strCtx.parts.toList.toExpr}: _*)} + } + } + + /** Interpolates the arguments to the formatting String given inside a StringContext + * + * @param strCtxExpr the Expr that holds the StringContext which contains all the chunks of the formatting string + * @param args the Expr that holds the sequence of arguments to interpolate to the String in the correct format + * @return the Expr containing the formatted and interpolated String or an error/warning if the parameters are not correct + */ + private def interpolate(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): Expr[String] = { + import reflect._ + val sourceFile = strCtxExpr.unseal.pos.sourceFile + + val partsExpr = getListOfExpr(strCtxExpr) + val args = getArgsList(argsExpr) + + val reporter = new Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + val positionStart = partsExpr(index).unseal.pos.start + offset + reflect.error(message, sourceFile, positionStart, positionStart) + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + val positionStart = partsExpr(index).unseal.pos.start + offset + reflect.warning(message, sourceFile, positionStart, positionStart) + } + + def argError(message : String, index : Int) : Unit = { + reported = true + reflect.error(message, args(index).unseal.pos) + } + def argWarning(message : String, index : Int) : Unit = { + reported = true + reflect.warning(message, args(index).unseal.pos) + } + + def strCtxError(message : String) : Unit = { + reported = true + val positionStart = strCtxExpr.unseal.pos.start + reflect.error(message, sourceFile, positionStart, positionStart) + } + def argsError(message : String) : Unit = { + reported = true + reflect.error(message, argsExpr.unseal.pos) + } + + def hasReported() : Boolean = { + reported + } + + def resetReported() : Unit = { + oldReported = reported + reported = false + } + + def restoreReported() : Unit = { + reported = oldReported + } + } + + interpolate(partsExpr, args, argsExpr, reporter) + } + + /** Helper function for the interpolate function above + * + * @param partsExpr the list of parts enumerated as Expr + * @param args the list of arguments enumerated as Expr + * @param reporter the reporter to return any error/warning when a problem is encountered + * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct + */ + private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { + import reflect.{Literal => LiteralTree, _} + + /** Checks whether a part contains a formatting substring + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ + def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { + var i = index + var result : Option[Int] = None + while (i < l){ + if (part.charAt(i) == '%' && result.isEmpty) + result = Some(i) + i += 1 + } + result + } + + /** Adds the default "%s" to the Strings that do not have any given format + * + * @param parts the list of parts contained in the StringContext + * @return a new list of string with all a defined formatting or reports an error if the '%' and + * formatting parameter are too far away from the argument that they refer to + * For example : f2"${d}random-leading-junk%d" will lead to an error + */ + def addDefaultFormat(parts : List[String]) : List[String] = parts match { + case Nil => Nil + case p :: parts1 => p :: parts1.map((part : String) => { + if (!part.startsWith("%")) { + val index = part.indexOf('%') + if (index != -1) { + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) + "%s" + part + } else "%s" + part + } else part + }) + } + + /** Finds all the flags that are inside a formatting String from a given index + * + * @param i the index in the String s where to start to check + * @param l the length of s + * @param s the String to check + * @return a list containing all the flags that are inside the formatting String, + * and their index in the String + */ + def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { + def isFlag(c : Char) : Boolean = c match { + case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true + case _ => false + } + if (i < l && isFlag(s.charAt(i))) (s.charAt(i), i) :: getFlags(i + 1, l, s) + else Nil + } + + /** Skips the Characters that are width or argumentIndex parameters + * + * @param i the index where to start checking in the given String + * @param s the String to check + * @param l the length of s + * @return a tuple containing the index in the String after skipping + * the parameters, true if it has a width parameter and its value, false otherwise + */ + def skipWidth(i : Int, s : String, l : Int) = { + var j = i + var width = (false, 0) + while (j < l && Character.isDigit(s.charAt(j))){ + width = (true, j) + j += 1 + } + (j, width._1, width._2) + } + + /** Retrieves all the formatting parameters from a part and their index in it + * + * @param part the String containing the formatting parameters + * @param argIndex the index of the current argument inside the list of arguments to interpolate + * @param partIndex the index of the current part inside the list of parts in the StringContext + * @param noArg true if there is no arg, i.e. "%%" or "%n" + * @param pos the initial index where to start checking the part + * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion + * parameter is missing. Otherwise, + * the index where the format specifier substring is, + * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and + * flags that contains the list of flags (empty if there is none), + * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), + * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), + * hasRelative (true if the specifiers use relative indexing, false otherwise) and + * conversion character index + */ + def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { + var conversion = pos + var hasArgumentIndex = false + var argumentIndex = pos + var hasPrecision = false + var precision = pos + val l = part.length + + if (l >= 1 && part.charAt(conversion) == '%') + conversion += 1 + else if (!noArg) + reporter.argError("too many arguments for interpolated string", argIndex) + + //argument index or width + val (i, hasWidth1, width1) = skipWidth(conversion, part, l) + conversion = i + + //argument index + if (conversion < l && part.charAt(conversion) == '$'){ + hasArgumentIndex = true + argumentIndex = width1 + conversion += 1 + } + + //relative indexing + val hasRelative = conversion < l && part.charAt(conversion) == '<' + val relativeIndex = conversion + 1 + if (hasRelative) + conversion += 1 + + //flags + val flags = getFlags(conversion, l, part) + conversion += flags.size + + //width + val (j, hasWidth2, width2) = skipWidth(conversion, part, l) + conversion = j + + //precision + if (conversion < l && part.charAt(conversion) == '.') { + precision = conversion + conversion += 1 + hasPrecision = true + val oldConversion = conversion + while (conversion < l && Character.isDigit(part.charAt(conversion))) { + conversion += 1 + } + if (oldConversion == conversion) { + reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) + hasPrecision = false + } + } + + //conversion + if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) + reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) + + val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 + val width = if (hasWidth1 && !hasArgumentIndex) width1 else width2 + (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) + } + + /** Checks if the number of arguments are the same as the number of formatting strings + * + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise + */ + def checkSizes(format : Int, argument : Int) : Unit = { + if (format > argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too few arguments for interpolated string") + else + reporter.argError("too few arguments for interpolated string", argument - 1) + if (format < argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too many arguments for interpolated string") + else + reporter.argError("too many arguments for interpolated string", argument - 1) + if (format == -1) + reporter.strCtxError("there are no parts") + } + + /** Checks if a given type is a subtype of any of the possibilities + * + * @param actualType the given type + * @param expectedType the type we are expecting + * @param argIndex the index of the argument that should type check + * @param possibilities all the types within which we want to find a super type of the actualType + * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, + * nothing otherwise + */ + def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { + if (possibilities.find(actualType <:< _).isEmpty) + reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) + } + + /** Checks whether a given argument index, relative or not, is in the correct bounds + * + * @param partIndex the index of the part we are checking + * @param offset the index in the part where there might be an error + * @param relative true if relative indexing is used, false otherwise + * @param argumentIndex the argument index parameter in the formatting String + * @param expectedArgumentIndex the expected argument index parameter + * @param maxArgumentIndex the maximum argument index parameter that can be used + * @return reports a warning if relative indexing is used but an argument is still given, + * an error is the argument index is not in the bounds [1, number of arguments] + */ + def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { + if (relative) + reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) + + if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) + reporter.partError("Argument index out of range", partIndex, offset) + + if (expectedArgumentIndex != argumentIndex) + reporter.partWarning("Index is not this arg", partIndex, offset) + } + + /** Checks if a parameter is specified whereas it is not allowed + * + * @param hasParameter true if parameter is specified, false otherwise + * @param partIndex the index of the part inside the parts + * @param offset the index in the part where to report an error + * @param parameter the parameter that is not allowed + * @return reports an error if hasParameter is true, nothing otherwise + */ + def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { + if (hasParameter) + reporter.partError(parameter + " not allowed", partIndex, offset) + } + + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error if the flag is not allowed, nothing otherwise + */ + def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} + reporter.partError(message, partIndex, flag._2) + } + + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise + */ + def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + reporter.resetReported() + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { + if (!reporter.hasReported()) + reporter.partError(message, partIndex, flag._2) + } + if (!reporter.hasReported()) + reporter.restoreReported() + } + + /** Checks all the formatting parameters for a Character conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are different from '-' + */ + def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precisionIndex, "precision") + } + + /** Checks all the formatting parameters for an Integral conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are not allowed : + * ’d’: only ’#’ is allowed, + * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step + */ + def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if (conversionChar == 'd') + checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) + + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + } + + /** Checks all the formatting parameters for a Floating Point conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' + */ + def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if(conversionChar == 'a' || conversionChar == 'A'){ + for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} + reporter.partError("'" + flag._1 + "' not allowed for a, A", partIndex, flag._2) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + } + } + + /** Checks all the formatting parameters for a Time conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part that we are checking + * @param conversionIndex the index of the conversion Char used in the part + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified, if the time suffix is not given/incorrect or if the used flags are + * different from '-' + */ + def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + /** + * Checks whether a time suffix is given and whether it is allowed + * + * @param part the part that we are checking + * @param partIndex the index of the part inside of the parts of the StringContext + * @param conversionIndex the index of the conversion Char inside the part + * @param return reports an error if no suffix is specified or if the given suffix is not + * part of the allowed ones + */ + def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { + val partSize = part.size + + if (conversionIndex == partSize - 1) + reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) + + part.charAt(conversionIndex + 1) match { + case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times + case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates + case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times + case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) + } + } + + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for date/time conversions") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + checkTime(part, partIndex, conversionIndex) + } + + /** Checks all the formatting parameters for a General conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @return reports an error + * if '#' flag is used or if any other flag is used + */ + def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { + for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} + reporter.partError("Illegal flag : '" + flag._1 + "'", partIndex, flag._2) + } + + /** Checks all the formatting parameters for a special Char such as '%' and end of line + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @param hasWidth true if width parameter is specified, false otherwise + * @param width the index of the width parameter inside the part + * @return reports an error if precision or width is specified for '%' or + * if precision is specified for end of line + */ + def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { + case 'n' => { + checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + checkNotAllowedParameter(hasWidth, partIndex, width, "width") + } + case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + case _ => // OK + } + + /** Checks whether the format specifiers are correct depending on the conversion parameter + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part to check + * The rest of the inputs correspond to the output of the function getFormatSpecifiers + * @param hasArgumentIndex + * @param actualArgumentIndex + * @param expectedArgumentIndex + * @param maxArgumentIndex + * @param hasRelative + * @param hasWidth + * @param hasPrecision + * @param precision + * @param flags + * @param conversion + * @param argType + * @return the argument index and its type if there is an argument, the flags and the conversion parameter + * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise + */ + def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], + hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { + val conversionChar = part.charAt(conversion) + + if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) + checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) + + conversionChar match { + case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) + case 'd' | 'o' | 'x' | 'X' => checkIntegralConversion(partIndex, argType, conversionChar, flags, hasPrecision, precision) + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) + case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) + case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) + case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) + case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) + } + + (if (argType.isEmpty) None else Some(argType.get, (partIndex - 1)), conversionChar, flags) + } + + /** Checks whether the argument type, if there is one, type checks with the formatting parameters + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the character used for the conversion + * @param argument an option containing the type and index of the argument, None if there is no argument + * @param flags the flags used for the formatting + * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise + */ + def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { + if (argument.nonEmpty) + checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) + else + checkTypeWithoutArgs(conversionChar, partIndex, flags) + } + + /** Checks whether the argument type checks with the formatting parameters + * + * @param argument the given argument to check + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the argument type does not correspond with the conversion character, + * nothing otherwise + */ + def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + val booleans = List(definitions.BooleanType, definitions.NullType) + val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) + val floatingPoints = List(definitions.DoubleType, definitions.FloatType, typeOf[java.math.BigDecimal]) + val integral = List(definitions.IntType, definitions.LongType, definitions.ShortType, definitions.ByteType, typeOf[java.math.BigInteger]) + val character = List(definitions.CharType, definitions.ByteType, definitions.ShortType, definitions.IntType) + + val (argType, argIndex) = argument + conversionChar match { + case 'c' | 'C' => checkSubtype(argType, "Char", argIndex, character : _*) + case 'd' | 'o' | 'x' | 'X' => { + checkSubtype(argType, "Int", argIndex, integral : _*) + if (conversionChar != 'd') { + val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), + (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), + ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + } + } + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkSubtype(argType, "Double", argIndex, floatingPoints : _*) + case 't' | 'T' => checkSubtype(argType, "Date", argIndex, dates : _*) + case 'b' | 'B' => checkSubtype(argType, "Boolean", argIndex, booleans : _*) + case 'h' | 'H' | 'S' | 's' => + if (!(argType <:< typeOf[java.util.Formattable])) + for {flag <- flags ; if (flag._1 == '#')} + reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) + case 'n' | '%' => + case illegal => + } + } + + /** Reports error when the formatting parameter require a specific type but no argument is given + * + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given + * nothing otherwise + */ + def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + conversionChar match { + case 'o' | 'x' | 'X' => { + val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), + (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), + ('(', true, "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + } + case _ => //OK + } + + if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) + } + + /** Checks that a given part of the String Context respects every formatting constraint per parameter + * + * @param part a particular part of the String Context + * @param start the index from which we start checking the part + * @param argument an Option containing the argument corresponding to the part and its index in the list of args, + * None if no args are specified. + * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified + * @return a list with all the elements of the conversion per formatting string + */ + def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { + reporter.resetReported() + val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) + if (hasFormattingSubstring.nonEmpty) { + val formattingStart = hasFormattingSubstring.get + var nextStart = formattingStart + + argument match { + case Some(argIndex, arg) => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) + nextStart = conversion + 1 + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + } + case None => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) + if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) + reporter.partError("Argument index out of range", 0, argumentIndex + 1) + if (hasRelative) + reporter.partError("No last arg", 0, relativeIndex) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) + nextStart = conversion + 1 + if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + } + } + } else { + reporter.restoreReported() + Nil + } + } + + val argument = args.size + + // add default format + val parts = addDefaultFormat(partsExpr.map(literalToString)) + + // check validity of formatting + checkSizes(parts.size - 1, argument) + + if (!parts.isEmpty) { + if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ + val argTypeWithConversion = checkPart(parts.head, 0, None, None) + if (!reporter.hasReported()) + for ((argument, conversionChar, flags) <- argTypeWithConversion) + checkArgTypeWithConversion(0, conversionChar, argument, flags) + } + else { + val partWithArgs = parts.tail.zip(args) + for (i <- (0 until args.size)){ + val (part, arg) = partWithArgs(i) + val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) + if (!reporter.hasReported()) + for ((argument, conversionChar, flags) <- argTypeWithConversion) + checkArgTypeWithConversion(0, conversionChar, argument, flags) + } + } + } + + // macro expansion + '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} + } + + /** Applies the interpolation to the input of the f-interpolator macro + * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String + * @param argsExpr the Expr containing the list of arguments to interpolate + * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not + * respect the formatting rules + */ + final def apply(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { + import reflect._ + try interpolate(strCtxExpr, args) + catch { + case ex: Exception => + QuoteError(ex.getMessage) + } + } + } diff --git a/library/src-3.x/scala/internal/quoted/Matcher.scala b/library/src-3.x/scala/internal/quoted/Matcher.scala index 9f4a7919e3b9..4520664a633b 100644 --- a/library/src-3.x/scala/internal/quoted/Matcher.scala +++ b/library/src-3.x/scala/internal/quoted/Matcher.scala @@ -8,7 +8,7 @@ import scala.tasty._ object Matcher { - private final val debug = false + private final val debug = true /** Pattern matches an the scrutineeExpr aquainsnt the patternExpr and returns a tuple * with the matched holes if successful. diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala new file mode 100755 index 000000000000..561b529e9714 --- /dev/null +++ b/tests/run-macros/f-interpolator-tests.scala @@ -0,0 +1,42 @@ +/** + * These tests test all the possible formats the f interpolator has to deal with. + * The tests are sorted by argument category as the arguments are on https://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#detail + * + * + * Some also test (briefly) the three other macros that are implemented ; namely the s, the raw and the foo interpolators. + */ + +object Test { + def main(args: Array[String]) = { + def test1(n: Int) = { + println(s"Bob is $n years old") + println(f"Bob is $n%2d years old") + println(s"Bob will be ${n+1} years old") + // println(f"Bob will be ${n+1}%2d years old") + println(s"$n+1 = ${n+1}") + // println(f"$n%d+1 = ${n+1}%d") + } + + def test2(f: Float) = { + println(s"Best price: $f") + println(f"Best price: $f%.2f") + println(s"$f% discount included") + println(f"$f%3.2f%% discount included") + } + + + test1(1) + test1(12) + test1(123) + + test2(10.0f) + test2(13.345f) + + println(s"") + println(s"${0}") + println(s"${0}${0}") + println(f"") + println(f"${0}") + println(f"${0}${0}") + } +} diff --git a/tests/run-macros/f-interpolator/NegativeTests.scala b/tests/run-macros/f-interpolator/NegativeTests.scala new file mode 100644 index 000000000000..1c5bc2dcc7c8 --- /dev/null +++ b/tests/run-macros/f-interpolator/NegativeTests.scala @@ -0,0 +1,232 @@ +// import org.junit.Test +// import org.junit.Assert._ + +// import compilationAssertions._ +// import Macro._ + +// /** +// * These tests test some combinations that should make the f interpolator fail. +// * They come from https://github.com/lampepfl/dotty/blob/master/tests/untried/neg/stringinterpolation_macro-neg.scala +// */ +// class NegativeTests { +// val s = "Scala" +// 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" +// } +// val emptyString = "" +// val is = " is " +// val yo = "%2d years old" + +// @Test def numberArgumentsTest1() = { +// assertNotCompile("new StringContext().f2()") +// } + +// @Test def numberArgumentsTest2() = { +// assertNotCompile("new StringContext(emptyString, is, yo).f2(s)") +// } + +// @Test def numberArgumentsTest3() = { +// assertNotCompile("new StringContext(emptyString, is, yo).f2(s, d, d)") +// } + +// @Test def numberArgumentsTest4() = { +// assertNotCompile("new StringContext(emptyString, emptyString).f2()") +// } + +// @Test def interpolationMismatchTest1() = { +// implicit val strToInt1 = (s: String) => 1 +// implicit val strToInt2 = (s: String) => 2 +// val string = "$s%d" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest2() = { +// val string = "$s%b" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest3() = { +// val string = "$s%i" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest4() = { +// val string = "$s%c" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest5() = { +// val string = "$f%c" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest6() = { +// val string = "$s%x" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest7() = { +// val string = "$b%d" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest8() = { +// val string = "$s%d" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest9() = { +// val string = "$f%o" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest10() = { +// val string = "$s%e" +// assertNotCompile("f2string") +// } + +// @Test def interpolationMismatchTest11() = { +// val string = "$b%f" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest1() = { +// val string = "$s%+ 0,(s" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest2() = { +// val string = "$c%#+ 0,(c" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest3() = { +// val string = "$d%#d" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest4() = { +// val string = "$d%,x" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest5() = { +// val string = "$d%+ (x" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest6() = { +// val string = "$f%,(a" +// assertNotCompile("f2string") +// } + +// @Test def flagMismatchTest7() = { +// val string = "$t%#+ 0,(tT" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest1() = { +// val string = "$c%.2c" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest2() = { +// val string = "$d%.2d" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest3() = { +// val string = "%.2%" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest4() = { +// val string = "%.2n" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest5() = { +// val string = "$f%.2a" +// assertNotCompile("f2string") +// } + +// @Test def badPrecisionTest6() = { +// val string = "$t%.2tT" +// assertNotCompile("f2string") +// } + +// @Test def badIndexTest1() = { +// val string = "% Int + and value strToInt1 of type String => Int + are possible conversion functions from String to Int + f"$s%d" + ^ +stringinterpolation_macro-neg.scala:44: error: illegal conversion character 'i' + f"$s%i" + ^ +stringinterpolation_macro-neg.scala:47: error: Illegal flag '+' + f"$s%+ 0,(s" + ^ +stringinterpolation_macro-neg.scala:47: error: Illegal flag ' ' + f"$s%+ 0,(s" + ^ +stringinterpolation_macro-neg.scala:47: error: Illegal flag '0' + f"$s%+ 0,(s" + ^ +stringinterpolation_macro-neg.scala:47: error: Illegal flag ',' + f"$s%+ 0,(s" + ^ +stringinterpolation_macro-neg.scala:47: error: Illegal flag '(' + f"$s%+ 0,(s" + ^ +stringinterpolation_macro-neg.scala:48: error: Only '-' allowed for c conversion + f"$c%#+ 0,(c" + ^ +stringinterpolation_macro-neg.scala:49: error: # not allowed for d conversion + f"$d%#d" + ^ +stringinterpolation_macro-neg.scala:50: error: ',' only allowed for d conversion of integral types + f"$d%,x" + ^ +stringinterpolation_macro-neg.scala:51: error: only use '+' for BigInt conversions to o, x, X + f"$d%+ (x" + ^ +stringinterpolation_macro-neg.scala:51: error: only use ' ' for BigInt conversions to o, x, X + f"$d%+ (x" + ^ +stringinterpolation_macro-neg.scala:51: error: only use '(' for BigInt conversions to o, x, X + f"$d%+ (x" + ^ +stringinterpolation_macro-neg.scala:52: error: ',' not allowed for a, A + f"$f%,(a" + ^ +stringinterpolation_macro-neg.scala:52: error: '(' not allowed for a, A + f"$f%,(a" + ^ +stringinterpolation_macro-neg.scala:53: error: Only '-' allowed for date/time conversions + f"$t%#+ 0,(tT" + ^ +stringinterpolation_macro-neg.scala:56: error: precision not allowed + f"$c%.2c" + ^ +stringinterpolation_macro-neg.scala:57: error: precision not allowed + f"$d%.2d" + ^ +stringinterpolation_macro-neg.scala:58: error: precision not allowed + f"%.2%" + ^ +stringinterpolation_macro-neg.scala:59: error: precision not allowed + f"%.2n" + ^ +stringinterpolation_macro-neg.scala:60: error: precision not allowed + f"$f%.2a" + ^ +stringinterpolation_macro-neg.scala:61: error: precision not allowed + f"$t%.2tT" + ^ +stringinterpolation_macro-neg.scala:64: error: No last arg + f"% 1 + // implicit val strToInt2 = (s: String) => 2 + // f2"$s%d" + // } + + // f2"$s%i" + + // 3) flag mismatches + // f2"$s%+ 0,(s" + // f2"$c%#+ 0,(c" + // f2"$d%#d" + // f2"$d%,x" + // f2"$d%+ (x" + // f2"$f%,(a" + // f2"$t%#+ 0,(tT" + + // 4) bad precisions + // f2"$c%.2c" + // f2"$d%.2d" + // f2"%.2%" + // f2"%.2n" + // f2"$f%.2a" + // f2"$t%.2tT" + + // 5) bad indexes + // f2"% println(e) } + try { println(s"""Bob is ${s"$n"} years ${s"$old"}!""") } catch catcher + // try { println(s"""Bob is ${f2"$n"} years ${s"$old"}!""") } catch catcher + // try { println(f2"""Bob is ${s"$n"} years ${s"$old"}!""") } catch catcher + // try { println(f2"""Bob is ${f2"$n"} years ${s"$old"}!""") } catch catcher + // try { println(f2"""Bob is ${f2"$n%2d"} years ${s"$old"}!""") } catch catcher + // try { println(f2"""Bob is ${s"$n%2d"} years ${s"$old"}!""") } catch catcher + // try { println(s"""Bob is ${f2"$n%2d"} years ${s"$old"}!""") } catch catcher + try { println(s"""Bob is ${s"$n%2d"} years ${s"$old"}!""") } catch catcher + } + + test1(1) + println("===============") + test1(12) + println("===============") + test1(123) + } + + def main(args: Array[String]): Unit = { + println(f2"integer: ${5}%d") + println(f2"string: ${"l"}%s") + println(f2"${5}%s, ${6}%d, ${"hello"}%s") + + val x = 5 + println(f2"$x%d") + // println(f2"${x + 1}%d") + + // ported from scalac + testA + testB + testC + } + } \ No newline at end of file From 3ee576e034d5d87e5012aff02c09a15eb53414a5 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 14 May 2019 16:31:25 +0200 Subject: [PATCH 02/36] Add positive tests --- .../dotty/internal/StringContext.scala | 16 +- .../scala/internal/quoted/Matcher.scala | 2 +- .../NegativeTests.scala | 0 tests/run-macros/f-interpolator-tests.check | 70 +++++++ tests/run-macros/f-interpolator-tests.scala | 193 ++++++++++++++++-- .../stringinterpolation_macro-pos.check | 58 ------ .../stringinterpolation_macro-pos.scala | 105 ---------- 7 files changed, 251 insertions(+), 193 deletions(-) rename tests/run-macros/{f-interpolator => f-interpolator-neg}/NegativeTests.scala (100%) create mode 100644 tests/run-macros/f-interpolator-tests.check delete mode 100644 tests/run-macros/stringinterpolation_macro-pos.check delete mode 100644 tests/run-macros/stringinterpolation_macro-pos.scala diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index bc64e427d02f..10a2f9155c97 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -82,15 +82,13 @@ object StringContext { * @throws an Exception if the given Expr does not correspond to a StringContext */ private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { - import reflect._ - println(strCtxExpr.unseal.show) - println(strCtxExpr.unseal.showExtractors) - println() - strCtxExpr match { - case '{ StringContext($partsExpr: _*) } => - val ExprSeq(parts) = partsExpr - parts.toList - // case '{ new StringContext(${ExprSeq(parts)}: _*) } => parts.toList + import reflect._ //TODO : keep comment - bootstraping issues + strCtxExpr.unseal.underlyingArgument match { + case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") + } case _ => throw new Exception("Expected statically known String Context") } } diff --git a/library/src-3.x/scala/internal/quoted/Matcher.scala b/library/src-3.x/scala/internal/quoted/Matcher.scala index 4520664a633b..9f4a7919e3b9 100644 --- a/library/src-3.x/scala/internal/quoted/Matcher.scala +++ b/library/src-3.x/scala/internal/quoted/Matcher.scala @@ -8,7 +8,7 @@ import scala.tasty._ object Matcher { - private final val debug = true + private final val debug = false /** Pattern matches an the scrutineeExpr aquainsnt the patternExpr and returns a tuple * with the matched holes if successful. diff --git a/tests/run-macros/f-interpolator/NegativeTests.scala b/tests/run-macros/f-interpolator-neg/NegativeTests.scala similarity index 100% rename from tests/run-macros/f-interpolator/NegativeTests.scala rename to tests/run-macros/f-interpolator-neg/NegativeTests.scala diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check new file mode 100644 index 000000000000..944033686970 --- /dev/null +++ b/tests/run-macros/f-interpolator-tests.check @@ -0,0 +1,70 @@ +integer: 5 +string: l +5, 6, hello +5 +6 +Bob is 1 years old +Bob will be 2 years old +1+1 = 2 +Bob is 12 years old +Bob will be 13 years old +12+1 = 13 +Bob is 123 years old +Bob will be 124 years old +123+1 = 124 +Best price: 10.00 +10.00% discount included +Best price: 13.35 +13.35% discount included +Bob is 1 years old! +Bob is 1 years old! +Bob is 1 years old! +Bob is 1%2d years old! +=============== +Bob is 12 years old! +Bob is 12 years old! +Bob is 12 years old! +Bob is 12%2d years old! +=============== +Bob is 123 years old! +Bob is 123 years old! +Bob is 123 years old! +Bob is 123%2d years old! +The boolean is false +The boolean is true +a +The string is null +The string is +The string is string1 +The string is null +The string is +The string is string2 +The unicode character is c +The decimal integer is 2, 2, 2+1 = 3 +The octal integer is 2 +The octal integer is 10 +The hexadecimal integer is 2 +The hexadecimal integer is 20 +The scientific notation is 2.000000e+00 +The scientific notation is 5.430000e-01 +The decimal floating point is 1234.567749, 1234.57, 1234.568 +The decimal floating point is 10 +The float value is 0.000100000 +The float value is 1.00000e-05 +The float value is -0x1.4f8b58p-17 +The float value is 0x0.0p0 +The float value is NaN +The float value is Infinity +The float value is -Infinity +10 +10 +10 +10 +20 +56 +000 +000000000 +am +the percentage is 100 % +we have a line separator now %n and now, we are on the next line +a b b diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index 561b529e9714..ae5ebe8a0626 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -3,27 +3,47 @@ * The tests are sorted by argument category as the arguments are on https://docs.oracle.com/javase/6/docs/api/java/util/Formatter.html#detail * * - * Some also test (briefly) the three other macros that are implemented ; namely the s, the raw and the foo interpolators. + * Some tests come from https://github.com/lampepfl/dotty/pull/3894/files */ - object Test { def main(args: Array[String]) = { - def test1(n: Int) = { - println(s"Bob is $n years old") - println(f"Bob is $n%2d years old") - println(s"Bob will be ${n+1} years old") - // println(f"Bob will be ${n+1}%2d years old") - println(s"$n+1 = ${n+1}") - // println(f"$n%d+1 = ${n+1}%d") - } + println(f"integer: ${5}%d") + println(f"string: ${"l"}%s") + println(f"${5}%s, ${6}%d, ${"hello"}%s") + + val x = 5 + println(f"$x%d") + println(f"${x + 1}%d") + multilineTests + generalArgsTests + characterArgsTests + integralArgsTests + floatingPointArgsTests + dateArgsTests + specificLiteralsTests + argumentsTests + } + + def multilineTests = { + def test1(n: Int) = { + println(f"""Bob is $n%2d years old""") + println(f"""Bob will be ${n+1}%2d years old""") + println(f"""$n%d+1 = ${n+1}%d""") + } def test2(f: Float) = { - println(s"Best price: $f") - println(f"Best price: $f%.2f") - println(s"$f% discount included") - println(f"$f%3.2f%% discount included") + println(f"""Best price: $f%.2f""") + println(f"""$f%3.2f%% discount included""") } + def test3(n: Int) = { + val old = "old" + val catcher: PartialFunction[Throwable, Unit] = { case e => println(e) } + try { println(f"""Bob is ${s"$n"} years ${s"$old"}!""") } catch catcher + try { println(f"""Bob is ${f"$n"} years ${s"$old"}!""") } catch catcher + try { println(f"""Bob is ${f"$n%2d"} years ${s"$old"}!""") } catch catcher + try { println(f"""Bob is ${s"$n%2d"} years ${s"$old"}!""") } catch catcher + } test1(1) test1(12) @@ -32,11 +52,144 @@ object Test { test2(10.0f) test2(13.345f) - println(s"") - println(s"${0}") - println(s"${0}${0}") - println(f"") - println(f"${0}") - println(f"${0}${0}") + test3(1) + println("===============") + test3(12) + println("===============") + test3(123) + } + + def generalArgsTests = { + + def booleanTest(b : Boolean) = println(f"The boolean is $b%b") + + def hTest(arg : Int) = println(f"$arg%h") + + def stringTest(s : String) = println(f"The string is $s%s") + + def noFormatHasStringDefault(s : String) = println(f"The string is $s") + + booleanTest(false) + booleanTest(true) + + hTest(10) + + stringTest(null) + stringTest("") + stringTest("string1") + + noFormatHasStringDefault(null) + noFormatHasStringDefault("") + noFormatHasStringDefault("string2") + } + + def characterArgsTests = { + + def charTest(c : Char) = println(f"The unicode character is $c%c") + + charTest('c') + } + + def integralArgsTests = { + + def decimalIntegerTest(i : Int) = println(f"The decimal integer is $i%d, $i%2d, $i%d+1 = ${i+1}%d") + + def octalIntegerTest(i : Int) = println(f"The octal integer is $i%o") + + def hexadecimalIntegerTest(i : Int) = println(f"The hexadecimal integer is $i%x") + + decimalIntegerTest(2) + + octalIntegerTest(2) + octalIntegerTest(8) + + hexadecimalIntegerTest(2) + hexadecimalIntegerTest(32) + } + + def floatingPointArgsTests = { + def scientificNotationTest(f : Float) = println(f"The scientific notation is $f%e") + + def decimalFloatingPointTest(f : Float) = println(f"The decimal floating point is $f%f, $f%3.2f, $f%15.3f") + + def noPointFloatingPointTest(f : Float) = println(f"The decimal floating point is $f%2.0f") + + def gTest(f : Float) = println(f"The float value is $f%g") + + def aTest(f : Float) = println(f"The float value is $f%a") + + scientificNotationTest(2f) + scientificNotationTest(0.543f) + + decimalFloatingPointTest(1234.5678f) + + noPointFloatingPointTest(10.0f) + + val i = Math.pow(10.0, -4).floatValue + + gTest(i) + gTest(i/10.0f) + + aTest(-i/10.0f) + aTest(+0f) + aTest(Float.NaN) + aTest(Float.PositiveInfinity) + aTest(Float.NegativeInfinity) + } + + def dateArgsTests = { + import java.text.SimpleDateFormat + + val sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss") + val dateInString = "31-08-1982 10:20:56" + val date = sdf.parse(dateInString) + + println(f"$date%tH") + println(f"$date%tI") + println(f"$date%tk") + println(f"$date%tl") + println(f"$date%tM") + println(f"$date%tS") + println(f"$date%tL") + println(f"$date%tN") + println(f"$date%tp") + // println(f"$date%tz") + // println(f"$date%tZ") + // println(f"$date%ts") + // println(f"$date%tQ") + // println(f"$date%tB") + // println(f"$date%tb") + // println(f"$date%th") + // println(f"$date%tA") + // println(f"$date%ta") + // println(f"$date%tC") + // println(f"$date%tY") + // println(f"$date%ty") + // println(f"$date%tj") + // println(f"$date%tm") + // println(f"$date%td") + // println(f"$date%te") + // println(f"$date%tR") + // println(f"$date%tT") + // println(f"$date%tr") + // println(f"$date%tD") + // println(f"$date%tF") + // println(f"$date%tc") + } + + def specificLiteralsTests = { + def percentArgsTest = println(f"the percentage is 100 %%") + + def lineSeparatorArgs = println(f"we have a line separator now %%n and now, we are on the next line") + + def nothingTest = println(f"we have nothing") + + percentArgsTest + lineSeparatorArgs + } + + def argumentsTests = { + println(f"${"a"}%s ${"b"}%s % println(e) } - try { println(s"""Bob is ${s"$n"} years ${s"$old"}!""") } catch catcher - // try { println(s"""Bob is ${f2"$n"} years ${s"$old"}!""") } catch catcher - // try { println(f2"""Bob is ${s"$n"} years ${s"$old"}!""") } catch catcher - // try { println(f2"""Bob is ${f2"$n"} years ${s"$old"}!""") } catch catcher - // try { println(f2"""Bob is ${f2"$n%2d"} years ${s"$old"}!""") } catch catcher - // try { println(f2"""Bob is ${s"$n%2d"} years ${s"$old"}!""") } catch catcher - // try { println(s"""Bob is ${f2"$n%2d"} years ${s"$old"}!""") } catch catcher - try { println(s"""Bob is ${s"$n%2d"} years ${s"$old"}!""") } catch catcher - } - - test1(1) - println("===============") - test1(12) - println("===============") - test1(123) - } - - def main(args: Array[String]): Unit = { - println(f2"integer: ${5}%d") - println(f2"string: ${"l"}%s") - println(f2"${5}%s, ${6}%d, ${"hello"}%s") - - val x = 5 - println(f2"$x%d") - // println(f2"${x + 1}%d") - - // ported from scalac - testA - testB - testC - } - } \ No newline at end of file From 55a871d463a320d50090689c28ca8449ca8f00bc Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Thu, 16 May 2019 12:24:44 +0200 Subject: [PATCH 03/36] Add negative tests but it fails due to imports To ask : how to import the macro without copying it? why do this import fail? --- .../dotty/internal/StringContext.scala | 1354 ++++++++--------- .../f-interpolator-neg/Macros_1.scala | 724 +++++++++ .../f-interpolator-neg/NegativeTests.scala | 232 --- .../f-interpolator-neg/Tests_2.scala | 124 ++ 4 files changed, 1525 insertions(+), 909 deletions(-) create mode 100644 tests/run-macros/f-interpolator-neg/Macros_1.scala delete mode 100644 tests/run-macros/f-interpolator-neg/NegativeTests.scala create mode 100644 tests/run-macros/f-interpolator-neg/Tests_2.scala diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 10a2f9155c97..cbea7444b3a1 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -14,775 +14,775 @@ object StringContext { } /** This object implements the f interpolator macro. - * - * This kind of interpolator lets the user prepend f to any string literal to create formatted strings. - * Every variable used in the string literal should have a format, like "%d" for integers, "%2.2f", etc. - * For example, f"$name%s is $height%2.2f meters tall") will return "James is 1.90 meters tall" - */ - object fImpl{ - - /** This trait defines a tool to report errors/warnings that do not depend on Position. */ - trait Reporter{ - - /** Reports error/warning of size 1 linked with a part of the StringContext. - * - * @param message the message to report as error/warning - * @param index the index of the part inside the list of parts of the StringContext - * @param offset the index in the part String where the error is - * @return an error/warning depending on the function - */ - def partError(message : String, index : Int, offset : Int) : Unit - def partWarning(message : String, index : Int, offset : Int) : Unit - - /** Reports error/warning linked with an argument to format. - * - * @param message the message to report as error/warning - * @param index the index of the argument inside the list of arguments of the format function - * @return an error/warning depending on the function - */ - def argError(message : String, index : Int) : Unit - def argWarning(message : String, index : Int) : Unit - - /** Reports error linked with the list of arguments or the StringContext. - * - * @param message the message to report in the error - * @return an error - */ - def strCtxError(message : String) : Unit - def argsError(message : String) : Unit - - /** Claims whether an error or a warning has been reported - * - * @return true if an error/warning has been reported, false - */ - def hasReported() : Boolean + * + * This kind of interpolator lets the user prepend f to any string literal to create formatted strings. + * Every variable used in the string literal should have a format, like "%d" for integers, "%2.2f", etc. + * For example, f"$name%s is $height%2.2f meters tall") will return "James is 1.90 meters tall" + */ +object fImpl{ - /** Stores the old value of the reported and reset it to false */ - def resetReported() : Unit + /** This trait defines a tool to report errors/warnings that do not depend on Position. */ + trait Reporter{ - /** Restores the value of the reported boolean that has been reset */ - def restoreReported() : Unit - } - - /** Retrieves a String from an Expr containing it + /** Reports error/warning of size 1 linked with a part of the StringContext. * - * @param expression the Expr containing the String - * @return the String contained in the given Expr - * @throws an Exception if the given Expr does not contain a String + * @param message the message to report as error/warning + * @param index the index of the part inside the list of parts of the StringContext + * @param offset the index in the part String where the error is + * @return an error/warning depending on the function */ - private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { - case Const(string : String) => string - case _ => throw new Exception("Expected statically known part list") - } + def partError(message : String, index : Int, offset : Int) : Unit + def partWarning(message : String, index : Int, offset : Int) : Unit - /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String + /** Reports error/warning linked with an argument to format. * - * @param strCtxExpr the Expr containing the StringContext - * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext - * @throws an Exception if the given Expr does not correspond to a StringContext + * @param message the message to report as error/warning + * @param index the index of the argument inside the list of arguments of the format function + * @return an error/warning depending on the function */ - private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { - import reflect._ //TODO : keep comment - bootstraping issues - strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") - } - case _ => throw new Exception("Expected statically known String Context") - } - } + def argError(message : String, index : Int) : Unit + def argWarning(message : String, index : Int) : Unit - /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments + /** Reports error linked with the list of arguments or the StringContext. * - * @param argsExpr the Expr containing the list of arguments - * @return a list of Expr containing arguments - * @throws an Exception if the given Expr does not contain a list of arguments + * @param message the message to report in the error + * @return an error */ - private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { - import reflect._ - argsExpr.unseal.underlyingArgument match { - case Typed(Repeated(args, _), _) => args.map(_.seal) - case tree => throw new Exception("Expected statically known argument list") - } - } + def strCtxError(message : String) : Unit + def argsError(message : String) : Unit - /** Lifts a StringContext from StringContext to Expr of StringContext + /** Claims whether an error or a warning has been reported * - * @return a Liftable StringContext by redefining the toExpr function + * @return true if an error/warning has been reported, false */ - private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { - def toExpr(strCtx: StringContext): Expr[StringContext] = { - implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { - override def toExpr(list: List[String]): Expr[List[String]] = list match { - case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} - case Nil => '{Nil} - } - } - '{StringContext(${strCtx.parts.toList.toExpr}: _*)} - } - } + def hasReported() : Boolean - /** Interpolates the arguments to the formatting String given inside a StringContext - * - * @param strCtxExpr the Expr that holds the StringContext which contains all the chunks of the formatting string - * @param args the Expr that holds the sequence of arguments to interpolate to the String in the correct format - * @return the Expr containing the formatted and interpolated String or an error/warning if the parameters are not correct - */ - private def interpolate(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): Expr[String] = { - import reflect._ - val sourceFile = strCtxExpr.unseal.pos.sourceFile - - val partsExpr = getListOfExpr(strCtxExpr) - val args = getArgsList(argsExpr) - - val reporter = new Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - val positionStart = partsExpr(index).unseal.pos.start + offset - reflect.error(message, sourceFile, positionStart, positionStart) - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - val positionStart = partsExpr(index).unseal.pos.start + offset - reflect.warning(message, sourceFile, positionStart, positionStart) - } + /** Stores the old value of the reported and reset it to false */ + def resetReported() : Unit - def argError(message : String, index : Int) : Unit = { - reported = true - reflect.error(message, args(index).unseal.pos) - } - def argWarning(message : String, index : Int) : Unit = { - reported = true - reflect.warning(message, args(index).unseal.pos) - } + /** Restores the value of the reported boolean that has been reset */ + def restoreReported() : Unit + } - def strCtxError(message : String) : Unit = { - reported = true - val positionStart = strCtxExpr.unseal.pos.start - reflect.error(message, sourceFile, positionStart, positionStart) - } - def argsError(message : String) : Unit = { - reported = true - reflect.error(message, argsExpr.unseal.pos) - } + /** Retrieves a String from an Expr containing it + * + * @param expression the Expr containing the String + * @return the String contained in the given Expr + * @throws an Exception if the given Expr does not contain a String + */ + private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { + case Const(string : String) => string + case _ => throw new Exception("Expected statically known part list") + } - def hasReported() : Boolean = { - reported + /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String + * + * @param strCtxExpr the Expr containing the StringContext + * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext + * @throws an Exception if the given Expr does not correspond to a StringContext + */ + private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + import reflect._ + strCtxExpr.unseal.underlyingArgument match { + case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") } + case _ => throw new Exception("Expected statically known String Context") + } + } - def resetReported() : Unit = { - oldReported = reported - reported = false - } + /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments + * + * @param argsExpr the Expr containing the list of arguments + * @return a list of Expr containing arguments + * @throws an Exception if the given Expr does not contain a list of arguments + */ + private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + import reflect._ + argsExpr.unseal.underlyingArgument match { + case Typed(Repeated(args, _), _) => args.map(_.seal) + case tree => throw new Exception("Expected statically known argument list") + } + } - def restoreReported() : Unit = { - reported = oldReported + /** Lifts a StringContext from StringContext to Expr of StringContext + * + * @return a Liftable StringContext by redefining the toExpr function + */ + private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { + def toExpr(strCtx: StringContext): Expr[StringContext] = { + implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { + override def toExpr(list: List[String]): Expr[List[String]] = list match { + case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} + case Nil => '{Nil} } } - - interpolate(partsExpr, args, argsExpr, reporter) + '{StringContext(${strCtx.parts.toList.toExpr}: _*)} } + } - /** Helper function for the interpolate function above - * - * @param partsExpr the list of parts enumerated as Expr - * @param args the list of arguments enumerated as Expr - * @param reporter the reporter to return any error/warning when a problem is encountered - * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct - */ - private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { - import reflect.{Literal => LiteralTree, _} + /** Interpolates the arguments to the formatting String given inside a StringContext + * + * @param strCtxExpr the Expr that holds the StringContext which contains all the chunks of the formatting string + * @param args the Expr that holds the sequence of arguments to interpolate to the String in the correct format + * @return the Expr containing the formatted and interpolated String or an error/warning if the parameters are not correct + */ + private def interpolate(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): Expr[String] = { + import reflect._ + val sourceFile = strCtxExpr.unseal.pos.sourceFile + + val partsExpr = getListOfExpr(strCtxExpr) + val args = getArgsList(argsExpr) + + val reporter = new Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + val positionStart = partsExpr(index).unseal.pos.start + offset + reflect.error(message, sourceFile, positionStart, positionStart) + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + val positionStart = partsExpr(index).unseal.pos.start + offset + reflect.warning(message, sourceFile, positionStart, positionStart) + } - /** Checks whether a part contains a formatting substring - * - * @param part the part to check - * @param l the length of the given part - * @param index the index where to start to look for a potential new formatting string - * @return an Option containing the index in the part where a new formatting String starts, None otherwise - */ - def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { - var i = index - var result : Option[Int] = None - while (i < l){ - if (part.charAt(i) == '%' && result.isEmpty) - result = Some(i) - i += 1 - } - result + def argError(message : String, index : Int) : Unit = { + reported = true + reflect.error(message, args(index).unseal.pos) + } + def argWarning(message : String, index : Int) : Unit = { + reported = true + reflect.warning(message, args(index).unseal.pos) } - /** Adds the default "%s" to the Strings that do not have any given format - * - * @param parts the list of parts contained in the StringContext - * @return a new list of string with all a defined formatting or reports an error if the '%' and - * formatting parameter are too far away from the argument that they refer to - * For example : f2"${d}random-leading-junk%d" will lead to an error - */ - def addDefaultFormat(parts : List[String]) : List[String] = parts match { - case Nil => Nil - case p :: parts1 => p :: parts1.map((part : String) => { - if (!part.startsWith("%")) { - val index = part.indexOf('%') - if (index != -1) { - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) - "%s" + part - } else "%s" + part - } else part - }) + def strCtxError(message : String) : Unit = { + reported = true + val positionStart = strCtxExpr.unseal.pos.start + reflect.error(message, sourceFile, positionStart, positionStart) + } + def argsError(message : String) : Unit = { + reported = true + reflect.error(message, argsExpr.unseal.pos) } - /** Finds all the flags that are inside a formatting String from a given index - * - * @param i the index in the String s where to start to check - * @param l the length of s - * @param s the String to check - * @return a list containing all the flags that are inside the formatting String, - * and their index in the String - */ - def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { - def isFlag(c : Char) : Boolean = c match { - case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true - case _ => false - } - if (i < l && isFlag(s.charAt(i))) (s.charAt(i), i) :: getFlags(i + 1, l, s) - else Nil + def hasReported() : Boolean = { + reported } - /** Skips the Characters that are width or argumentIndex parameters - * - * @param i the index where to start checking in the given String - * @param s the String to check - * @param l the length of s - * @return a tuple containing the index in the String after skipping - * the parameters, true if it has a width parameter and its value, false otherwise - */ - def skipWidth(i : Int, s : String, l : Int) = { - var j = i - var width = (false, 0) - while (j < l && Character.isDigit(s.charAt(j))){ - width = (true, j) - j += 1 - } - (j, width._1, width._2) + def resetReported() : Unit = { + oldReported = reported + reported = false } - /** Retrieves all the formatting parameters from a part and their index in it - * - * @param part the String containing the formatting parameters - * @param argIndex the index of the current argument inside the list of arguments to interpolate - * @param partIndex the index of the current part inside the list of parts in the StringContext - * @param noArg true if there is no arg, i.e. "%%" or "%n" - * @param pos the initial index where to start checking the part - * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion - * parameter is missing. Otherwise, - * the index where the format specifier substring is, - * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and - * flags that contains the list of flags (empty if there is none), - * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), - * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), - * hasRelative (true if the specifiers use relative indexing, false otherwise) and - * conversion character index - */ - def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { - var conversion = pos - var hasArgumentIndex = false - var argumentIndex = pos - var hasPrecision = false - var precision = pos - val l = part.length - - if (l >= 1 && part.charAt(conversion) == '%') - conversion += 1 - else if (!noArg) - reporter.argError("too many arguments for interpolated string", argIndex) + def restoreReported() : Unit = { + reported = oldReported + } + } - //argument index or width - val (i, hasWidth1, width1) = skipWidth(conversion, part, l) - conversion = i + interpolate(partsExpr, args, argsExpr, reporter) + } - //argument index - if (conversion < l && part.charAt(conversion) == '$'){ - hasArgumentIndex = true - argumentIndex = width1 - conversion += 1 - } + /** Helper function for the interpolate function above + * + * @param partsExpr the list of parts enumerated as Expr + * @param args the list of arguments enumerated as Expr + * @param reporter the reporter to return any error/warning when a problem is encountered + * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct + */ + private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { + import reflect.{Literal => LiteralTree, _} - //relative indexing - val hasRelative = conversion < l && part.charAt(conversion) == '<' - val relativeIndex = conversion + 1 - if (hasRelative) - conversion += 1 + /** Checks whether a part contains a formatting substring + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ + def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { + var i = index + var result : Option[Int] = None + while (i < l){ + if (part.charAt(i) == '%' && result.isEmpty) + result = Some(i) + i += 1 + } + result + } - //flags - val flags = getFlags(conversion, l, part) - conversion += flags.size + /** Adds the default "%s" to the Strings that do not have any given format + * + * @param parts the list of parts contained in the StringContext + * @return a new list of string with all a defined formatting or reports an error if the '%' and + * formatting parameter are too far away from the argument that they refer to + * For example : f2"${d}random-leading-junk%d" will lead to an error + */ + def addDefaultFormat(parts : List[String]) : List[String] = parts match { + case Nil => Nil + case p :: parts1 => p :: parts1.map((part : String) => { + if (!part.startsWith("%")) { + val index = part.indexOf('%') + if (index != -1) { + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) + "%s" + part + } else "%s" + part + } else part + }) + } + + /** Finds all the flags that are inside a formatting String from a given index + * + * @param i the index in the String s where to start to check + * @param l the length of s + * @param s the String to check + * @return a list containing all the flags that are inside the formatting String, + * and their index in the String + */ + def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { + def isFlag(c : Char) : Boolean = c match { + case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true + case _ => false + } + if (i < l && isFlag(s.charAt(i))) (s.charAt(i), i) :: getFlags(i + 1, l, s) + else Nil + } - //width - val (j, hasWidth2, width2) = skipWidth(conversion, part, l) - conversion = j + /** Skips the Characters that are width or argumentIndex parameters + * + * @param i the index where to start checking in the given String + * @param s the String to check + * @param l the length of s + * @return a tuple containing the index in the String after skipping + * the parameters, true if it has a width parameter and its value, false otherwise + */ + def skipWidth(i : Int, s : String, l : Int) = { + var j = i + var width = (false, 0) + while (j < l && Character.isDigit(s.charAt(j))){ + width = (true, j) + j += 1 + } + (j, width._1, width._2) + } + + /** Retrieves all the formatting parameters from a part and their index in it + * + * @param part the String containing the formatting parameters + * @param argIndex the index of the current argument inside the list of arguments to interpolate + * @param partIndex the index of the current part inside the list of parts in the StringContext + * @param noArg true if there is no arg, i.e. "%%" or "%n" + * @param pos the initial index where to start checking the part + * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion + * parameter is missing. Otherwise, + * the index where the format specifier substring is, + * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and + * flags that contains the list of flags (empty if there is none), + * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), + * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), + * hasRelative (true if the specifiers use relative indexing, false otherwise) and + * conversion character index + */ + def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { + var conversion = pos + var hasArgumentIndex = false + var argumentIndex = pos + var hasPrecision = false + var precision = pos + val l = part.length + + if (l >= 1 && part.charAt(conversion) == '%') + conversion += 1 + else if (!noArg) + reporter.argError("too many arguments for interpolated string", argIndex) + + //argument index or width + val (i, hasWidth1, width1) = skipWidth(conversion, part, l) + conversion = i + + //argument index + if (conversion < l && part.charAt(conversion) == '$'){ + hasArgumentIndex = true + argumentIndex = width1 + conversion += 1 + } - //precision - if (conversion < l && part.charAt(conversion) == '.') { - precision = conversion + //relative indexing + val hasRelative = conversion < l && part.charAt(conversion) == '<' + val relativeIndex = conversion + 1 + if (hasRelative) + conversion += 1 + + //flags + val flags = getFlags(conversion, l, part) + conversion += flags.size + + //width + val (j, hasWidth2, width2) = skipWidth(conversion, part, l) + conversion = j + + //precision + if (conversion < l && part.charAt(conversion) == '.') { + precision = conversion + conversion += 1 + hasPrecision = true + val oldConversion = conversion + while (conversion < l && Character.isDigit(part.charAt(conversion))) { conversion += 1 - hasPrecision = true - val oldConversion = conversion - while (conversion < l && Character.isDigit(part.charAt(conversion))) { - conversion += 1 - } - if (oldConversion == conversion) { - reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) - hasPrecision = false - } } + if (oldConversion == conversion) { + reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) + hasPrecision = false + } + } - //conversion - if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) - reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) + //conversion + if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) + reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) - val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 - val width = if (hasWidth1 && !hasArgumentIndex) width1 else width2 - (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) - } + val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 + val width = if (hasWidth1 && !hasArgumentIndex) width1 else width2 + (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) + } - /** Checks if the number of arguments are the same as the number of formatting strings - * - * @param format the number of formatting parts in the StringContext - * @param argument the number of arguments to interpolate in the string - * @return reports an error if the number of arguments does not match with the number of formatting strings, - * nothing otherwise - */ - def checkSizes(format : Int, argument : Int) : Unit = { - if (format > argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too few arguments for interpolated string") - else - reporter.argError("too few arguments for interpolated string", argument - 1) - if (format < argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too many arguments for interpolated string") - else - reporter.argError("too many arguments for interpolated string", argument - 1) - if (format == -1) - reporter.strCtxError("there are no parts") - } + /** Checks if the number of arguments are the same as the number of formatting strings + * + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise + */ + def checkSizes(format : Int, argument : Int) : Unit = { + if (format > argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too few arguments for interpolated string") + else + reporter.argError("too few arguments for interpolated string", argument - 1) + if (format < argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too many arguments for interpolated string") + else + reporter.argError("too many arguments for interpolated string", argument - 1) + if (format == -1) + reporter.strCtxError("there are no parts") + } - /** Checks if a given type is a subtype of any of the possibilities - * - * @param actualType the given type - * @param expectedType the type we are expecting - * @param argIndex the index of the argument that should type check - * @param possibilities all the types within which we want to find a super type of the actualType - * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, - * nothing otherwise - */ - def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { - if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) - } + /** Checks if a given type is a subtype of any of the possibilities + * + * @param actualType the given type + * @param expectedType the type we are expecting + * @param argIndex the index of the argument that should type check + * @param possibilities all the types within which we want to find a super type of the actualType + * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, + * nothing otherwise + */ + def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { + if (possibilities.find(actualType <:< _).isEmpty) + reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) + } - /** Checks whether a given argument index, relative or not, is in the correct bounds - * - * @param partIndex the index of the part we are checking - * @param offset the index in the part where there might be an error - * @param relative true if relative indexing is used, false otherwise - * @param argumentIndex the argument index parameter in the formatting String - * @param expectedArgumentIndex the expected argument index parameter - * @param maxArgumentIndex the maximum argument index parameter that can be used - * @return reports a warning if relative indexing is used but an argument is still given, - * an error is the argument index is not in the bounds [1, number of arguments] - */ - def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { - if (relative) - reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) + /** Checks whether a given argument index, relative or not, is in the correct bounds + * + * @param partIndex the index of the part we are checking + * @param offset the index in the part where there might be an error + * @param relative true if relative indexing is used, false otherwise + * @param argumentIndex the argument index parameter in the formatting String + * @param expectedArgumentIndex the expected argument index parameter + * @param maxArgumentIndex the maximum argument index parameter that can be used + * @return reports a warning if relative indexing is used but an argument is still given, + * an error is the argument index is not in the bounds [1, number of arguments] + */ + def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { + if (relative) + reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) - if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) - reporter.partError("Argument index out of range", partIndex, offset) + if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) + reporter.partError("Argument index out of range", partIndex, offset) - if (expectedArgumentIndex != argumentIndex) - reporter.partWarning("Index is not this arg", partIndex, offset) - } + if (expectedArgumentIndex != argumentIndex) + reporter.partWarning("Index is not this arg", partIndex, offset) + } - /** Checks if a parameter is specified whereas it is not allowed - * - * @param hasParameter true if parameter is specified, false otherwise - * @param partIndex the index of the part inside the parts - * @param offset the index in the part where to report an error - * @param parameter the parameter that is not allowed - * @return reports an error if hasParameter is true, nothing otherwise - */ - def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { - if (hasParameter) - reporter.partError(parameter + " not allowed", partIndex, offset) - } + /** Checks if a parameter is specified whereas it is not allowed + * + * @param hasParameter true if parameter is specified, false otherwise + * @param partIndex the index of the part inside the parts + * @param offset the index in the part where to report an error + * @param parameter the parameter that is not allowed + * @return reports an error if hasParameter is true, nothing otherwise + */ + def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { + if (hasParameter) + reporter.partError(parameter + " not allowed", partIndex, offset) + } - /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error if the flag is not allowed, nothing otherwise - */ - def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { - for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} - reporter.partError(message, partIndex, flag._2) - } + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error if the flag is not allowed, nothing otherwise + */ + def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} + reporter.partError(message, partIndex, flag._2) + } - /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise - */ - def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { - reporter.resetReported() - for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { - if (!reporter.hasReported()) - reporter.partError(message, partIndex, flag._2) - } + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise + */ + def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + reporter.resetReported() + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { if (!reporter.hasReported()) - reporter.restoreReported() + reporter.partError(message, partIndex, flag._2) } + if (!reporter.hasReported()) + reporter.restoreReported() + } - /** Checks all the formatting parameters for a Character conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are different from '-' - */ - def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { - val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") - checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - checkNotAllowedParameter(hasPrecision, partIndex, precisionIndex, "precision") - } + /** Checks all the formatting parameters for a Character conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are different from '-' + */ + def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precisionIndex, "precision") + } - /** Checks all the formatting parameters for an Integral conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are not allowed : - * ’d’: only ’#’ is allowed, - * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step - */ - def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - if (conversionChar == 'd') - checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) + /** Checks all the formatting parameters for an Integral conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are not allowed : + * ’d’: only ’#’ is allowed, + * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step + */ + def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if (conversionChar == 'd') + checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - } + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + } - /** Checks all the formatting parameters for a Floating Point conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' - */ - def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - if(conversionChar == 'a' || conversionChar == 'A'){ - for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} - reporter.partError("'" + flag._1 + "' not allowed for a, A", partIndex, flag._2) - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - } + /** Checks all the formatting parameters for a Floating Point conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' + */ + def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if(conversionChar == 'a' || conversionChar == 'A'){ + for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} + reporter.partError("'" + flag._1 + "' not allowed for a, A", partIndex, flag._2) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") } + } - /** Checks all the formatting parameters for a Time conversion + /** Checks all the formatting parameters for a Time conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part that we are checking + * @param conversionIndex the index of the conversion Char used in the part + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified, if the time suffix is not given/incorrect or if the used flags are + * different from '-' + */ + def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + /** + * Checks whether a time suffix is given and whether it is allowed * - * @param partIndex the index of the part, that we are checking, inside the parts * @param part the part that we are checking - * @param conversionIndex the index of the conversion Char used in the part - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified, if the time suffix is not given/incorrect or if the used flags are - * different from '-' + * @param partIndex the index of the part inside of the parts of the StringContext + * @param conversionIndex the index of the conversion Char inside the part + * @param return reports an error if no suffix is specified or if the given suffix is not + * part of the allowed ones */ - def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - /** - * Checks whether a time suffix is given and whether it is allowed - * - * @param part the part that we are checking - * @param partIndex the index of the part inside of the parts of the StringContext - * @param conversionIndex the index of the conversion Char inside the part - * @param return reports an error if no suffix is specified or if the given suffix is not - * part of the allowed ones - */ - def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { - val partSize = part.size - - if (conversionIndex == partSize - 1) - reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) - - part.charAt(conversionIndex + 1) match { - case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times - case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates - case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times - case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) - } - } - - val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for date/time conversions") - checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - checkTime(part, partIndex, conversionIndex) - } + def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { + val partSize = part.size - /** Checks all the formatting parameters for a General conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @return reports an error - * if '#' flag is used or if any other flag is used - */ - def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { - for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} - reporter.partError("Illegal flag : '" + flag._1 + "'", partIndex, flag._2) - } + if (conversionIndex == partSize - 1) + reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) - /** Checks all the formatting parameters for a special Char such as '%' and end of line - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @param hasWidth true if width parameter is specified, false otherwise - * @param width the index of the width parameter inside the part - * @return reports an error if precision or width is specified for '%' or - * if precision is specified for end of line - */ - def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { - case 'n' => { - checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") - checkNotAllowedParameter(hasWidth, partIndex, width, "width") + part.charAt(conversionIndex + 1) match { + case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times + case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates + case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times + case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) } - case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") - case _ => // OK } - /** Checks whether the format specifiers are correct depending on the conversion parameter - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param part the part to check - * The rest of the inputs correspond to the output of the function getFormatSpecifiers - * @param hasArgumentIndex - * @param actualArgumentIndex - * @param expectedArgumentIndex - * @param maxArgumentIndex - * @param hasRelative - * @param hasWidth - * @param hasPrecision - * @param precision - * @param flags - * @param conversion - * @param argType - * @return the argument index and its type if there is an argument, the flags and the conversion parameter - * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise - */ - def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], - hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { - val conversionChar = part.charAt(conversion) - - if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) - checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) - - conversionChar match { - case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) - case 'd' | 'o' | 'x' | 'X' => checkIntegralConversion(partIndex, argType, conversionChar, flags, hasPrecision, precision) - case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) - case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) - case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) - case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) - case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) - } + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for date/time conversions") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + checkTime(part, partIndex, conversionIndex) + } - (if (argType.isEmpty) None else Some(argType.get, (partIndex - 1)), conversionChar, flags) + /** Checks all the formatting parameters for a General conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @return reports an error + * if '#' flag is used or if any other flag is used + */ + def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { + for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} + reporter.partError("Illegal flag '" + flag._1 + "'", partIndex, flag._2) + } + + /** Checks all the formatting parameters for a special Char such as '%' and end of line + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @param hasWidth true if width parameter is specified, false otherwise + * @param width the index of the width parameter inside the part + * @return reports an error if precision or width is specified for '%' or + * if precision is specified for end of line + */ + def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { + case 'n' => { + checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + checkNotAllowedParameter(hasWidth, partIndex, width, "width") } + case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + case _ => // OK + } - /** Checks whether the argument type, if there is one, type checks with the formatting parameters - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the character used for the conversion - * @param argument an option containing the type and index of the argument, None if there is no argument - * @param flags the flags used for the formatting - * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise - */ - def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { - if (argument.nonEmpty) - checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) - else - checkTypeWithoutArgs(conversionChar, partIndex, flags) + /** Checks whether the format specifiers are correct depending on the conversion parameter + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part to check + * The rest of the inputs correspond to the output of the function getFormatSpecifiers + * @param hasArgumentIndex + * @param actualArgumentIndex + * @param expectedArgumentIndex + * @param maxArgumentIndex + * @param hasRelative + * @param hasWidth + * @param hasPrecision + * @param precision + * @param flags + * @param conversion + * @param argType + * @return the argument index and its type if there is an argument, the flags and the conversion parameter + * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise + */ + def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], + hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { + val conversionChar = part.charAt(conversion) + + if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) + checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) + + conversionChar match { + case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) + case 'd' | 'o' | 'x' | 'X' => checkIntegralConversion(partIndex, argType, conversionChar, flags, hasPrecision, precision) + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) + case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) + case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) + case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) + case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) } - /** Checks whether the argument type checks with the formatting parameters - * - * @param argument the given argument to check - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the argument type does not correspond with the conversion character, - * nothing otherwise - */ - def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { - val booleans = List(definitions.BooleanType, definitions.NullType) - val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) - val floatingPoints = List(definitions.DoubleType, definitions.FloatType, typeOf[java.math.BigDecimal]) - val integral = List(definitions.IntType, definitions.LongType, definitions.ShortType, definitions.ByteType, typeOf[java.math.BigInteger]) - val character = List(definitions.CharType, definitions.ByteType, definitions.ShortType, definitions.IntType) - - val (argType, argIndex) = argument - conversionChar match { - case 'c' | 'C' => checkSubtype(argType, "Char", argIndex, character : _*) - case 'd' | 'o' | 'x' | 'X' => { - checkSubtype(argType, "Int", argIndex, integral : _*) - if (conversionChar != 'd') { - val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), - (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), - ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), - (',', true, "',' only allowed for d conversion of integral types")) - checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - } + (if (argType.isEmpty) None else Some(argType.get, (partIndex - 1)), conversionChar, flags) + } + + /** Checks whether the argument type, if there is one, type checks with the formatting parameters + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the character used for the conversion + * @param argument an option containing the type and index of the argument, None if there is no argument + * @param flags the flags used for the formatting + * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise + */ + def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { + if (argument.nonEmpty) + checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) + else + checkTypeWithoutArgs(conversionChar, partIndex, flags) + } + + /** Checks whether the argument type checks with the formatting parameters + * + * @param argument the given argument to check + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the argument type does not correspond with the conversion character, + * nothing otherwise + */ + def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + val booleans = List(definitions.BooleanType, definitions.NullType) + val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) + val floatingPoints = List(definitions.DoubleType, definitions.FloatType, typeOf[java.math.BigDecimal]) + val integral = List(definitions.IntType, definitions.LongType, definitions.ShortType, definitions.ByteType, typeOf[java.math.BigInteger]) + val character = List(definitions.CharType, definitions.ByteType, definitions.ShortType, definitions.IntType) + + val (argType, argIndex) = argument + conversionChar match { + case 'c' | 'C' => checkSubtype(argType, "Char", argIndex, character : _*) + case 'd' | 'o' | 'x' | 'X' => { + checkSubtype(argType, "Int", argIndex, integral : _*) + if (conversionChar != 'd') { + val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), + (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), + ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) } - case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkSubtype(argType, "Double", argIndex, floatingPoints : _*) - case 't' | 'T' => checkSubtype(argType, "Date", argIndex, dates : _*) - case 'b' | 'B' => checkSubtype(argType, "Boolean", argIndex, booleans : _*) - case 'h' | 'H' | 'S' | 's' => - if (!(argType <:< typeOf[java.util.Formattable])) - for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) - case 'n' | '%' => - case illegal => } + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkSubtype(argType, "Double", argIndex, floatingPoints : _*) + case 't' | 'T' => checkSubtype(argType, "Date", argIndex, dates : _*) + case 'b' | 'B' => checkSubtype(argType, "Boolean", argIndex, booleans : _*) + case 'h' | 'H' | 'S' | 's' => + if (!(argType <:< typeOf[java.util.Formattable])) + for {flag <- flags ; if (flag._1 == '#')} + reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) + case 'n' | '%' => + case illegal => } + } - /** Reports error when the formatting parameter require a specific type but no argument is given - * - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given - * nothing otherwise - */ - def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { - conversionChar match { - case 'o' | 'x' | 'X' => { - val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), - (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), - ('(', true, "Only use '(' for BigInt conversions to o, x, X"), - (',', true, "',' only allowed for d conversion of integral types")) - checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - } - case _ => //OK + /** Reports error when the formatting parameter require a specific type but no argument is given + * + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given + * nothing otherwise + */ + def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + conversionChar match { + case 'o' | 'x' | 'X' => { + val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), + (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), + ('(', true, "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) } + case _ => //OK + } - if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) - } + if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) + } - /** Checks that a given part of the String Context respects every formatting constraint per parameter - * - * @param part a particular part of the String Context - * @param start the index from which we start checking the part - * @param argument an Option containing the argument corresponding to the part and its index in the list of args, - * None if no args are specified. - * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified - * @return a list with all the elements of the conversion per formatting string - */ - def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { - reporter.resetReported() - val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) - if (hasFormattingSubstring.nonEmpty) { - val formattingStart = hasFormattingSubstring.get - var nextStart = formattingStart - - argument match { - case Some(argIndex, arg) => { - val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) - if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) - nextStart = conversion + 1 - conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) - } else checkPart(part, conversion + 1, argument, maxArgumentIndex) - } - case None => { - val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) - if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) - reporter.partError("Argument index out of range", 0, argumentIndex + 1) - if (hasRelative) - reporter.partError("No last arg", 0, relativeIndex) - if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) - nextStart = conversion + 1 - if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) - conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) - } else checkPart(part, conversion + 1, argument, maxArgumentIndex) - } + /** Checks that a given part of the String Context respects every formatting constraint per parameter + * + * @param part a particular part of the String Context + * @param start the index from which we start checking the part + * @param argument an Option containing the argument corresponding to the part and its index in the list of args, + * None if no args are specified. + * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified + * @return a list with all the elements of the conversion per formatting string + */ + def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { + reporter.resetReported() + val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) + if (hasFormattingSubstring.nonEmpty) { + val formattingStart = hasFormattingSubstring.get + var nextStart = formattingStart + + argument match { + case Some(argIndex, arg) => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) + nextStart = conversion + 1 + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + } + case None => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) + if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) + reporter.partError("Argument index out of range", 0, argumentIndex + 1) + if (hasRelative) + reporter.partError("No last arg", 0, relativeIndex) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) + nextStart = conversion + 1 + if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) } - } else { - reporter.restoreReported() - Nil } + } else { + reporter.restoreReported() + Nil } + } - val argument = args.size + val argument = args.size - // add default format - val parts = addDefaultFormat(partsExpr.map(literalToString)) + // add default format + val parts = addDefaultFormat(partsExpr.map(literalToString)) - // check validity of formatting - checkSizes(parts.size - 1, argument) + // check validity of formatting + checkSizes(parts.size - 1, argument) - if (!parts.isEmpty) { - if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ - val argTypeWithConversion = checkPart(parts.head, 0, None, None) + if (!parts.isEmpty) { + if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ + val argTypeWithConversion = checkPart(parts.head, 0, None, None) + if (!reporter.hasReported()) + for ((argument, conversionChar, flags) <- argTypeWithConversion) + checkArgTypeWithConversion(0, conversionChar, argument, flags) + } + else { + val partWithArgs = parts.tail.zip(args) + for (i <- (0 until args.size)){ + val (part, arg) = partWithArgs(i) + val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) if (!reporter.hasReported()) for ((argument, conversionChar, flags) <- argTypeWithConversion) checkArgTypeWithConversion(0, conversionChar, argument, flags) } - else { - val partWithArgs = parts.tail.zip(args) - for (i <- (0 until args.size)){ - val (part, arg) = partWithArgs(i) - val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) - if (!reporter.hasReported()) - for ((argument, conversionChar, flags) <- argTypeWithConversion) - checkArgTypeWithConversion(0, conversionChar, argument, flags) - } - } } - - // macro expansion - '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} } - /** Applies the interpolation to the input of the f-interpolator macro - * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String - * @param argsExpr the Expr containing the list of arguments to interpolate - * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not - * respect the formatting rules - */ - final def apply(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { - import reflect._ - try interpolate(strCtxExpr, args) - catch { - case ex: Exception => - QuoteError(ex.getMessage) - } + // macro expansion + '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} + } + + /** Applies the interpolation to the input of the f-interpolator macro + * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String + * @param argsExpr the Expr containing the list of arguments to interpolate + * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not + * respect the formatting rules + */ + final def apply(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { + import reflect._ + try interpolate(strCtxExpr, args) + catch { + case ex: Exception => + QuoteError(ex.getMessage) } } +} diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala new file mode 100644 index 000000000000..a7689a27a5ca --- /dev/null +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -0,0 +1,724 @@ +import scala.quoted._ +import scala.quoted.autolift._ +import scala.quoted.matching._ +import scala.tasty.Reflection + +import scala.language.implicitConversions + +object TestFooErrors { // Defined in tests + implicit object StringContextOps { + inline def (ctx: => StringContext) foo (args: => Any*): List[(Boolean, Int, Int, Int, String)] = ${ Macro.fooErrors('ctx, 'args) } + } +} + +object Macro { + + /** Retrieves a String from an Expr containing it + * + * @param expression the Expr containing the String + * @return the String contained in the given Expr + * @throws an Exception if the given Expr does not contain a String + */ + private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { + case Const(string : String) => string + case _ => throw new Exception("Expected statically known part list") + } + + /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String + * + * @param strCtxExpr the Expr containing the StringContext + * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext + * @throws an Exception if the given Expr does not correspond to a StringContext + */ + private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + import reflect._ + strCtxExpr.unseal.underlyingArgument match { + case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") + } + case _ => throw new Exception("Expected statically known String Context") + } + } + + /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments + * + * @param argsExpr the Expr containing the list of arguments + * @return a list of Expr containing arguments + * @throws an Exception if the given Expr does not contain a list of arguments + */ + private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + import reflect._ + argsExpr.unseal.underlyingArgument match { + case Typed(Repeated(args, _), _) => args.map(_.seal) + case tree => throw new Exception("Expected statically known argument list") + } + } + + /** Lifts a StringContext from StringContext to Expr of StringContext + * + * @return a Liftable StringContext by redefining the toExpr function + */ + private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { + def toExpr(strCtx: StringContext): Expr[StringContext] = { + implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { + override def toExpr(list: List[String]): Expr[List[String]] = list match { + case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} + case Nil => '{Nil} + } + } + '{StringContext(${strCtx.parts.toList.toExpr}: _*)} + } + } + + def fooErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection): Expr[List[(Boolean, Int, Int, Int, String)]] = { + (strCtxExpr, argsExpr) match { + case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => + val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } + } + + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } + def argWarning(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 1, $index, 0, $message) } + } + + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } + } + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } + } + + def hasReported() : Boolean = { + reported + } + + def resetReported() : Unit = { + oldReported = reported + reported = false + } + + def restoreReported() : Unit = { + reported = oldReported + } + } + val partsExpr = getListOfExpr(strCtxExpr) + val args = getArgsList(argsExpr) + fooCore(partsExpr, args, argsExpr, reporter) // Discard result + errors.result().toExprOfList + } + } + + private def fooCore(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter) given Reflection: Expr[String] = { + import reflect.{Literal => LiteralTree, _} + + /** Checks whether a part contains a formatting substring + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ + def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { + var i = index + var result : Option[Int] = None + while (i < l){ + if (part.charAt(i) == '%' && result.isEmpty) + result = Some(i) + i += 1 + } + result + } + + /** Adds the default "%s" to the Strings that do not have any given format + * + * @param parts the list of parts contained in the StringContext + * @return a new list of string with all a defined formatting or reports an error if the '%' and + * formatting parameter are too far away from the argument that they refer to + * For example : f2"${d}random-leading-junk%d" will lead to an error + */ + def addDefaultFormat(parts : List[String]) : List[String] = parts match { + case Nil => Nil + case p :: parts1 => p :: parts1.map((part : String) => { + if (!part.startsWith("%")) { + val index = part.indexOf('%') + if (index != -1) { + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) + "%s" + part + } else "%s" + part + } else part + }) + } + + /** Finds all the flags that are inside a formatting String from a given index + * + * @param i the index in the String s where to start to check + * @param l the length of s + * @param s the String to check + * @return a list containing all the flags that are inside the formatting String, + * and their index in the String + */ + def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { + def isFlag(c : Char) : Boolean = c match { + case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true + case _ => false + } + if (i < l && isFlag(s.charAt(i))) (s.charAt(i), i) :: getFlags(i + 1, l, s) + else Nil + } + + /** Skips the Characters that are width or argumentIndex parameters + * + * @param i the index where to start checking in the given String + * @param s the String to check + * @param l the length of s + * @return a tuple containing the index in the String after skipping + * the parameters, true if it has a width parameter and its value, false otherwise + */ + def skipWidth(i : Int, s : String, l : Int) = { + var j = i + var width = (false, 0) + while (j < l && Character.isDigit(s.charAt(j))){ + width = (true, j) + j += 1 + } + (j, width._1, width._2) + } + + /** Retrieves all the formatting parameters from a part and their index in it + * + * @param part the String containing the formatting parameters + * @param argIndex the index of the current argument inside the list of arguments to interpolate + * @param partIndex the index of the current part inside the list of parts in the StringContext + * @param noArg true if there is no arg, i.e. "%%" or "%n" + * @param pos the initial index where to start checking the part + * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion + * parameter is missing. Otherwise, + * the index where the format specifier substring is, + * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and + * flags that contains the list of flags (empty if there is none), + * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), + * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), + * hasRelative (true if the specifiers use relative indexing, false otherwise) and + * conversion character index + */ + def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { + var conversion = pos + var hasArgumentIndex = false + var argumentIndex = pos + var hasPrecision = false + var precision = pos + val l = part.length + + if (l >= 1 && part.charAt(conversion) == '%') + conversion += 1 + else if (!noArg) + reporter.argError("too many arguments for interpolated string", argIndex) + + //argument index or width + val (i, hasWidth1, width1) = skipWidth(conversion, part, l) + conversion = i + + //argument index + if (conversion < l && part.charAt(conversion) == '$'){ + hasArgumentIndex = true + argumentIndex = width1 + conversion += 1 + } + + //relative indexing + val hasRelative = conversion < l && part.charAt(conversion) == '<' + val relativeIndex = conversion + 1 + if (hasRelative) + conversion += 1 + + //flags + val flags = getFlags(conversion, l, part) + conversion += flags.size + + //width + val (j, hasWidth2, width2) = skipWidth(conversion, part, l) + conversion = j + + //precision + if (conversion < l && part.charAt(conversion) == '.') { + precision = conversion + conversion += 1 + hasPrecision = true + val oldConversion = conversion + while (conversion < l && Character.isDigit(part.charAt(conversion))) { + conversion += 1 + } + if (oldConversion == conversion) { + reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) + hasPrecision = false + } + } + + //conversion + if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) + reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) + + val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 + val width = if (hasWidth1 && !hasArgumentIndex) width1 else width2 + (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) + } + + /** Checks if the number of arguments are the same as the number of formatting strings + * + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise + */ + def checkSizes(format : Int, argument : Int) : Unit = { + if (format > argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too few arguments for interpolated string") + else + reporter.argError("too few arguments for interpolated string", argument - 1) + if (format < argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too many arguments for interpolated string") + else + reporter.argError("too many arguments for interpolated string", argument - 1) + if (format == -1) + reporter.strCtxError("there are no parts") + } + + /** Checks if a given type is a subtype of any of the possibilities + * + * @param actualType the given type + * @param expectedType the type we are expecting + * @param argIndex the index of the argument that should type check + * @param possibilities all the types within which we want to find a super type of the actualType + * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, + * nothing otherwise + */ + def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { + if (possibilities.find(actualType <:< _).isEmpty) + reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) + } + + /** Checks whether a given argument index, relative or not, is in the correct bounds + * + * @param partIndex the index of the part we are checking + * @param offset the index in the part where there might be an error + * @param relative true if relative indexing is used, false otherwise + * @param argumentIndex the argument index parameter in the formatting String + * @param expectedArgumentIndex the expected argument index parameter + * @param maxArgumentIndex the maximum argument index parameter that can be used + * @return reports a warning if relative indexing is used but an argument is still given, + * an error is the argument index is not in the bounds [1, number of arguments] + */ + def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { + if (relative) + reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) + + if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) + reporter.partError("Argument index out of range", partIndex, offset) + + if (expectedArgumentIndex != argumentIndex) + reporter.partWarning("Index is not this arg", partIndex, offset) + } + + /** Checks if a parameter is specified whereas it is not allowed + * + * @param hasParameter true if parameter is specified, false otherwise + * @param partIndex the index of the part inside the parts + * @param offset the index in the part where to report an error + * @param parameter the parameter that is not allowed + * @return reports an error if hasParameter is true, nothing otherwise + */ + def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { + if (hasParameter) + reporter.partError(parameter + " not allowed", partIndex, offset) + } + + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error if the flag is not allowed, nothing otherwise + */ + def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} + reporter.partError(message, partIndex, flag._2) + } + + /** Checks if the flags are allowed for the conversion + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise + */ + def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { + reporter.resetReported() + for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { + if (!reporter.hasReported()) + reporter.partError(message, partIndex, flag._2) + } + if (!reporter.hasReported()) + reporter.restoreReported() + } + + /** Checks all the formatting parameters for a Character conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are different from '-' + */ + def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precisionIndex, "precision") + } + + /** Checks all the formatting parameters for an Integral conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are not allowed : + * ’d’: only ’#’ is allowed, + * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step + */ + def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if (conversionChar == 'd') + checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) + + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + } + + /** Checks all the formatting parameters for a Floating Point conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' + */ + def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + if(conversionChar == 'a' || conversionChar == 'A'){ + for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} + reporter.partError("'" + flag._1 + "' not allowed for a, A", partIndex, flag._2) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + } + } + + /** Checks all the formatting parameters for a Time conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part that we are checking + * @param conversionIndex the index of the conversion Char used in the part + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified, if the time suffix is not given/incorrect or if the used flags are + * different from '-' + */ + def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { + /** + * Checks whether a time suffix is given and whether it is allowed + * + * @param part the part that we are checking + * @param partIndex the index of the part inside of the parts of the StringContext + * @param conversionIndex the index of the conversion Char inside the part + * @param return reports an error if no suffix is specified or if the given suffix is not + * part of the allowed ones + */ + def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { + val partSize = part.size + + if (conversionIndex == partSize - 1) + reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) + + part.charAt(conversionIndex + 1) match { + case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times + case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates + case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times + case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) + } + } + + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for date/time conversions") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + checkTime(part, partIndex, conversionIndex) + } + + /** Checks all the formatting parameters for a General conversion + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @return reports an error + * if '#' flag is used or if any other flag is used + */ + def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { + for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} + reporter.partError("Illegal flag '" + flag._1 + "'", partIndex, flag._2) + } + + /** Checks all the formatting parameters for a special Char such as '%' and end of line + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @param hasWidth true if width parameter is specified, false otherwise + * @param width the index of the width parameter inside the part + * @return reports an error if precision or width is specified for '%' or + * if precision is specified for end of line + */ + def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { + case 'n' => { + checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + checkNotAllowedParameter(hasWidth, partIndex, width, "width") + } + case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + case _ => // OK + } + + /** Checks whether the format specifiers are correct depending on the conversion parameter + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part to check + * The rest of the inputs correspond to the output of the function getFormatSpecifiers + * @param hasArgumentIndex + * @param actualArgumentIndex + * @param expectedArgumentIndex + * @param maxArgumentIndex + * @param hasRelative + * @param hasWidth + * @param hasPrecision + * @param precision + * @param flags + * @param conversion + * @param argType + * @return the argument index and its type if there is an argument, the flags and the conversion parameter + * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise + */ + def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], + hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { + val conversionChar = part.charAt(conversion) + + if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) + checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) + + conversionChar match { + case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) + case 'd' | 'o' | 'x' | 'X' => checkIntegralConversion(partIndex, argType, conversionChar, flags, hasPrecision, precision) + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) + case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) + case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) + case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) + case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) + } + + (if (argType.isEmpty) None else Some(argType.get, (partIndex - 1)), conversionChar, flags) + } + + /** Checks whether the argument type, if there is one, type checks with the formatting parameters + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the character used for the conversion + * @param argument an option containing the type and index of the argument, None if there is no argument + * @param flags the flags used for the formatting + * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise + */ + def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { + if (argument.nonEmpty) + checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) + else + checkTypeWithoutArgs(conversionChar, partIndex, flags) + } + + /** Checks whether the argument type checks with the formatting parameters + * + * @param argument the given argument to check + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the argument type does not correspond with the conversion character, + * nothing otherwise + */ + def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + val booleans = List(definitions.BooleanType, definitions.NullType) + val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) + val floatingPoints = List(definitions.DoubleType, definitions.FloatType, typeOf[java.math.BigDecimal]) + val integral = List(definitions.IntType, definitions.LongType, definitions.ShortType, definitions.ByteType, typeOf[java.math.BigInteger]) + val character = List(definitions.CharType, definitions.ByteType, definitions.ShortType, definitions.IntType) + + val (argType, argIndex) = argument + conversionChar match { + case 'c' | 'C' => checkSubtype(argType, "Char", argIndex, character : _*) + case 'd' | 'o' | 'x' | 'X' => { + checkSubtype(argType, "Int", argIndex, integral : _*) + if (conversionChar != 'd') { + val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), + (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), + ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + } + } + case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkSubtype(argType, "Double", argIndex, floatingPoints : _*) + case 't' | 'T' => checkSubtype(argType, "Date", argIndex, dates : _*) + case 'b' | 'B' => checkSubtype(argType, "Boolean", argIndex, booleans : _*) + case 'h' | 'H' | 'S' | 's' => + if (!(argType <:< typeOf[java.util.Formattable])) + for {flag <- flags ; if (flag._1 == '#')} + reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) + case 'n' | '%' => + case illegal => + } + } + + /** Reports error when the formatting parameter require a specific type but no argument is given + * + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given + * nothing otherwise + */ + def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + conversionChar match { + case 'o' | 'x' | 'X' => { + val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), + (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), + ('(', true, "Only use '(' for BigInt conversions to o, x, X"), + (',', true, "',' only allowed for d conversion of integral types")) + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + } + case _ => //OK + } + + if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) + } + + /** Checks that a given part of the String Context respects every formatting constraint per parameter + * + * @param part a particular part of the String Context + * @param start the index from which we start checking the part + * @param argument an Option containing the argument corresponding to the part and its index in the list of args, + * None if no args are specified. + * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified + * @return a list with all the elements of the conversion per formatting string + */ + def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { + reporter.resetReported() + val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) + if (hasFormattingSubstring.nonEmpty) { + val formattingStart = hasFormattingSubstring.get + var nextStart = formattingStart + + argument match { + case Some(argIndex, arg) => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) + nextStart = conversion + 1 + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + } + case None => { + val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) + if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) + reporter.partError("Argument index out of range", 0, argumentIndex + 1) + if (hasRelative) + reporter.partError("No last arg", 0, relativeIndex) + if (!reporter.hasReported()){ + val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) + nextStart = conversion + 1 + if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) + conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) + } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + } + } + } else { + reporter.restoreReported() + Nil + } + } + + val argument = args.size + + // add default format + val parts = addDefaultFormat(partsExpr.map(literalToString)) + + // check validity of formatting + checkSizes(parts.size - 1, argument) + + if (!parts.isEmpty) { + if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ + val argTypeWithConversion = checkPart(parts.head, 0, None, None) + if (!reporter.hasReported()) + for ((argument, conversionChar, flags) <- argTypeWithConversion) + checkArgTypeWithConversion(0, conversionChar, argument, flags) + } + else { + val partWithArgs = parts.tail.zip(args) + for (i <- (0 until args.size)){ + val (part, arg) = partWithArgs(i) + val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) + if (!reporter.hasReported()) + for ((argument, conversionChar, flags) <- argTypeWithConversion) + checkArgTypeWithConversion(0, conversionChar, argument, flags) + } + } + } + + // macro expansion + '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} + } + + trait Reporter{ //Same reporter as in the StringContext + def partError(message : String, index : Int, offset : Int) : Unit + def partWarning(message : String, index : Int, offset : Int) : Unit + def argError(message : String, index : Int) : Unit + def argWarning(message : String, index : Int) : Unit + def strCtxError(message : String) : Unit + def argsError(message : String) : Unit + def hasReported() : Boolean + def resetReported() : Unit + def restoreReported() : Unit + } + +} \ No newline at end of file diff --git a/tests/run-macros/f-interpolator-neg/NegativeTests.scala b/tests/run-macros/f-interpolator-neg/NegativeTests.scala deleted file mode 100644 index 1c5bc2dcc7c8..000000000000 --- a/tests/run-macros/f-interpolator-neg/NegativeTests.scala +++ /dev/null @@ -1,232 +0,0 @@ -// import org.junit.Test -// import org.junit.Assert._ - -// import compilationAssertions._ -// import Macro._ - -// /** -// * These tests test some combinations that should make the f interpolator fail. -// * They come from https://github.com/lampepfl/dotty/blob/master/tests/untried/neg/stringinterpolation_macro-neg.scala -// */ -// class NegativeTests { -// val s = "Scala" -// 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" -// } -// val emptyString = "" -// val is = " is " -// val yo = "%2d years old" - -// @Test def numberArgumentsTest1() = { -// assertNotCompile("new StringContext().f2()") -// } - -// @Test def numberArgumentsTest2() = { -// assertNotCompile("new StringContext(emptyString, is, yo).f2(s)") -// } - -// @Test def numberArgumentsTest3() = { -// assertNotCompile("new StringContext(emptyString, is, yo).f2(s, d, d)") -// } - -// @Test def numberArgumentsTest4() = { -// assertNotCompile("new StringContext(emptyString, emptyString).f2()") -// } - -// @Test def interpolationMismatchTest1() = { -// implicit val strToInt1 = (s: String) => 1 -// implicit val strToInt2 = (s: String) => 2 -// val string = "$s%d" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest2() = { -// val string = "$s%b" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest3() = { -// val string = "$s%i" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest4() = { -// val string = "$s%c" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest5() = { -// val string = "$f%c" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest6() = { -// val string = "$s%x" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest7() = { -// val string = "$b%d" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest8() = { -// val string = "$s%d" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest9() = { -// val string = "$f%o" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest10() = { -// val string = "$s%e" -// assertNotCompile("f2string") -// } - -// @Test def interpolationMismatchTest11() = { -// val string = "$b%f" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest1() = { -// val string = "$s%+ 0,(s" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest2() = { -// val string = "$c%#+ 0,(c" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest3() = { -// val string = "$d%#d" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest4() = { -// val string = "$d%,x" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest5() = { -// val string = "$d%+ (x" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest6() = { -// val string = "$f%,(a" -// assertNotCompile("f2string") -// } - -// @Test def flagMismatchTest7() = { -// val string = "$t%#+ 0,(tT" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest1() = { -// val string = "$c%.2c" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest2() = { -// val string = "$d%.2d" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest3() = { -// val string = "%.2%" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest4() = { -// val string = "%.2n" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest5() = { -// val string = "$f%.2a" -// assertNotCompile("f2string") -// } - -// @Test def badPrecisionTest6() = { -// val string = "$t%.2tT" -// assertNotCompile("f2string") -// } - -// @Test def badIndexTest1() = { -// val string = "% 1 + implicit val strToInt2 = (s: String) => 2 + assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int\nNote that implicit conversions are not applicable because they are ambiguous:\nboth value strToInt2 of type String => Int\nand value strToInt1 of type String => Int\nare possible conversion functions from String to Int"))) + } + + assertEquals(foo"$s%i", List((true, 0, 0, 1, "illegal conversion character 'i'"))) + } + + def flagMismatches(s : String, c : Char, d : Int, f : Double, t : java.util.Date) = { + import TestFooErrors._ + assertEquals(foo"$s%+ 0,(s", List((true, 0, 0, 1, "Illegal flag '+'"), (true, 0, 0, 2, "Illegal flag ' '"), + (true, 0, 0, 3, "Illegal flag '0'"), (true, 0, 0, 4, "Illegal flag ','"), (true, 0, 0, 5, "Illegal flag '('"))) + assertEquals(foo"$c%#+ 0,(c", List((true, 0, 0, 1, "Only '-' allowed for c conversion"))) + assertEquals(foo"$d%#d", List((true, 0, 0, 1, "# not allowed for d conversion"))) + assertEquals(foo"$d%,x", List((true, 0, 0, 1, "',' only allowed for d conversion of integral types"))) + assertEquals(foo"$d%+ (x", List((true, 0, 0, 1, "only use '+' for BigInt conversions to o, x, X"), (true, 0, 0, 2, "only use ' ' for BigInt conversions to o, x, X"), + (true, 0, 0, 3, "only use '(' for BigInt conversions to o, x, X"))) + assertEquals(foo"$f%,(a", List((true, 0, 0, 1, "',' not allowed for a, A"), (true, 0, 0, 2, "'(' not allowed for a, A"))) + assertEquals(foo"$t%#+ 0,(tT", List((true, 0, 0, 1, "Only '-' allowed for date/time conversions"))) + } + + def badPrecisions(c : Char, d : Int, f : Double, t : java.util.Date) = { + import TestFooErrors._ + assertEquals(foo"$c%.2c", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"$d%.2d", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"%.2%", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"%.2n", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"$f%.2a", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"$t%.2tT", List((true, 0, 0, 1, "precision not allowed"))) + } + + def badIndexes() = { + import TestFooErrors._ + assertEquals(foo"% Date: Thu, 16 May 2019 16:15:10 +0200 Subject: [PATCH 04/36] Fix small error found with report. --- .../dotty/internal/StringContext.scala | 124 +++++++++--------- .../f-interpolator-neg/Macros_1.scala | 5 - .../f-interpolator-neg/Tests_2.scala | 96 +++++++------- 3 files changed, 107 insertions(+), 118 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index cbea7444b3a1..476152348c8b 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -34,14 +34,13 @@ object fImpl{ def partError(message : String, index : Int, offset : Int) : Unit def partWarning(message : String, index : Int, offset : Int) : Unit - /** Reports error/warning linked with an argument to format. + /** Reports error linked with an argument to format. * * @param message the message to report as error/warning * @param index the index of the argument inside the list of arguments of the format function - * @return an error/warning depending on the function + * @return an error depending on the function */ def argError(message : String, index : Int) : Unit - def argWarning(message : String, index : Int) : Unit /** Reports error linked with the list of arguments or the StringContext. * @@ -68,20 +67,20 @@ object fImpl{ * * @param expression the Expr containing the String * @return the String contained in the given Expr - * @throws an Exception if the given Expr does not contain a String + * quotes an error if the given Expr does not contain a String */ private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { case Const(string : String) => string - case _ => throw new Exception("Expected statically known part list") + case _ => QuoteError("Expected statically known part list") } /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String * * @param strCtxExpr the Expr containing the StringContext * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext - * @throws an Exception if the given Expr does not correspond to a StringContext + * quotes an error if the given Expr does not correspond to a StringContext */ - private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => @@ -89,7 +88,7 @@ object fImpl{ case ExprSeq(parts2) => parts2.toList case _ => throw new Exception("Expected statically known String Context") } - case _ => throw new Exception("Expected statically known String Context") + case _ => QuoteError("Expected statically known String Context") } } @@ -97,13 +96,13 @@ object fImpl{ * * @param argsExpr the Expr containing the list of arguments * @return a list of Expr containing arguments - * @throws an Exception if the given Expr does not contain a list of arguments + * quotes an error if the given Expr does not contain a list of arguments */ - private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + private def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { import reflect._ argsExpr.unseal.underlyingArgument match { case Typed(Repeated(args, _), _) => args.map(_.seal) - case tree => throw new Exception("Expected statically known argument list") + case tree => QuoteError("Expected statically known argument list") } } @@ -133,8 +132,8 @@ object fImpl{ import reflect._ val sourceFile = strCtxExpr.unseal.pos.sourceFile - val partsExpr = getListOfExpr(strCtxExpr) - val args = getArgsList(argsExpr) + val partsExpr = getPartsExprs(strCtxExpr) + val args = getArgsExprs(argsExpr) val reporter = new Reporter{ private[this] var reported = false @@ -154,10 +153,6 @@ object fImpl{ reported = true reflect.error(message, args(index).unseal.pos) } - def argWarning(message : String, index : Int) : Unit = { - reported = true - reflect.warning(message, args(index).unseal.pos) - } def strCtxError(message : String) : Unit = { reported = true @@ -196,22 +191,26 @@ object fImpl{ private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { import reflect.{Literal => LiteralTree, _} - /** Checks whether a part contains a formatting substring + /** Checks if the number of arguments are the same as the number of formatting strings * - * @param part the part to check - * @param l the length of the given part - * @param index the index where to start to look for a potential new formatting string - * @return an Option containing the index in the part where a new formatting String starts, None otherwise + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise */ - def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { - var i = index - var result : Option[Int] = None - while (i < l){ - if (part.charAt(i) == '%' && result.isEmpty) - result = Some(i) - i += 1 - } - result + def checkSizes(format : Int, argument : Int) : Unit = { + if (format > argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too few arguments for interpolated string") + else + reporter.argError("too few arguments for interpolated string", argument - 1) + if (format < argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too many arguments for interpolated string") + else + reporter.argError("too many arguments for interpolated string", format) + if (format == -1) + reporter.strCtxError("there are no parts") } /** Adds the default "%s" to the Strings that do not have any given format @@ -234,6 +233,24 @@ object fImpl{ }) } + /** Checks whether a part contains a formatting substring + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ + def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { + var i = index + var result : Option[Int] = None + while (i < l){ + if (part.charAt(i) == '%' && result.isEmpty) + result = Some(i) + i += 1 + } + result + } + /** Finds all the flags that are inside a formatting String from a given index * * @param i the index in the String s where to start to check @@ -305,9 +322,13 @@ object fImpl{ //argument index if (conversion < l && part.charAt(conversion) == '$'){ - hasArgumentIndex = true - argumentIndex = width1 - conversion += 1 + if (hasWidth1){ + hasArgumentIndex = true + argumentIndex = width1 + conversion += 1 + } else { + reporter.partError("Missing conversion operator in '" + part.substring(0, conversion) + "'; use %% for literal %, %n for newline", partIndex, 0) + } } //relative indexing @@ -348,28 +369,6 @@ object fImpl{ (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) } - /** Checks if the number of arguments are the same as the number of formatting strings - * - * @param format the number of formatting parts in the StringContext - * @param argument the number of arguments to interpolate in the string - * @return reports an error if the number of arguments does not match with the number of formatting strings, - * nothing otherwise - */ - def checkSizes(format : Int, argument : Int) : Unit = { - if (format > argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too few arguments for interpolated string") - else - reporter.argError("too few arguments for interpolated string", argument - 1) - if (format < argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too many arguments for interpolated string") - else - reporter.argError("too many arguments for interpolated string", argument - 1) - if (format == -1) - reporter.strCtxError("there are no parts") - } - /** Checks if a given type is a subtype of any of the possibilities * * @param actualType the given type @@ -742,12 +741,12 @@ object fImpl{ val argument = args.size + // check validity of formatting + checkSizes(partsExpr.size - 1, argument) + // add default format val parts = addDefaultFormat(partsExpr.map(literalToString)) - // check validity of formatting - checkSizes(parts.size - 1, argument) - if (!parts.isEmpty) { if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ val argTypeWithConversion = checkPart(parts.head, 0, None, None) @@ -778,11 +777,6 @@ object fImpl{ * respect the formatting rules */ final def apply(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { - import reflect._ - try interpolate(strCtxExpr, args) - catch { - case ex: Exception => - QuoteError(ex.getMessage) - } + interpolate(strCtxExpr, args) } } diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index a7689a27a5ca..85332ed6dc6e 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -97,10 +97,6 @@ object Macro { reported = true errors += '{ Tuple5(true, 1, $index, 0, $message) } } - def argWarning(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 1, $index, 0, $message) } - } def strCtxError(message : String) : Unit = { reported = true @@ -713,7 +709,6 @@ object Macro { def partError(message : String, index : Int, offset : Int) : Unit def partWarning(message : String, index : Int, offset : Int) : Unit def argError(message : String, index : Int) : Unit - def argWarning(message : String, index : Int) : Unit def strCtxError(message : String) : Unit def argsError(message : String) : Unit def hasReported() : Boolean diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 6c5cd4cb4c98..9c39c1718814 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -31,91 +31,91 @@ object Test { def interpolationMismatches(s : String, f : Double, b : Boolean) = { import TestFooErrors._ - assertEquals(foo"$s%b", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Boolean"))) - assertEquals(foo"$s%c", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Char"))) - assertEquals(foo"$f%c", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Char"))) - assertEquals(foo"$s%x", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$b%d", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Int"))) - assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$f%o", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Int"))) - assertEquals(foo"$s%e", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Double"))) - assertEquals(foo"$b%f", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) + assertEquals(foo"$s%b", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Boolean"))) + assertEquals(foo"$s%c", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Char"))) + assertEquals(foo"$f%c", List((true, 1, 1, 0, "type mismatch;\nfound : Double\nrequired: Char"))) + assertEquals(foo"$s%x", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) + assertEquals(foo"$b%d", List((true, 1, 1, 0, "type mismatch;\nfound : Boolean\nrequired: Int"))) + assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) + assertEquals(foo"$f%o", List((true, 1, 1, 0, "type mismatch;\nfound : Double\nrequired: Int"))) + assertEquals(foo"$s%e", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Double"))) + assertEquals(foo"$b%f", List((true, 1, 1, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) { implicit val strToInt1 = (s: String) => 1 implicit val strToInt2 = (s: String) => 2 - assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int\nNote that implicit conversions are not applicable because they are ambiguous:\nboth value strToInt2 of type String => Int\nand value strToInt1 of type String => Int\nare possible conversion functions from String to Int"))) + assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int\nNote that implicit conversions are not applicable because they are ambiguous:\nboth value strToInt2 of type String => Int\nand value strToInt1 of type String => Int\nare possible conversion functions from String to Int"))) } - assertEquals(foo"$s%i", List((true, 0, 0, 1, "illegal conversion character 'i'"))) + assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) } def flagMismatches(s : String, c : Char, d : Int, f : Double, t : java.util.Date) = { import TestFooErrors._ - assertEquals(foo"$s%+ 0,(s", List((true, 0, 0, 1, "Illegal flag '+'"), (true, 0, 0, 2, "Illegal flag ' '"), - (true, 0, 0, 3, "Illegal flag '0'"), (true, 0, 0, 4, "Illegal flag ','"), (true, 0, 0, 5, "Illegal flag '('"))) - assertEquals(foo"$c%#+ 0,(c", List((true, 0, 0, 1, "Only '-' allowed for c conversion"))) - assertEquals(foo"$d%#d", List((true, 0, 0, 1, "# not allowed for d conversion"))) - assertEquals(foo"$d%,x", List((true, 0, 0, 1, "',' only allowed for d conversion of integral types"))) - assertEquals(foo"$d%+ (x", List((true, 0, 0, 1, "only use '+' for BigInt conversions to o, x, X"), (true, 0, 0, 2, "only use ' ' for BigInt conversions to o, x, X"), - (true, 0, 0, 3, "only use '(' for BigInt conversions to o, x, X"))) - assertEquals(foo"$f%,(a", List((true, 0, 0, 1, "',' not allowed for a, A"), (true, 0, 0, 2, "'(' not allowed for a, A"))) - assertEquals(foo"$t%#+ 0,(tT", List((true, 0, 0, 1, "Only '-' allowed for date/time conversions"))) + assertEquals(foo"$s%+ 0,(s", List((true, 0, 1, 1, "Illegal flag '+'"), (true, 0, 1, 2, "Illegal flag ' '"), + (true, 0, 1, 3, "Illegal flag '0'"), (true, 0, 1, 4, "Illegal flag ','"), (true, 0, 1, 5, "Illegal flag '('"))) + assertEquals(foo"$c%#+ 0,(c", List((true, 0, 1, 1, "Only '-' allowed for c conversion"))) + assertEquals(foo"$d%#d", List((true, 0, 1, 1, "# not allowed for d conversion"))) + assertEquals(foo"$d%,x", List((true, 0, 1, 1, "',' only allowed for d conversion of integral types"))) + assertEquals(foo"$d%+ (x", List((true, 0, 1, 1, "only use '+' for BigInt conversions to o, x, X"), (true, 0, 1, 2, "only use ' ' for BigInt conversions to o, x, X"), + (true, 0, 1, 3, "only use '(' for BigInt conversions to o, x, X"))) + assertEquals(foo"$f%,(a", List((true, 0, 1, 1, "',' not allowed for a, A"), (true, 0, 1, 2, "'(' not allowed for a, A"))) + assertEquals(foo"$t%#+ 0,(tT", List((true, 0, 1, 1, "Only '-' allowed for date/time conversions"))) } def badPrecisions(c : Char, d : Int, f : Double, t : java.util.Date) = { import TestFooErrors._ - assertEquals(foo"$c%.2c", List((true, 0, 0, 1, "precision not allowed"))) - assertEquals(foo"$d%.2d", List((true, 0, 0, 1, "precision not allowed"))) - assertEquals(foo"%.2%", List((true, 0, 0, 1, "precision not allowed"))) - assertEquals(foo"%.2n", List((true, 0, 0, 1, "precision not allowed"))) - assertEquals(foo"$f%.2a", List((true, 0, 0, 1, "precision not allowed"))) - assertEquals(foo"$t%.2tT", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"$c%.2c", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"$d%.2d", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"%.2%", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"%.2n", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"$f%.2a", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"$t%.2tT", List((true, 0, 1, 1, "precision not allowed"))) } def badIndexes() = { import TestFooErrors._ - assertEquals(foo"% Date: Tue, 21 May 2019 13:03:50 +0200 Subject: [PATCH 05/36] Use same macro in negative tests --- .../dotty/internal/StringContext.scala | 7 +- .../f-interpolator-neg/Macros_1.scala | 122 ++++++++++-------- 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 476152348c8b..9e58ed7d3aba 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -83,11 +83,8 @@ object fImpl{ private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") - } + case '{ scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case '{ new scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList case _ => QuoteError("Expected statically known String Context") } } diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index 85332ed6dc6e..91a2f18df844 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -17,28 +17,25 @@ object Macro { * * @param expression the Expr containing the String * @return the String contained in the given Expr - * @throws an Exception if the given Expr does not contain a String + * quotes an error if the given Expr does not contain a String */ private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { case Const(string : String) => string - case _ => throw new Exception("Expected statically known part list") + case _ => QuoteError("Expected statically known part list") } /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String * * @param strCtxExpr the Expr containing the StringContext * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext - * @throws an Exception if the given Expr does not correspond to a StringContext + * quotes an error if the given Expr does not correspond to a StringContext */ - private def getListOfExpr(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") - } - case _ => throw new Exception("Expected statically known String Context") + case '{ scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case '{ new scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case _ => QuoteError("Expected statically known String Context") } } @@ -46,13 +43,13 @@ object Macro { * * @param argsExpr the Expr containing the list of arguments * @return a list of Expr containing arguments - * @throws an Exception if the given Expr does not contain a list of arguments + * quotes an error if the given Expr does not contain a list of arguments */ - private def getArgsList(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + private def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { import reflect._ argsExpr.unseal.underlyingArgument match { case Typed(Repeated(args, _), _) => args.map(_.seal) - case tree => throw new Exception("Expected statically known argument list") + case tree => QuoteError("Expected statically known argument list") } } @@ -122,30 +119,41 @@ object Macro { } val partsExpr = getListOfExpr(strCtxExpr) val args = getArgsList(argsExpr) - fooCore(partsExpr, args, argsExpr, reporter) // Discard result + interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList } } - private def fooCore(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter) given Reflection: Expr[String] = { + /** Helper function for the interpolate function above + * + * @param partsExpr the list of parts enumerated as Expr + * @param args the list of arguments enumerated as Expr + * @param reporter the reporter to return any error/warning when a problem is encountered + * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct + */ + private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { import reflect.{Literal => LiteralTree, _} - /** Checks whether a part contains a formatting substring + /** Checks if the number of arguments are the same as the number of formatting strings * - * @param part the part to check - * @param l the length of the given part - * @param index the index where to start to look for a potential new formatting string - * @return an Option containing the index in the part where a new formatting String starts, None otherwise + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise */ - def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { - var i = index - var result : Option[Int] = None - while (i < l){ - if (part.charAt(i) == '%' && result.isEmpty) - result = Some(i) - i += 1 - } - result + def checkSizes(format : Int, argument : Int) : Unit = { + if (format > argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too few arguments for interpolated string") + else + reporter.argError("too few arguments for interpolated string", argument - 1) + if (format < argument && !(format == -1 && argument == 0)) + if (argument == 0) + reporter.argsError("too many arguments for interpolated string") + else + reporter.argError("too many arguments for interpolated string", format) + if (format == -1) + reporter.strCtxError("there are no parts") } /** Adds the default "%s" to the Strings that do not have any given format @@ -168,6 +176,24 @@ object Macro { }) } + /** Checks whether a part contains a formatting substring + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ + def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { + var i = index + var result : Option[Int] = None + while (i < l){ + if (part.charAt(i) == '%' && result.isEmpty) + result = Some(i) + i += 1 + } + result + } + /** Finds all the flags that are inside a formatting String from a given index * * @param i the index in the String s where to start to check @@ -239,9 +265,13 @@ object Macro { //argument index if (conversion < l && part.charAt(conversion) == '$'){ - hasArgumentIndex = true - argumentIndex = width1 - conversion += 1 + if (hasWidth1){ + hasArgumentIndex = true + argumentIndex = width1 + conversion += 1 + } else { + reporter.partError("Missing conversion operator in '" + part.substring(0, conversion) + "'; use %% for literal %, %n for newline", partIndex, 0) + } } //relative indexing @@ -282,28 +312,6 @@ object Macro { (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) } - /** Checks if the number of arguments are the same as the number of formatting strings - * - * @param format the number of formatting parts in the StringContext - * @param argument the number of arguments to interpolate in the string - * @return reports an error if the number of arguments does not match with the number of formatting strings, - * nothing otherwise - */ - def checkSizes(format : Int, argument : Int) : Unit = { - if (format > argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too few arguments for interpolated string") - else - reporter.argError("too few arguments for interpolated string", argument - 1) - if (format < argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too many arguments for interpolated string") - else - reporter.argError("too many arguments for interpolated string", argument - 1) - if (format == -1) - reporter.strCtxError("there are no parts") - } - /** Checks if a given type is a subtype of any of the possibilities * * @param actualType the given type @@ -676,12 +684,12 @@ object Macro { val argument = args.size + // check validity of formatting + checkSizes(partsExpr.size - 1, argument) + // add default format val parts = addDefaultFormat(partsExpr.map(literalToString)) - // check validity of formatting - checkSizes(parts.size - 1, argument) - if (!parts.isEmpty) { if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ val argTypeWithConversion = checkPart(parts.head, 0, None, None) From 83b741234db494f43d6368d1550e3eb5c1d454ac Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 21 May 2019 13:11:40 +0200 Subject: [PATCH 06/36] Revert last commit as does not work --- library/src-3.x/dotty/internal/StringContext.scala | 7 +++++-- tests/run-macros/f-interpolator-neg/Macros_1.scala | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 9e58ed7d3aba..476152348c8b 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -83,8 +83,11 @@ object fImpl{ private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { - case '{ scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList - case '{ new scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") + } case _ => QuoteError("Expected statically known String Context") } } diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index 91a2f18df844..71315764b737 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -33,8 +33,11 @@ object Macro { private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { - case '{ scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList - case '{ new scala.StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") + } case _ => QuoteError("Expected statically known String Context") } } From 67712226e47501a9eda506ac4b6b2d64d8750de2 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 21 May 2019 16:33:25 +0200 Subject: [PATCH 07/36] fix compilation error, out of bounds to fix --- .../dotty/internal/StringContext.scala | 26 +- .../f-interpolator-neg/Macros_1.scala | 698 ++---------------- .../f-interpolator-neg/Tests_2.scala | 8 +- 3 files changed, 59 insertions(+), 673 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 476152348c8b..986a63070cc5 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -11,15 +11,6 @@ object StringContext { /** Implemetation of scala.StringContext.f used in Dotty while the standard library is still not bootstrapped */ inline def f(sc: => scala.StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } -} - -/** This object implements the f interpolator macro. - * - * This kind of interpolator lets the user prepend f to any string literal to create formatted strings. - * Every variable used in the string literal should have a format, like "%d" for integers, "%2.2f", etc. - * For example, f"$name%s is $height%2.2f meters tall") will return "James is 1.90 meters tall" - */ -object fImpl{ /** This trait defines a tool to report errors/warnings that do not depend on Position. */ trait Reporter{ @@ -80,15 +71,20 @@ object fImpl{ * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext * quotes an error if the given Expr does not correspond to a StringContext */ - private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { + def getPartsExprs(strCtxExpr : Expr[scala.StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => + case Apply(Select(Select(_, "StringContext") | Ident("StringContext"), "apply"), List(parts1)) => + parts1.seal.cast[Seq[String]] match { + case ExprSeq(parts2) => parts2.toList + case _ => throw new Exception("Expected statically known String Context") + } + case Apply(Select(New(TypeIdent("StringContext")), _), List(parts1)) => parts1.seal.cast[Seq[String]] match { case ExprSeq(parts2) => parts2.toList case _ => throw new Exception("Expected statically known String Context") } - case _ => QuoteError("Expected statically known String Context") + case _ => QuoteError("Expected statically known String Context") //TODO : quotes? } } @@ -98,7 +94,7 @@ object fImpl{ * @return a list of Expr containing arguments * quotes an error if the given Expr does not contain a list of arguments */ - private def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { + def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { import reflect._ argsExpr.unseal.underlyingArgument match { case Typed(Repeated(args, _), _) => args.map(_.seal) @@ -188,7 +184,7 @@ object fImpl{ * @param reporter the reporter to return any error/warning when a problem is encountered * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct */ - private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { + def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { import reflect.{Literal => LiteralTree, _} /** Checks if the number of arguments are the same as the number of formatting strings @@ -776,7 +772,7 @@ object fImpl{ * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not * respect the formatting rules */ - final def apply(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { + final def fImpl(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { interpolate(strCtxExpr, args) } } diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index 71315764b737..a373397a6495 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -13,65 +13,6 @@ object TestFooErrors { // Defined in tests object Macro { - /** Retrieves a String from an Expr containing it - * - * @param expression the Expr containing the String - * @return the String contained in the given Expr - * quotes an error if the given Expr does not contain a String - */ - private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { - case Const(string : String) => string - case _ => QuoteError("Expected statically known part list") - } - - /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String - * - * @param strCtxExpr the Expr containing the StringContext - * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext - * quotes an error if the given Expr does not correspond to a StringContext - */ - private def getPartsExprs(strCtxExpr : Expr[StringContext])(implicit reflect : Reflection): List[Expr[String]] = { - import reflect._ - strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(Select(Ident("_root_"), "scala"), "StringContext"), "apply"), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") - } - case _ => QuoteError("Expected statically known String Context") - } - } - - /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments - * - * @param argsExpr the Expr containing the list of arguments - * @return a list of Expr containing arguments - * quotes an error if the given Expr does not contain a list of arguments - */ - private def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { - import reflect._ - argsExpr.unseal.underlyingArgument match { - case Typed(Repeated(args, _), _) => args.map(_.seal) - case tree => QuoteError("Expected statically known argument list") - } - } - - /** Lifts a StringContext from StringContext to Expr of StringContext - * - * @return a Liftable StringContext by redefining the toExpr function - */ - private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { - def toExpr(strCtx: StringContext): Expr[StringContext] = { - implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { - override def toExpr(list: List[String]): Expr[List[String]] = list match { - case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} - case Nil => '{Nil} - } - } - '{StringContext(${strCtx.parts.toList.toExpr}: _*)} - } - } - def fooErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection): Expr[List[(Boolean, Int, Int, Int, String)]] = { (strCtxExpr, argsExpr) match { case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => @@ -81,7 +22,7 @@ object Macro { // index in the list if arg or part, -1 otherwise // offset, 0 if strCtx, args or arg // message as given - val reporter = new Reporter{ + val reporter = new dotty.internal.StringContext.Reporter{ private[this] var reported = false private[this] var oldReported = false def partError(message : String, index : Int, offset : Int) : Unit = { @@ -120,611 +61,60 @@ object Macro { reported = oldReported } } - val partsExpr = getListOfExpr(strCtxExpr) - val args = getArgsList(argsExpr) - interpolate(partsExpr, args, argsExpr, reporter) // Discard result + val partsExpr = dotty.internal.StringContext.getPartsExprs(strCtxExpr) + val args = dotty.internal.StringContext.getArgsExprs(argsExpr) + dotty.internal.StringContext.interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList - } - } - - /** Helper function for the interpolate function above - * - * @param partsExpr the list of parts enumerated as Expr - * @param args the list of arguments enumerated as Expr - * @param reporter the reporter to return any error/warning when a problem is encountered - * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct - */ - private def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { - import reflect.{Literal => LiteralTree, _} - - /** Checks if the number of arguments are the same as the number of formatting strings - * - * @param format the number of formatting parts in the StringContext - * @param argument the number of arguments to interpolate in the string - * @return reports an error if the number of arguments does not match with the number of formatting strings, - * nothing otherwise - */ - def checkSizes(format : Int, argument : Int) : Unit = { - if (format > argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too few arguments for interpolated string") - else - reporter.argError("too few arguments for interpolated string", argument - 1) - if (format < argument && !(format == -1 && argument == 0)) - if (argument == 0) - reporter.argsError("too many arguments for interpolated string") - else - reporter.argError("too many arguments for interpolated string", format) - if (format == -1) - reporter.strCtxError("there are no parts") - } - - /** Adds the default "%s" to the Strings that do not have any given format - * - * @param parts the list of parts contained in the StringContext - * @return a new list of string with all a defined formatting or reports an error if the '%' and - * formatting parameter are too far away from the argument that they refer to - * For example : f2"${d}random-leading-junk%d" will lead to an error - */ - def addDefaultFormat(parts : List[String]) : List[String] = parts match { - case Nil => Nil - case p :: parts1 => p :: parts1.map((part : String) => { - if (!part.startsWith("%")) { - val index = part.indexOf('%') - if (index != -1) { - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) - "%s" + part - } else "%s" + part - } else part - }) - } - - /** Checks whether a part contains a formatting substring - * - * @param part the part to check - * @param l the length of the given part - * @param index the index where to start to look for a potential new formatting string - * @return an Option containing the index in the part where a new formatting String starts, None otherwise - */ - def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { - var i = index - var result : Option[Int] = None - while (i < l){ - if (part.charAt(i) == '%' && result.isEmpty) - result = Some(i) - i += 1 - } - result - } - - /** Finds all the flags that are inside a formatting String from a given index - * - * @param i the index in the String s where to start to check - * @param l the length of s - * @param s the String to check - * @return a list containing all the flags that are inside the formatting String, - * and their index in the String - */ - def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { - def isFlag(c : Char) : Boolean = c match { - case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true - case _ => false - } - if (i < l && isFlag(s.charAt(i))) (s.charAt(i), i) :: getFlags(i + 1, l, s) - else Nil - } - - /** Skips the Characters that are width or argumentIndex parameters - * - * @param i the index where to start checking in the given String - * @param s the String to check - * @param l the length of s - * @return a tuple containing the index in the String after skipping - * the parameters, true if it has a width parameter and its value, false otherwise - */ - def skipWidth(i : Int, s : String, l : Int) = { - var j = i - var width = (false, 0) - while (j < l && Character.isDigit(s.charAt(j))){ - width = (true, j) - j += 1 - } - (j, width._1, width._2) - } - - /** Retrieves all the formatting parameters from a part and their index in it - * - * @param part the String containing the formatting parameters - * @param argIndex the index of the current argument inside the list of arguments to interpolate - * @param partIndex the index of the current part inside the list of parts in the StringContext - * @param noArg true if there is no arg, i.e. "%%" or "%n" - * @param pos the initial index where to start checking the part - * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion - * parameter is missing. Otherwise, - * the index where the format specifier substring is, - * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and - * flags that contains the list of flags (empty if there is none), - * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), - * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), - * hasRelative (true if the specifiers use relative indexing, false otherwise) and - * conversion character index - */ - def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { - var conversion = pos - var hasArgumentIndex = false - var argumentIndex = pos - var hasPrecision = false - var precision = pos - val l = part.length - - if (l >= 1 && part.charAt(conversion) == '%') - conversion += 1 - else if (!noArg) - reporter.argError("too many arguments for interpolated string", argIndex) - - //argument index or width - val (i, hasWidth1, width1) = skipWidth(conversion, part, l) - conversion = i - - //argument index - if (conversion < l && part.charAt(conversion) == '$'){ - if (hasWidth1){ - hasArgumentIndex = true - argumentIndex = width1 - conversion += 1 - } else { - reporter.partError("Missing conversion operator in '" + part.substring(0, conversion) + "'; use %% for literal %, %n for newline", partIndex, 0) - } - } - - //relative indexing - val hasRelative = conversion < l && part.charAt(conversion) == '<' - val relativeIndex = conversion + 1 - if (hasRelative) - conversion += 1 - - //flags - val flags = getFlags(conversion, l, part) - conversion += flags.size - - //width - val (j, hasWidth2, width2) = skipWidth(conversion, part, l) - conversion = j - - //precision - if (conversion < l && part.charAt(conversion) == '.') { - precision = conversion - conversion += 1 - hasPrecision = true - val oldConversion = conversion - while (conversion < l && Character.isDigit(part.charAt(conversion))) { - conversion += 1 - } - if (oldConversion == conversion) { - reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) - hasPrecision = false - } - } - - //conversion - if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) - reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) - - val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 - val width = if (hasWidth1 && !hasArgumentIndex) width1 else width2 - (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) - } - - /** Checks if a given type is a subtype of any of the possibilities - * - * @param actualType the given type - * @param expectedType the type we are expecting - * @param argIndex the index of the argument that should type check - * @param possibilities all the types within which we want to find a super type of the actualType - * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, - * nothing otherwise - */ - def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { - if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) - } - - /** Checks whether a given argument index, relative or not, is in the correct bounds - * - * @param partIndex the index of the part we are checking - * @param offset the index in the part where there might be an error - * @param relative true if relative indexing is used, false otherwise - * @param argumentIndex the argument index parameter in the formatting String - * @param expectedArgumentIndex the expected argument index parameter - * @param maxArgumentIndex the maximum argument index parameter that can be used - * @return reports a warning if relative indexing is used but an argument is still given, - * an error is the argument index is not in the bounds [1, number of arguments] - */ - def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { - if (relative) - reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) - - if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) - reporter.partError("Argument index out of range", partIndex, offset) - - if (expectedArgumentIndex != argumentIndex) - reporter.partWarning("Index is not this arg", partIndex, offset) - } - - /** Checks if a parameter is specified whereas it is not allowed - * - * @param hasParameter true if parameter is specified, false otherwise - * @param partIndex the index of the part inside the parts - * @param offset the index in the part where to report an error - * @param parameter the parameter that is not allowed - * @return reports an error if hasParameter is true, nothing otherwise - */ - def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { - if (hasParameter) - reporter.partError(parameter + " not allowed", partIndex, offset) - } - - /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error if the flag is not allowed, nothing otherwise - */ - def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { - for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} - reporter.partError(message, partIndex, flag._2) - } - - /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise - */ - def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { - reporter.resetReported() - for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { - if (!reporter.hasReported()) - reporter.partError(message, partIndex, flag._2) - } - if (!reporter.hasReported()) - reporter.restoreReported() - } - - /** Checks all the formatting parameters for a Character conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are different from '-' - */ - def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { - val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") - checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - checkNotAllowedParameter(hasPrecision, partIndex, precisionIndex, "precision") - } - - /** Checks all the formatting parameters for an Integral conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are not allowed : - * ’d’: only ’#’ is allowed, - * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step - */ - def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - if (conversionChar == 'd') - checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) - - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - } - - /** Checks all the formatting parameters for a Floating Point conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' - */ - def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - if(conversionChar == 'a' || conversionChar == 'A'){ - for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} - reporter.partError("'" + flag._1 + "' not allowed for a, A", partIndex, flag._2) - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - } - } - - /** Checks all the formatting parameters for a Time conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param part the part that we are checking - * @param conversionIndex the index of the conversion Char used in the part - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified, if the time suffix is not given/incorrect or if the used flags are - * different from '-' - */ - def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - /** - * Checks whether a time suffix is given and whether it is allowed - * - * @param part the part that we are checking - * @param partIndex the index of the part inside of the parts of the StringContext - * @param conversionIndex the index of the conversion Char inside the part - * @param return reports an error if no suffix is specified or if the given suffix is not - * part of the allowed ones - */ - def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { - val partSize = part.size - - if (conversionIndex == partSize - 1) - reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) - - part.charAt(conversionIndex + 1) match { - case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times - case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates - case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times - case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) - } - } - - val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for date/time conversions") - checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) - checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") - checkTime(part, partIndex, conversionIndex) - } - - /** Checks all the formatting parameters for a General conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @return reports an error - * if '#' flag is used or if any other flag is used - */ - def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { - for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} - reporter.partError("Illegal flag '" + flag._1 + "'", partIndex, flag._2) - } - - /** Checks all the formatting parameters for a special Char such as '%' and end of line - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @param hasWidth true if width parameter is specified, false otherwise - * @param width the index of the width parameter inside the part - * @return reports an error if precision or width is specified for '%' or - * if precision is specified for end of line - */ - def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { - case 'n' => { - checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") - checkNotAllowedParameter(hasWidth, partIndex, width, "width") - } - case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") - case _ => // OK - } - - /** Checks whether the format specifiers are correct depending on the conversion parameter - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param part the part to check - * The rest of the inputs correspond to the output of the function getFormatSpecifiers - * @param hasArgumentIndex - * @param actualArgumentIndex - * @param expectedArgumentIndex - * @param maxArgumentIndex - * @param hasRelative - * @param hasWidth - * @param hasPrecision - * @param precision - * @param flags - * @param conversion - * @param argType - * @return the argument index and its type if there is an argument, the flags and the conversion parameter - * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise - */ - def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], - hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { - val conversionChar = part.charAt(conversion) - - if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) - checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) - - conversionChar match { - case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) - case 'd' | 'o' | 'x' | 'X' => checkIntegralConversion(partIndex, argType, conversionChar, flags, hasPrecision, precision) - case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) - case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) - case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) - case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) - case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) - } - - (if (argType.isEmpty) None else Some(argType.get, (partIndex - 1)), conversionChar, flags) - } - - /** Checks whether the argument type, if there is one, type checks with the formatting parameters - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the character used for the conversion - * @param argument an option containing the type and index of the argument, None if there is no argument - * @param flags the flags used for the formatting - * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise - */ - def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { - if (argument.nonEmpty) - checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) - else - checkTypeWithoutArgs(conversionChar, partIndex, flags) - } - - /** Checks whether the argument type checks with the formatting parameters - * - * @param argument the given argument to check - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the argument type does not correspond with the conversion character, - * nothing otherwise - */ - def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { - val booleans = List(definitions.BooleanType, definitions.NullType) - val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) - val floatingPoints = List(definitions.DoubleType, definitions.FloatType, typeOf[java.math.BigDecimal]) - val integral = List(definitions.IntType, definitions.LongType, definitions.ShortType, definitions.ByteType, typeOf[java.math.BigInteger]) - val character = List(definitions.CharType, definitions.ByteType, definitions.ShortType, definitions.IntType) - - val (argType, argIndex) = argument - conversionChar match { - case 'c' | 'C' => checkSubtype(argType, "Char", argIndex, character : _*) - case 'd' | 'o' | 'x' | 'X' => { - checkSubtype(argType, "Int", argIndex, integral : _*) - if (conversionChar != 'd') { - val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), - (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), - ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), - (',', true, "',' only allowed for d conversion of integral types")) - checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => + val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new dotty.internal.StringContext.Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } } - } - case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkSubtype(argType, "Double", argIndex, floatingPoints : _*) - case 't' | 'T' => checkSubtype(argType, "Date", argIndex, dates : _*) - case 'b' | 'B' => checkSubtype(argType, "Boolean", argIndex, booleans : _*) - case 'h' | 'H' | 'S' | 's' => - if (!(argType <:< typeOf[java.util.Formattable])) - for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) - case 'n' | '%' => - case illegal => - } - } - - /** Reports error when the formatting parameter require a specific type but no argument is given - * - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given - * nothing otherwise - */ - def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { - conversionChar match { - case 'o' | 'x' | 'X' => { - val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), - (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), - ('(', true, "Only use '(' for BigInt conversions to o, x, X"), - (',', true, "',' only allowed for d conversion of integral types")) - checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } } - case _ => //OK - } - - if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) - } - /** Checks that a given part of the String Context respects every formatting constraint per parameter - * - * @param part a particular part of the String Context - * @param start the index from which we start checking the part - * @param argument an Option containing the argument corresponding to the part and its index in the list of args, - * None if no args are specified. - * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified - * @return a list with all the elements of the conversion per formatting string - */ - def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { - reporter.resetReported() - val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) - if (hasFormattingSubstring.nonEmpty) { - val formattingStart = hasFormattingSubstring.get - var nextStart = formattingStart + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } - argument match { - case Some(argIndex, arg) => { - val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) - if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) - nextStart = conversion + 1 - conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) - } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } } - case None => { - val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) - if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) - reporter.partError("Argument index out of range", 0, argumentIndex + 1) - if (hasRelative) - reporter.partError("No last arg", 0, relativeIndex) - if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) - nextStart = conversion + 1 - if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) - conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) - } else checkPart(part, conversion + 1, argument, maxArgumentIndex) + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } } - } - } else { - reporter.restoreReported() - Nil - } - } - - val argument = args.size - // check validity of formatting - checkSizes(partsExpr.size - 1, argument) + def hasReported() : Boolean = { + reported + } - // add default format - val parts = addDefaultFormat(partsExpr.map(literalToString)) + def resetReported() : Unit = { + oldReported = reported + reported = false + } - if (!parts.isEmpty) { - if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ - val argTypeWithConversion = checkPart(parts.head, 0, None, None) - if (!reporter.hasReported()) - for ((argument, conversionChar, flags) <- argTypeWithConversion) - checkArgTypeWithConversion(0, conversionChar, argument, flags) - } - else { - val partWithArgs = parts.tail.zip(args) - for (i <- (0 until args.size)){ - val (part, arg) = partWithArgs(i) - val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) - if (!reporter.hasReported()) - for ((argument, conversionChar, flags) <- argTypeWithConversion) - checkArgTypeWithConversion(0, conversionChar, argument, flags) + def restoreReported() : Unit = { + reported = oldReported + } } - } + val partsExpr = dotty.internal.StringContext.getPartsExprs(strCtxExpr) + val args = dotty.internal.StringContext.getArgsExprs(argsExpr) + dotty.internal.StringContext.interpolate(partsExpr, args, argsExpr, reporter) // Discard result + errors.result().toExprOfList } - - // macro expansion - '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} } - - trait Reporter{ //Same reporter as in the StringContext - def partError(message : String, index : Int, offset : Int) : Unit - def partWarning(message : String, index : Int, offset : Int) : Unit - def argError(message : String, index : Int) : Unit - def strCtxError(message : String) : Unit - def argsError(message : String) : Unit - def hasReported() : Boolean - def resetReported() : Unit - def restoreReported() : Unit - } - } \ No newline at end of file diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 9c39c1718814..a094520e4624 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -43,7 +43,7 @@ object Test { { implicit val strToInt1 = (s: String) => 1 implicit val strToInt2 = (s: String) => 2 - assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int\nNote that implicit conversions are not applicable because they are ambiguous:\nboth value strToInt2 of type String => Int\nand value strToInt1 of type String => Int\nare possible conversion functions from String to Int"))) + assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) } assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) @@ -90,14 +90,14 @@ object Test { def badArgTypes(s : String) = { import TestFooErrors._ - assertEquals(foo"$s%#s", List((true, 1, 1, 0, "error: type mismatch;\nfound : String\nrequired: java.util.Formattable"))) + assertEquals(foo"$s%#s", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: java.util.Formattable"))) } def misunderstoodConversions(t : java.util.Date, s : String) = { import TestFooErrors._ assertEquals(foo"$t%tG", List((true, 0, 1, 2, "'G' doesn't seem to be a date or time conversion"))) - assertEquals(foo$"$t%t", List((true, 0, 1, 1, "Date/time conversion must have two characters"))) - assertEquals(foo$"$s%10.5", List((true, 0, 1, 0, "Missing conversion operator in '%10.5'; use %% for literal %, %n for newline"))) + assertEquals(foo"$t%t", List((true, 0, 1, 1, "Date/time conversion must have two characters"))) + assertEquals(foo"$s%10.5", List((true, 0, 1, 0, "Missing conversion operator in '%10.5'; use %% for literal %, %n for newline"))) } def otherBrainFailures(d : Int) = { From 1b67f4398cef007b9ba52f2b7c15c573b41a050c Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 21 May 2019 17:00:41 +0200 Subject: [PATCH 08/36] Fix out of bounds but needs type check --- .../dotty/internal/StringContext.scala | 19 +++++++++---------- .../f-interpolator-neg/Tests_2.scala | 7 +------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 986a63070cc5..6cd43d98ea61 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -517,16 +517,15 @@ object StringContext { * part of the allowed ones */ def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { - val partSize = part.size - - if (conversionIndex == partSize - 1) + if (conversionIndex + 1 >= part.size) reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) - - part.charAt(conversionIndex + 1) match { - case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times - case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates - case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times - case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) + else { + part.charAt(conversionIndex + 1) match { + case 'H' | 'I' | 'k' | 'l' | 'M' | 'S' | 'L' | 'N' | 'p' | 'z' | 'Z' | 's' | 'Q' => //times + case 'B' | 'b' | 'h' | 'A' | 'a' | 'C' | 'Y' | 'y' | 'j' | 'm' | 'd' | 'e' => //dates + case 'R' | 'T' | 'r' | 'D' | 'F' | 'c' => //dates and times + case c => reporter.partError("'" + c + "' doesn't seem to be a date or time conversion", partIndex, conversionIndex + 1) + } } } @@ -743,7 +742,7 @@ object StringContext { // add default format val parts = addDefaultFormat(partsExpr.map(literalToString)) - if (!parts.isEmpty) { + if (!parts.isEmpty && !reporter.hasReported()) { if (parts.size == 1 && args.size == 0 && parts.head.size != 0){ val argTypeWithConversion = checkPart(parts.head, 0, None, None) if (!reporter.hasReported()) diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index a094520e4624..6e1b9acd9386 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -40,12 +40,7 @@ object Test { assertEquals(foo"$f%o", List((true, 1, 1, 0, "type mismatch;\nfound : Double\nrequired: Int"))) assertEquals(foo"$s%e", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Double"))) assertEquals(foo"$b%f", List((true, 1, 1, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) - { - implicit val strToInt1 = (s: String) => 1 - implicit val strToInt2 = (s: String) => 2 - assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) - } - + assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) } From efe3b46ad80bcc4ab26664d0a3ae856d0c384abc Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Wed, 22 May 2019 10:32:17 +0200 Subject: [PATCH 09/36] Add type widen --- .../dotty/internal/StringContext.scala | 4 ++-- .../f-interpolator-neg/Tests_2.scala | 21 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContext.scala index 6cd43d98ea61..ad9ad767a744 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContext.scala @@ -376,7 +376,7 @@ object StringContext { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.show + "\nrequired : " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show + "\nrequired : " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -658,7 +658,7 @@ object StringContext { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.show + "\nrequired : java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.show + "\nrequired : java.util.Formattable\n", argIndex) case 'n' | '%' => case illegal => } diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 6e1b9acd9386..0edc3dbbc633 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -31,17 +31,16 @@ object Test { def interpolationMismatches(s : String, f : Double, b : Boolean) = { import TestFooErrors._ - assertEquals(foo"$s%b", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Boolean"))) - assertEquals(foo"$s%c", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Char"))) - assertEquals(foo"$f%c", List((true, 1, 1, 0, "type mismatch;\nfound : Double\nrequired: Char"))) - assertEquals(foo"$s%x", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$b%d", List((true, 1, 1, 0, "type mismatch;\nfound : Boolean\nrequired: Int"))) - assertEquals(foo"$s%d", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$f%o", List((true, 1, 1, 0, "type mismatch;\nfound : Double\nrequired: Int"))) - assertEquals(foo"$s%e", List((true, 1, 1, 0, "type mismatch;\nfound : String\nrequired: Double"))) - assertEquals(foo"$b%f", List((true, 1, 1, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) - assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) - assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) + assertEquals(foo"$s%b", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Boolean"))) + assertEquals(foo"$s%c", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Char"))) + assertEquals(foo"$f%c", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Char"))) + assertEquals(foo"$s%x", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) + assertEquals(foo"$b%d", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Int"))) + assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) + assertEquals(foo"$f%o", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Int"))) + assertEquals(foo"$s%e", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Double"))) + assertEquals(foo"$b%f", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) + assertEquals(foo"$s%i", List((true, 0, 0, 1, "illegal conversion character 'i'"))) } def flagMismatches(s : String, c : Char, d : Int, f : Double, t : java.util.Date) = { From 26a5d2bfce873176d02482008e4f08b9f608e34d Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 14 May 2019 15:44:28 +0200 Subject: [PATCH 10/36] Avoid ambiguities between scala.StringContext and internal.StringContext --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 8 ++++---- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../{StringContext.scala => StringContextMacro.scala} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename library/src-3.x/dotty/internal/{StringContext.scala => StringContextMacro.scala} (99%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8f17da8500de..e1e6a0162789 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -637,10 +637,10 @@ class Definitions { lazy val StringContextModule_applyR: TermRef = StringContextModule.requiredMethodRef(nme.apply) def StringContextModule_apply(implicit ctx: Context): Symbol = StringContextModule_applyR.symbol - lazy val InternalStringContextModuleR: TermRef = ctx.requiredModuleRef("dotty.internal.StringContext") - def InternalStringContextModule(implicit ctx: Context): Symbol = InternalStringContextModuleR.termSymbol - lazy val InternalStringContextModule_fR: TermRef = InternalStringContextModule.requiredMethodRef(nme.f) - def InternalStringContextModule_f(implicit ctx: Context): Symbol = InternalStringContextModule_fR.symbol + lazy val InternalStringContextMacroModuleR: TermRef = ctx.requiredModuleRef("dotty.internal.StringContextMacro") + def InternalStringContextMacroModule(implicit ctx: Context): Symbol = InternalStringContextMacroModuleR.termSymbol + lazy val InternalStringContextMacroModule_fR: TermRef = InternalStringContextMacroModule.requiredMethodRef(nme.f) + def InternalStringContextMacroModule_f(implicit ctx: Context): Symbol = InternalStringContextMacroModule_fR.symbol lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction") def PartialFunctionClass(implicit ctx: Context): ClassSymbol = PartialFunctionType.symbol.asClass diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 841651128888..ff0603716391 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2806,7 +2806,7 @@ class Typer extends Namer // a call to dotty.internal.StringContext.f which we can implement using the new macros. // As the macro is implemented in the bootstrapped library, it can only be used from the bootstrapped compiler. val Apply(TypeApply(Select(sc, _), _), args) = tree - val newCall = ref(defn.InternalStringContextModule_f).appliedTo(sc).appliedToArgs(args) + val newCall = ref(defn.InternalStringContextMacroModule_f).appliedTo(sc).appliedToArgs(args) readaptSimplified(Inliner.inlineCall(newCall)) } else { ctx.error("Scala 2 macro cannot be used in Dotty. See http://dotty.epfl.ch/docs/reference/dropped-features/macros.html", tree.sourcePos) diff --git a/library/src-3.x/dotty/internal/StringContext.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala similarity index 99% rename from library/src-3.x/dotty/internal/StringContext.scala rename to library/src-3.x/dotty/internal/StringContextMacro.scala index ad9ad767a744..948c4d010847 100644 --- a/library/src-3.x/dotty/internal/StringContext.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -7,10 +7,10 @@ import scala.language.implicitConversions import scala.quoted.Exprs.LiftedExpr import reflect._ -object StringContext { +object StringContextMacro { /** Implemetation of scala.StringContext.f used in Dotty while the standard library is still not bootstrapped */ - inline def f(sc: => scala.StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } + inline def f(sc: => StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } /** This trait defines a tool to report errors/warnings that do not depend on Position. */ trait Reporter{ From 0ed1c76bb6c6acd485266221ae26f40b0f033675 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 23 May 2019 10:24:16 +0200 Subject: [PATCH 11/36] Rename src-2.x StringContext to StringContextMacro --- .../internal/{StringContext.scala => StringContextMacro.scala} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename library/src-2.x/dotty/internal/{StringContext.scala => StringContextMacro.scala} (84%) diff --git a/library/src-2.x/dotty/internal/StringContext.scala b/library/src-2.x/dotty/internal/StringContextMacro.scala similarity index 84% rename from library/src-2.x/dotty/internal/StringContext.scala rename to library/src-2.x/dotty/internal/StringContextMacro.scala index c09d39b67b3f..0d1184941740 100644 --- a/library/src-2.x/dotty/internal/StringContext.scala +++ b/library/src-2.x/dotty/internal/StringContextMacro.scala @@ -1,6 +1,6 @@ package dotty.internal -object StringContext { +object StringContextMacro { @forceInline def f(sc: => scala.StringContext)(args: Any*): String = throw new Exception("non-boostrapped library") From d93063f2d4507c9047a3e0055d8a59173b0a865f Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 23 May 2019 10:28:16 +0200 Subject: [PATCH 12/36] Rename internal.StringContext to internal.StringContextMacro --- .../run-macros/f-interpolator-neg/Macros_1.scala | 16 ++++++++-------- .../stringinterpolation_macro-neg.scala | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index a373397a6495..ca893d5436a0 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -22,7 +22,7 @@ object Macro { // index in the list if arg or part, -1 otherwise // offset, 0 if strCtx, args or arg // message as given - val reporter = new dotty.internal.StringContext.Reporter{ + val reporter = new dotty.internal.StringContextMacro.Reporter{ private[this] var reported = false private[this] var oldReported = false def partError(message : String, index : Int, offset : Int) : Unit = { @@ -61,9 +61,9 @@ object Macro { reported = oldReported } } - val partsExpr = dotty.internal.StringContext.getPartsExprs(strCtxExpr) - val args = dotty.internal.StringContext.getArgsExprs(argsExpr) - dotty.internal.StringContext.interpolate(partsExpr, args, argsExpr, reporter) // Discard result + val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) + val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) + dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] @@ -72,7 +72,7 @@ object Macro { // index in the list if arg or part, -1 otherwise // offset, 0 if strCtx, args or arg // message as given - val reporter = new dotty.internal.StringContext.Reporter{ + val reporter = new dotty.internal.StringContextMacro.Reporter{ private[this] var reported = false private[this] var oldReported = false def partError(message : String, index : Int, offset : Int) : Unit = { @@ -111,9 +111,9 @@ object Macro { reported = oldReported } } - val partsExpr = dotty.internal.StringContext.getPartsExprs(strCtxExpr) - val args = dotty.internal.StringContext.getArgsExprs(argsExpr) - dotty.internal.StringContext.interpolate(partsExpr, args, argsExpr, reporter) // Discard result + val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) + val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) + dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList } } diff --git a/tests/run-macros/stringinterpolation_macro-neg.scala b/tests/run-macros/stringinterpolation_macro-neg.scala index cbf5c98f2216..7fcd6a2dec46 100644 --- a/tests/run-macros/stringinterpolation_macro-neg.scala +++ b/tests/run-macros/stringinterpolation_macro-neg.scala @@ -1,5 +1,4 @@ -import Macro._ /** * These tests test some combinations that should make the f interpolator fail. From fc14fb4b0467ba22f62e3f3b1aa5f2fccea833c2 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Thu, 23 May 2019 11:13:28 +0200 Subject: [PATCH 13/36] Update StringContextMacro.scala Apply the comments given by @nicolasstucki --- .../dotty/internal/StringContextMacro.scala | 492 +++++++++--------- 1 file changed, 238 insertions(+), 254 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 0e3133ba86b5..bbcbe4174ff6 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -3,17 +3,15 @@ package dotty.internal import scala.quoted._ import scala.quoted.matching._ import scala.tasty.Reflection -import scala.language.implicitConversions -import scala.quoted.Exprs.LiftedExpr import reflect._ object StringContextMacro { - /** Implemetation of scala.StringContext.f used in Dotty while the standard library is still not bootstrapped */ + /** Implementation of scala.StringContext.f used in Dotty */ inline def f(sc: => StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } /** This trait defines a tool to report errors/warnings that do not depend on Position. */ - trait Reporter{ + trait Reporter { /** Reports error/warning of size 1 linked with a part of the StringContext. * @@ -55,75 +53,59 @@ object StringContextMacro { } /** Retrieves a String from an Expr containing it - * - * @param expression the Expr containing the String - * @return the String contained in the given Expr - * quotes an error if the given Expr does not contain a String - */ + * + * @param expression the Expr containing the String + * @return the String contained in the given Expr + * quotes an error if the given Expr does not contain a String + */ private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { case Const(string : String) => string - case _ => QuoteError("Expected statically known part list") + case _ => QuoteError("Expected statically known part list", expression) } /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String - * - * @param strCtxExpr the Expr containing the StringContext - * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext - * quotes an error if the given Expr does not correspond to a StringContext - */ + * + * @param strCtxExpr the Expr containing the StringContext + * @return a list of Expr containing Strings, each corresponding to one parts of the given StringContext + * quotes an error if the given Expr does not correspond to a StringContext + */ def getPartsExprs(strCtxExpr : Expr[scala.StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ strCtxExpr.unseal.underlyingArgument match { case Apply(Select(Select(_, "StringContext") | Ident("StringContext"), "apply"), List(parts1)) => parts1.seal.cast[Seq[String]] match { case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") + case _ => QuoteError("Expected statically known String Context", strCtxExpr) } case Apply(Select(New(TypeIdent("StringContext")), _), List(parts1)) => parts1.seal.cast[Seq[String]] match { case ExprSeq(parts2) => parts2.toList - case _ => throw new Exception("Expected statically known String Context") + case _ => QuoteError("Expected statically known String Context", strCtxExpr) } - case _ => QuoteError("Expected statically known String Context") //TODO : quotes? + case _ => QuoteError("Expected statically known String Context", strCtxExpr) //TODO : quotes? } } /** Retrieves a list of Expr, each containing an argument, from an Expr of list of arguments - * - * @param argsExpr the Expr containing the list of arguments - * @return a list of Expr containing arguments - * quotes an error if the given Expr does not contain a list of arguments - */ + * + * @param argsExpr the Expr containing the list of arguments + * @return a list of Expr containing arguments + * quotes an error if the given Expr does not contain a list of arguments + */ def getArgsExprs(argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): List[Expr[Any]] = { import reflect._ argsExpr.unseal.underlyingArgument match { case Typed(Repeated(args, _), _) => args.map(_.seal) - case tree => QuoteError("Expected statically known argument list") - } - } - - /** Lifts a StringContext from StringContext to Expr of StringContext - * - * @return a Liftable StringContext by redefining the toExpr function - */ - private implicit def StringContextIsLiftable: Liftable[StringContext] = new Liftable[StringContext] { - def toExpr(strCtx: StringContext): Expr[StringContext] = { - implicit def ListIsLiftable: Liftable[List[String]] = new Liftable[List[String]] { - override def toExpr(list: List[String]): Expr[List[String]] = list match { - case x :: xs => '{${x.toExpr} :: ${toExpr(xs)}} - case Nil => '{Nil} - } - } - '{StringContext(${strCtx.parts.toList.toExpr}: _*)} + case tree => QuoteError("Expected statically known argument list", argsExpr) } } /** Interpolates the arguments to the formatting String given inside a StringContext - * - * @param strCtxExpr the Expr that holds the StringContext which contains all the chunks of the formatting string - * @param args the Expr that holds the sequence of arguments to interpolate to the String in the correct format - * @return the Expr containing the formatted and interpolated String or an error/warning if the parameters are not correct - */ + * + * @param strCtxExpr the Expr that holds the StringContext which contains all the chunks of the formatting string + * @param args the Expr that holds the sequence of arguments to interpolate to the String in the correct format + * @return the Expr containing the formatted and interpolated String or an error/warning if the parameters are not correct + */ private def interpolate(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]])(implicit reflect: Reflection): Expr[String] = { import reflect._ val sourceFile = strCtxExpr.unseal.pos.sourceFile @@ -178,22 +160,22 @@ object StringContextMacro { } /** Helper function for the interpolate function above - * - * @param partsExpr the list of parts enumerated as Expr - * @param args the list of arguments enumerated as Expr - * @param reporter the reporter to return any error/warning when a problem is encountered - * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct - */ + * + * @param partsExpr the list of parts enumerated as Expr + * @param args the list of arguments enumerated as Expr + * @param reporter the reporter to return any error/warning when a problem is encountered + * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct + */ def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { import reflect.{Literal => LiteralTree, _} /** Checks if the number of arguments are the same as the number of formatting strings - * - * @param format the number of formatting parts in the StringContext - * @param argument the number of arguments to interpolate in the string - * @return reports an error if the number of arguments does not match with the number of formatting strings, - * nothing otherwise - */ + * + * @param format the number of formatting parts in the StringContext + * @param argument the number of arguments to interpolate in the string + * @return reports an error if the number of arguments does not match with the number of formatting strings, + * nothing otherwise + */ def checkSizes(format : Int, argument : Int) : Unit = { if (format > argument && !(format == -1 && argument == 0)) if (argument == 0) @@ -210,12 +192,12 @@ object StringContextMacro { } /** Adds the default "%s" to the Strings that do not have any given format - * - * @param parts the list of parts contained in the StringContext - * @return a new list of string with all a defined formatting or reports an error if the '%' and - * formatting parameter are too far away from the argument that they refer to - * For example : f2"${d}random-leading-junk%d" will lead to an error - */ + * + * @param parts the list of parts contained in the StringContext + * @return a new list of string with all a defined formatting or reports an error if the '%' and + * formatting parameter are too far away from the argument that they refer to + * For example : f2"${d}random-leading-junk%d" will lead to an error + */ def addDefaultFormat(parts : List[String]) : List[String] = parts match { case Nil => Nil case p :: parts1 => p :: parts1.map((part : String) => { @@ -230,12 +212,12 @@ object StringContextMacro { } /** Checks whether a part contains a formatting substring - * - * @param part the part to check - * @param l the length of the given part - * @param index the index where to start to look for a potential new formatting string - * @return an Option containing the index in the part where a new formatting String starts, None otherwise - */ + * + * @param part the part to check + * @param l the length of the given part + * @param index the index where to start to look for a potential new formatting string + * @return an Option containing the index in the part where a new formatting String starts, None otherwise + */ def getFormattingSubstring(part : String, l : Int, index : Int) : Option[Int] = { var i = index var result : Option[Int] = None @@ -248,13 +230,13 @@ object StringContextMacro { } /** Finds all the flags that are inside a formatting String from a given index - * - * @param i the index in the String s where to start to check - * @param l the length of s - * @param s the String to check - * @return a list containing all the flags that are inside the formatting String, - * and their index in the String - */ + * + * @param i the index in the String s where to start to check + * @param l the length of s + * @param s the String to check + * @return a list containing all the flags that are inside the formatting String, + * and their index in the String + */ def getFlags(i : Int, l : Int, s : String) : List[(Char, Int)] = { def isFlag(c : Char) : Boolean = c match { case '-' | '#' | '+' | ' ' | '0' | ',' | '(' => true @@ -265,13 +247,13 @@ object StringContextMacro { } /** Skips the Characters that are width or argumentIndex parameters - * - * @param i the index where to start checking in the given String - * @param s the String to check - * @param l the length of s - * @return a tuple containing the index in the String after skipping - * the parameters, true if it has a width parameter and its value, false otherwise - */ + * + * @param i the index where to start checking in the given String + * @param s the String to check + * @param l the length of s + * @return a tuple containing the index in the String after skipping + * the parameters, true if it has a width parameter and its value, false otherwise + */ def skipWidth(i : Int, s : String, l : Int) = { var j = i var width = (false, 0) @@ -283,22 +265,22 @@ object StringContextMacro { } /** Retrieves all the formatting parameters from a part and their index in it - * - * @param part the String containing the formatting parameters - * @param argIndex the index of the current argument inside the list of arguments to interpolate - * @param partIndex the index of the current part inside the list of parts in the StringContext - * @param noArg true if there is no arg, i.e. "%%" or "%n" - * @param pos the initial index where to start checking the part - * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion - * parameter is missing. Otherwise, - * the index where the format specifier substring is, - * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and - * flags that contains the list of flags (empty if there is none), - * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), - * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), - * hasRelative (true if the specifiers use relative indexing, false otherwise) and - * conversion character index - */ + * + * @param part the String containing the formatting parameters + * @param argIndex the index of the current argument inside the list of arguments to interpolate + * @param partIndex the index of the current part inside the list of parts in the StringContext + * @param noArg true if there is no arg, i.e. "%%" or "%n" + * @param pos the initial index where to start checking the part + * @return reports an error if any of the size of the arguments and the parts do not match or if a conversion + * parameter is missing. Otherwise, + * the index where the format specifier substring is, + * hasArgumentIndex (true and the index of its corresponding argumentIndex if there is an argument index, false and 0 otherwise) and + * flags that contains the list of flags (empty if there is none), + * hasWidth (true and the index of the width parameter if there is a width, false and 0 otherwise), + * hasPrecision (true and the index of the precision if there is a precision, false and 0 otherwise), + * hasRelative (true if the specifiers use relative indexing, false otherwise) and + * conversion character index + */ def getFormatSpecifiers(part : String, argIndex : Int, partIndex : Int, noArg : Boolean, pos : Int) : (Boolean, Int, List[(Char, Int)], Boolean, Int, Boolean, Int, Boolean, Int, Int) = { var conversion = pos var hasArgumentIndex = false @@ -366,30 +348,30 @@ object StringContextMacro { } /** Checks if a given type is a subtype of any of the possibilities - * - * @param actualType the given type - * @param expectedType the type we are expecting - * @param argIndex the index of the argument that should type check - * @param possibilities all the types within which we want to find a super type of the actualType - * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, - * nothing otherwise - */ + * + * @param actualType the given type + * @param expectedType the type we are expecting + * @param argIndex the index of the argument that should type check + * @param possibilities all the types within which we want to find a super type of the actualType + * @return reports a type mismatch error if the actual type is not a subtype of any of the possibilities, + * nothing otherwise + */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) reporter.argError("type mismatch;\n found : " + actualType.widen.show + "\nrequired : " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds - * - * @param partIndex the index of the part we are checking - * @param offset the index in the part where there might be an error - * @param relative true if relative indexing is used, false otherwise - * @param argumentIndex the argument index parameter in the formatting String - * @param expectedArgumentIndex the expected argument index parameter - * @param maxArgumentIndex the maximum argument index parameter that can be used - * @return reports a warning if relative indexing is used but an argument is still given, - * an error is the argument index is not in the bounds [1, number of arguments] - */ + * + * @param partIndex the index of the part we are checking + * @param offset the index in the part where there might be an error + * @param relative true if relative indexing is used, false otherwise + * @param argumentIndex the argument index parameter in the formatting String + * @param expectedArgumentIndex the expected argument index parameter + * @param maxArgumentIndex the maximum argument index parameter that can be used + * @return reports a warning if relative indexing is used but an argument is still given, + * an error is the argument index is not in the bounds [1, number of arguments] + */ def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { if (relative) reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) @@ -402,35 +384,37 @@ object StringContextMacro { } /** Checks if a parameter is specified whereas it is not allowed - * - * @param hasParameter true if parameter is specified, false otherwise - * @param partIndex the index of the part inside the parts - * @param offset the index in the part where to report an error - * @param parameter the parameter that is not allowed - * @return reports an error if hasParameter is true, nothing otherwise - */ + * + * @param hasParameter true if parameter is specified, false otherwise + * @param partIndex the index of the part inside the parts + * @param offset the index in the part where to report an error + * @param parameter the parameter that is not allowed + * @return reports an error if hasParameter is true, nothing otherwise + */ def checkNotAllowedParameter(hasParameter : Boolean, partIndex : Int, offset : Int, parameter : String) = { if (hasParameter) reporter.partError(parameter + " not allowed", partIndex, offset) } /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error if the flag is not allowed, nothing otherwise - */ + * + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error if the flag is not allowed, nothing otherwise + */ def checkFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} reporter.partError(message, partIndex, flag._2) } /** Checks if the flags are allowed for the conversion - * @param partIndex the index of the part in the String Context - * @param flags the specified flags to check - * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char - * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise - */ + * + * @param partIndex the index of the part in the String Context + * @param flags the specified flags to check + * @param notAllowedFlagsOnCondition a list that maps which flags are allowed depending on the conversion Char + * @return reports an error only once if at least one of the flags is not allowed, nothing otherwise + */ def checkUniqueFlags(partIndex : Int, flags : List[(Char, Int)], notAllowedFlagOnCondition : (Char, Boolean, String)*) = { reporter.resetReported() for {flag <- flags ; (nonAllowedFlag, condition, message) <- notAllowedFlagOnCondition ; if (flag._1 == nonAllowedFlag && condition)} { @@ -442,14 +426,14 @@ object StringContextMacro { } /** Checks all the formatting parameters for a Character conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are different from '-' - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are different from '-' + */ def checkCharacterConversion(partIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precisionIndex : Int) = { val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Only '-' allowed for c conversion") checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) @@ -457,18 +441,18 @@ object StringContextMacro { } /** Checks all the formatting parameters for an Integral conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified or if the used flags are not allowed : - * ’d’: only ’#’ is allowed, - * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified or if the used flags are not allowed : + * ’d’: only ’#’ is allowed, + * ’o’, ’x’, ’X’: ’-’, ’#’, ’0’ are always allowed, depending on the type, this will be checked in the type check step + */ def checkIntegralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { if (conversionChar == 'd') checkFlags(partIndex, flags, ('#', true, "# not allowed for d conversion")) @@ -477,15 +461,15 @@ object StringContextMacro { } /** Checks all the formatting parameters for a Floating Point conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified for 'a', 'A' conversion or if the used flags are '(' and ',' for 'a', 'A' + */ def checkFloatingPointConversion(partIndex: Int, conversionChar : Char, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { if(conversionChar == 'a' || conversionChar == 'A'){ for {flag <- flags ; if (flag._1 == ',' || flag._1 == '(')} @@ -495,27 +479,26 @@ object StringContextMacro { } /** Checks all the formatting parameters for a Time conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param part the part that we are checking - * @param conversionIndex the index of the conversion Char used in the part - * @param flags the flags parameters inside the formatting part - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @return reports an error - * if precision is specified, if the time suffix is not given/incorrect or if the used flags are - * different from '-' - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part that we are checking + * @param conversionIndex the index of the conversion Char used in the part + * @param flags the flags parameters inside the formatting part + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @return reports an error + * if precision is specified, if the time suffix is not given/incorrect or if the used flags are + * different from '-' + */ def checkTimeConversion(partIndex : Int, part : String, conversionIndex : Int, flags : List[(Char, Int)], hasPrecision : Boolean, precision : Int) = { - /** - * Checks whether a time suffix is given and whether it is allowed - * - * @param part the part that we are checking - * @param partIndex the index of the part inside of the parts of the StringContext - * @param conversionIndex the index of the conversion Char inside the part - * @param return reports an error if no suffix is specified or if the given suffix is not - * part of the allowed ones - */ + /** Checks whether a time suffix is given and whether it is allowed + * + * @param part the part that we are checking + * @param partIndex the index of the part inside of the parts of the StringContext + * @param conversionIndex the index of the conversion Char inside the part + * @param return reports an error if no suffix is specified or if the given suffix is not + * part of the allowed ones + */ def checkTime(part : String, partIndex : Int, conversionIndex : Int) : Unit = { if (conversionIndex + 1 >= part.size) reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) @@ -536,30 +519,30 @@ object StringContextMacro { } /** Checks all the formatting parameters for a General conversion - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param argType the type of the argument matching with the given part - * @param conversionChar the Char used for the formatting conversion - * @param flags the flags parameters inside the formatting part - * @return reports an error - * if '#' flag is used or if any other flag is used - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param argType the type of the argument matching with the given part + * @param conversionChar the Char used for the formatting conversion + * @param flags the flags parameters inside the formatting part + * @return reports an error + * if '#' flag is used or if any other flag is used + */ def checkGeneralConversion(partIndex : Int, argType : Option[Type], conversionChar : Char, flags : List[(Char, Int)]) = { for {flag <- flags ; if (flag._1 != '-' && flag._1 != '#')} reporter.partError("Illegal flag '" + flag._1 + "'", partIndex, flag._2) } /** Checks all the formatting parameters for a special Char such as '%' and end of line - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the Char used for the formatting conversion - * @param hasPrecision true if precision parameter is specified, false otherwise - * @param precision the index of the precision parameter inside the part - * @param hasWidth true if width parameter is specified, false otherwise - * @param width the index of the width parameter inside the part - * @return reports an error if precision or width is specified for '%' or - * if precision is specified for end of line - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the Char used for the formatting conversion + * @param hasPrecision true if precision parameter is specified, false otherwise + * @param precision the index of the precision parameter inside the part + * @param hasWidth true if width parameter is specified, false otherwise + * @param width the index of the width parameter inside the part + * @return reports an error if precision or width is specified for '%' or + * if precision is specified for end of line + */ def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { case 'n' => { checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") @@ -570,24 +553,24 @@ object StringContextMacro { } /** Checks whether the format specifiers are correct depending on the conversion parameter - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param part the part to check - * The rest of the inputs correspond to the output of the function getFormatSpecifiers - * @param hasArgumentIndex - * @param actualArgumentIndex - * @param expectedArgumentIndex - * @param maxArgumentIndex - * @param hasRelative - * @param hasWidth - * @param hasPrecision - * @param precision - * @param flags - * @param conversion - * @param argType - * @return the argument index and its type if there is an argument, the flags and the conversion parameter - * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param part the part to check + * The rest of the inputs correspond to the output of the function getFormatSpecifiers + * @param hasArgumentIndex + * @param actualArgumentIndex + * @param expectedArgumentIndex + * @param maxArgumentIndex + * @param hasRelative + * @param hasWidth + * @param hasPrecision + * @param precision + * @param flags + * @param conversion + * @param argType + * @return the argument index and its type if there is an argument, the flags and the conversion parameter + * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise + */ def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { val conversionChar = part.charAt(conversion) @@ -609,13 +592,13 @@ object StringContextMacro { } /** Checks whether the argument type, if there is one, type checks with the formatting parameters - * - * @param partIndex the index of the part, that we are checking, inside the parts - * @param conversionChar the character used for the conversion - * @param argument an option containing the type and index of the argument, None if there is no argument - * @param flags the flags used for the formatting - * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise - */ + * + * @param partIndex the index of the part, that we are checking, inside the parts + * @param conversionChar the character used for the conversion + * @param argument an option containing the type and index of the argument, None if there is no argument + * @param flags the flags used for the formatting + * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise + */ def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { if (argument.nonEmpty) checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) @@ -624,14 +607,14 @@ object StringContextMacro { } /** Checks whether the argument type checks with the formatting parameters - * - * @param argument the given argument to check - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the argument type does not correspond with the conversion character, - * nothing otherwise - */ + * + * @param argument the given argument to check + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the argument type does not correspond with the conversion character, + * nothing otherwise + */ def checkTypeWithArgs(argument : (Type, Int), conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { val booleans = List(definitions.BooleanType, definitions.NullType) val dates = List(definitions.LongType, typeOf[java.util.Calendar], typeOf[java.util.Date]) @@ -665,13 +648,13 @@ object StringContextMacro { } /** Reports error when the formatting parameter require a specific type but no argument is given - * - * @param conversionChar the conversion parameter inside the formatting String - * @param partIndex index of the part inside the String Context - * @param flags the list of flags, and their index, used inside the formatting String - * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given - * nothing otherwise - */ + * + * @param conversionChar the conversion parameter inside the formatting String + * @param partIndex index of the part inside the String Context + * @param flags the list of flags, and their index, used inside the formatting String + * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given + * nothing otherwise + */ def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { conversionChar match { case 'o' | 'x' | 'X' => { @@ -689,14 +672,14 @@ object StringContextMacro { } /** Checks that a given part of the String Context respects every formatting constraint per parameter - * - * @param part a particular part of the String Context - * @param start the index from which we start checking the part - * @param argument an Option containing the argument corresponding to the part and its index in the list of args, - * None if no args are specified. - * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified - * @return a list with all the elements of the conversion per formatting string - */ + * + * @param part a particular part of the String Context + * @param start the index from which we start checking the part + * @param argument an Option containing the argument corresponding to the part and its index in the list of args, + * None if no args are specified. + * @param maxArgumentIndex an Option containing the maximum argument index possible, None if no args are specified + * @return a list with all the elements of the conversion per formatting string + */ def checkPart(part : String, start : Int, argument : Option[(Int, Expr[Any])], maxArgumentIndex : Option[Int]) : List[(Option[(Type, Int)], Char, List[(Char, Int)])] = { reporter.resetReported() val hasFormattingSubstring = getFormattingSubstring(part, part.size, start) @@ -766,11 +749,12 @@ object StringContextMacro { } /** Applies the interpolation to the input of the f-interpolator macro - * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String - * @param argsExpr the Expr containing the list of arguments to interpolate - * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not - * respect the formatting rules - */ + * + * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String + * @param argsExpr the Expr containing the list of arguments to interpolate + * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not + * respect the formatting rules + */ final def fImpl(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { interpolate(strCtxExpr, args) } From 920976175d55d64980b65698d5d7cf6a729ce8c1 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Thu, 23 May 2019 12:44:48 +0200 Subject: [PATCH 14/36] Delete .check tests for negative testing --- .../stringinterpolation_macro-neg.check | 172 ------------------ .../stringinterpolation_macro-neg.scala | 100 ---------- 2 files changed, 272 deletions(-) delete mode 100755 tests/run-macros/stringinterpolation_macro-neg.check delete mode 100644 tests/run-macros/stringinterpolation_macro-neg.scala diff --git a/tests/run-macros/stringinterpolation_macro-neg.check b/tests/run-macros/stringinterpolation_macro-neg.check deleted file mode 100755 index f7fc678ae9d3..000000000000 --- a/tests/run-macros/stringinterpolation_macro-neg.check +++ /dev/null @@ -1,172 +0,0 @@ -stringinterpolation_macro-neg.scala:22: error: there are no parts - new StringContext().f() - ^ -stringinterpolation_macro-neg.scala:23: error: too few arguments for interpolated string - new StringContext("", " is ", "%2d years old").f(s) - ^ -stringinterpolation_macro-neg.scala:24: error: too many arguments for interpolated string - new StringContext("", " is ", "%2d years old").f(s, d, d) - ^ -stringinterpolation_macro-neg.scala:25: error: too few arguments for interpolated string - new StringContext("", "").f() - ^ -stringinterpolation_macro-neg.scala:28: error: type mismatch; - found : String - required: Boolean - f"$s%b" - ^ -stringinterpolation_macro-neg.scala:29: error: type mismatch; - found : String - required: Char - f"$s%c" - ^ -stringinterpolation_macro-neg.scala:30: error: type mismatch; - found : Double - required: Char - f"$f%c" - ^ -stringinterpolation_macro-neg.scala:31: error: type mismatch; - found : String - required: Int - f"$s%x" - ^ -stringinterpolation_macro-neg.scala:32: error: type mismatch; - found : Boolean - required: Int - f"$b%d" - ^ -stringinterpolation_macro-neg.scala:33: error: type mismatch; - found : String - required: Int - f"$s%d" - ^ -stringinterpolation_macro-neg.scala:34: error: type mismatch; - found : Double - required: Int - f"$f%o" - ^ -stringinterpolation_macro-neg.scala:35: error: type mismatch; - found : String - required: Double - f"$s%e" - ^ -stringinterpolation_macro-neg.scala:36: error: type mismatch; - found : Boolean - required: Double - f"$b%f" - ^ -stringinterpolation_macro-neg.scala:41: error: type mismatch; - found : String - required: Int -Note that implicit conversions are not applicable because they are ambiguous: - both value strToInt2 of type String => Int - and value strToInt1 of type String => Int - are possible conversion functions from String to Int - f"$s%d" - ^ -stringinterpolation_macro-neg.scala:44: error: illegal conversion character 'i' - f"$s%i" - ^ -stringinterpolation_macro-neg.scala:47: error: Illegal flag '+' - f"$s%+ 0,(s" - ^ -stringinterpolation_macro-neg.scala:47: error: Illegal flag ' ' - f"$s%+ 0,(s" - ^ -stringinterpolation_macro-neg.scala:47: error: Illegal flag '0' - f"$s%+ 0,(s" - ^ -stringinterpolation_macro-neg.scala:47: error: Illegal flag ',' - f"$s%+ 0,(s" - ^ -stringinterpolation_macro-neg.scala:47: error: Illegal flag '(' - f"$s%+ 0,(s" - ^ -stringinterpolation_macro-neg.scala:48: error: Only '-' allowed for c conversion - f"$c%#+ 0,(c" - ^ -stringinterpolation_macro-neg.scala:49: error: # not allowed for d conversion - f"$d%#d" - ^ -stringinterpolation_macro-neg.scala:50: error: ',' only allowed for d conversion of integral types - f"$d%,x" - ^ -stringinterpolation_macro-neg.scala:51: error: only use '+' for BigInt conversions to o, x, X - f"$d%+ (x" - ^ -stringinterpolation_macro-neg.scala:51: error: only use ' ' for BigInt conversions to o, x, X - f"$d%+ (x" - ^ -stringinterpolation_macro-neg.scala:51: error: only use '(' for BigInt conversions to o, x, X - f"$d%+ (x" - ^ -stringinterpolation_macro-neg.scala:52: error: ',' not allowed for a, A - f"$f%,(a" - ^ -stringinterpolation_macro-neg.scala:52: error: '(' not allowed for a, A - f"$f%,(a" - ^ -stringinterpolation_macro-neg.scala:53: error: Only '-' allowed for date/time conversions - f"$t%#+ 0,(tT" - ^ -stringinterpolation_macro-neg.scala:56: error: precision not allowed - f"$c%.2c" - ^ -stringinterpolation_macro-neg.scala:57: error: precision not allowed - f"$d%.2d" - ^ -stringinterpolation_macro-neg.scala:58: error: precision not allowed - f"%.2%" - ^ -stringinterpolation_macro-neg.scala:59: error: precision not allowed - f"%.2n" - ^ -stringinterpolation_macro-neg.scala:60: error: precision not allowed - f"$f%.2a" - ^ -stringinterpolation_macro-neg.scala:61: error: precision not allowed - f"$t%.2tT" - ^ -stringinterpolation_macro-neg.scala:64: error: No last arg - f"% 1 - // implicit val strToInt2 = (s: String) => 2 - // f2"$s%d" - // } - - // f2"$s%i" - - // 3) flag mismatches - // f2"$s%+ 0,(s" - // f2"$c%#+ 0,(c" - // f2"$d%#d" - // f2"$d%,x" - // f2"$d%+ (x" - // f2"$f%,(a" - // f2"$t%#+ 0,(tT" - - // 4) bad precisions - // f2"$c%.2c" - // f2"$d%.2d" - // f2"%.2%" - // f2"%.2n" - // f2"$f%.2a" - // f2"$t%.2tT" - - // 5) bad indexes - // f2"% Date: Mon, 27 May 2019 13:26:36 +0200 Subject: [PATCH 15/36] Ignore type checking --- tests/run-macros/f-interpolator-neg/Tests_2.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 0edc3dbbc633..c797b0ef54e1 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -11,7 +11,7 @@ object Test { def formatTo(ff: java.util.Formatter, g: Int, w: Int, p: Int): Unit = ff format "xxx" } numberArgumentsTests(s, d) - interpolationMismatches(s, f, b) + // interpolationMismatches(s, f, b) flagMismatches(s, c, d, f, t) badPrecisions(c, d, f, t) badIndexes() From bd3d88abd67e616e5b69180a40bed36c68b939b4 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Mon, 27 May 2019 13:51:40 +0200 Subject: [PATCH 16/36] Add dealias to have types without packages --- library/src-3.x/dotty/internal/StringContextMacro.scala | 4 ++-- tests/run-macros/f-interpolator-neg/Tests_2.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index bbcbe4174ff6..9c1139c74559 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show + "\nrequired : " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.dealias.show + "\nrequired : " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -641,7 +641,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.widen.show + "\nrequired : java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.dealias.show + "\nrequired : java.util.Formattable\n", argIndex) case 'n' | '%' => case illegal => } diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index c797b0ef54e1..0edc3dbbc633 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -11,7 +11,7 @@ object Test { def formatTo(ff: java.util.Formatter, g: Int, w: Int, p: Int): Unit = ff format "xxx" } numberArgumentsTests(s, d) - // interpolationMismatches(s, f, b) + interpolationMismatches(s, f, b) flagMismatches(s, c, d, f, t) badPrecisions(c, d, f, t) badIndexes() From 0d3d83bda8e381f50e951200c76d091e6d41a573 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Wed, 29 May 2019 18:36:50 +0200 Subject: [PATCH 17/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 9c1139c74559..1d033ff0e344 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.dealias.show + "\nrequired : " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\nrequired : " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -641,7 +641,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.widen.dealias.show + "\nrequired : java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.") + "\nrequired : java.util.Formattable\n", argIndex) case 'n' | '%' => case illegal => } From bd404ee77d07d526d6d084e76d730304a7f2ec02 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Fri, 31 May 2019 10:17:53 +0200 Subject: [PATCH 18/36] Add ' ' for the type expected and actual --- .../dotty/internal/StringContextMacro.scala | 4 ++-- .../f-interpolator-neg/Tests_2.scala | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 1d033ff0e344..d8842ef6191d 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\nrequired : " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required : " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -641,7 +641,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.") + "\nrequired : java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.") + "\n required : java.util.Formattable\n", argIndex) case 'n' | '%' => case illegal => } diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 0edc3dbbc633..dfcdefd47629 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -31,15 +31,15 @@ object Test { def interpolationMismatches(s : String, f : Double, b : Boolean) = { import TestFooErrors._ - assertEquals(foo"$s%b", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Boolean"))) - assertEquals(foo"$s%c", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Char"))) - assertEquals(foo"$f%c", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Char"))) - assertEquals(foo"$s%x", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$b%d", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Int"))) - assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Int"))) - assertEquals(foo"$f%o", List((true, 1, 0, 0, "type mismatch;\nfound : Double\nrequired: Int"))) - assertEquals(foo"$s%e", List((true, 1, 0, 0, "type mismatch;\nfound : String\nrequired: Double"))) - assertEquals(foo"$b%f", List((true, 1, 0, 0, "type mismatch;\nfound : Boolean\nrequired: Double"))) + assertEquals(foo"$s%b", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Boolean"))) + assertEquals(foo"$s%c", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Char"))) + assertEquals(foo"$f%c", List((true, 1, 0, 0, "type mismatch;\n found : Double\n required: Char"))) + assertEquals(foo"$s%x", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Int"))) + assertEquals(foo"$b%d", List((true, 1, 0, 0, "type mismatch;\n found : Boolean\n required: Int"))) + assertEquals(foo"$s%d", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Int"))) + assertEquals(foo"$f%o", List((true, 1, 0, 0, "type mismatch;\n found : Double\n required: Int"))) + assertEquals(foo"$s%e", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Double"))) + assertEquals(foo"$b%f", List((true, 1, 0, 0, "type mismatch;\n found : Boolean\n required: Double"))) assertEquals(foo"$s%i", List((true, 0, 0, 1, "illegal conversion character 'i'"))) } From 3de568d5dc33bb77adcb2ea83aabe9a5569c99b5 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Fri, 31 May 2019 16:15:58 +0200 Subject: [PATCH 19/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index d8842ef6191d..252be3cfaf21 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required : " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required: " + expectedType + "\n", argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -641,7 +641,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.") + "\n required : java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found :" + argType.widen.show.stripPrefix("scala.Predef.") + "\n required: java.util.Formattable\n", argIndex) case 'n' | '%' => case illegal => } From 835a70123c16b4e279eb71063ed5009d16964810 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Fri, 31 May 2019 16:41:06 +0200 Subject: [PATCH 20/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 252be3cfaf21..f086cd6739e6 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required: " + expectedType + "\n", argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required: " + expectedType, argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -641,7 +641,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found :" + argType.widen.show.stripPrefix("scala.Predef.") + "\n required: java.util.Formattable\n", argIndex) + reporter.argError("type mismatch;\n found :" + argType.widen.show.stripPrefix("scala.Predef.") + "\n required: java.util.Formattable", argIndex) case 'n' | '%' => case illegal => } From 3a1e403eb7fbcf34f3d9402566c8e2d2a673bde8 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Mon, 3 Jun 2019 10:42:24 +0200 Subject: [PATCH 21/36] Fix errors on tests and test/fix code --- .../dotty/internal/StringContextMacro.scala | 52 +++++++++---------- .../f-interpolator-neg/Tests_2.scala | 38 +++++++------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index f086cd6739e6..3f54f9826073 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -203,7 +203,7 @@ object StringContextMacro { case p :: parts1 => p :: parts1.map((part : String) => { if (!part.startsWith("%")) { val index = part.indexOf('%') - if (index != -1) { + if (!reporter.hasReported() && index != -1) { reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", parts.indexOf(part), index) "%s" + part } else "%s" + part @@ -311,7 +311,7 @@ object StringContextMacro { //relative indexing val hasRelative = conversion < l && part.charAt(conversion) == '<' - val relativeIndex = conversion + 1 + val relativeIndex = conversion if (hasRelative) conversion += 1 @@ -333,13 +333,13 @@ object StringContextMacro { conversion += 1 } if (oldConversion == conversion) { - reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, 0) + reporter.partError("Missing conversion operator in '" + part.substring(pos, oldConversion - 1) + "'; use %% for literal %, %n for newline", partIndex, pos) hasPrecision = false } } //conversion - if(conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) + if((conversion >= l || (!part.charAt(conversion).isLetter && part.charAt(conversion) != '%')) && !reporter.hasReported()) reporter.partError("Missing conversion operator in '" + part.substring(pos, conversion) + "'; use %% for literal %, %n for newline", partIndex, pos) val hasWidth = (hasWidth1 && !hasArgumentIndex) || hasWidth2 @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.") + "\n required: " + expectedType, argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.").stripPrefix("scala.") + "\n required: " + expectedType, argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -379,7 +379,7 @@ object StringContextMacro { if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) reporter.partError("Argument index out of range", partIndex, offset) - if (expectedArgumentIndex != argumentIndex) + if (expectedArgumentIndex != argumentIndex && !reporter.hasReported()) reporter.partWarning("Index is not this arg", partIndex, offset) } @@ -545,10 +545,10 @@ object StringContextMacro { */ def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { case 'n' => { - checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") checkNotAllowedParameter(hasWidth, partIndex, width, "width") } - case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision + 1, "precision") + case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") case _ => // OK } @@ -597,13 +597,14 @@ object StringContextMacro { * @param conversionChar the character used for the conversion * @param argument an option containing the type and index of the argument, None if there is no argument * @param flags the flags used for the formatting + * @param formattingStart the index in the part where the formatting substring starts, i.e. where the '%' is * @return reports an error/warning if the formatting parameters are not allowed/wrong depending on the type, nothing otherwise */ - def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)]) = { + def checkArgTypeWithConversion(partIndex : Int, conversionChar : Char, argument : Option[(Type, Int)], flags : List[(Char, Int)], formattingStart : Int) = { if (argument.nonEmpty) checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) else - checkTypeWithoutArgs(conversionChar, partIndex, flags) + checkTypeWithoutArgs(conversionChar, partIndex, flags, formattingStart) } /** Checks whether the argument type checks with the formatting parameters @@ -628,9 +629,9 @@ object StringContextMacro { case 'd' | 'o' | 'x' | 'X' => { checkSubtype(argType, "Int", argIndex, integral : _*) if (conversionChar != 'd') { - val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "Only use '+' for BigInt conversions to o, x, X"), - (' ', !(argType <:< typeOf[java.math.BigInteger]), "Only use ' ' for BigInt conversions to o, x, X"), - ('(', !(argType <:< typeOf[java.math.BigInteger]), "Only use '(' for BigInt conversions to o, x, X"), + val notAllowedFlagOnCondition = List(('+', !(argType <:< typeOf[java.math.BigInteger]), "only use '+' for BigInt conversions to o, x, X"), + (' ', !(argType <:< typeOf[java.math.BigInteger]), "only use ' ' for BigInt conversions to o, x, X"), + ('(', !(argType <:< typeOf[java.math.BigInteger]), "only use '(' for BigInt conversions to o, x, X"), (',', true, "',' only allowed for d conversion of integral types")) checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) } @@ -641,7 +642,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found :" + argType.widen.show.stripPrefix("scala.Predef.") + "\n required: java.util.Formattable", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.").stripPrefix("scala.") + "\n required: java.util.Formattable", argIndex) case 'n' | '%' => case illegal => } @@ -652,23 +653,21 @@ object StringContextMacro { * @param conversionChar the conversion parameter inside the formatting String * @param partIndex index of the part inside the String Context * @param flags the list of flags, and their index, used inside the formatting String + * @param formattingStart the index in the part where the formatting substring starts, i.e. where the '%' is * @return reports an error if the formatting parameter refer to the type of the parameter but no parameter is given * nothing otherwise */ - def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)]) = { + def checkTypeWithoutArgs(conversionChar : Char, partIndex : Int, flags : List[(Char, Int)], formattingStart : Int) = { conversionChar match { case 'o' | 'x' | 'X' => { - val notAllowedFlagOnCondition = List(('+', true, "Only use '+' for BigInt conversions to o, x, X"), - (' ', true, "Only use ' ' for BigInt conversions to o, x, X"), - ('(', true, "Only use '(' for BigInt conversions to o, x, X"), + val notAllowedFlagOnCondition = List(('+', true, "only use '+' for BigInt conversions to o, x, X"), + (' ', true, "only use ' ' for BigInt conversions to o, x, X"), + ('(', true, "only use '(' for BigInt conversions to o, x, X"), (',', true, "',' only allowed for d conversion of integral types")) checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) } case _ => //OK } - - if (!reporter.hasReported() && conversionChar != '%' && conversionChar != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", partIndex, 0) } /** Checks that a given part of the String Context respects every formatting constraint per parameter @@ -699,14 +698,14 @@ object StringContextMacro { case None => { val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, 0, 0, true, formattingStart) if (hasArgumentIndex && !(part.charAt(argumentIndex).asDigit == 1 && (part.charAt(conversion) == 'n' || part.charAt(conversion) == '%'))) - reporter.partError("Argument index out of range", 0, argumentIndex + 1) + reporter.partError("Argument index out of range", 0, argumentIndex) if (hasRelative) reporter.partError("No last arg", 0, relativeIndex) if (!reporter.hasReported()){ val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) nextStart = conversion + 1 if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') - reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, 0) + reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, part.indexOf('%')) conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) } else checkPart(part, conversion + 1, argument, maxArgumentIndex) } @@ -730,16 +729,15 @@ object StringContextMacro { val argTypeWithConversion = checkPart(parts.head, 0, None, None) if (!reporter.hasReported()) for ((argument, conversionChar, flags) <- argTypeWithConversion) - checkArgTypeWithConversion(0, conversionChar, argument, flags) - } - else { + checkArgTypeWithConversion(0, conversionChar, argument, flags, parts.head.indexOf('%')) + } else { val partWithArgs = parts.tail.zip(args) for (i <- (0 until args.size)){ val (part, arg) = partWithArgs(i) val argTypeWithConversion = checkPart(part, 0, Some((i, arg)), Some(args.size)) if (!reporter.hasReported()) for ((argument, conversionChar, flags) <- argTypeWithConversion) - checkArgTypeWithConversion(0, conversionChar, argument, flags) + checkArgTypeWithConversion(i + 1, conversionChar, argument, flags, parts(i).indexOf('%')) } } } diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index dfcdefd47629..52a2ef8e1131 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -40,7 +40,7 @@ object Test { assertEquals(foo"$f%o", List((true, 1, 0, 0, "type mismatch;\n found : Double\n required: Int"))) assertEquals(foo"$s%e", List((true, 1, 0, 0, "type mismatch;\n found : String\n required: Double"))) assertEquals(foo"$b%f", List((true, 1, 0, 0, "type mismatch;\n found : Boolean\n required: Double"))) - assertEquals(foo"$s%i", List((true, 0, 0, 1, "illegal conversion character 'i'"))) + assertEquals(foo"$s%i", List((true, 0, 1, 1, "illegal conversion character 'i'"))) } def flagMismatches(s : String, c : Char, d : Int, f : Double, t : java.util.Date) = { @@ -60,31 +60,31 @@ object Test { import TestFooErrors._ assertEquals(foo"$c%.2c", List((true, 0, 1, 1, "precision not allowed"))) assertEquals(foo"$d%.2d", List((true, 0, 1, 1, "precision not allowed"))) - assertEquals(foo"%.2%", List((true, 0, 1, 1, "precision not allowed"))) - assertEquals(foo"%.2n", List((true, 0, 1, 1, "precision not allowed"))) + assertEquals(foo"%.2%", List((true, 0, 0, 1, "precision not allowed"))) + assertEquals(foo"%.2n", List((true, 0, 0, 1, "precision not allowed"))) assertEquals(foo"$f%.2a", List((true, 0, 1, 1, "precision not allowed"))) assertEquals(foo"$t%.2tT", List((true, 0, 1, 1, "precision not allowed"))) } def badIndexes() = { import TestFooErrors._ - assertEquals(foo"% Date: Mon, 3 Jun 2019 15:47:02 +0200 Subject: [PATCH 22/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 3f54f9826073..74ceffacee18 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -358,7 +358,7 @@ object StringContextMacro { */ def checkSubtype(actualType : Type, expectedType : String, argIndex : Int, possibilities : Type*) = { if (possibilities.find(actualType <:< _).isEmpty) - reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.").stripPrefix("scala.") + "\n required: " + expectedType, argIndex) + reporter.argError("type mismatch;\n found : " + actualType.widen.show.stripPrefix("scala.Predef.").stripPrefix("java.lang.").stripPrefix("scala.") + "\n required: " + expectedType, argIndex) } /** Checks whether a given argument index, relative or not, is in the correct bounds @@ -642,7 +642,7 @@ object StringContextMacro { case 'h' | 'H' | 'S' | 's' => if (!(argType <:< typeOf[java.util.Formattable])) for {flag <- flags ; if (flag._1 == '#')} - reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.").stripPrefix("scala.") + "\n required: java.util.Formattable", argIndex) + reporter.argError("type mismatch;\n found : " + argType.widen.show.stripPrefix("scala.Predef.").stripPrefix("java.lang.").stripPrefix("scala.") + "\n required: java.util.Formattable", argIndex) case 'n' | '%' => case illegal => } From 956572f05bf6dac9cf02192d4bdb09d3b78ee07b Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Mon, 3 Jun 2019 19:39:00 +0200 Subject: [PATCH 23/36] Update StringContextMacro.scala --- .../src-3.x/dotty/internal/StringContextMacro.scala | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 74ceffacee18..d6ac2e0c6edd 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -8,7 +8,7 @@ import reflect._ object StringContextMacro { /** Implementation of scala.StringContext.f used in Dotty */ - inline def f(sc: => StringContext)(args: Any*): String = ${ fImpl('sc, 'args) } + inline def f(sc: => StringContext)(args: Any*): String = ${ interpolate('sc, 'args) } /** This trait defines a tool to report errors/warnings that do not depend on Position. */ trait Reporter { @@ -745,15 +745,4 @@ object StringContextMacro { // macro expansion '{(${parts.mkString.toExpr}).format(${argsExpr}: _*)} } - - /** Applies the interpolation to the input of the f-interpolator macro - * - * @param strCtxExpr the Expr containing the StringContext with the parts of the formatting String - * @param argsExpr the Expr containing the list of arguments to interpolate - * @return the Expr containing the interpolated String, reports an error/warning if any formatting parameter does not - * respect the formatting rules - */ - final def fImpl(strCtxExpr: Expr[StringContext], args: Expr[Seq[Any]])(implicit reflect:Reflection): Expr[String] = { - interpolate(strCtxExpr, args) - } } \ No newline at end of file From d21414169a985c22ab0cba6cd297172116d304f2 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 14:43:41 +0200 Subject: [PATCH 24/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index d6ac2e0c6edd..40c5a92711c3 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -82,7 +82,7 @@ object StringContextMacro { case ExprSeq(parts2) => parts2.toList case _ => QuoteError("Expected statically known String Context", strCtxExpr) } - case _ => QuoteError("Expected statically known String Context", strCtxExpr) //TODO : quotes? + case _ => QuoteError("Expected statically known String Context", strCtxExpr) } } From 5969614eaeefa90fbcdc6b7efaaffc435cc9ca54 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 14:51:26 +0200 Subject: [PATCH 25/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 40c5a92711c3..571bd5cae6b0 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -60,7 +60,7 @@ object StringContextMacro { */ private def literalToString(expression : Expr[String])(implicit reflect: Reflection) : String = expression match { case Const(string : String) => string - case _ => QuoteError("Expected statically known part list", expression) + case _ => QuoteError("Expected statically known literal", expression) } /** Retrieves the parts from a StringContext, given inside an Expr, and returns them as a list of Expr of String From e6caa6f33518a0a4a84e18556e3015546b88c255 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 16:32:50 +0200 Subject: [PATCH 26/36] Update Macros_1.scala --- .../f-interpolator-neg/Macros_1.scala | 134 ++++++------------ 1 file changed, 42 insertions(+), 92 deletions(-) diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index ca893d5436a0..64816019e4ab 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -14,107 +14,57 @@ object TestFooErrors { // Defined in tests object Macro { def fooErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection): Expr[List[(Boolean, Int, Int, Int, String)]] = { - (strCtxExpr, argsExpr) match { - case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => - val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning - // 0 if part, 1 if arg, 2 if strCtx, 3 if args - // index in the list if arg or part, -1 otherwise - // offset, 0 if strCtx, args or arg - // message as given - val reporter = new dotty.internal.StringContextMacro.Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 0, $index, $offset, $message) } - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 0, $index, $offset, $message) } - } + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new dotty.internal.StringContextMacro.Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } + } - def argError(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 1, $index, 0, $message) } - } + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } - def strCtxError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 2, -1, 0, $message) } - } - def argsError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 3, -1, 0, $message) } - } + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } + } + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } + } - def hasReported() : Boolean = { - reported - } + def hasReported() : Boolean = { + reported + } - def resetReported() : Unit = { - oldReported = reported - reported = false - } + def resetReported() : Unit = { + oldReported = reported + reported = false + } - def restoreReported() : Unit = { - reported = oldReported - } - } - val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) - val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) - dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result - errors.result().toExprOfList - case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => + def restoreReported() : Unit = { + reported = oldReported + } + } + (strCtxExpr, argsExpr) match { + case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) | ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning - // 0 if part, 1 if arg, 2 if strCtx, 3 if args - // index in the list if arg or part, -1 otherwise - // offset, 0 if strCtx, args or arg - // message as given - val reporter = new dotty.internal.StringContextMacro.Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 0, $index, $offset, $message) } - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 0, $index, $offset, $message) } - } - - def argError(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 1, $index, 0, $message) } - } - - def strCtxError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 2, -1, 0, $message) } - } - def argsError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 3, -1, 0, $message) } - } - - def hasReported() : Boolean = { - reported - } - - def resetReported() : Unit = { - oldReported = reported - reported = false - } - - def restoreReported() : Unit = { - reported = oldReported - } - } val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList - } + case _ => QuoteError("expected statically known StringContext") } } \ No newline at end of file From 26b508097b8a7dfb2e9627d0e1a5762e295a5c77 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 17:17:45 +0200 Subject: [PATCH 27/36] Revert "Update Macros_1.scala" This reverts commit e6caa6f33518a0a4a84e18556e3015546b88c255. --- .../f-interpolator-neg/Macros_1.scala | 134 ++++++++++++------ 1 file changed, 92 insertions(+), 42 deletions(-) diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index 64816019e4ab..ca893d5436a0 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -14,57 +14,107 @@ object TestFooErrors { // Defined in tests object Macro { def fooErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection): Expr[List[(Boolean, Int, Int, Int, String)]] = { - // true if error, false if warning - // 0 if part, 1 if arg, 2 if strCtx, 3 if args - // index in the list if arg or part, -1 otherwise - // offset, 0 if strCtx, args or arg - // message as given - val reporter = new dotty.internal.StringContextMacro.Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 0, $index, $offset, $message) } - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 0, $index, $offset, $message) } - } + (strCtxExpr, argsExpr) match { + case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => + val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new dotty.internal.StringContextMacro.Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } + } - def argError(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 1, $index, 0, $message) } - } + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } - def strCtxError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 2, -1, 0, $message) } - } - def argsError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 3, -1, 0, $message) } - } + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } + } + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } + } - def hasReported() : Boolean = { - reported - } + def hasReported() : Boolean = { + reported + } - def resetReported() : Unit = { - oldReported = reported - reported = false - } + def resetReported() : Unit = { + oldReported = reported + reported = false + } - def restoreReported() : Unit = { - reported = oldReported - } - } - (strCtxExpr, argsExpr) match { - case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) | ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => + def restoreReported() : Unit = { + reported = oldReported + } + } + val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) + val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) + dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result + errors.result().toExprOfList + case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new dotty.internal.StringContextMacro.Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } + } + + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } + + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } + } + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } + } + + def hasReported() : Boolean = { + reported + } + + def resetReported() : Unit = { + oldReported = reported + reported = false + } + + def restoreReported() : Unit = { + reported = oldReported + } + } val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result errors.result().toExprOfList - case _ => QuoteError("expected statically known StringContext") + } } } \ No newline at end of file From f4ceaf3fb124bef4859b20ce736a4b24e2de5e41 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 17:39:37 +0200 Subject: [PATCH 28/36] Update StringContextMacro.scala --- library/src-3.x/dotty/internal/StringContextMacro.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 571bd5cae6b0..4fac492f92e2 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -704,7 +704,7 @@ object StringContextMacro { if (!reporter.hasReported()){ val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) nextStart = conversion + 1 - if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n') + if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n' && !hasArgumentIndex && !hasRelative) reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, part.indexOf('%')) conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) } else checkPart(part, conversion + 1, argument, maxArgumentIndex) From a9c8fe37c1f057a5e4d856b31112a96a2a9d2c20 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 4 Jun 2019 18:55:32 +0200 Subject: [PATCH 29/36] Fixed indexed argument errors --- .../dotty/internal/StringContextMacro.scala | 18 +++++++++++------- tests/run-macros/f-interpolator-tests.check | 10 +++++----- tests/run-macros/f-interpolator-tests.scala | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 4fac492f92e2..4baa763626e1 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -367,19 +367,20 @@ object StringContextMacro { * @param offset the index in the part where there might be an error * @param relative true if relative indexing is used, false otherwise * @param argumentIndex the argument index parameter in the formatting String + * @param expected true if we have an expectedArgumentIndex, false otherwise * @param expectedArgumentIndex the expected argument index parameter * @param maxArgumentIndex the maximum argument index parameter that can be used * @return reports a warning if relative indexing is used but an argument is still given, * an error is the argument index is not in the bounds [1, number of arguments] */ - def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { + def checkArgumentIndex(partIndex : Int, offset : Int, relative : Boolean, argumentIndex : Int, expected : Boolean, expectedArgumentIndex : Int, maxArgumentIndex : Int) = { if (relative) reporter.partWarning("Argument index ignored if '<' flag is present", partIndex, offset) if (argumentIndex > maxArgumentIndex || argumentIndex <= 0) reporter.partError("Argument index out of range", partIndex, offset) - if (expectedArgumentIndex != argumentIndex && !reporter.hasReported()) + if (expected && expectedArgumentIndex != argumentIndex && !reporter.hasReported()) reporter.partWarning("Index is not this arg", partIndex, offset) } @@ -560,6 +561,7 @@ object StringContextMacro { * @param hasArgumentIndex * @param actualArgumentIndex * @param expectedArgumentIndex + * @param firstFormattingSubstring true if it is the first in the list, i.e. not an indexed argument * @param maxArgumentIndex * @param hasRelative * @param hasWidth @@ -571,12 +573,14 @@ object StringContextMacro { * @return the argument index and its type if there is an argument, the flags and the conversion parameter * reports an error/warning if the formatting parameters are not allowed/wrong, nothing otherwise */ - def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], maxArgumentIndex : Option[Int], + def checkFormatSpecifiers(partIndex : Int, hasArgumentIndex : Boolean, actualArgumentIndex : Int, expectedArgumentIndex : Option[Int], firstFormattingSubstring : Boolean, maxArgumentIndex : Option[Int], hasRelative : Boolean, hasWidth : Boolean, width : Int, hasPrecision : Boolean, precision : Int, flags : List[(Char, Int)], conversion : Int, argType : Option[Type], part : String) : (Option[(Type, Int)], Char, List[(Char, Int)])= { val conversionChar = part.charAt(conversion) - if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty) - checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, expectedArgumentIndex.get, maxArgumentIndex.get) + if (hasArgumentIndex && expectedArgumentIndex.nonEmpty && maxArgumentIndex.nonEmpty && firstFormattingSubstring) + checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, true, expectedArgumentIndex.get, maxArgumentIndex.get) + else if(hasArgumentIndex && maxArgumentIndex.nonEmpty && !firstFormattingSubstring) + checkArgumentIndex(partIndex, actualArgumentIndex, hasRelative, part.charAt(actualArgumentIndex).asDigit, false, 0, maxArgumentIndex.get) conversionChar match { case 'c' | 'C' => checkCharacterConversion(partIndex, flags, hasPrecision, precision) @@ -690,7 +694,7 @@ object StringContextMacro { case Some(argIndex, arg) => { val (hasArgumentIndex, argumentIndex, flags, hasWidth, width, hasPrecision, precision, hasRelative, relativeIndex, conversion) = getFormatSpecifiers(part, argIndex, argIndex + 1, false, formattingStart) if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) + val conversionWithType = checkFormatSpecifiers(argIndex + 1, hasArgumentIndex, argumentIndex, Some(argIndex + 1), start == 0, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.unseal.tpe), part) nextStart = conversion + 1 conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) } else checkPart(part, conversion + 1, argument, maxArgumentIndex) @@ -702,7 +706,7 @@ object StringContextMacro { if (hasRelative) reporter.partError("No last arg", 0, relativeIndex) if (!reporter.hasReported()){ - val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) + val conversionWithType = checkFormatSpecifiers(0, hasArgumentIndex, argumentIndex, None, start == 0, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, None, part) nextStart = conversion + 1 if (!reporter.hasReported() && part.charAt(conversion) != '%' && part.charAt(conversion) != 'n' && !hasArgumentIndex && !hasRelative) reporter.partError("conversions must follow a splice; use %% for literal %, %n for newline", 0, part.indexOf('%')) diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check index 944033686970..6ac22594dbfb 100644 --- a/tests/run-macros/f-interpolator-tests.check +++ b/tests/run-macros/f-interpolator-tests.check @@ -30,12 +30,12 @@ Bob is 123 years old! Bob is 123 years old! Bob is 123 years old! Bob is 123%2d years old! -The boolean is false -The boolean is true +The boolean is false false +The boolean is true true a -The string is null -The string is -The string is string1 +The string is null null null +The string is +The string is string1 string1 string1 The string is null The string is The string is string2 diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index ae5ebe8a0626..77afde33f52c 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -61,11 +61,11 @@ object Test { def generalArgsTests = { - def booleanTest(b : Boolean) = println(f"The boolean is $b%b") + def booleanTest(b : Boolean) = println(f"The boolean is $b%b % Date: Tue, 11 Jun 2019 11:12:45 +0200 Subject: [PATCH 30/36] Try to set the local for time testing --- tests/run-macros/f-interpolator-tests.check | 22 ++++++++++ tests/run-macros/f-interpolator-tests.scala | 47 +++++++++++---------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check index 6ac22594dbfb..ee4918d9d68d 100644 --- a/tests/run-macros/f-interpolator-tests.check +++ b/tests/run-macros/f-interpolator-tests.check @@ -65,6 +65,28 @@ The float value is -Infinity 000 000000000 am ++0200 +CEST +399630056 +399630056000 +août +août +août +mardi +mar. +19 +1982 +82 +243 +08 +31 +31 +10:20 +10:20:56 +10:20:56 AM +08/31/82 +1982-08-31 +mar. août 31 10:20:56 CEST 1982 the percentage is 100 % we have a line separator now %n and now, we are on the next line a b b diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index 77afde33f52c..a29d369e3010 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -139,8 +139,9 @@ object Test { def dateArgsTests = { import java.text.SimpleDateFormat + import java.util.Locale - val sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss") + val sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss", Locale.FRANCE) val dateInString = "31-08-1982 10:20:56" val date = sdf.parse(dateInString) @@ -153,28 +154,28 @@ object Test { println(f"$date%tL") println(f"$date%tN") println(f"$date%tp") - // println(f"$date%tz") - // println(f"$date%tZ") - // println(f"$date%ts") - // println(f"$date%tQ") - // println(f"$date%tB") - // println(f"$date%tb") - // println(f"$date%th") - // println(f"$date%tA") - // println(f"$date%ta") - // println(f"$date%tC") - // println(f"$date%tY") - // println(f"$date%ty") - // println(f"$date%tj") - // println(f"$date%tm") - // println(f"$date%td") - // println(f"$date%te") - // println(f"$date%tR") - // println(f"$date%tT") - // println(f"$date%tr") - // println(f"$date%tD") - // println(f"$date%tF") - // println(f"$date%tc") + println(f"$date%tz") + println(f"$date%tZ") + println(f"$date%ts") + println(f"$date%tQ") + println(f"$date%tB") + println(f"$date%tb") + println(f"$date%th") + println(f"$date%tA") + println(f"$date%ta") + println(f"$date%tC") + println(f"$date%tY") + println(f"$date%ty") + println(f"$date%tj") + println(f"$date%tm") + println(f"$date%td") + println(f"$date%te") + println(f"$date%tR") + println(f"$date%tT") + println(f"$date%tr") + println(f"$date%tD") + println(f"$date%tF") + println(f"$date%tc") } def specificLiteralsTests = { From b426f355a6937f09e81d239aa269ab359a4eae6d Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 11 Jun 2019 12:22:10 +0200 Subject: [PATCH 31/36] Remove time local and comment time tests --- tests/run-macros/f-interpolator-tests.check | 8 ------- tests/run-macros/f-interpolator-tests.scala | 24 +++++++++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check index ee4918d9d68d..3570bccb8e87 100644 --- a/tests/run-macros/f-interpolator-tests.check +++ b/tests/run-macros/f-interpolator-tests.check @@ -65,15 +65,8 @@ The float value is -Infinity 000 000000000 am -+0200 -CEST 399630056 399630056000 -août -août -août -mardi -mar. 19 1982 82 @@ -86,7 +79,6 @@ mar. 10:20:56 AM 08/31/82 1982-08-31 -mar. août 31 10:20:56 CEST 1982 the percentage is 100 % we have a line separator now %n and now, we are on the next line a b b diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index a29d369e3010..05da6b5e76eb 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -137,11 +137,17 @@ object Test { aTest(Float.NegativeInfinity) } + /** + * Note that dates may appear in English or in any other language. This means that they + * depend on the place where the program is run. For that reason, some tests are commented, + * to avoid this dependence. + */ def dateArgsTests = { import java.text.SimpleDateFormat import java.util.Locale + import java.util.TimeZone - val sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss", Locale.FRANCE) + val sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss") val dateInString = "31-08-1982 10:20:56" val date = sdf.parse(dateInString) @@ -154,15 +160,15 @@ object Test { println(f"$date%tL") println(f"$date%tN") println(f"$date%tp") - println(f"$date%tz") - println(f"$date%tZ") + // println(f"$date%tz") + // println(f"$date%tZ") println(f"$date%ts") println(f"$date%tQ") - println(f"$date%tB") - println(f"$date%tb") - println(f"$date%th") - println(f"$date%tA") - println(f"$date%ta") + // println(f"$date%tB") + // println(f"$date%tb") + // println(f"$date%th") + // println(f"$date%tA") + // println(f"$date%ta") println(f"$date%tC") println(f"$date%tY") println(f"$date%ty") @@ -175,7 +181,7 @@ object Test { println(f"$date%tr") println(f"$date%tD") println(f"$date%tF") - println(f"$date%tc") + // println(f"$date%tc") } def specificLiteralsTests = { From 0f1c9e1c782cd445b859567443b1c572a44be16e Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 11 Jun 2019 13:16:56 +0200 Subject: [PATCH 32/36] Remove server dependent tests --- tests/run-macros/f-interpolator-tests.check | 2 -- tests/run-macros/f-interpolator-tests.scala | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check index 3570bccb8e87..27b23829dd41 100644 --- a/tests/run-macros/f-interpolator-tests.check +++ b/tests/run-macros/f-interpolator-tests.check @@ -65,8 +65,6 @@ The float value is -Infinity 000 000000000 am -399630056 -399630056000 19 1982 82 diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index 05da6b5e76eb..04385a13feb5 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -162,8 +162,8 @@ object Test { println(f"$date%tp") // println(f"$date%tz") // println(f"$date%tZ") - println(f"$date%ts") - println(f"$date%tQ") + // println(f"$date%ts") + // println(f"$date%tQ") // println(f"$date%tB") // println(f"$date%tb") // println(f"$date%th") From db2b39d7323a5f86f2791f0543ed9005b48c35d8 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Sun, 16 Jun 2019 16:25:36 +0200 Subject: [PATCH 33/36] Add flags check for specials --- .../src-3.x/dotty/internal/StringContextMacro.scala | 12 +++++++++--- tests/run-macros/f-interpolator-neg/Tests_2.scala | 3 +++ tests/run-macros/f-interpolator-tests.check | 3 ++- tests/run-macros/f-interpolator-tests.scala | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 4baa763626e1..28b96554d501 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -544,12 +544,18 @@ object StringContextMacro { * @return reports an error if precision or width is specified for '%' or * if precision is specified for end of line */ - def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int) = conversionChar match { + def checkSpecials(partIndex : Int, conversionChar : Char, hasPrecision : Boolean, precision : Int, hasWidth : Boolean, width : Int, flags : List[(Char, Int)]) = conversionChar match { case 'n' => { checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") checkNotAllowedParameter(hasWidth, partIndex, width, "width") + val notAllowedFlagOnCondition = for (flag <- List('-', '#', '+', ' ', '0', ',', '(')) yield (flag, true, "flags not allowed") + checkUniqueFlags(partIndex, flags, notAllowedFlagOnCondition : _*) + } + case '%' => { + checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") + val notAllowedFlagOnCondition = for (flag <- List('#', '+', ' ', '0', ',', '(')) yield (flag, true, "Illegal flag '" + flag + "'") + checkFlags(partIndex, flags, notAllowedFlagOnCondition : _*) } - case '%' => checkNotAllowedParameter(hasPrecision, partIndex, precision, "precision") case _ => // OK } @@ -588,7 +594,7 @@ object StringContextMacro { case 'e' | 'E' |'f' | 'g' | 'G' | 'a' | 'A' => checkFloatingPointConversion(partIndex, conversionChar, flags, hasPrecision, precision) case 't' | 'T' => checkTimeConversion(partIndex, part, conversion, flags, hasPrecision, precision) case 'b' | 'B' | 'h' | 'H' | 'S' | 's' => checkGeneralConversion(partIndex, argType, conversionChar, flags) - case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width) + case 'n' | '%' => checkSpecials(partIndex, conversionChar, hasPrecision, precision, hasWidth, width, flags) case illegal => reporter.partError("illegal conversion character '" + illegal + "'", partIndex, conversion) } diff --git a/tests/run-macros/f-interpolator-neg/Tests_2.scala b/tests/run-macros/f-interpolator-neg/Tests_2.scala index 52a2ef8e1131..4648c719772a 100644 --- a/tests/run-macros/f-interpolator-neg/Tests_2.scala +++ b/tests/run-macros/f-interpolator-neg/Tests_2.scala @@ -54,6 +54,9 @@ object Test { (true, 0, 1, 3, "only use '(' for BigInt conversions to o, x, X"))) assertEquals(foo"$f%,(a", List((true, 0, 1, 1, "',' not allowed for a, A"), (true, 0, 1, 2, "'(' not allowed for a, A"))) assertEquals(foo"$t%#+ 0,(tT", List((true, 0, 1, 1, "Only '-' allowed for date/time conversions"))) + assertEquals(foo"%-#+ 0,(n", List((true, 0, 0, 1, "flags not allowed"))) + assertEquals(foo"%#+ 0,(%", List((true, 0, 0, 1, "Illegal flag '#'"), (true, 0, 0, 2, "Illegal flag '+'"), + (true, 0, 0, 3, "Illegal flag ' '"), (true, 0, 0, 4, "Illegal flag '0'"), (true, 0, 0, 5, "Illegal flag ','"), (true, 0, 0, 6, "Illegal flag '('"))) } def badPrecisions(c : Char, d : Int, f : Double, t : java.util.Date) = { diff --git a/tests/run-macros/f-interpolator-tests.check b/tests/run-macros/f-interpolator-tests.check index 27b23829dd41..be5091b8ef39 100644 --- a/tests/run-macros/f-interpolator-tests.check +++ b/tests/run-macros/f-interpolator-tests.check @@ -78,5 +78,6 @@ am 08/31/82 1982-08-31 the percentage is 100 % -we have a line separator now %n and now, we are on the next line +we have a line separator now +and now, we are on the next line a b b diff --git a/tests/run-macros/f-interpolator-tests.scala b/tests/run-macros/f-interpolator-tests.scala index 04385a13feb5..ad712948a0d7 100755 --- a/tests/run-macros/f-interpolator-tests.scala +++ b/tests/run-macros/f-interpolator-tests.scala @@ -187,7 +187,7 @@ object Test { def specificLiteralsTests = { def percentArgsTest = println(f"the percentage is 100 %%") - def lineSeparatorArgs = println(f"we have a line separator now %%n and now, we are on the next line") + def lineSeparatorArgs = println(f"we have a line separator now %nand now, we are on the next line") def nothingTest = println(f"we have nothing") From 1e6ce2a7d39901baed148187f6fa4ef87d3ec1ee Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 18 Jun 2019 18:59:32 +0200 Subject: [PATCH 34/36] Modify expression extraction --- .../dotty/internal/StringContextMacro.scala | 14 +++----------- tests/run-macros/f-interpolator-neg/Macros_1.scala | 4 ++-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 28b96554d501..73c38c0bea88 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -71,17 +71,9 @@ object StringContextMacro { */ def getPartsExprs(strCtxExpr : Expr[scala.StringContext])(implicit reflect : Reflection): List[Expr[String]] = { import reflect._ - strCtxExpr.unseal.underlyingArgument match { - case Apply(Select(Select(_, "StringContext") | Ident("StringContext"), "apply"), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => QuoteError("Expected statically known String Context", strCtxExpr) - } - case Apply(Select(New(TypeIdent("StringContext")), _), List(parts1)) => - parts1.seal.cast[Seq[String]] match { - case ExprSeq(parts2) => parts2.toList - case _ => QuoteError("Expected statically known String Context", strCtxExpr) - } + strCtxExpr match { + case '{ StringContext(${ExprSeq(parts)}: _*) } => parts.toList + case '{ new StringContext(${ExprSeq(parts)}: _*) } => parts.toList case _ => QuoteError("Expected statically known String Context", strCtxExpr) } } diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index ca893d5436a0..b5f3d97bd964 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -17,7 +17,7 @@ object Macro { (strCtxExpr, argsExpr) match { case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning + // true if error, false if warning // 0 if part, 1 if arg, 2 if strCtx, 3 if args // index in the list if arg or part, -1 otherwise // offset, 0 if strCtx, args or arg @@ -67,7 +67,7 @@ object Macro { errors.result().toExprOfList case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning + // true if error, false if warning // 0 if part, 1 if arg, 2 if strCtx, 3 if args // index in the list if arg or part, -1 otherwise // offset, 0 if strCtx, args or arg From 752ae03fa0d8dfab71ee06b39a72ca2f567f80d0 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 18 Jun 2019 19:13:07 +0200 Subject: [PATCH 35/36] Update Macros_1.scala Remove code duplication --- .../f-interpolator-neg/Macros_1.scala | 140 ++++++------------ 1 file changed, 47 insertions(+), 93 deletions(-) diff --git a/tests/run-macros/f-interpolator-neg/Macros_1.scala b/tests/run-macros/f-interpolator-neg/Macros_1.scala index b5f3d97bd964..2752945ff75e 100644 --- a/tests/run-macros/f-interpolator-neg/Macros_1.scala +++ b/tests/run-macros/f-interpolator-neg/Macros_1.scala @@ -16,105 +16,59 @@ object Macro { def fooErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection): Expr[List[(Boolean, Int, Int, Int, String)]] = { (strCtxExpr, argsExpr) match { case ('{ StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => - val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning - // 0 if part, 1 if arg, 2 if strCtx, 3 if args - // index in the list if arg or part, -1 otherwise - // offset, 0 if strCtx, args or arg - // message as given - val reporter = new dotty.internal.StringContextMacro.Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 0, $index, $offset, $message) } - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 0, $index, $offset, $message) } - } - - def argError(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 1, $index, 0, $message) } - } - - def strCtxError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 2, -1, 0, $message) } - } - def argsError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 3, -1, 0, $message) } - } - - def hasReported() : Boolean = { - reported - } - - def resetReported() : Unit = { - oldReported = reported - reported = false - } - - def restoreReported() : Unit = { - reported = oldReported - } - } - val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) - val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) - dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result - errors.result().toExprOfList + fooErrorsImpl(parts, args, argsExpr) case ('{ new StringContext(${ExprSeq(parts)}: _*) }, ExprSeq(args)) => - val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] - // true if error, false if warning - // 0 if part, 1 if arg, 2 if strCtx, 3 if args - // index in the list if arg or part, -1 otherwise - // offset, 0 if strCtx, args or arg - // message as given - val reporter = new dotty.internal.StringContextMacro.Reporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 0, $index, $offset, $message) } - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - errors += '{ Tuple5(false, 0, $index, $offset, $message) } - } + fooErrorsImpl(parts, args, argsExpr) + } + } + + def fooErrorsImpl(parts : Seq[Expr[String]], args: Seq[Expr[Any]], argsExpr: Expr[Seq[Any]]) given (reflect: Reflection)= { + val errors = List.newBuilder[Expr[(Boolean, Int, Int, Int, String)]] + // true if error, false if warning + // 0 if part, 1 if arg, 2 if strCtx, 3 if args + // index in the list if arg or part, -1 otherwise + // offset, 0 if strCtx, args or arg + // message as given + val reporter = new dotty.internal.StringContextMacro.Reporter{ + private[this] var reported = false + private[this] var oldReported = false + def partError(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 0, $index, $offset, $message) } + } + def partWarning(message : String, index : Int, offset : Int) : Unit = { + reported = true + errors += '{ Tuple5(false, 0, $index, $offset, $message) } + } - def argError(message : String, index : Int) : Unit = { - reported = true - errors += '{ Tuple5(true, 1, $index, 0, $message) } - } + def argError(message : String, index : Int) : Unit = { + reported = true + errors += '{ Tuple5(true, 1, $index, 0, $message) } + } - def strCtxError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 2, -1, 0, $message) } - } - def argsError(message : String) : Unit = { - reported = true - errors += '{ Tuple5(true, 3, -1, 0, $message) } - } + def strCtxError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 2, -1, 0, $message) } + } + def argsError(message : String) : Unit = { + reported = true + errors += '{ Tuple5(true, 3, -1, 0, $message) } + } - def hasReported() : Boolean = { - reported - } + def hasReported() : Boolean = { + reported + } - def resetReported() : Unit = { - oldReported = reported - reported = false - } + def resetReported() : Unit = { + oldReported = reported + reported = false + } - def restoreReported() : Unit = { - reported = oldReported - } - } - val partsExpr = dotty.internal.StringContextMacro.getPartsExprs(strCtxExpr) - val args = dotty.internal.StringContextMacro.getArgsExprs(argsExpr) - dotty.internal.StringContextMacro.interpolate(partsExpr, args, argsExpr, reporter) // Discard result - errors.result().toExprOfList + def restoreReported() : Unit = { + reported = oldReported + } } + dotty.internal.StringContextMacro.interpolate(parts.toList, args.toList, argsExpr, reporter) // Discard result + errors.result().toExprOfList } } \ No newline at end of file From 3e38ef3c62e7870d53466a89c6e65c186bf9a4d9 Mon Sep 17 00:00:00 2001 From: Sara Alemanno Date: Tue, 18 Jun 2019 19:17:49 +0200 Subject: [PATCH 36/36] Update StringContextMacro.scala modify imports --- library/src-3.x/dotty/internal/StringContextMacro.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src-3.x/dotty/internal/StringContextMacro.scala b/library/src-3.x/dotty/internal/StringContextMacro.scala index 73c38c0bea88..ba087917b922 100644 --- a/library/src-3.x/dotty/internal/StringContextMacro.scala +++ b/library/src-3.x/dotty/internal/StringContextMacro.scala @@ -159,7 +159,7 @@ object StringContextMacro { * @return the Expr containing the formatted and interpolated String or an error/warning report if the parameters are not correct */ def interpolate(partsExpr : List[Expr[String]], args : List[Expr[Any]], argsExpr: Expr[Seq[Any]], reporter : Reporter)(implicit reflect: Reflection) : Expr[String] = { - import reflect.{Literal => LiteralTree, _} + import reflect._ /** Checks if the number of arguments are the same as the number of formatting strings *