Skip to content

Commit 2c1c98f

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 0dc07b0 commit 2c1c98f

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
@@ -773,19 +773,25 @@ object Scanners {
773773
}
774774
fetchDoubleQuote()
775775
case '\'' =>
776-
def fetchSingleQuote() = {
776+
def fetchSingleQuote(): Unit = {
777777
nextChar()
778-
if (isIdentifierStart(ch))
778+
if isIdentifierStart(ch) then
779779
charLitOr { getIdentRest(); QUOTEID }
780-
else if (isOperatorPart(ch) && (ch != '\\'))
780+
else if isOperatorPart(ch) && ch != '\\' then
781781
charLitOr { getOperatorRest(); QUOTEID }
782782
else ch match {
783783
case '{' | '[' | ' ' | '\t' if lookaheadChar() != '\'' =>
784784
token = QUOTE
785-
case _ =>
785+
case _ if !isAtEnd && (ch != SU && ch != CR && ch != LF || isUnicodeEscape) =>
786+
val isEmptyCharLit = (ch == '\'')
786787
getLitChar()
787-
if (ch == '\'') finishCharLit()
788+
if ch == '\'' then
789+
if isEmptyCharLit then error("empty character literal (use '\\'' for single quote)")
790+
else finishCharLit()
791+
else if isEmptyCharLit then error("empty character literal")
788792
else error("unclosed character literal")
793+
case _ =>
794+
error("unclosed character literal")
789795
}
790796
}
791797
fetchSingleQuote()

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

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

672-
val possibleTypos = List("//error" -> "// error" , "//nopos-error" -> "// nopos-error")
672+
val anyposErrors = line.toSeq.sliding("// anypos-error".length).count(_.unwrap == "// anypos-error")
673+
if (anyposErrors > 0) {
674+
val anypos = errorMap.get("anypos")
675+
val existing: Integer = if (anypos eq null) 0 else anypos
676+
errorMap.put("anypos", anyposErrors + existing)
677+
}
678+
679+
val possibleTypos = List("//error" -> "// error" , "//nopos-error" -> "// nopos-error", "//anypos-error" -> "// anypos-error")
673680
for ((possibleTypo, expected) <- possibleTypos) {
674681
if (line.contains(possibleTypo))
675682
echo(s"Warning: Possible typo in error tag in file ${file.getCanonicalPath}:$lineNbr: found `$possibleTypo` but expected `$expected`")
676683
}
677684

678-
expectedErrors += noposErrors + errors
685+
expectedErrors += anyposErrors + noposErrors + errors
679686
}
680687
}
681688

@@ -694,15 +701,21 @@ trait ParallelTesting extends RunnerOrchestration { self =>
694701

695702
val errors = errorMap.get(key)
696703

704+
def missing = { echo(s"Error reported in ${pos1.source}, but no annotation found") ; false }
705+
697706
if (errors ne null) {
698707
if (errors == 1) errorMap.remove(key)
699708
else errorMap.put(key, errors - 1)
700709
true
701710
}
702-
else {
703-
echo(s"Error reported in ${pos1.source}, but no annotation found")
704-
false
705-
}
711+
else if key == "nopos" then
712+
missing
713+
else
714+
errorMap.get("anypos") match
715+
case null => missing
716+
case 1 => errorMap.remove("anypos") ; true
717+
case slack => if slack < 1 then missing
718+
else errorMap.put("anypos", slack - 1) ; true
706719
}
707720
}
708721

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)