From dff31fb0766c237d43fea07b413c302f23d8a9b3 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 10 Feb 2020 22:43:27 -0800 Subject: [PATCH] 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. --- .../dotty/tools/dotc/parsing/Scanners.scala | 16 ++++++--- .../dotty/tools/vulpix/ParallelTesting.scala | 33 +++++++++++++------ .../dotty/tools/vulpix/VulpixUnitTests.scala | 3 ++ tests/neg/t6810.check | 24 ++++++++++++++ tests/neg/t6810.scala | 33 +++++++++++++++++++ .../unit/negAnyPositionAnnots.scala | 5 +++ 6 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 tests/neg/t6810.check create mode 100644 tests/neg/t6810.scala create mode 100644 tests/vulpix-tests/unit/negAnyPositionAnnots.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index c83882829748..88ebda483b4c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -804,19 +804,25 @@ object Scanners { } fetchDoubleQuote() case '\'' => - def fetchSingleQuote() = { + def fetchSingleQuote(): Unit = { nextChar() - if (isIdentifierStart(ch)) + if isIdentifierStart(ch) then charLitOr { getIdentRest(); QUOTEID } - else if (isOperatorPart(ch) && (ch != '\\')) + else if isOperatorPart(ch) && ch != '\\' then charLitOr { getOperatorRest(); QUOTEID } else ch match { case '{' | '[' | ' ' | '\t' if lookaheadChar() != '\'' => token = QUOTE - case _ => + case _ if !isAtEnd && (ch != SU && ch != CR && ch != LF || isUnicodeEscape) => + val isEmptyCharLit = (ch == '\'') getLitChar() - if (ch == '\'') finishCharLit() + if ch == '\'' then + if isEmptyCharLit then error("empty character literal (use '\\'' for single quote)") + else finishCharLit() + else if isEmptyCharLit then error("empty character literal") else error("unclosed character literal") + case _ => + error("unclosed character literal") } } fetchSingleQuote() diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 307031e98112..2417294c50ed 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -109,9 +109,9 @@ trait ParallelTesting extends RunnerOrchestration { self => case source: JointCompilationSource => { source.sourceFiles.map(_.getPath).foreach { path => sb.append(delimiter) - sb += ''' + sb += '\'' sb.append(path) - sb += ''' + sb += '\'' sb += ' ' } sb.toString + "\n\n" @@ -123,9 +123,9 @@ trait ParallelTesting extends RunnerOrchestration { self => files.map(_.getPath).foreach { path => fsb.append(delimiter) lineLen = 8 - fsb += ''' + fsb += '\'' fsb.append(path) - fsb += ''' + fsb += '\'' fsb += ' ' } fsb.append("\n\n") @@ -666,13 +666,20 @@ trait ParallelTesting extends RunnerOrchestration { self => errorMap.put("nopos", noposErrors + existing) } - val possibleTypos = List("//error" -> "// error", "//nopos-error" -> "// nopos-error") + val anyposErrors = line.toSeq.sliding("// anypos-error".length).count(_.unwrap == "// anypos-error") + if (anyposErrors > 0) { + val anypos = errorMap.get("anypos") + val existing: Integer = if (anypos eq null) 0 else anypos + errorMap.put("anypos", anyposErrors + existing) + } + + val possibleTypos = List("//error" -> "// error", "//nopos-error" -> "// nopos-error", "//anypos-error" -> "// anypos-error") for ((possibleTypo, expected) <- possibleTypos) { if (line.contains(possibleTypo)) echo(s"Warning: Possible typo in error tag in file ${file.getCanonicalPath}:$lineNbr: found `$possibleTypo` but expected `$expected`") } - expectedErrors += noposErrors + errors + expectedErrors += anyposErrors + noposErrors + errors } } @@ -691,15 +698,21 @@ trait ParallelTesting extends RunnerOrchestration { self => val errors = errorMap.get(key) + def missing = { echo(s"Error reported in ${pos1.source}, but no annotation found") ; false } + if (errors ne null) { if (errors == 1) errorMap.remove(key) else errorMap.put(key, errors - 1) true } - else { - echo(s"Error reported in ${pos1.source}, but no annotation found") - false - } + else if key == "nopos" then + missing + else + errorMap.get("anypos") match + case null => missing + case 1 => errorMap.remove("anypos") ; true + case slack => if slack < 1 then missing + else errorMap.put("anypos", slack - 1) ; true } } diff --git a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala index 7b5738caf57c..595367fd33c1 100644 --- a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala +++ b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala @@ -49,6 +49,9 @@ class VulpixUnitTests extends ParallelTesting { @Test def negNoPositionAnnot: Unit = compileFile("tests/vulpix-tests/unit/negNoPositionAnnots.scala", defaultOptions).expectFailure.checkExpectedErrors() + @Test def negAnyPositionAnnot: Unit = + compileFile("tests/vulpix-tests/unit/negAnyPositionAnnots.scala", defaultOptions).checkExpectedErrors() + @Test def runCompileFail: Unit = compileFile("tests/vulpix-tests/unit/posFail1Error.scala", defaultOptions).expectFailure.checkRuns() diff --git a/tests/neg/t6810.check b/tests/neg/t6810.check new file mode 100644 index 000000000000..55f9f7ca2443 --- /dev/null +++ b/tests/neg/t6810.check @@ -0,0 +1,24 @@ +-- Error: tests/neg/t6810.scala:5:10 ----------------------------------------------------------------------------------- +5 | val y = ' + | ^ + | unclosed character literal +-- Error: tests/neg/t6810.scala:12:10 ---------------------------------------------------------------------------------- +12 | val Y = " + | ^ + | unclosed string literal +-- Error: tests/neg/t6810.scala:13:0 ----------------------------------------------------------------------------------- +13 |" // error obviously not + |^ + |unclosed string literal +-- Error: tests/neg/t6810.scala:24:6 ----------------------------------------------------------------------------------- +24 | val ` + | ^ + | unclosed quoted identifier +-- Error: tests/neg/t6810.scala:25:0 ----------------------------------------------------------------------------------- +25 |` = EOL // error not raw string literals aka triple-quoted, multiline strings + |^ + |unclosed quoted identifier +-- Error: tests/neg/t6810.scala:30:10 ---------------------------------------------------------------------------------- +30 | val b = ' + | ^ + | unclosed character literal diff --git a/tests/neg/t6810.scala b/tests/neg/t6810.scala new file mode 100644 index 000000000000..ff353bb08f1d --- /dev/null +++ b/tests/neg/t6810.scala @@ -0,0 +1,33 @@ + +trait t6810 { + val x = '\u000A' // char literals accept arbitrary unicode escapes + // anypos-error so as not to interfere with the following bad syntax + val y = ' +' // but not embedded EOL sequences not represented as escapes + println(); // scanner firewall + val z = '\n' // normally, expect this escape + + val X = "\u000A" // it's the same as ordinary string literals + // anypos-error so as not to interfere with the following bad syntax + val Y = " +" // error obviously not + val Z = "\n" // normally, expect this escape + + val A = """ +""" // which is what these are for + val B = s""" +""" // or the same for interpolated strings + + import System.{lineSeparator => EOL} + val `\u000A` = EOL // backquoted identifiers are arbitrary string literals + // anypos-error so as not to interfere with the following bad syntax + val ` +` = EOL // error not raw string literals aka triple-quoted, multiline strings + + val firebreak = 42 // help parser recovery, could also use rbrace + + val a = '\u000D' // similar treatment of CR + val b = ' ' // anypos-error CR seen as EOL by scanner; FSR, error only on open quote, unlike `y` + println(); // scanner firewall + val c = '\r' // traditionally +} diff --git a/tests/vulpix-tests/unit/negAnyPositionAnnots.scala b/tests/vulpix-tests/unit/negAnyPositionAnnots.scala new file mode 100644 index 000000000000..5448c1698d53 --- /dev/null +++ b/tests/vulpix-tests/unit/negAnyPositionAnnots.scala @@ -0,0 +1,5 @@ +object Foo { + def bar: Int = "LOL" + + // anypos-error +}