diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index cbcc62b7fb6b..1ddc626d2646 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -85,7 +85,7 @@ class Compiler { new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only) new ExplicitOuter, // Add accessors to outer classes from nested ones. new ExplicitSelf, // Make references to non-trivial self types explicit as casts - new StringInterpolatorOpt) :: // Optimizes raw and s string interpolators by rewriting them to string concatenations + new StringInterpolatorOpt) :: // Optimizes raw and s and f string interpolators by rewriting them to string concatenations or formats List(new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions new UninitializedDefs, // Replaces `compiletime.uninitialized` by `_` new InlinePatterns, // Remove placeholders of inlined patterns diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 1a2f3cd3d86a..18f4f542b86c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1168,7 +1168,9 @@ object Scanners { finishNamedToken(IDENTIFIER, target = next) } else - error("invalid string interpolation: `$$`, `$\"`, `$`ident or `$`BlockExpr expected") + error("invalid string interpolation: `$$`, `$\"`, `$`ident or `$`BlockExpr expected", off = charOffset - 2) + putChar('$') + getStringPart(multiLine) } else { val isUnclosedLiteral = !isUnicodeEscape && (ch == SU || (!multiLine && (ch == CR || ch == LF))) @@ -1251,7 +1253,7 @@ object Scanners { nextChar() } } - val alt = if oct == LF then raw"\n" else f"\u$oct%04x" + val alt = if oct == LF then raw"\n" else f"\\u$oct%04x" error(s"octal escape literals are unsupported: use $alt instead", start) putChar(oct.toChar) } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d2efbeff2901..dd5d55b21f50 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -542,7 +542,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case '"' => "\\\"" case '\'' => "\\\'" case '\\' => "\\\\" - case _ => if (ch.isControl) f"\u${ch.toInt}%04x" else String.valueOf(ch) + case _ => if ch.isControl then f"\\u${ch.toInt}%04x" else String.valueOf(ch) } def toText(const: Constant): Text = const.tag match { diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala new file mode 100644 index 000000000000..0ba7bd14a9b6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala @@ -0,0 +1,287 @@ +package dotty.tools.dotc +package transform.localopt + +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer +import scala.util.chaining.* +import scala.util.matching.Regex.Match + +import java.util.{Calendar, Date, Formattable} + +import PartialFunction.cond + +import dotty.tools.dotc.ast.tpd.{Match => _, *} +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.core.Types._ +import dotty.tools.dotc.core.Phases.typerPhase +import dotty.tools.dotc.util.Spans.Span + +/** Formatter string checker. */ +class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List[Tree])(using Context): + + val argTypes = args.map(_.tpe) + val actuals = ListBuffer.empty[Tree] + + // count of args, for checking indexes + val argc = argTypes.length + + // Pick the first runtime type which the i'th arg can satisfy. + // If conversion is required, implementation must emit it. + def argType(argi: Int, types: Type*): Type = + require(argi < argc, s"$argi out of range picking from $types") + val tpe = argTypes(argi) + types.find(t => argConformsTo(argi, tpe, t)) + .orElse(types.find(t => argConvertsTo(argi, tpe, t))) + .getOrElse { + report.argError(s"Found: ${tpe.show}, Required: ${types.map(_.show).mkString(", ")}", argi) + actuals += args(argi) + types.head + } + + object formattableTypes: + val FormattableType = requiredClassRef("java.util.Formattable") + val BigIntType = requiredClassRef("scala.math.BigInt") + val BigDecimalType = requiredClassRef("scala.math.BigDecimal") + val CalendarType = requiredClassRef("java.util.Calendar") + val DateType = requiredClassRef("java.util.Date") + import formattableTypes.* + def argConformsTo(argi: Int, arg: Type, target: Type): Boolean = (arg <:< target).tap(if _ then actuals += args(argi)) + def argConvertsTo(argi: Int, arg: Type, target: Type): Boolean = + import typer.Implicits.SearchSuccess + atPhase(typerPhase) { + ctx.typer.inferView(args(argi), target) match + case SearchSuccess(view, ref, _, _) => actuals += view ; true + case _ => false + } + + // match a conversion specifier + val formatPattern = """%(?:(\d+)\$)?([-#+ 0,(<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""".r + + // ordinal is the regex group index in the format pattern + enum SpecGroup: + case Spec, Index, Flags, Width, Precision, CC + import SpecGroup.* + + /** For N part strings and N-1 args to interpolate, normalize parts and check arg types. + * + * Returns normalized part strings and args, where args correcpond to conversions in tail of parts. + */ + def checked: (List[String], List[Tree]) = + val amended = ListBuffer.empty[String] + val convert = ListBuffer.empty[Conversion] + + @tailrec + def loop(remaining: List[String], n: Int): Unit = + remaining match + case part0 :: more => + def badPart(t: Throwable): String = "".tap(_ => report.partError(t.getMessage, index = n, offset = 0)) + val part = try StringContext.processEscapes(part0) catch badPart + val matches = formatPattern.findAllMatchIn(part) + + def insertStringConversion(): Unit = + amended += "%s" + part + convert += Conversion(formatPattern.findAllMatchIn("%s").next(), n) // improve + argType(n-1, defn.AnyType) + def errorLeading(op: Conversion) = op.errorAt(Spec)(s"conversions must follow a splice; ${Conversion.literalHelp}") + def accept(op: Conversion): Unit = + if !op.isLeading then errorLeading(op) + op.accepts(argType(n-1, op.acceptableVariants*)) + amended += part + convert += op + + // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed + if n == 0 then amended += part + else if !matches.hasNext then insertStringConversion() + else + val cv = Conversion(matches.next(), n) + if cv.isLiteral then insertStringConversion() + else if cv.isIndexed then + if cv.index.getOrElse(-1) == n then accept(cv) else insertStringConversion() + else if !cv.isError then accept(cv) + + // any remaining conversions in this part must be either literals or indexed + while matches.hasNext do + val cv = Conversion(matches.next(), n) + if n == 0 && cv.hasFlag('<') then cv.badFlag('<', "No last arg") + else if !cv.isLiteral && !cv.isIndexed then errorLeading(cv) + + loop(more, n + 1) + case Nil => () + end loop + + loop(parts, n = 0) + if reported then (Nil, Nil) + else + assert(argc == actuals.size, s"Expected ${argc} args but got ${actuals.size} for [${parts.mkString(", ")}]") + (amended.toList, actuals.toList) + end checked + + extension (descriptor: Match) + def at(g: SpecGroup): Int = descriptor.start(g.ordinal) + def end(g: SpecGroup): Int = descriptor.end(g.ordinal) + def offset(g: SpecGroup, i: Int = 0): Int = at(g) + i + def group(g: SpecGroup): Option[String] = Option(descriptor.group(g.ordinal)) + def stringOf(g: SpecGroup): String = group(g).getOrElse("") + def intOf(g: SpecGroup): Option[Int] = group(g).map(_.toInt) + + extension (inline value: Boolean) + inline def or(inline body: => Unit): Boolean = value || { body ; false } + inline def orElse(inline body: => Unit): Boolean = value || { body ; true } + inline def and(inline body: => Unit): Boolean = value && { body ; true } + inline def but(inline body: => Unit): Boolean = value && { body ; false } + + enum Kind: + case StringXn, HashXn, BooleanXn, CharacterXn, IntegralXn, FloatingPointXn, DateTimeXn, LiteralXn, ErrorXn + import Kind.* + + /** A conversion specifier matched in the argi'th string part, with `argc` arguments to interpolate. + */ + final class Conversion(val descriptor: Match, val argi: Int, val kind: Kind): + // the descriptor fields + val index: Option[Int] = descriptor.intOf(Index) + val flags: String = descriptor.stringOf(Flags) + val width: Option[Int] = descriptor.intOf(Width) + val precision: Option[Int] = descriptor.group(Precision).map(_.drop(1).toInt) + val op: String = descriptor.stringOf(CC) + + // the conversion char is the head of the op string (but see DateTimeXn) + val cc: Char = + kind match + case ErrorXn => if op.isEmpty then '?' else op(0) + case DateTimeXn => if op.length > 1 then op(1) else '?' + case _ => op(0) + + def isIndexed: Boolean = index.nonEmpty || hasFlag('<') + def isError: Boolean = kind == ErrorXn + def isLiteral: Boolean = kind == LiteralXn + + // descriptor is at index 0 of the part string + def isLeading: Boolean = descriptor.at(Spec) == 0 + + // true if passes. + def verify: Boolean = + // various assertions + def goodies = goodFlags && goodIndex + def noFlags = flags.isEmpty or errorAt(Flags)("flags not allowed") + def noWidth = width.isEmpty or errorAt(Width)("width not allowed") + def noPrecision = precision.isEmpty or errorAt(Precision)("precision not allowed") + def only_-(msg: String) = + val badFlags = flags.filterNot { case '-' | '<' => true case _ => false } + badFlags.isEmpty or badFlag(badFlags(0), s"Only '-' allowed for $msg") + def goodFlags = + val badFlags = flags.filterNot(okFlags.contains) + for f <- badFlags do badFlag(f, s"Illegal flag '$f'") + badFlags.isEmpty + def goodIndex = + if index.nonEmpty && hasFlag('<') then warningAt(Index)("Argument index ignored if '<' flag is present") + val okRange = index.map(i => i > 0 && i <= argc).getOrElse(true) + okRange || hasFlag('<') or errorAt(Index)("Argument index out of range") + // begin verify + kind match + case StringXn => goodies + case BooleanXn => goodies + case HashXn => goodies + case CharacterXn => goodies && noPrecision && only_-("c conversion") + case IntegralXn => + def d_# = cc == 'd' && hasFlag('#') and badFlag('#', "# not allowed for d conversion") + def x_comma = cc != 'd' && hasFlag(',') and badFlag(',', "',' only allowed for d conversion of integral types") + goodies && noPrecision && !d_# && !x_comma + case FloatingPointXn => + goodies && (cc match + case 'a' | 'A' => + val badFlags = ",(".filter(hasFlag) + noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s"'$badf' not allowed for a, A")) + case _ => true + ) + case DateTimeXn => + def hasCC = op.length == 2 or errorAt(CC)("Date/time conversion must have two characters") + def goodCC = "HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc".contains(cc) or errorAt(CC, 1)(s"'$cc' doesn't seem to be a date or time conversion") + goodies && hasCC && goodCC && noPrecision && only_-("date/time conversions") + case LiteralXn => + op match + case "%" => goodies && noPrecision and width.foreach(_ => warningAt(Width)("width ignored on literal")) + case "n" => noFlags && noWidth && noPrecision + case ErrorXn => + errorAt(CC)(s"illegal conversion character '$cc'") + false + end verify + + // is the specifier OK with the given arg + def accepts(arg: Type): Boolean = + kind match + case BooleanXn => arg == defn.BooleanType orElse warningAt(CC)("Boolean format is null test for non-Boolean") + case IntegralXn => + arg == BigIntType || !cond(cc) { + case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => "+ (".filter(hasFlag).foreach(bad => badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X")) ; true + } + case _ => true + + // what arg type if any does the conversion accept + def acceptableVariants: List[Type] = + kind match + case StringXn => if hasFlag('#') then FormattableType :: Nil else defn.AnyType :: Nil + case BooleanXn => defn.BooleanType :: defn.NullType :: Nil + case HashXn => defn.AnyType :: Nil + case CharacterXn => defn.CharType :: defn.ByteType :: defn.ShortType :: defn.IntType :: Nil + case IntegralXn => defn.IntType :: defn.LongType :: defn.ByteType :: defn.ShortType :: BigIntType :: Nil + case FloatingPointXn => defn.DoubleType :: defn.FloatType :: BigDecimalType :: Nil + case DateTimeXn => defn.LongType :: CalendarType :: DateType :: Nil + case LiteralXn => Nil + case ErrorXn => Nil + + // what flags does the conversion accept? + private def okFlags: String = + kind match + case StringXn => "-#<" + case BooleanXn | HashXn => "-<" + case LiteralXn => "-" + case _ => "-#+ 0,(<" + + def hasFlag(f: Char) = flags.contains(f) + def hasAnyFlag(fs: String) = fs.exists(hasFlag) + + def badFlag(f: Char, msg: String) = + val i = flags.indexOf(f) match { case -1 => 0 case j => j } + errorAt(Flags, i)(msg) + + def errorAt(g: SpecGroup, i: Int = 0)(msg: String) = report.partError(msg, argi, descriptor.offset(g, i), descriptor.end(g)) + def warningAt(g: SpecGroup, i: Int = 0)(msg: String) = report.partWarning(msg, argi, descriptor.offset(g, i), descriptor.end(g)) + + object Conversion: + def apply(m: Match, i: Int): Conversion = + def kindOf(cc: Char) = cc match + case 's' | 'S' => StringXn + case 'h' | 'H' => HashXn + case 'b' | 'B' => BooleanXn + case 'c' | 'C' => CharacterXn + case 'd' | 'o' | + 'x' | 'X' => IntegralXn + case 'e' | 'E' | + 'f' | + 'g' | 'G' | + 'a' | 'A' => FloatingPointXn + case 't' | 'T' => DateTimeXn + case '%' | 'n' => LiteralXn + case _ => ErrorXn + end kindOf + m.group(CC) match + case Some(cc) => new Conversion(m, i, kindOf(cc(0))).tap(_.verify) + case None => new Conversion(m, i, ErrorXn).tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp")) + end apply + val literalHelp = "use %% for literal %, %n for newline" + end Conversion + + var reported = false + + private def partPosAt(index: Int, offset: Int, end: Int) = + val pos = partsElems(index).sourcePos + val bgn = pos.span.start + offset + val fin = if end < 0 then pos.span.end else pos.span.start + end + pos.withSpan(Span(bgn, fin, bgn)) + + extension (r: report.type) + def argError(message: String, index: Int): Unit = r.error(message, args(index).srcPos).tap(_ => reported = true) + def partError(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.error(message, partPosAt(index, offset, end)).tap(_ => reported = true) + def partWarning(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.warning(message, partPosAt(index, offset, end)).tap(_ => reported = true) +end TypedFormatChecker diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/FormatInterpolatorTransform.scala b/compiler/src/dotty/tools/dotc/transform/localopt/FormatInterpolatorTransform.scala new file mode 100644 index 000000000000..79d94c26c692 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/localopt/FormatInterpolatorTransform.scala @@ -0,0 +1,39 @@ +package dotty.tools.dotc +package transform.localopt + +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.* + +object FormatInterpolatorTransform: + + /** For f"${arg}%xpart", check format conversions and return (format, args) + * suitable for String.format(format, args). + */ + def checked(fun: Tree, args0: Tree)(using Context): (Tree, Tree) = + val (partsExpr, parts) = fun match + case TypeApply(Select(Apply(_, (parts: SeqLiteral) :: Nil), _), _) => + (parts.elems, parts.elems.map { case Literal(Constant(s: String)) => s }) + case _ => + report.error("Expected statically known StringContext", fun.srcPos) + (Nil, Nil) + val (args, elemtpt) = args0 match + case seqlit: SeqLiteral => (seqlit.elems, seqlit.elemtpt) + case _ => + report.error("Expected statically known argument list", args0.srcPos) + (Nil, EmptyTree) + + def literally(s: String) = Literal(Constant(s)) + if parts.lengthIs != args.length + 1 then + val badParts = + if parts.isEmpty then "there are no parts" + else s"too ${if parts.lengthIs > args.length + 1 then "few" else "many"} arguments for interpolated string" + report.error(badParts, fun.srcPos) + (literally(""), args0) + else + val checker = TypedFormatChecker(partsExpr, parts, args) + val (format, formatArgs) = checker.checked + if format.isEmpty then (literally(parts.mkString), args0) + else (literally(format.mkString), SeqLiteral(formatArgs.toList, elemtpt)) + end checked +end FormatInterpolatorTransform diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringContextChecker.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringContextChecker.scala deleted file mode 100644 index fbd09f43b853..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringContextChecker.scala +++ /dev/null @@ -1,714 +0,0 @@ -package dotty.tools.dotc -package transform.localopt - -import dotty.tools.dotc.ast.Trees._ -import dotty.tools.dotc.ast.tpd -import dotty.tools.dotc.core.Decorators._ -import dotty.tools.dotc.core.Constants.Constant -import dotty.tools.dotc.core.Contexts._ -import dotty.tools.dotc.core.StdNames._ -import dotty.tools.dotc.core.NameKinds._ -import dotty.tools.dotc.core.Symbols._ -import dotty.tools.dotc.core.Types._ - -// Ported from old dotty.internal.StringContextMacro -// TODO: port Scala 2 logic? (see https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/reflect/FormatInterpolator.scala#L74) -object StringContextChecker { - import tpd._ - - /** This trait defines a tool to report errors/warnings that do not depend on Position. */ - trait InterpolationReporter { - - /** 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 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 depending on the function - */ - def argError(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 - - /** 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 - } - - /** Check the format of the parts of the f".." arguments and returns the string parts of the StringContext */ - def checkedParts(strContext_f: Tree, args0: Tree)(using Context): String = { - - val (partsExpr, parts) = strContext_f match { - case TypeApply(Select(Apply(_, (parts: SeqLiteral) :: Nil), _), _) => - (parts.elems, parts.elems.map { case Literal(Constant(str: String)) => str } ) - case _ => - report.error("Expected statically known String Context", strContext_f.srcPos) - return "" - } - - val args = args0 match { - case args: SeqLiteral => args.elems - case _ => - report.error("Expected statically known argument list", args0.srcPos) - return "" - } - - val reporter = new InterpolationReporter{ - private[this] var reported = false - private[this] var oldReported = false - def partError(message : String, index : Int, offset : Int) : Unit = { - reported = true - val pos = partsExpr(index).sourcePos - val posOffset = pos.withSpan(pos.span.shift(offset)) - report.error(message, posOffset) - } - def partWarning(message : String, index : Int, offset : Int) : Unit = { - reported = true - val pos = partsExpr(index).sourcePos - val posOffset = pos.withSpan(pos.span.shift(offset)) - report.warning(message, posOffset) - } - - def argError(message : String, index : Int) : Unit = { - reported = true - report.error(message, args(index).srcPos) - } - - def strCtxError(message : String) : Unit = { - reported = true - report.error(message, strContext_f.srcPos) - } - def argsError(message : String) : Unit = { - reported = true - report.error(message, args0.srcPos) - } - - def hasReported() : Boolean = { - reported - } - - def resetReported() : Unit = { - oldReported = reported - reported = false - } - - def restoreReported() : Unit = { - reported = oldReported - } - } - - checked(parts, args, reporter) - } - - def checked(parts0: List[String], args: List[Tree], reporter: InterpolationReporter)(using Context): String = { - - - /** 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 (!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 - } 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 - 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, pos) - hasPrecision = false - } - } - - //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 - 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: List[Type]) = { - if !possibilities.exists(actualType <:< _) then - 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 - * - * @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 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, 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 (expected && expectedArgumentIndex != argumentIndex && !reporter.hasReported()) - 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: List[(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 : List[(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, List(('#', 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 = { - if (conversionIndex + 1 >= part.size) - reporter.partError("Date/time conversion must have two characters", partIndex, conversionIndex) - 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) - } - } - } - - 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, 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 _ => // 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 firstFormattingSubstring true if it is the first in the list, i.e. not an indexed argument - * @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], 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 && 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) - 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, flags) - 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 - * @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)], formattingStart : Int) = { - if (argument.nonEmpty) - checkTypeWithArgs(argument.get, conversionChar, partIndex, flags) - else - checkTypeWithoutArgs(conversionChar, partIndex, flags, formattingStart) - } - - /** 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)]) = { - def booleans = List(defn.BooleanType, defn.NullType) - def dates = List(defn.LongType, defn.JavaCalendarClass.typeRef, defn.JavaDateClass.typeRef) - def floatingPoints = List(defn.DoubleType, defn.FloatType, defn.JavaBigDecimalClass.typeRef) - def integral = List(defn.IntType, defn.LongType, defn.ShortType, defn.ByteType, defn.JavaBigIntegerClass.typeRef) - def character = List(defn.CharType, defn.ByteType, defn.ShortType, defn.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 <:< defn.JavaBigIntegerClass.typeRef), "only use '+' for BigInt conversions to o, x, X"), - (' ', !(argType <:< defn.JavaBigIntegerClass.typeRef), "only use ' ' for BigInt conversions to o, x, X"), - ('(', !(argType <:< defn.JavaBigIntegerClass.typeRef), "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 <:< defn.JavaFormattableClass.typeRef) then - for flag <- flags; if flag._1 == '#' do - 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 => - } - } - - /** 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 - * @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)], 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"), - (',', true, "',' only allowed for d conversion of integral types")) - checkFlags(partIndex, flags, notAllowedFlagOnCondition) - } - case _ => //OK - } - } - - /** 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, Tree)], 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), start == 0, maxArgumentIndex, hasRelative, hasWidth, width, hasPrecision, precision, flags, conversion, Some(arg.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) - if (hasRelative) - reporter.partError("No last arg", 0, relativeIndex) - if (!reporter.hasReported()){ - 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('%')) - conversionWithType :: checkPart(part, nextStart, argument, maxArgumentIndex) - } else checkPart(part, conversion + 1, argument, maxArgumentIndex) - } - } - } else { - reporter.restoreReported() - Nil - } - } - - val argument = args.size - - // check validity of formatting - checkSizes(parts0.size - 1, argument) - - // add default format - val parts = addDefaultFormat(parts0) - - 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()) - for ((argument, conversionChar, flags) <- argTypeWithConversion) - 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(i + 1, conversionChar, argument, flags, parts(i).indexOf('%')) - } - } - } - - parts.mkString - } -} diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 741803d41a66..b8e6300f4e04 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -13,157 +13,139 @@ import dotty.tools.dotc.core.Types._ import dotty.tools.dotc.transform.MegaPhase.MiniPhase import dotty.tools.dotc.typer.ConstFold -/** - * MiniPhase to transform s and raw string interpolators from using StringContext to string - * concatenation. Since string concatenation uses the Java String builder, we get a performance - * improvement in terms of these two interpolators. - * - * More info here: - * https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd - */ -class StringInterpolatorOpt extends MiniPhase { - import tpd._ +/** MiniPhase to transform s and raw string interpolators from using StringContext to string + * concatenation. Since string concatenation uses the Java String builder, we get a performance + * improvement in terms of these two interpolators. + * + * More info here: + * https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd + */ +class StringInterpolatorOpt extends MiniPhase: + import tpd.* override def phaseName: String = StringInterpolatorOpt.name override def description: String = StringInterpolatorOpt.description - override def checkPostCondition(tree: tpd.Tree)(using Context): Unit = { - tree match { + override def checkPostCondition(tree: tpd.Tree)(using Context): Unit = + tree match case tree: RefTree => val sym = tree.symbol - assert(sym != defn.StringContext_raw && sym != defn.StringContext_s, + assert(sym != defn.StringContext_raw && sym != defn.StringContext_s && sym != defn.StringContext_f, i"$tree in ${ctx.owner.showLocated} should have been rewritten by phase $phaseName") case _ => - } - } /** Matches a list of constant literals */ - private object Literals { - def unapply(tree: SeqLiteral)(using Context): Option[List[Literal]] = { - tree.elems match { - case literals if literals.forall(_.isInstanceOf[Literal]) => - Some(literals.map(_.asInstanceOf[Literal])) + private object Literals: + def unapply(tree: SeqLiteral)(using Context): Option[List[Literal]] = + tree.elems match + case literals if literals.forall(_.isInstanceOf[Literal]) => Some(literals.map(_.asInstanceOf[Literal])) case _ => None - } - } - } - private object StringContextApply { - def unapply(tree: Select)(using Context): Boolean = { - tree.symbol.eq(defn.StringContextModule_apply) && - tree.qualifier.symbol.eq(defn.StringContextModule) - } - } + private object StringContextApply: + def unapply(tree: Select)(using Context): Boolean = + (tree.symbol eq defn.StringContextModule_apply) && (tree.qualifier.symbol eq defn.StringContextModule) /** Matches an s or raw string interpolator */ - private object SOrRawInterpolator { - def unapply(tree: Tree)(using Context): Option[(List[Literal], List[Tree])] = { - tree match { - case Apply(Select(Apply(StringContextApply(), List(Literals(strs))), _), - List(SeqLiteral(elems, _))) if elems.length == strs.length - 1 => - Some(strs, elems) + private object SOrRawInterpolator: + def unapply(tree: Tree)(using Context): Option[(List[Literal], List[Tree])] = + tree match + case Apply(Select(Apply(StringContextApply(), List(Literals(strs))), _), List(SeqLiteral(elems, _))) + if elems.length == strs.length - 1 => Some(strs, elems) case _ => None - } - } - } //Extract the position from InvalidUnicodeEscapeException //which due to bincompat reasons is unaccessible. //TODO: remove once there is less restrictive bincompat - private object InvalidEscapePosition { - def unapply(t: Throwable): Option[Int] = t match { + private object InvalidEscapePosition: + def unapply(t: Throwable): Option[Int] = t match case iee: StringContext.InvalidEscapeException => Some(iee.index) - case il: IllegalArgumentException => il.getMessage() match { - case s"""invalid unicode escape at index $index of $_""" => index.toIntOption - case _ => None - } + case iae: IllegalArgumentException => iae.getMessage() match + case s"""invalid unicode escape at index $index of $_""" => index.toIntOption + case _ => None case _ => None - } - } - /** - * Match trees that resemble s and raw string interpolations. In the case of the s - * interpolator, escapes the string constants. Exposes the string constants as well as - * the variable references. - */ - private object StringContextIntrinsic { - def unapply(tree: Apply)(using Context): Option[(List[Literal], List[Tree])] = { - tree match { + /** Match trees that resemble s and raw string interpolations. In the case of the s + * interpolator, escapes the string constants. Exposes the string constants as well as + * the variable references. + */ + private object StringContextIntrinsic: + def unapply(tree: Apply)(using Context): Option[(List[Literal], List[Tree])] = + tree match case SOrRawInterpolator(strs, elems) => - if (tree.symbol == defn.StringContext_raw) Some(strs, elems) - else { // tree.symbol == defn.StringContextS + if tree.symbol == defn.StringContext_raw then Some(strs, elems) + else // tree.symbol == defn.StringContextS import dotty.tools.dotc.util.SourcePosition var stringPosition: SourcePosition = null - try { - val escapedStrs = strs.map(str => { + try + val escapedStrs = strs.map { str => stringPosition = str.sourcePos val escaped = StringContext.processEscapes(str.const.stringValue) cpy.Literal(str)(Constant(escaped)) - }) + } Some(escapedStrs, elems) - } catch { - case t @ InvalidEscapePosition(p) => { + catch + case t @ InvalidEscapePosition(p) => val errorSpan = stringPosition.span.startPos.shift(p) val errorPosition = stringPosition.withSpan(errorSpan) report.error(t.getMessage() + "\n", errorPosition) None - } - } - } case _ => None - } - } - } - override def transformApply(tree: Apply)(using Context): Tree = { + override def transformApply(tree: Apply)(using Context): Tree = + def mkConcat(strs: List[Literal], elems: List[Tree]): Tree = + val stri = strs.iterator + val elemi = elems.iterator + var result: Tree = stri.next + def concat(tree: Tree): Unit = + result = result.select(defn.String_+).appliedTo(tree).withSpan(tree.span) + while elemi.hasNext + do + concat(elemi.next) + val str = stri.next + if !str.const.stringValue.isEmpty then concat(str) + result + end mkConcat val sym = tree.symbol - val isInterpolatedMethod = // Test names first to avoid loading scala.StringContext if not used - (sym.name == nme.raw_ && sym.eq(defn.StringContext_raw)) || - (sym.name == nme.f && sym.eq(defn.StringContext_f)) || - (sym.name == nme.s && sym.eq(defn.StringContext_s)) - if (isInterpolatedMethod) - (tree: @unchecked) match { + // Test names first to avoid loading scala.StringContext if not used, and common names first + val isInterpolatedMethod = + sym.name match + case nme.s => sym eq defn.StringContext_s + case nme.raw_ => sym eq defn.StringContext_raw + case nme.f => sym eq defn.StringContext_f + case _ => false + // Perform format checking and normalization, then make it StringOps(fmt).format(args1) with tweaked args + def transformF(fun: Tree, args: Tree): Tree = + val (fmt, args1) = FormatInterpolatorTransform.checked(fun, args) + resolveConstructor(defn.StringOps.typeRef, List(fmt)) + .select(nme.format) + .appliedTo(args1) + // Starting with Scala 2.13, s and raw are macros in the standard + // library, so we need to expand them manually. + // sc.s(args) --> standardInterpolator(processEscapes, args, sc.parts) + // sc.raw(args) --> standardInterpolator(x => x, args, sc.parts) + def transformS(fun: Tree, args: Tree, isRaw: Boolean): Tree = + val pre = fun match + case Select(pre, _) => pre + case intp: Ident => tpd.desugarIdentPrefix(intp) + val stringToString = defn.StringContextModule_processEscapes.info.asInstanceOf[MethodType] + val process = tpd.Lambda(stringToString, args => + if isRaw then args.head else ref(defn.StringContextModule_processEscapes).appliedToTermArgs(args) + ) + evalOnce(pre) { sc => + val parts = sc.select(defn.StringContext_parts) + ref(defn.StringContextModule_standardInterpolator) + .appliedToTermArgs(List(process, args, parts)) + } + end transformS + // begin transformApply + if isInterpolatedMethod then + (tree: @unchecked) match case StringContextIntrinsic(strs: List[Literal], elems: List[Tree]) => - val stri = strs.iterator - val elemi = elems.iterator - var result: Tree = stri.next - def concat(tree: Tree): Unit = { - result = result.select(defn.String_+).appliedTo(tree).withSpan(tree.span) - } - while (elemi.hasNext) { - concat(elemi.next) - val str = stri.next - if (!str.const.stringValue.isEmpty) concat(str) - } - result - case Apply(intp, args :: Nil) if sym.eq(defn.StringContext_f) => - val partsStr = StringContextChecker.checkedParts(intp, args).mkString - resolveConstructor(defn.StringOps.typeRef, List(Literal(Constant(partsStr)))) - .select(nme.format) - .appliedTo(args) - // Starting with Scala 2.13, s and raw are macros in the standard - // library, so we need to expand them manually. - // sc.s(args) --> standardInterpolator(processEscapes, args, sc.parts) - // sc.raw(args) --> standardInterpolator(x => x, args, sc.parts) + mkConcat(strs, elems) case Apply(intp, args :: Nil) => - val pre = intp match { - case Select(pre, _) => pre - case intp: Ident => tpd.desugarIdentPrefix(intp) - } - val isRaw = sym eq defn.StringContext_raw - val stringToString = defn.StringContextModule_processEscapes.info.asInstanceOf[MethodType] - - val process = tpd.Lambda(stringToString, args => - if (isRaw) args.head else ref(defn.StringContextModule_processEscapes).appliedToTermArgs(args)) - - evalOnce(pre) { sc => - val parts = sc.select(defn.StringContext_parts) - - ref(defn.StringContextModule_standardInterpolator) - .appliedToTermArgs(List(process, args, parts)) - } - } + if sym eq defn.StringContext_f then transformF(intp, args) + else transformS(intp, args, isRaw = sym eq defn.StringContext_raw) else tree.tpe match case _: ConstantType => tree @@ -171,16 +153,12 @@ class StringInterpolatorOpt extends MiniPhase { ConstFold.Apply(tree).tpe match case ConstantType(x) => Literal(x).withSpan(tree.span).ensureConforms(tree.tpe) case _ => tree - } - override def transformSelect(tree: Select)(using Context): Tree = { + override def transformSelect(tree: Select)(using Context): Tree = ConstFold.Select(tree).tpe match case ConstantType(x) => Literal(x).withSpan(tree.span).ensureConforms(tree.tpe) case _ => tree - } - -} object StringInterpolatorOpt: val name: String = "stringInterpolatorOpt" - val description: String = "optimize raw and s string interpolators" + val description: String = "optimize s, f and raw string interpolators" diff --git a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala index d92c16cfe54d..88ee3e985277 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala @@ -1423,7 +1423,7 @@ object SourceCode { case '"' => "\\\"" case '\'' => "\\\'" case '\\' => "\\\\" - case _ => if (ch.isControl) f"\u${ch.toInt}%04x" else String.valueOf(ch) + case _ => if ch.isControl then f"\\u${ch.toInt}%04x" else String.valueOf(ch) } private def escapedString(str: String): String = str flatMap escapedChar diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 55f8afa26260..d5256986f874 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -63,8 +63,9 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na case "" => Nil case nonEmptyLine => nonEmptyLine :: Nil } + def nonBlank(line: String): Boolean = line.exists(!Character.isWhitespace(_)) - val expectedOutput = lines.flatMap(filterEmpties) + val expectedOutput = lines.filter(nonBlank) val actualOutput = { val opts = toolArgsParse(lines.take(1)) val (optsLine, inputLines) = if opts.isEmpty then ("", lines) else (lines.head, lines.drop(1)) @@ -80,7 +81,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na out.linesIterator.foreach(buf.append) nstate } - (optsLine :: buf.toList).flatMap(filterEmpties) + (optsLine :: buf.toList).filter(nonBlank) } if !FileDiff.matches(actualOutput, expectedOutput) then diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index a497eed7072e..bd30e7fff98e 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -14,13 +14,14 @@ import java.util.concurrent.{TimeUnit, TimeoutException, Executors => JExecutors import scala.collection.mutable import scala.io.{Codec, Source} +import scala.jdk.CollectionConverters.* import scala.util.{Random, Try, Failure => TryFailure, Success => TrySuccess, Using} import scala.util.control.NonFatal import scala.util.matching.Regex import scala.collection.mutable.ListBuffer import dotc.{Compiler, Driver} -import dotc.core.Contexts._ +import dotc.core.Contexts.* import dotc.decompiler import dotc.report import dotc.interfaces.Diagnostic.ERROR @@ -750,17 +751,26 @@ trait ParallelTesting extends RunnerOrchestration { self => def compilerCrashed = reporters.exists(_.compilerCrashed) lazy val (errorMap, expectedErrors) = getErrorMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq) lazy val actualErrors = reporters.foldLeft(0)(_ + _.errorCount) - def hasMissingAnnotations = getMissingExpectedErrors(errorMap, reporters.iterator.flatMap(_.errors)) + lazy val (expected, unexpected) = getMissingExpectedErrors(errorMap, reporters.iterator.flatMap(_.errors)) + def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty def showErrors = "-> following the errors:\n" + - reporters.flatMap(_.allErrors.map(e => (e.pos.line + 1).toString + ": " + e.message)).mkString(start = "at ", sep = "\n at ", end = "") - - if (compilerCrashed) Some(s"Compiler crashed when compiling: ${testSource.title}") - else if (actualErrors == 0) Some(s"\nNo errors found when compiling neg test $testSource") - else if (expectedErrors == 0) Some(s"\nNo errors expected/defined in $testSource -- use // error or // nopos-error") - else if (expectedErrors != actualErrors) Some(s"\nWrong number of errors encountered when compiling $testSource\nexpected: $expectedErrors, actual: $actualErrors " + showErrors) - else if (hasMissingAnnotations) Some(s"\nErrors found on incorrect row numbers when compiling $testSource\n$showErrors") - else if (!errorMap.isEmpty) Some(s"\nExpected error(s) have {=}: $errorMap") - else None + reporters.flatMap(_.allErrors.sortBy(_.pos.line).map(e => s"${e.pos.line + 1}: ${e.message}")).mkString(" at ", "\n at ", "") + + Option { + if compilerCrashed then s"Compiler crashed when compiling: ${testSource.title}" + else if actualErrors == 0 then s"\nNo errors found when compiling neg test $testSource" + else if expectedErrors == 0 then s"\nNo errors expected/defined in $testSource -- use // error or // nopos-error" + else if expectedErrors != actualErrors then + s"""|Wrong number of errors encountered when compiling $testSource + |expected: $expectedErrors, actual: $actualErrors + |${expected.mkString("Unfulfilled expectations:\n", "\n", "")} + |${unexpected.mkString("Unexpected errors:\n", "\n", "")} + |$showErrors + |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "") + else if hasMissingAnnotations then s"\nErrors found on incorrect row numbers when compiling $testSource\n$showErrors" + else if !errorMap.isEmpty then s"\nExpected error(s) have {=}: $errorMap" + else null + } } override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit = @@ -775,72 +785,59 @@ trait ParallelTesting extends RunnerOrchestration { self => // // We collect these in a map `"file:row" -> numberOfErrors`, for // nopos errors we save them in `"file" -> numberOfNoPosErrors` - def getErrorMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) = { + def getErrorMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) = + val comment = raw"//( *)(nopos-|anypos-)?error".r val errorMap = new HashMap[String, Integer]() var expectedErrors = 0 + def bump(key: String): Unit = + errorMap.get(key) match + case null => errorMap.put(key, 1) + case n => errorMap.put(key, n+1) + expectedErrors += 1 files.filter(isSourceFile).foreach { file => Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source => source.getLines.zipWithIndex.foreach { case (line, lineNbr) => - val errors = line.toSeq.sliding("// error".length).count(_.unwrap == "// error") - if (errors > 0) - errorMap.put(s"${file.getPath}:$lineNbr", errors) - - val noposErrors = line.toSeq.sliding("// nopos-error".length).count(_.unwrap == "// nopos-error") - if (noposErrors > 0) { - val nopos = errorMap.get("nopos") - val existing: Integer = if (nopos eq null) 0 else nopos - errorMap.put("nopos", noposErrors + existing) - } - - val anyposErrors = line.toSeq.sliding("// anypos-error".length).count(_.unwrap == "// anypos-error") - if (anyposErrors > 0) { - val anypos = errorMap.get("anypos") - val existing: Integer = if (anypos eq null) 0 else anypos - errorMap.put("anypos", anyposErrors + existing) + comment.findAllMatchIn(line).foreach { m => + m.group(2) match + case prefix if m.group(1).isEmpty => + val what = Option(prefix).getOrElse("") + echo(s"Warning: ${file.getCanonicalPath}:${lineNbr}: found `//${what}error` but expected `// ${what}error`, skipping comment") + case "nopos-" => bump("nopos") + case "anypos-" => bump("anypos") + case _ => bump(s"${file.getPath}:${lineNbr+1}") } - - val possibleTypos = List("//error" -> "// error", "//nopos-error" -> "// nopos-error", "//anypos-error" -> "// anypos-error") - for ((possibleTypo, expected) <- possibleTypos) { - if (line.contains(possibleTypo)) - echo(s"Warning: Possible typo in error tag in file ${file.getCanonicalPath}:$lineNbr: found `$possibleTypo` but expected `$expected`") - } - - expectedErrors += anyposErrors + noposErrors + errors } }.get } - (errorMap, expectedErrors) - } - - def getMissingExpectedErrors(errorMap: HashMap[String, Integer], reporterErrors: Iterator[Diagnostic]) = !reporterErrors.forall { error => - val pos1 = error.pos.nonInlined - val key = if (pos1.exists) { - def toRelative(path: String): String = // For some reason, absolute paths leak from the compiler itself... - path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator) - val fileName = toRelative(pos1.source.file.toString) - s"$fileName:${pos1.line}" - - } else "nopos" - - val errors = errorMap.get(key) - - def missing = { echo(s"Error reported in ${pos1.source}, but no annotation found") ; false } - - if (errors ne null) { - if (errors == 1) errorMap.remove(key) - else errorMap.put(key, errors - 1) - true - } - else if key == "nopos" then - missing - else - errorMap.get("anypos") match - case null => missing - case 1 => errorMap.remove("anypos") ; true - case slack => if slack < 1 then missing - else errorMap.put("anypos", slack - 1) ; true - } + end getErrorMapAndExpectedCount + + // return unfulfilled expected errors and unexpected diagnostics + def getMissingExpectedErrors(errorMap: HashMap[String, Integer], reporterErrors: Iterator[Diagnostic]): (List[String], List[String]) = + val unexpected, unpositioned = ListBuffer.empty[String] + // For some reason, absolute paths leak from the compiler itself... + def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator) + def seenAt(key: String): Boolean = + errorMap.get(key) match + case null => false + case 1 => errorMap.remove(key) ; true + case n => errorMap.put(key, n - 1) ; true + def sawDiagnostic(d: Diagnostic): Unit = + d.pos.nonInlined match + case srcpos if srcpos.exists => + val key = s"${relativize(srcpos.source.file.toString)}:${srcpos.line + 1}" + if !seenAt(key) then unexpected += key + case srcpos => + if !seenAt("nopos") then unpositioned += relativize(srcpos.source.file.toString) + + reporterErrors.foreach(sawDiagnostic) + + errorMap.get("anypos") match + case n if n == unexpected.size => errorMap.remove("anypos") ; unexpected.clear() + case _ => + + (errorMap.asScala.keys.toList, (unexpected ++ unpositioned).toList) + end getMissingExpectedErrors } private final class NoCrashTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) diff --git a/tests/neg/f-interpolator-neg.check b/tests/neg/f-interpolator-neg.check new file mode 100644 index 000000000000..ea8df052589e --- /dev/null +++ b/tests/neg/f-interpolator-neg.check @@ -0,0 +1,200 @@ +-- Error: tests/neg/f-interpolator-neg.scala:4:4 ----------------------------------------------------------------------- +4 | new StringContext().f() // error + | ^^^^^^^^^^^^^^^^^^^^^ + | there are no parts +-- Error: tests/neg/f-interpolator-neg.scala:5:4 ----------------------------------------------------------------------- +5 | new StringContext("", " is ", "%2d years old").f(s) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | too few arguments for interpolated string +-- Error: tests/neg/f-interpolator-neg.scala:6:4 ----------------------------------------------------------------------- +6 | new StringContext("", " is ", "%2d years old").f(s, d, d) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | too many arguments for interpolated string +-- Error: tests/neg/f-interpolator-neg.scala:7:4 ----------------------------------------------------------------------- +7 | new StringContext("", "").f() // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | too few arguments for interpolated string +-- Error: tests/neg/f-interpolator-neg.scala:11:7 ---------------------------------------------------------------------- +11 | f"$s%b" // error + | ^ + | Found: (s : String), Required: Boolean, Null +-- Error: tests/neg/f-interpolator-neg.scala:12:7 ---------------------------------------------------------------------- +12 | f"$s%c" // error + | ^ + | Found: (s : String), Required: Char, Byte, Short, Int +-- Error: tests/neg/f-interpolator-neg.scala:13:7 ---------------------------------------------------------------------- +13 | f"$f%c" // error + | ^ + | Found: (f : Double), Required: Char, Byte, Short, Int +-- Error: tests/neg/f-interpolator-neg.scala:14:7 ---------------------------------------------------------------------- +14 | f"$s%x" // error + | ^ + | Found: (s : String), Required: Int, Long, Byte, Short, BigInt +-- Error: tests/neg/f-interpolator-neg.scala:15:7 ---------------------------------------------------------------------- +15 | f"$b%d" // error + | ^ + | Found: (b : Boolean), Required: Int, Long, Byte, Short, BigInt +-- Error: tests/neg/f-interpolator-neg.scala:16:7 ---------------------------------------------------------------------- +16 | f"$s%d" // error + | ^ + | Found: (s : String), Required: Int, Long, Byte, Short, BigInt +-- Error: tests/neg/f-interpolator-neg.scala:17:7 ---------------------------------------------------------------------- +17 | f"$f%o" // error + | ^ + | Found: (f : Double), Required: Int, Long, Byte, Short, BigInt +-- Error: tests/neg/f-interpolator-neg.scala:18:7 ---------------------------------------------------------------------- +18 | f"$s%e" // error + | ^ + | Found: (s : String), Required: Double, Float, BigDecimal +-- Error: tests/neg/f-interpolator-neg.scala:19:7 ---------------------------------------------------------------------- +19 | f"$b%f" // error + | ^ + | Found: (b : Boolean), Required: Double, Float, BigDecimal +-- Error: tests/neg/f-interpolator-neg.scala:20:9 ---------------------------------------------------------------------- +20 | f"$s%i" // error + | ^ + | illegal conversion character 'i' +-- Error: tests/neg/f-interpolator-neg.scala:24:9 ---------------------------------------------------------------------- +24 | f"$s%+ 0,(s" // error + | ^^^^^ + | Illegal flag '+' +-- Error: tests/neg/f-interpolator-neg.scala:25:9 ---------------------------------------------------------------------- +25 | f"$c%#+ 0,(c" // error + | ^^^^^^ + | Only '-' allowed for c conversion +-- Error: tests/neg/f-interpolator-neg.scala:26:9 ---------------------------------------------------------------------- +26 | f"$d%#d" // error + | ^ + | # not allowed for d conversion +-- Error: tests/neg/f-interpolator-neg.scala:27:9 ---------------------------------------------------------------------- +27 | f"$d%,x" // error + | ^ + | ',' only allowed for d conversion of integral types +-- Error: tests/neg/f-interpolator-neg.scala:28:9 ---------------------------------------------------------------------- +28 | f"$d%+ (x" // error + | ^^^ + | only use '+' for BigInt conversions to o, x, X +-- Error: tests/neg/f-interpolator-neg.scala:29:9 ---------------------------------------------------------------------- +29 | f"$f%,(a" // error + | ^^ + | ',' not allowed for a, A +-- Error: tests/neg/f-interpolator-neg.scala:30:9 ---------------------------------------------------------------------- +30 | f"$t%#+ 0,(tT" // error + | ^^^^^^ + | Only '-' allowed for date/time conversions +-- Error: tests/neg/f-interpolator-neg.scala:31:7 ---------------------------------------------------------------------- +31 | f"%-#+ 0,(n" // error + | ^^^^^^^ + | flags not allowed +-- Error: tests/neg/f-interpolator-neg.scala:32:7 ---------------------------------------------------------------------- +32 | f"%#+ 0,(%" // error + | ^^^^^^ + | Illegal flag '#' +-- Error: tests/neg/f-interpolator-neg.scala:36:9 ---------------------------------------------------------------------- +36 | f"$c%.2c" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:37:9 ---------------------------------------------------------------------- +37 | f"$d%.2d" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:38:7 ---------------------------------------------------------------------- +38 | f"%.2%" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:39:7 ---------------------------------------------------------------------- +39 | f"%.2n" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:40:9 ---------------------------------------------------------------------- +40 | f"$f%.2a" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:41:9 ---------------------------------------------------------------------- +41 | f"$t%.2tT" // error + | ^^ + | precision not allowed +-- Error: tests/neg/f-interpolator-neg.scala:45:7 ---------------------------------------------------------------------- +45 | f"% "false", + f"${b_true}%b" -> "true", + + f"${null}%b" -> "false", + f"${false}%b" -> "false", + f"${true}%b" -> "true", + f"${true && false}%b" -> "false", + f"${java.lang.Boolean.valueOf(false)}%b" -> "false", + f"${java.lang.Boolean.valueOf(true)}%b" -> "true", + + f"${null}%B" -> "FALSE", + f"${false}%B" -> "FALSE", + f"${true}%B" -> "TRUE", + f"${java.lang.Boolean.valueOf(false)}%B" -> "FALSE", + f"${java.lang.Boolean.valueOf(true)}%B" -> "TRUE", + + f"${"true"}%b" -> "true", + f"${"false"}%b"-> "false", + + // 'h' | 'H' (category: general) + // ----------------------------- + f"${null}%h" -> "null", + f"${f_zero}%h" -> "0", + f"${f_zero_-}%h" -> "80000000", + f"${s}%h" -> "4c01926", + + f"${null}%H" -> "NULL", + f"${s}%H" -> "4C01926", + + // 's' | 'S' (category: general) + // ----------------------------- + f"${null}%s" -> "null", + f"${null}%S" -> "NULL", + f"${s}%s" -> "Scala", + f"${s}%S" -> "SCALA", + f"${5}" -> "5", + f"${i}" -> "42", + f"${Symbol("foo")}" -> "Symbol(foo)", + + f"${Thread.State.NEW}" -> "NEW", + + // 'c' | 'C' (category: character) + // ------------------------------- + f"${120:Char}%c" -> "x", + f"${120:Byte}%c" -> "x", + f"${120:Short}%c" -> "x", + f"${120:Int}%c" -> "x", + f"${java.lang.Character.valueOf('x')}%c" -> "x", + f"${java.lang.Byte.valueOf(120:Byte)}%c" -> "x", + f"${java.lang.Short.valueOf(120:Short)}%c" -> "x", + f"${java.lang.Integer.valueOf(120)}%c" -> "x", + + f"${'x' : java.lang.Character}%c" -> "x", + f"${(120:Byte) : java.lang.Byte}%c" -> "x", + f"${(120:Short) : java.lang.Short}%c" -> "x", + f"${120 : java.lang.Integer}%c" -> "x", + + f"${"Scala"}%c" -> "S", + + // 'd' | 'o' | 'x' | 'X' (category: integral) + // ------------------------------------------ + f"${120:Byte}%d" -> "120", + f"${120:Short}%d" -> "120", + f"${120:Int}%d" -> "120", + f"${120:Long}%d" -> "120", + f"${60 * 2}%d" -> "120", + f"${java.lang.Byte.valueOf(120:Byte)}%d" -> "120", + f"${java.lang.Short.valueOf(120:Short)}%d" -> "120", + f"${java.lang.Integer.valueOf(120)}%d" -> "120", + f"${java.lang.Long.valueOf(120)}%d" -> "120", + f"${120 : java.lang.Integer}%d" -> "120", + f"${120 : java.lang.Long}%d" -> "120", + f"${BigInt(120)}%d" -> "120", + + f"${new java.math.BigInteger("120")}%d" -> "120", + + f"${4}%#10X" -> " 0X4", + + f"She is ${fff}%#s feet tall." -> "She is 4 feet tall.", + + f"Just want to say ${"hello, world"}%#s..." -> "Just want to say hello, world...", + + //{ implicit val strToShort: Conversion[String, Short] = java.lang.Short.parseShort ; f"${"120"}%d" } -> "120", + //{ implicit val strToInt = (s: String) => 42 ; f"${"120"}%d" } -> "42", + + // 'e' | 'E' | 'g' | 'G' | 'f' | 'a' | 'A' (category: floating point) + // ------------------------------------------------------------------ + f"${3.4f}%e" -> locally"3.400000e+00", + f"${3.4}%e" -> locally"3.400000e+00", + f"${3.4f : java.lang.Float}%e" -> locally"3.400000e+00", + f"${3.4 : java.lang.Double}%e" -> locally"3.400000e+00", + + f"${BigDecimal(3.4)}%e" -> locally"3.400000e+00", + + f"${new java.math.BigDecimal(3.4)}%e" -> locally"3.400000e+00", + + f"${3}%e" -> locally"3.000000e+00", + f"${3L}%e" -> locally"3.000000e+00", + + // 't' | 'T' (category: date/time) + // ------------------------------- + f"${cal}%TD" -> "05/26/12", + f"${cal.getTime}%TD" -> "05/26/12", + f"${cal.getTime.getTime}%TD" -> "05/26/12", + f"""${"1234"}%TD""" -> "05/26/12", + + // literals and arg indexes + f"%%" -> "%", + f" mind%n------%nmatter" -> + """| mind + |------ + |matter""".stripMargin.linesIterator.mkString(System.lineSeparator), + f"${i}%d % "42 42 9", + f"${7}%d % "7 7 9", + f"${7}%d %2$$d ${9}%d" -> "7 9 9", + + f"${null}%d % "null FALSE", + + f"${5: Any}" -> "5", + f"${5}%s% "55", + f"${3.14}%s,% locally"3.14,${"3.140000"}", + + f"z" -> "z" + ) + + for ((f, s) <- ss) assertEquals(s, f) + end `f interpolator baseline` + + def fIf = + val res = f"${if true then 2.5 else 2.5}%.2f" + val expected = locally"2.50" + assertEquals(expected, res) + + def fIfNot = + val res = f"${if false then 2.5 else 3.5}%.2f" + val expected = locally"3.50" + assertEquals(expected, res) + + // in Scala 2, [A >: Any] forced not to convert 3 to 3.0; Scala 3 harmonics should also respect lower bound. + def fHeteroArgs() = + val res = f"${3.14}%.2f rounds to ${3}%d" + val expected = locally"${"3.14"} rounds to 3" + assertEquals(expected, res) } +object StringContextTestUtils: + private val decimalSeparator: Char = new DecimalFormat().getDecimalFormatSymbols().getDecimalSeparator() + private val numberPattern = """(\d+)\.(\d+.*)""".r + private def applyProperLocale(number: String): String = + val numberPattern(intPart, fractionalPartAndSuffix) = number + s"$intPart$decimalSeparator$fractionalPartAndSuffix" + + extension (sc: StringContext) + // Use this String interpolator to avoid problems with a locale-dependent decimal mark. + def locally(numbers: String*): String = + val numbersWithCorrectLocale = numbers.map(applyProperLocale) + sc.s(numbersWithCorrectLocale: _*) + + // Handles cases like locally"3.14" - it's prettier than locally"${"3.14"}". + def locally(): String = sc.parts.map(applyProperLocale).mkString diff --git a/tests/run/t6476.check b/tests/run/t6476.check index 69bf68978177..e2a080bcf6dc 100644 --- a/tests/run/t6476.check +++ b/tests/run/t6476.check @@ -1,13 +1,18 @@ "Hello", Alice "Hello", Alice + +"Hello", Alice +"Hello", Alice + \"Hello\", Alice \"Hello\", Alice -\"Hello\", Alice -\"Hello\", Alice + \TILT\ -\\TILT\\ -\\TILT\\ \TILT\ \\TILT\\ + +\TILT\ +\TILT\ \\TILT\\ + \TILT\ diff --git a/tests/run/t6476.scala b/tests/run/t6476.scala index a04645065a2a..25a1d5f03ec1 100644 --- a/tests/run/t6476.scala +++ b/tests/run/t6476.scala @@ -3,21 +3,21 @@ object Test { val person = "Alice" println(s"\"Hello\", $person") println(s"""\"Hello\", $person""") - + println() println(f"\"Hello\", $person") println(f"""\"Hello\", $person""") - + println() println(raw"\"Hello\", $person") println(raw"""\"Hello\", $person""") - + println() println(s"\\TILT\\") println(f"\\TILT\\") println(raw"\\TILT\\") - + println() println(s"""\\TILT\\""") println(f"""\\TILT\\""") println(raw"""\\TILT\\""") - + println() println(raw"""\TILT\""") } }