diff --git a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala index 788f04121509..4f0c7a580060 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Parsers.scala @@ -693,19 +693,10 @@ self => def isLiteralToken(token: Token) = token match { case CHARLIT | INTLIT | LONGLIT | FLOATLIT | DOUBLELIT | STRINGLIT | INTERPOLATIONID | SYMBOLLIT | TRUE | FALSE | NULL => true - case _ => false + case _ => false } def isLiteral = isLiteralToken(in.token) - def isSimpleExprIntroToken(token: Token): Boolean = isLiteralToken(token) || (token match { - case IDENTIFIER | BACKQUOTED_IDENT | - THIS | SUPER | NEW | USCORE | - LPAREN | LBRACE | XMLSTART => true - case _ => false - }) - - def isSimpleExprIntro: Boolean = isExprIntroToken(in.token) - def isExprIntroToken(token: Token): Boolean = isLiteralToken(token) || (token match { case IDENTIFIER | BACKQUOTED_IDENT | THIS | SUPER | IF | FOR | NEW | USCORE | TRY | WHILE | @@ -1707,23 +1698,22 @@ self => * PrefixExpr ::= [`-` | `+` | `~` | `!`] SimpleExpr * }}} */ - def prefixExpr(): Tree = { - if (isUnaryOp) { + def prefixExpr(): Tree = + if (isUnaryOp) atPos(in.offset) { - if (lookingAhead(isSimpleExprIntro)) { + if (lookingAhead(isExprIntro)) { val namePos = in.offset val uname = nme.toUnaryName(rawIdent().toTermName) if (uname == nme.UNARY_- && isNumericLit) - /* start at the -, not the number */ + // start at the -, not the number simpleExprRest(literal(isNegated = true, start = namePos), canApply = true) else Select(stripParens(simpleExpr()), uname) } else simpleExpr() } - } else simpleExpr() - } + def xmlLiteral(): Tree /** {{{ diff --git a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala index c27b28fd513a..805949962a9c 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala @@ -159,6 +159,9 @@ trait Scanners extends ScannersCommon { } abstract class Scanner extends CharArrayReader with TokenData with ScannerData with ScannerCommon with DocScanner { + /** A switch whether operators at the start of lines can be infix operators. */ + private var allowLeadingInfixOperators = true + private def isDigit(c: Char) = java.lang.Character isDigit c private var openComments = 0 @@ -380,6 +383,45 @@ trait Scanners extends ScannersCommon { next.token = EMPTY } + def lookingAhead[A](body: => A): A = { + val saved = new ScannerData {} copyFrom this + val aLIO = allowLeadingInfixOperators + allowLeadingInfixOperators = false + nextToken() + try body finally { + this copyFrom saved + allowLeadingInfixOperators = aLIO + } + } + + def isSimpleExprIntroToken(token: Token): Boolean = token match { + case CHARLIT | INTLIT | LONGLIT | FLOATLIT | DOUBLELIT | + STRINGLIT | INTERPOLATIONID | SYMBOLLIT | TRUE | FALSE | NULL | // literals + IDENTIFIER | BACKQUOTED_IDENT | THIS | SUPER | NEW | USCORE | + LPAREN | LBRACE | XMLSTART => true + case _ => false + } + + def insertNL(nl: Token): Unit = { + next.copyFrom(this) + // todo: make offset line-end of previous line? + offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset + token = nl + } + + /** A leading symbolic or backquoted identifier is treated as an infix operator + * if it is followed by at least one ' ' and a token on the same line + * that can start an expression. + */ + def isLeadingInfixOperator = + allowLeadingInfixOperators && + (token == BACKQUOTED_IDENT || + token == IDENTIFIER && isOperatorPart(name.charAt(name.length - 1))) && + (ch == ' ') && lookingAhead { + // force a NEWLINE after current token if it is on its own line + isSimpleExprIntroToken(token) + } + /* Insert NEWLINE or NEWLINES if * - we are after a newline * - we are within a { ... } or on toplevel (wrt sepRegions) @@ -388,9 +430,16 @@ trait Scanners extends ScannersCommon { */ if (!applyBracePatch() && afterLineEnd() && inLastOfStat(lastToken) && inFirstOfStat(token) && (sepRegions.isEmpty || sepRegions.head == RBRACE)) { - next copyFrom this - offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset - token = if (pastBlankLine()) NEWLINES else NEWLINE + if (pastBlankLine()) insertNL(NEWLINES) + else if (!isLeadingInfixOperator) insertNL(NEWLINE) + else if (!currentRun.isScala214) { + val msg = """|Line starts with an operator that in future + |will be taken as an infix expression continued from the previous line. + |To force the previous interpretation as a separate statement, + |add an explicit `;`, add an empty line, or remove spaces after the operator.""".stripMargin + deprecationWarning(msg, "2.13.2") + insertNL(NEWLINE) + } } // Join CASE + CLASS => CASECLASS, CASE + OBJECT => CASEOBJECT, SEMI + ELSE => ELSE diff --git a/test/files/neg/multiLineOps-b.check b/test/files/neg/multiLineOps-b.check new file mode 100644 index 000000000000..b3b366378795 --- /dev/null +++ b/test/files/neg/multiLineOps-b.check @@ -0,0 +1,4 @@ +multiLineOps-b.scala:7: error: ';' expected but integer literal found. + */*one more*/22 // error: end of statement expected + ^ +one error found diff --git a/test/files/neg/multiLineOps-b.scala b/test/files/neg/multiLineOps-b.scala new file mode 100644 index 000000000000..0497fcdb9d79 --- /dev/null +++ b/test/files/neg/multiLineOps-b.scala @@ -0,0 +1,9 @@ +// scalac: -Werror -Xsource:2.14 + +class Test { + val b1 = { + 22 + * 22 // ok + */*one more*/22 // error: end of statement expected + } // error: ';' expected, but '}' found +} diff --git a/test/files/neg/multiLineOps-c.check b/test/files/neg/multiLineOps-c.check new file mode 100644 index 000000000000..cf8d24c04fa4 --- /dev/null +++ b/test/files/neg/multiLineOps-c.check @@ -0,0 +1,5 @@ +multiLineOps-c.scala:7: error: value ! is not a member of Unit +possible cause: maybe a semicolon is missing before `value !`? + ! "hello".isEmpty // error: value ! is not a member of Unit + ^ +one error found diff --git a/test/files/neg/multiLineOps-c.scala b/test/files/neg/multiLineOps-c.scala new file mode 100644 index 000000000000..d7d889be019f --- /dev/null +++ b/test/files/neg/multiLineOps-c.scala @@ -0,0 +1,9 @@ +// scalac: -Werror -Xsource:2.14 + +class Test { + val x = 42 + val b2: Boolean = { + println(x) + ! "hello".isEmpty // error: value ! is not a member of Unit + } +} diff --git a/test/files/neg/multiLineOps.check b/test/files/neg/multiLineOps.check new file mode 100644 index 000000000000..67332fd684be --- /dev/null +++ b/test/files/neg/multiLineOps.check @@ -0,0 +1,6 @@ +multiLineOps.scala:6: warning: a pure expression does nothing in statement position; multiline expressions may require enclosing parentheses + +3 // error: Expected a toplevel definition + ^ +error: No warnings can be incurred under -Werror. +one warning found +one error found diff --git a/test/files/neg/multiLineOps.scala b/test/files/neg/multiLineOps.scala new file mode 100644 index 000000000000..f94946257df4 --- /dev/null +++ b/test/files/neg/multiLineOps.scala @@ -0,0 +1,7 @@ +// scalac: -Werror -Xsource:2.14 + +class Test { + val x = 1 + + 2 + +3 // error: Expected a toplevel definition +} diff --git a/test/files/neg/stmt-expr-discard.check b/test/files/neg/stmt-expr-discard.check index 864433ae06fc..e02229091787 100644 --- a/test/files/neg/stmt-expr-discard.check +++ b/test/files/neg/stmt-expr-discard.check @@ -1,3 +1,15 @@ +stmt-expr-discard.scala:5: warning: Line starts with an operator that in future +will be taken as an infix expression continued from the previous line. +To force the previous interpretation as a separate statement, +add an explicit `;`, add an empty line, or remove spaces after the operator. + + 2 + ^ +stmt-expr-discard.scala:6: warning: Line starts with an operator that in future +will be taken as an infix expression continued from the previous line. +To force the previous interpretation as a separate statement, +add an explicit `;`, add an empty line, or remove spaces after the operator. + - 4 + ^ stmt-expr-discard.scala:5: warning: a pure expression does nothing in statement position; multiline expressions may require enclosing parentheses + 2 ^ @@ -5,5 +17,5 @@ stmt-expr-discard.scala:6: warning: a pure expression does nothing in statement - 4 ^ error: No warnings can be incurred under -Werror. -two warnings found +four warnings found one error found diff --git a/test/files/neg/stmt-expr-discard.scala b/test/files/neg/stmt-expr-discard.scala index db94495205d4..35c014f6b4ca 100644 --- a/test/files/neg/stmt-expr-discard.scala +++ b/test/files/neg/stmt-expr-discard.scala @@ -1,4 +1,4 @@ -// scalac: -Xfatal-warnings +// scalac: -Werror -Xsource:2.13 -Xlint:deprecation // class A { def f = 1 diff --git a/test/files/neg/t9847.check b/test/files/neg/t9847.check index 2dcf1584d912..0adfb9c371c0 100644 --- a/test/files/neg/t9847.check +++ b/test/files/neg/t9847.check @@ -1,3 +1,15 @@ +t9847.scala:10: warning: Line starts with an operator that in future +will be taken as an infix expression continued from the previous line. +To force the previous interpretation as a separate statement, +add an explicit `;`, add an empty line, or remove spaces after the operator. + + 1 + ^ +t9847.scala:14: warning: Line starts with an operator that in future +will be taken as an infix expression continued from the previous line. +To force the previous interpretation as a separate statement, +add an explicit `;`, add an empty line, or remove spaces after the operator. + + 1 + ^ t9847.scala:6: warning: discarded non-Unit value def f(): Unit = 42 ^ @@ -35,5 +47,5 @@ t9847.scala:24: warning: a pure expression does nothing in statement position; m class D { 42 ; 17 } ^ error: No warnings can be incurred under -Werror. -12 warnings found +14 warnings found one error found diff --git a/test/files/neg/t9847.scala b/test/files/neg/t9847.scala index a23b24fb6d4b..fba4739f169c 100644 --- a/test/files/neg/t9847.scala +++ b/test/files/neg/t9847.scala @@ -1,4 +1,4 @@ -// scalac: -Xfatal-warnings -Ywarn-value-discard +// scalac: -Werror -Xlint:deprecation -Ywarn-value-discard // trait T { diff --git a/test/files/pos/multiLineOps.scala b/test/files/pos/multiLineOps.scala new file mode 100644 index 000000000000..5030809c99c6 --- /dev/null +++ b/test/files/pos/multiLineOps.scala @@ -0,0 +1,30 @@ +// scalac: -Werror -Xsource:2.14 + +class Channel { + def ! (msg: String): Channel = this + def send_! (msg: String): Channel = this +} + +class Test { + val x = 1 + + 2 + + 3 + + val c = new Channel() + + def send() = + c ! "hello" + ! "world" + send_! "!" + + val b: Boolean = + "hello".isEmpty + && true && + !"hello".isEmpty + + val b2: Boolean = { + println(x) + !"hello".isEmpty + ??? + } +} diff --git a/test/files/run/multiLineOps.scala b/test/files/run/multiLineOps.scala new file mode 100644 index 000000000000..ce855abb8c98 --- /dev/null +++ b/test/files/run/multiLineOps.scala @@ -0,0 +1,12 @@ +// scalac: -Xsource:2.14 +// +// without backticks, "not found: value +" +// +object Test extends App { + val a = 7 + val x = 1 + + // + `a` * 6 + + assert(x == 1) +}