Skip to content

Commit

Permalink
Allow infix operators at start of line
Browse files Browse the repository at this point in the history
Backport from dotty under `-Xsource:2.14`.

The simple expr token test was unused, and is moved
to scanner. Partests are more verbose than in dotty.
  • Loading branch information
som-snytt committed Jan 24, 2020
1 parent 114d330 commit 73a2152
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 23 deletions.
22 changes: 6 additions & 16 deletions src/compiler/scala/tools/nsc/ast/parser/Parsers.scala
Expand Up @@ -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 |
Expand Down Expand Up @@ -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

/** {{{
Expand Down
55 changes: 52 additions & 3 deletions src/compiler/scala/tools/nsc/ast/parser/Scanners.scala
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions 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
^
1 error
9 changes: 9 additions & 0 deletions 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
}
5 changes: 5 additions & 0 deletions 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
^
1 error
9 changes: 9 additions & 0 deletions 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
}
}
6 changes: 6 additions & 0 deletions 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.
1 warning
1 error
7 changes: 7 additions & 0 deletions 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
}
14 changes: 13 additions & 1 deletion test/files/neg/stmt-expr-discard.check
@@ -1,9 +1,21 @@
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
^
stmt-expr-discard.scala:6: warning: a pure expression does nothing in statement position; multiline expressions may require enclosing parentheses
- 4
^
error: No warnings can be incurred under -Werror.
2 warnings
4 warnings
1 error
2 changes: 1 addition & 1 deletion 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
Expand Down
14 changes: 13 additions & 1 deletion 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
^
Expand Down Expand Up @@ -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
14 warnings
1 error
2 changes: 1 addition & 1 deletion test/files/neg/t9847.scala
@@ -1,4 +1,4 @@
// scalac: -Xfatal-warnings -Ywarn-value-discard
// scalac: -Werror -Xlint:deprecation -Ywarn-value-discard
//

trait T {
Expand Down
30 changes: 30 additions & 0 deletions 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
???
}
}
12 changes: 12 additions & 0 deletions 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)
}

0 comments on commit 73a2152

Please sign in to comment.