Skip to content

Commit 62e5731

Browse files
committed
Reject char literal of raw newline
Don't allow char literal to span a natural line ending, but do allow `'\u000A'` to mean `'\n'`. The spec was updated to reflect that meaning, and the direction is to eliminate Unicode escapes outside quotes. Update the test rig so that `// anypos-error` can be used to match any position, since the test cannot be expressed on one line with a trailing comment.
1 parent 04d5068 commit 62e5731

File tree

6 files changed

+95
-11
lines changed

6 files changed

+95
-11
lines changed

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -804,19 +804,25 @@ object Scanners {
804804
}
805805
fetchDoubleQuote()
806806
case '\'' =>
807-
def fetchSingleQuote() = {
807+
def fetchSingleQuote(): Unit = {
808808
nextChar()
809-
if (isIdentifierStart(ch))
809+
if isIdentifierStart(ch) then
810810
charLitOr { getIdentRest(); QUOTEID }
811-
else if (isOperatorPart(ch) && (ch != '\\'))
811+
else if isOperatorPart(ch) && ch != '\\' then
812812
charLitOr { getOperatorRest(); QUOTEID }
813813
else ch match {
814814
case '{' | '[' | ' ' | '\t' if lookaheadChar() != '\'' =>
815815
token = QUOTE
816-
case _ =>
816+
case _ if !isAtEnd && (ch != SU && ch != CR && ch != LF || isUnicodeEscape) =>
817+
val isEmptyCharLit = (ch == '\'')
817818
getLitChar()
818-
if (ch == '\'') finishCharLit()
819+
if ch == '\'' then
820+
if isEmptyCharLit then error("empty character literal (use '\\'' for single quote)")
821+
else finishCharLit()
822+
else if isEmptyCharLit then error("empty character literal")
819823
else error("unclosed character literal")
824+
case _ =>
825+
error("unclosed character literal")
820826
}
821827
}
822828
fetchSingleQuote()

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -663,13 +663,20 @@ trait ParallelTesting extends RunnerOrchestration { self =>
663663
errorMap.put("nopos", noposErrors + existing)
664664
}
665665

666-
val possibleTypos = List("//error" -> "// error" , "//nopos-error" -> "// nopos-error")
666+
val anyposErrors = line.toSeq.sliding("// anypos-error".length).count(_.unwrap == "// anypos-error")
667+
if (anyposErrors > 0) {
668+
val anypos = errorMap.get("anypos")
669+
val existing: Integer = if (anypos eq null) 0 else anypos
670+
errorMap.put("anypos", anyposErrors + existing)
671+
}
672+
673+
val possibleTypos = List("//error" -> "// error" , "//nopos-error" -> "// nopos-error", "//anypos-error" -> "// anypos-error")
667674
for ((possibleTypo, expected) <- possibleTypos) {
668675
if (line.contains(possibleTypo))
669676
echo(s"Warning: Possible typo in error tag in file ${file.getCanonicalPath}:$lineNbr: found `$possibleTypo` but expected `$expected`")
670677
}
671678

672-
expectedErrors += noposErrors + errors
679+
expectedErrors += anyposErrors + noposErrors + errors
673680
}
674681
}
675682

@@ -688,15 +695,21 @@ trait ParallelTesting extends RunnerOrchestration { self =>
688695

689696
val errors = errorMap.get(key)
690697

698+
def missing = { echo(s"Error reported in ${pos1.source}, but no annotation found") ; false }
699+
691700
if (errors ne null) {
692701
if (errors == 1) errorMap.remove(key)
693702
else errorMap.put(key, errors - 1)
694703
true
695704
}
696-
else {
697-
echo(s"Error reported in ${pos1.source}, but no annotation found")
698-
false
699-
}
705+
else if key == "nopos" then
706+
missing
707+
else
708+
errorMap.get("anypos") match
709+
case null => missing
710+
case 1 => errorMap.remove("anypos") ; true
711+
case slack => if slack < 1 then missing
712+
else errorMap.put("anypos", slack - 1) ; true
700713
}
701714
}
702715

compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class VulpixUnitTests extends ParallelTesting {
4949
@Test def negNoPositionAnnot: Unit =
5050
compileFile("tests/vulpix-tests/unit/negNoPositionAnnots.scala", defaultOptions).expectFailure.checkExpectedErrors()
5151

52+
@Test def negAnyPositionAnnot: Unit =
53+
compileFile("tests/vulpix-tests/unit/negAnyPositionAnnots.scala", defaultOptions).checkExpectedErrors()
54+
5255
@Test def runCompileFail: Unit =
5356
compileFile("tests/vulpix-tests/unit/posFail1Error.scala", defaultOptions).expectFailure.checkRuns()
5457

tests/neg/t6810.check

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Error: tests/neg/t6810.scala:5:10 -----------------------------------------------------------------------------------
2+
5 | val y = '
3+
| ^
4+
| unclosed character literal
5+
-- Error: tests/neg/t6810.scala:12:10 ----------------------------------------------------------------------------------
6+
12 | val Y = "
7+
| ^
8+
| unclosed string literal
9+
-- Error: tests/neg/t6810.scala:13:0 -----------------------------------------------------------------------------------
10+
13 |" // error obviously not
11+
|^
12+
|unclosed string literal
13+
-- Error: tests/neg/t6810.scala:24:6 -----------------------------------------------------------------------------------
14+
24 | val `
15+
| ^
16+
| unclosed quoted identifier
17+
-- Error: tests/neg/t6810.scala:25:0 -----------------------------------------------------------------------------------
18+
25 |` = EOL // error not raw string literals aka triple-quoted, multiline strings
19+
|^
20+
|unclosed quoted identifier
21+
-- Error: tests/neg/t6810.scala:30:10 ----------------------------------------------------------------------------------
22+
30 | val b = '
23+
| ^
24+
| unclosed character literal

tests/neg/t6810.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
trait t6810 {
3+
val x = '\u000A' // char literals accept arbitrary unicode escapes
4+
// anypos-error so as not to interfere with the following bad syntax
5+
val y = '
6+
' // but not embedded EOL sequences not represented as escapes
7+
println(); // scanner firewall
8+
val z = '\n' // normally, expect this escape
9+
10+
val X = "\u000A" // it's the same as ordinary string literals
11+
// anypos-error so as not to interfere with the following bad syntax
12+
val Y = "
13+
" // error obviously not
14+
val Z = "\n" // normally, expect this escape
15+
16+
val A = """
17+
""" // which is what these are for
18+
val B = s"""
19+
""" // or the same for interpolated strings
20+
21+
import System.{lineSeparator => EOL}
22+
val `\u000A` = EOL // backquoted identifiers are arbitrary string literals
23+
// anypos-error so as not to interfere with the following bad syntax
24+
val `
25+
` = EOL // error not raw string literals aka triple-quoted, multiline strings
26+
27+
val firebreak = 42 // help parser recovery, could also use rbrace
28+
29+
val a = '\u000D' // similar treatment of CR
30+
val b = '' // anypos-error CR seen as EOL by scanner; FSR, error only on open quote, unlike `y`
31+
println(); // scanner firewall
32+
val c = '\r' // traditionally
33+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object Foo {
2+
def bar: Int = "LOL"
3+
4+
// anypos-error
5+
}

0 commit comments

Comments
 (0)