Skip to content

Commit dff31fb

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 8a42819 commit dff31fb

File tree

6 files changed

+99
-15
lines changed

6 files changed

+99
-15
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: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ trait ParallelTesting extends RunnerOrchestration { self =>
109109
case source: JointCompilationSource => {
110110
source.sourceFiles.map(_.getPath).foreach { path =>
111111
sb.append(delimiter)
112-
sb += '''
112+
sb += '\''
113113
sb.append(path)
114-
sb += '''
114+
sb += '\''
115115
sb += ' '
116116
}
117117
sb.toString + "\n\n"
@@ -123,9 +123,9 @@ trait ParallelTesting extends RunnerOrchestration { self =>
123123
files.map(_.getPath).foreach { path =>
124124
fsb.append(delimiter)
125125
lineLen = 8
126-
fsb += '''
126+
fsb += '\''
127127
fsb.append(path)
128-
fsb += '''
128+
fsb += '\''
129129
fsb += ' '
130130
}
131131
fsb.append("\n\n")
@@ -666,13 +666,20 @@ trait ParallelTesting extends RunnerOrchestration { self =>
666666
errorMap.put("nopos", noposErrors + existing)
667667
}
668668

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

675-
expectedErrors += noposErrors + errors
682+
expectedErrors += anyposErrors + noposErrors + errors
676683
}
677684
}
678685

@@ -691,15 +698,21 @@ trait ParallelTesting extends RunnerOrchestration { self =>
691698

692699
val errors = errorMap.get(key)
693700

701+
def missing = { echo(s"Error reported in ${pos1.source}, but no annotation found") ; false }
702+
694703
if (errors ne null) {
695704
if (errors == 1) errorMap.remove(key)
696705
else errorMap.put(key, errors - 1)
697706
true
698707
}
699-
else {
700-
echo(s"Error reported in ${pos1.source}, but no annotation found")
701-
false
702-
}
708+
else if key == "nopos" then
709+
missing
710+
else
711+
errorMap.get("anypos") match
712+
case null => missing
713+
case 1 => errorMap.remove("anypos") ; true
714+
case slack => if slack < 1 then missing
715+
else errorMap.put("anypos", slack - 1) ; true
703716
}
704717
}
705718

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)