From c81ce5409199334e225873920009b9956e2eec67 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 9 Feb 2020 22:06:13 -0800 Subject: [PATCH] Don't strip CR from multiline interpolations Forward port of bug fix from 2.12. Leaving an interpolated expression would sanitize an immediately following line separator. --- .../dotty/tools/dotc/parsing/Scanners.scala | 10 ++- .../tools/dotc/parsing/ParserEdgeTest.scala | 73 +++++++++++++++++++ .../dotty/tools/dotc/parsing/ParserTest.scala | 9 ++- 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 compiler/test/dotty/tools/dotc/parsing/ParserEdgeTest.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 5bedbb06375b..0f57677ac2dd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -256,6 +256,13 @@ object Scanners { case _ => false } + /** Are we in a `${ }` block? such that RBRACE exits back into multiline string. */ + private def inMultiLineInterpolatedExpression = + currentRegion match { + case InBraces(_, InString(true, _)) => true + case _ => false + } + /** read next token and return last offset */ def skipToken(): Offset = { @@ -806,7 +813,8 @@ object Scanners { case ')' => nextChar(); token = RPAREN case '}' => - nextChar(); token = RBRACE + if (inMultiLineInterpolatedExpression) nextRawChar() else nextChar() + token = RBRACE case '[' => nextChar(); token = LBRACKET case ']' => diff --git a/compiler/test/dotty/tools/dotc/parsing/ParserEdgeTest.scala b/compiler/test/dotty/tools/dotc/parsing/ParserEdgeTest.scala new file mode 100644 index 000000000000..1b5a6ec8d5b2 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/parsing/ParserEdgeTest.scala @@ -0,0 +1,73 @@ +package dotty.tools +package dotc +package parsing + +import ast.untpd._ +import core.Constants._ + +import org.junit.Test + +class ParserEdgeTest extends ParserTest { + // + // CR after right brace of interpolated expression was stripped. + // + @Test def `t9944 respect line separator`: Unit = { + // avoid git stripping CR from a test file; also, inlining doesn't demonstrate the behavior. + val CR = "\u000D" + val NL = "\u000A" + val triple = "\"" * 3 + val text = s"""class C { def g = "X" ; def f = s${triple}123${CR}${NL}$${g}${CR}${NL}456${triple} }""" + def isStripped(s: String) = s.contains(NL) && !s.contains(CR) + + // sanity check + assert(text.linesIterator.size == 3, s"line count ${text.linesIterator.size}") + assert(text.linesIterator.forall(!isStripped(_))) + + val t = parseText(text) + //println(t.show) + assert(!t.existsSubTree { + case Literal(const @ Constant(_)) if const.tag == StringTag => isStripped(const.stringValue) + case st => false + }) + } + /* was: + package { + class C { + def g = "X" + def f = + s"123\r\n{ + { + g + } + }\n456" + } + } + * now: + package { + class C { + def g = "X" + def f = + s"123\r\n{ + { + g + } + }\r\n456" + } + } + * tests/run/t9944 expressed ordinarily, with CR as indicated: +class C { + def g = 42 + def f = + s"""123^M + |${ g }^M + |123""".stripMargin +} + +object Test extends App { + println(new C().f.getBytes.toList.map(c => f"$c%02x")) +} + * was: +➜ dotr Test +List(31, 32, 33, 0d, 0a, 34, 32, 0a, 31, 32, 33) + */ +} diff --git a/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala b/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala index 93357b9f2fa1..b247ba1a525d 100644 --- a/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala +++ b/compiler/test/dotty/tools/dotc/parsing/ParserTest.scala @@ -23,9 +23,10 @@ class ParserTest extends DottyTest { parsedTrees.clear() } - def parse(file: PlainFile): Tree = { - //println("***** parsing " + file) - val source = new SourceFile(file, Codec.UTF8) + def parse(file: PlainFile): Tree = parseSource(new SourceFile(file, Codec.UTF8)) + + private def parseSource(source: SourceFile): Tree = { + //println("***** parsing " + source.file) val parser = new Parser(source) val tree = parser.parse() parsed += 1 @@ -41,4 +42,6 @@ class ParserTest extends DottyTest { for (d <- dir.dirs) parseDir(d.path) } + + def parseText(code: String): Tree = parseSource(SourceFile.virtual("", code)) }