Repository: scalameta/scalameta (scala)

Issue:
ParseException `; expected but . found`
input :

```scala
    Flow.fromGraph(GraphDSL.create() {
      implicit b =>
        import GraphDSL.Implicits._

        val removeItems       = b.add(RemoveLineItems(settings.lineItems.removal).graph)
        val filtering         = b.add(Filtering(settings.filtering).flow)
        val projection        = b.add(Projection(settings.projection).graph)

        val input_<       = b.add(Broadcast[FileContent](2))

        // format: off
        input_< ~> filtering          ~>       removeItems.in0
        // format: on

        FlowShape(input_<.in, projection.out)
    })
```

dialect : `scala213source3`
scalameta : `v4.5.13`
scala : `2.13.8`

Pull request description:
https://docs.scala-lang.org/scala3/reference/changed-features/operators.html#syntax-change

Fixes #2880. Supersedes #2888.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
index 613ce47cb7..94ede40271 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/LazyTokenIterator.scala
@@ -364,7 +364,7 @@ private[parsers] class LazyTokenIterator private (
           case _: RegionIndent | _: RegionIndentEnum => true
           case x: RegionParen => x.canProduceLF
           case _ => false
-        } && !isLeadingInfixOperator(nextPos)
+        }
       }
 
       def getIfCanProduceLF =
@@ -395,7 +395,8 @@ private[parsers] class LazyTokenIterator private (
               val ok =
                 if (nextIndent < r.indent)
                   r.closeOnNonCase ||
-                  !(!newlines && isLeadingInfixOperator(nextPos) && // exclude leading infix op
+                  !(!newlines && dialect.allowInfixOperatorAfterNL &&
+                    isLeadingInfixOperator(nextPos) && // exclude leading infix op
                     tail.find(_.isIndented).forall(_.indent <= nextIndent)) &&
                   // need to check prev.prev in case of `end match`
                   (prev.isNot[CanContinueOnNextLine] || prev.prev.is[soft.KwEnd])
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
index 68cc1689d7..91816ca564 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
@@ -1489,7 +1489,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
     if (dialect.allowSignificantIndentation) {
       val startPos = tokenPos
       val simpleExpr = condExpr()
-      val otherRest = if (!token.is[AtEOL]) tryParse {
+      tryParse {
         val simpleRest = simpleExprRest(simpleExpr, canApply = true, startPos = startPos)
         Try {
           postfixExpr(startPos, simpleRest, location = NoStat, allowRepeated = false)
@@ -1498,9 +1498,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           newLinesOpt()
           if (acceptOpt[T]) Some(exprCond) else None
         }
-      }
-      else None
-      otherRest.getOrElse {
+      }.getOrElse {
         newLinesOpt()
         if (!acceptOpt[T]) in.observeIndented()
         simpleExpr
@@ -2030,6 +2028,32 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
         }
       }
 
+      def tryGetNextRhsIfLeading: Option[Either[Term, Term]] =
+        tryAhead(token match {
+          case opToken: Ident if opToken.isIdentSymbolicInfixOperator =>
+            val op = atCurPosNext(Term.Name(opToken.value))
+            newLineOpt()
+            val argToken = token
+            val opPos = opToken.pos
+            val argPos = argToken.pos
+            val opLine = opPos.startLine
+            val argLine = argPos.startLine
+            val opCol = opPos.startColumn
+            val okInfix = argToken.is[ExprIntro] && {
+              if (opLine == argLine) opPos.endColumn < argPos.startColumn // space on same line
+              else opLine == argLine - 1 && opCol <= argPos.startColumn // indent on next line
+            } && !isLeadingInfixOperator(tokenPos)
+            if (okInfix) Some(Right(getNextRhs(op, autoPos(Type.ArgClause(Nil)))))
+            else {
+              val indentedWithoutArg = opLine != argLine &&
+                opCol > scannerTokens.getTokenAtLineStart(startPos).pos.startColumn
+              if (indentedWithoutArg)
+                syntaxError(s"Invalid indented leading infix operator found", at = opToken)
+              None
+            }
+          case _ => None
+        })
+
       val resOpt = token match {
         case t: Unquote =>
           val op = unquote[Term.Name](t)
@@ -2041,6 +2065,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           val op = atCurPosNext(Term.Name("match"))
           val lhs = getPrevLhs(op)
           Some(Right(matchClause(lhs, lhs.startTokenPos)))
+        case _: LF if dialect.allowInfixOperatorAfterNL =>
+          tryGetNextRhsIfLeading
         case _ => None
       }
       resOpt match {
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
index 5ecda133c4..991d46696d 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScannerTokens.scala
@@ -126,7 +126,7 @@ class ScannerTokens(val tokens: Tokens, input: Input)(implicit dialect: Dialect)
     }
   }
 
-  def isLeadingInfixOperator(index: Int): Boolean = dialect.allowInfixOperatorAfterNL && {
+  def isLeadingInfixOperator(index: Int): Boolean = {
     val token = tokens(index)
     token.is[Ident] && token.isIdentSymbolicInfixOperator && {
       val nextSafeIndex = getNextSafeIndex(index)
@@ -138,6 +138,19 @@ class ScannerTokens(val tokens: Tokens, input: Input)(implicit dialect: Dialect)
     }
   }
 
+  def getTokenAtLineStart(idx: Int): Token =
+    getTokenAtLineStart(idx, tokens(idx))
+
+  @tailrec
+  private def getTokenAtLineStart(idx: Int, nextNonSpace: Token): Token =
+    if (idx == 0) nextNonSpace
+    else
+      tokens(idx) match {
+        case _: AtEOL => nextNonSpace
+        case _: HSpace => getTokenAtLineStart(idx - 1, nextNonSpace)
+        case t => getTokenAtLineStart(idx - 1, t)
+      }
+
   val soft = new SoftKeywords(dialect)
 
   @classifier


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/Scala213Suite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/Scala213Suite.scala
index c742e435d7..ab13d37a18 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/Scala213Suite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/Scala213Suite.scala
@@ -138,17 +138,34 @@ class Scala213Suite extends ParseSuite {
 
   test("issue-2880") {
     implicit val Scala213 = dialects.Scala213Source3
-    runTestError[Stat](
+    runAssert(
       """|Flow {
          |    b.add()
          |
          |    input_< ~> filtering ~> removeItems.in0
          |}
-         |""".stripMargin,
-      """|error: ; expected but . found
-         |    input_< ~> filtering ~> removeItems.in0
-         |                                       ^""".stripMargin
-    )(templStat(_)(_), dialects.Scala213Source3)
+         |""".stripMargin
+    )(
+      Term.Apply(
+        Term.Name("Flow"),
+        Term.Block(
+          List(
+            Term.Apply(Term.Select(Term.Name("b"), Term.Name("add")), Nil),
+            Term.ApplyInfix(
+              Term.ApplyInfix(
+                Term.Name("input_<"),
+                Term.Name("~>"),
+                Nil,
+                List(Term.Name("filtering"))
+              ),
+              Term.Name("~>"),
+              Nil,
+              List(Term.Select(Term.Name("removeItems"), Term.Name("in0")))
+            )
+          )
+        ) :: Nil
+      )
+    )
   }
 
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InfixSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InfixSuite.scala
index 00b0d101d7..ca10dabc33 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InfixSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/InfixSuite.scala
@@ -131,8 +131,7 @@ class InfixSuite extends BaseDottySuite {
   }
 
   test("issue-2880 1") {
-    implicit val dialect = dialects.Scala3Future
-    runTestError[Stat](
+    runTestAssert[Stat](
       """|Flow {
          |    b.add()
          |
@@ -141,9 +140,33 @@ class InfixSuite extends BaseDottySuite {
          |    ~> removeItems
          |}
          |""".stripMargin,
-      """|error: ; expected but . found
-         |      ~> removeItems.in0
-         |                    ^""".stripMargin
+      Some(
+        """|Flow {
+           |  b.add()
+           |  input_< ~> filtering ~> removeItems.in0 ~> removeItems
+           |}
+           |""".stripMargin
+      )
+    )(
+      Term.Apply(
+        tname("Flow"),
+        Term.Block(
+          List(
+            Term.Apply(Term.Select(tname("b"), tname("add")), Nil),
+            Term.ApplyInfix(
+              Term.ApplyInfix(
+                Term.ApplyInfix(tname("input_<"), tname("~>"), Nil, List(tname("filtering"))),
+                tname("~>"),
+                Nil,
+                List(Term.Select(tname("removeItems"), tname("in0")))
+              ),
+              tname("~>"),
+              Nil,
+              List(tname("removeItems"))
+            )
+          )
+        ) :: Nil
+      )
     )
   }
 
@@ -160,7 +183,10 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|Flow {
-           |  def foo = (b.add() input_< ~> filtering ~> removeItems1 ~>).removeItems2
+           |  def foo = {
+           |    b.add()
+           |    input_< ~> filtering ~> removeItems1 ~> removeItems2
+           |  }
            |}
            |""".stripMargin
       )
@@ -173,24 +199,21 @@ class InfixSuite extends BaseDottySuite {
             tname("foo"),
             None,
             None,
-            Term.Select(
-              Term.ApplyInfix(
+            Term.Block(
+              List(
+                Term.Apply(Term.Select(tname("b"), tname("add")), Nil),
                 Term.ApplyInfix(
                   Term.ApplyInfix(
-                    Term.Apply(Term.Select(tname("b"), tname("add")), Nil),
-                    tname("input_<"),
+                    Term.ApplyInfix(tname("input_<"), tname("~>"), Nil, List(tname("filtering"))),
+                    tname("~>"),
                     Nil,
-                    List(tname("~>"))
+                    List(tname("removeItems1"))
                   ),
-                  tname("filtering"),
+                  tname("~>"),
                   Nil,
-                  List(tname("~>"))
-                ),
-                tname("removeItems1"),
-                Nil,
-                List(tname("~>"))
-              ),
-              tname("removeItems2")
+                  List(tname("removeItems2"))
+                )
+              )
             )
           ) :: Nil
         ) :: Nil
@@ -232,11 +255,7 @@ class InfixSuite extends BaseDottySuite {
          |  || xs.isEmpty
          |""".stripMargin,
       Some(
-        """|def condition = {
-           |  x > 0
-           |  ||
-           |  xs.exists(_ > 0) || xs.isEmpty
-           |}
+        """|def condition = x > 0 || xs.exists(_ > 0) || xs.isEmpty
            |""".stripMargin
       )
     )(
@@ -245,22 +264,35 @@ class InfixSuite extends BaseDottySuite {
         tname("condition"),
         None,
         None,
-        Term.Block(
-          List(
-            Term.ApplyInfix(tname("x"), tname(">"), Nil, List(int(0))),
-            tname("||"),
+        Term.ApplyInfix(
+          Term.ApplyInfix(
             Term.ApplyInfix(
+              tname("x"),
+              tname(">"),
+              Nil,
+              List(int(0))
+            ),
+            tname("||"),
+            Nil,
+            List(
               Term.Apply(
                 Term.Select(tname("xs"), tname("exists")),
-                Term.AnonymousFunction(
-                  Term.ApplyInfix(Term.Placeholder(), tname(">"), Nil, List(int(0)))
-                ) :: Nil
-              ),
-              tname("||"),
-              Nil,
-              List(Term.Select(tname("xs"), tname("isEmpty")))
+                List(
+                  Term.AnonymousFunction(
+                    Term.ApplyInfix(
+                      Term.Placeholder(),
+                      tname(">"),
+                      Nil,
+                      List(int(0))
+                    )
+                  )
+                )
+              )
             )
-          )
+          ),
+          tname("||"),
+          Nil,
+          List(Term.Select(tname("xs"), tname("isEmpty")))
         )
       )
     )
@@ -294,12 +326,13 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  freezing | boiling
+           |  freezing
+           |  |.boiling
            |}
            |""".stripMargin
       )
     )(
-      Term.Block(List(Term.ApplyInfix(tname("freezing"), tname("|"), Nil, List(tname("boiling")))))
+      Term.Block(List(tname("freezing"), Term.Select(tname("|"), tname("boiling"))))
     )
   }
 
@@ -313,14 +346,12 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  freezing
-           |  |
-           |  boiling
+           |  freezing | boiling
            |}
            |""".stripMargin
       )
     )(
-      Term.Block(List(tname("freezing"), tname("|"), tname("boiling")))
+      Term.Block(List(Term.ApplyInfix(tname("freezing"), tname("|"), Nil, List(tname("boiling")))))
     )
   }
 
@@ -334,35 +365,26 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  freezing
-           |  |
-           |  boiling
+           |  freezing | boiling
            |}
            |""".stripMargin
       )
     )(
-      Term.Block(List(Term.Name("freezing"), Term.Name("|"), Term.Name("boiling")))
+      Term.Block(List(Term.ApplyInfix(tname("freezing"), tname("|"), Nil, List(tname("boiling")))))
     )
   }
 
   test("scala3 infix syntax 3.5") {
-    runTestAssert[Stat](
+    runTestError[Stat](
       """|{
          |  freezing
          |    |
          |  boiling
          |}
          |""".stripMargin,
-      Some(
-        """|{
-           |  freezing
-           |  |
-           |  boiling
-           |}
-           |""".stripMargin
-      )
-    )(
-      Term.Block(List(tname("freezing"), tname("|"), tname("boiling")))
+      """|error: Invalid indented leading infix operator found
+         |    |
+         |    ^""".stripMargin
     )
   }
 
@@ -397,9 +419,7 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  println("hello")
-           |  ???
-           |  ??? match {
+           |  println("hello") ??? ??? match {
            |    case 0 => 1
            |  }
            |}
@@ -407,11 +427,16 @@ class InfixSuite extends BaseDottySuite {
       )
     )(
       Term.Block(
-        List(
-          Term.Apply(tname("println"), List(Lit.String("hello"))),
-          tname("???"),
-          Term.Match(tname("???"), List(Case(int(0), None, int(1))), Nil)
-        )
+        Term.Match(
+          Term.ApplyInfix(
+            Term.Apply(tname("println"), List(str("hello"))),
+            tname("???"),
+            Nil,
+            List(tname("???"))
+          ),
+          List(Case(int(0), None, int(1))),
+          Nil
+        ) :: Nil
       )
     )
   }
@@ -428,9 +453,7 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  println("hello")
-           |  ???
-           |  ??? match {
+           |  println("hello") ??? ??? match {
            |    case 0 => 1
            |  }
            |}
@@ -438,11 +461,16 @@ class InfixSuite extends BaseDottySuite {
       )
     )(
       Term.Block(
-        List(
-          Term.Apply(tname("println"), List(Lit.String("hello"))),
-          tname("???"),
-          Term.Match(tname("???"), List(Case(int(0), None, int(1))), Nil)
-        )
+        Term.Match(
+          Term.ApplyInfix(
+            Term.Apply(tname("println"), List(str("hello"))),
+            tname("???"),
+            Nil,
+            List(tname("???"))
+          ),
+          List(Case(int(0), None, int(1))),
+          Nil
+        ) :: Nil
       )
     )
   }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
index 71c8e3a1d7..36cd9c0d55 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MinorDottySuite.scala
@@ -426,31 +426,45 @@ class MinorDottySuite extends BaseDottySuite {
 
   test("changed-operator-syntax") {
     // https://dotty.epfl.ch/docs/reference/changed-features/operators.html#syntax-change
-    runTestAssert[Source]("""|object X {
-                             |  println("hello")
-                             |  ???
-                             |  ??? match {
-                             |    case 0 => 1
-                             |  }
-                             |}
-                             |""".stripMargin)(
+    runTestAssert[Source](
+      """|object X {
+         |  println("hello")
+         |  ???
+         |  ??? match {
+         |    case 0 => 1
+         |  }
+         |}
+         |""".stripMargin,
+      Some(
+        """|object X {
+           |  println("hello") ??? ??? match {
+           |    case 0 => 1
+           |  }
+           |}
+           |""".stripMargin
+      )
+    )(
       Source(
-        List(
-          Defn.Object(
+        Defn.Object(
+          Nil,
+          tname("X"),
+          Template(
             Nil,
-            Term.Name("X"),
-            Template(
-              Nil,
-              Nil,
-              Self(Name(""), None),
-              List(
-                Term.Apply(Term.Name("println"), List(Lit.String("hello"))),
-                Term.Name("???"),
-                Term.Match(Term.Name("???"), List(Case(Lit.Int(0), None, Lit.Int(1))))
-              )
-            )
+            Nil,
+            Self(Name(""), None),
+            Term.Match(
+              Term.ApplyInfix(
+                Term.Apply(tname("println"), List(str("hello"))),
+                tname("???"),
+                Nil,
+                List(tname("???"))
+              ),
+              List(Case(int(0), None, int(1))),
+              Nil
+            ) :: Nil,
+            Nil
           )
-        )
+        ) :: Nil
       )
     )
   }