diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 91627864ac64..121806dd36ed 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -39,6 +39,11 @@ object desugar { */ val CheckIrrefutable: Property.Key[MatchCheck] = Property.StickyKey() + /** A multi-line infix operation with the infix operator starting a new line. + * Used for explaining potential errors. + */ + val MultiLineInfix: Property.Key[Unit] = Property.StickyKey() + /** What static check should be applied to a Match? */ enum MatchCheck { case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom @@ -1194,7 +1199,10 @@ object desugar { case Tuple(args) => args.mapConserve(assignToNamedArg) case _ => arg :: Nil } - Apply(Select(fn, op.name).withSpan(selectPos), args) + val sel = Select(fn, op.name).withSpan(selectPos) + if (left.sourcePos.endLine < op.sourcePos.startLine) + sel.pushAttachment(MultiLineInfix, ()) + Apply(sel, args) } if (isLeftAssoc(op.name)) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 78bed73dce3a..6181c5ac7c44 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -219,6 +219,9 @@ object Scanners { class Scanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) { val keepComments: Boolean = !ctx.settings.YdropComments.value + /** A switch whether operators at the start of lines can be infix operators */ + private var allowLeadingInfixOperators = true + /** All doc comments kept by their end position in a `Map` */ private[this] var docstringMap: SortedMap[Int, Comment] = SortedMap.empty @@ -265,12 +268,12 @@ object Scanners { else IDENTIFIER } - private class TokenData0 extends TokenData + def newTokenData: TokenData = new TokenData {} /** We need one token lookahead and one token history */ - val next : TokenData = new TokenData0 - private val prev : TokenData = new TokenData0 + val next = newTokenData + private val prev = newTokenData /** a stack of tokens which indicates whether line-ends can be statement separators * also used for keeping track of nesting levels. @@ -378,6 +381,30 @@ object Scanners { next.token = EMPTY } + 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(name.length - 1))) && + (ch == ' ') && { + val lookahead = lookaheadScanner + lookahead.allowLeadingInfixOperators = false + // force a NEWLINE a after current token if it is on its own line + lookahead.nextToken() + canStartExpressionTokens.contains(lookahead.token) + } + /** Insert NEWLINE or NEWLINES if * - we are after a newline * - we are within a { ... } or on toplevel (wrt sepRegions) @@ -389,10 +416,15 @@ object Scanners { (canStartStatTokens contains token) && (sepRegions.isEmpty || sepRegions.head == RBRACE || sepRegions.head == ARROW && token == CASE)) { - next copyFrom this - // todo: make offset line-end of previous line? - offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset - token = if (pastBlankLine()) NEWLINES else NEWLINE + if (pastBlankLine()) + insertNL(NEWLINES) + else if (!isLeadingInfixOperator) + insertNL(NEWLINE) + else if (isScala2Mode) + ctx.warning(em"""Line starts with an operator; + |it is now treated as a continuation of the expression on the previous line, + |not as a separate statement.""", + source.atSpan(Span(offset))) } postProcessToken() @@ -1087,8 +1119,6 @@ object Scanners { case _ => showToken(token) } -// (does not seem to be needed) def flush = { charOffset = offset; nextChar(); this } - /* Resume normal scanning after XML */ def resume(lastToken: Token): Unit = { token = lastToken diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index b36a6a7097aa..b08f79904337 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -271,7 +271,13 @@ trait TypeAssigner { |An extension method was tried, but could not be fully constructed: | | ${failure.tree.show.replace("\n", "\n ")}""" - case _ => "" + case _ => + if (tree.hasAttachment(desugar.MultiLineInfix)) + i""". + |Note that `$name` is treated as an infix operator in Scala 3. + |If you do not want that, insert a `;` or empty line in front + |or drop any spaces behind the operator.""" + else "" } errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos) } diff --git a/tests/neg/multiLineOps.scala b/tests/neg/multiLineOps.scala new file mode 100644 index 000000000000..4f48c18af3ce --- /dev/null +++ b/tests/neg/multiLineOps.scala @@ -0,0 +1,15 @@ +val x = 1 + + 2 + +3 // error: Expected a toplevel definition + +val b1 = { + 22 + * 22 // ok + */*one more*/22 // error: end of statement expected +} // error: ';' expected, but '}' found + +val b2: Boolean = { + println(x) + ! "hello".isEmpty // error: value ! is not a member of Unit +} + diff --git a/tests/pos/multiLineOps.scala b/tests/pos/multiLineOps.scala new file mode 100644 index 000000000000..84af353f6f6b --- /dev/null +++ b/tests/pos/multiLineOps.scala @@ -0,0 +1,26 @@ +val x = 1 + + 2 + + 3 + +class Channel { + def ! (msg: String): Channel = this + def send_! (msg: String): Channel = this +} + +val c = Channel() + +def send() = + c ! "hello" + ! "world" + send_! "!" + +val b: Boolean = + "hello".isEmpty + && true && + !"hello".isEmpty + +val b2: Boolean = { + println(x) + !"hello".isEmpty + ??? +}