Skip to content

Commit

Permalink
SI-6476 Accept backslash at end of interpolation
Browse files Browse the repository at this point in the history
Everyone wants to say `raw"\hello, world.\"`.

Parser accepts arbitrary escaped quotes optionally followed by
a quote. That is, an escaped quote is deemed to terminate the
interpolated string if it is not followed by another quote on
the same line.
  • Loading branch information
som-snytt authored and eed3si9n committed Mar 27, 2020
1 parent 3e100a5 commit 04c2c0e
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 58 deletions.
109 changes: 59 additions & 50 deletions src/compiler/scala/tools/nsc/ast/parser/Scanners.scala
Expand Up @@ -887,84 +887,93 @@ trait Scanners extends ScannersCommon {
}
}

@tailrec private def getStringPart(multiLine: Boolean): Unit = {
private def getStringPart(multiLine: Boolean): Unit = {
def finishStringPart() = {
setStrVal()
token = STRINGPART
next.lastOffset = charOffset - 1
next.offset = charOffset - 1
}
if (ch == '"') {
if (multiLine) {
def isLastQuoteOnLine: Boolean = {
val lookahead = lookaheadReader
var c: Char = 0
do {
c = lookahead.getc()
} while (c != '"' && c != LF && c != SU)
(c != '"')
}
@tailrec def loop(escaped: Boolean): Unit = ch match {
case '\\' if !multiLine =>
putChar(ch)
nextRawChar()
loop(escaped = !escaped)
case '"' if multiLine =>
nextRawChar()
if (isTripleQuote()) {
setStrVal()
token = STRINGLIT
} else
getStringPart(multiLine)
} else {
nextChar()
setStrVal()
token = STRINGLIT
}
} else if (ch == '\\') {
if (multiLine) {
putChar(ch)
nextRawChar()
getStringPart(multiLine)
} else {
putChar(ch)
nextRawChar()
if (ch == '"') {
case '"' if escaped =>
// at \", unescape and finish if this is the last quote on the line
if (!multiLine && isLastQuoteOnLine) {
nextChar()
setStrVal()
token = STRINGLIT
} else {
putChar(ch)
nextRawChar()
loop(escaped = false)
}
getStringPart(multiLine)
}
} else if (ch == '$') {
nextRawChar()
if (ch == '$') {
case '$' if escaped =>
putChar(ch)
nextRawChar()
getStringPart(multiLine)
} else if (ch == '{') {
finishStringPart()
nextRawChar()
next.token = LBRACE
} else if (ch == '_') {
finishStringPart()
loop(escaped = false)
case '"' =>
nextChar()
setStrVal()
token = STRINGLIT
case '$' =>
nextRawChar()
next.token = USCORE
} else if (Character.isUnicodeIdentifierStart(ch)) {
finishStringPart()
do {
if (ch == '$') {
putChar(ch)
nextRawChar()
} while (ch != SU && Character.isUnicodeIdentifierPart(ch))
next.token = IDENTIFIER
next.name = newTermName(cbuf.toString)
cbuf.clear()
val idx = next.name.start - kwOffset
if (idx >= 0 && idx < kwArray.length) {
next.token = kwArray(idx)
loop(escaped = false)
} else if (ch == '{') {
finishStringPart()
nextRawChar()
next.token = LBRACE
} else if (ch == '_') {
finishStringPart()
nextRawChar()
next.token = USCORE
} else if (Character.isUnicodeIdentifierStart(ch)) {
finishStringPart()
do {
putChar(ch)
nextRawChar()
} while (ch != SU && Character.isUnicodeIdentifierPart(ch))
next.token = IDENTIFIER
next.name = newTermName(cbuf.toString)
cbuf.clear()
val idx = next.name.start - kwOffset
if (idx >= 0 && idx < kwArray.length) {
next.token = kwArray(idx)
}
} else {
syntaxError(s"invalid string interpolation $$$ch, expected: $$$$, $$identifier or $${expression}")
}
} else {
syntaxError(s"invalid string interpolation $$$ch, expected: $$$$, $$identifier or $${expression}")
}
} else {
val isUnclosedLiteral = (ch == SU || (!multiLine && (ch == CR || ch == LF)))
if (isUnclosedLiteral) {
case _ if ch == SU || (!multiLine && (ch == CR || ch == LF)) =>
if (multiLine)
incompleteInputError("unclosed multi-line string literal")
else
unclosedStringLit()
}
else {
case _ =>
putChar(ch)
nextRawChar()
getStringPart(multiLine)
}
loop(escaped = false)
}
loop(escaped = false)
}

private def fetchStringPart() = {
Expand Down
12 changes: 12 additions & 0 deletions test/files/neg/string-interpolator.check
@@ -0,0 +1,12 @@
string-interpolator.scala:2: error: invalid escape at terminal index 1 in "x\". Use \\ for literal \.
val sa = s"x\"y
^
string-interpolator.scala:2: error: postfix operator y needs to be enabled
by making the implicit value scala.language.postfixOps visible.
This can be achieved by adding the import clause 'import scala.language.postfixOps'
or by setting the compiler option -language:postfixOps.
See the Scaladoc for value scala.language.postfixOps for a discussion
why the feature needs to be explicitly enabled.
val sa = s"x\"y
^
2 errors
3 changes: 3 additions & 0 deletions test/files/neg/string-interpolator.scala
@@ -0,0 +1,3 @@
object Test {
val sa = s"x\"y
}
17 changes: 10 additions & 7 deletions test/files/neg/t5510.check
@@ -1,19 +1,22 @@
t5510.scala:2: error: unclosed string literal
t5510.scala:5: error: unclosed string literal
val sb = "x\"y
^
t5510.scala:6: error: unclosed string literal
val s1 = s"xxx
^
t5510.scala:3: error: unclosed string literal
t5510.scala:7: error: unclosed string literal
val s2 = s"xxx $x
^
t5510.scala:4: error: unclosed string literal
t5510.scala:8: error: unclosed string literal
val s3 = s"xxx $$
^
t5510.scala:5: error: unclosed string literal
t5510.scala:9: error: unclosed string literal
val s4 = ""s"
^
t5510.scala:6: error: unclosed multi-line string literal
t5510.scala:10: error: unclosed multi-line string literal
val s5 = ""s""" $s1 $s2 s"
^
t5510.scala:7: error: unclosed multi-line string literal
t5510.scala:11: error: unclosed multi-line string literal
}
^
6 errors
7 errors
6 changes: 5 additions & 1 deletion test/files/neg/t5510.scala
@@ -1,4 +1,8 @@
object Test {
trait TypeErrors {
val sa = s"x\"y // see string-interpolator.scala
}
trait ParseErrors {
val sb = "x\"y
val s1 = s"xxx
val s2 = s"xxx $x
val s3 = s"xxx $$
Expand Down
1 change: 1 addition & 0 deletions test/files/run/string-interpolator.check
@@ -1 +1,2 @@
"Hello", Alice
oh hai C:\
1 change: 1 addition & 0 deletions test/files/run/string-interpolator.scala
Expand Up @@ -2,5 +2,6 @@ object Test {
def main(args: Array[String]): Unit = {
val person = "Alice"
println(s"\"Hello\", $person")
println(raw"oh hai C:\")
}
}

0 comments on commit 04c2c0e

Please sign in to comment.