Repository: scalameta/scalameta (scala)

Issue:
Scalameta parsers more than three `???` in line as ApplyInfix
Related https://github.com/scalameta/metals/pull/4877

From scalameta 4.7.2, Scala3 dialect parses the following program as


```scala
def foo =
  ???
  ???
  ???
```

```scala
Defn.Def(
  Nil,
  Term.Name("foo"),
  Nil,
  None,
  Term.ApplyInfix(
    Term.Name("???"),
    Term.Name("???"),
    Type.ArgClause(Nil),
    Term.ArgClause(List(Term.Name("???")), None)
  )
)
```

but it should be

```scala
Defn.Def(
  Nil,
  Term.Name("foo"),
  Nil,
  None,
  Term.Block(List(
    Term.Name("???"),
    Term.Name("???"),
    Term.Name("???")
  ))
)
```


```scala
scala> import scala.meta._
import scala.meta._

scala> val program = """
     | def foo =
     |   ???
     |   ???
     |   ???
     | """
val program: String =
"
def foo =
  ???
  ???
  ???
"

scala> dialects.Scala3(program).parse[Source].get.structure
val res9: String = Source(List(Defn.Def(Nil, Term.Name("foo"), Nil, None, Term.ApplyInfix(Term.Name("???"), Term.Name("???"), Type.ArgClause(Nil), Term.ArgClause(List(Term.Name("???")), None)))))
```

---

Interestingly, 

```scala
def foo =
  x
  x
  x
```

will be parsed as 

```scala
Defn.Def(
  Nil,
  Term.Name("foo"),
  Nil,
  None,
  Term.Block(List(
    Term.Name("x"),
    Term.Name("x"),
    Term.Name("x")
  ))
)
```

Pull request:
Fixes #3094.

Patch:
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 d66aead52a..c2ac790e6a 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
@@ -2024,10 +2024,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
             val opLine = opPos.startLine
             val argLine = argPos.startLine
             val opCol = opPos.startColumn
-            val okInfix = isExprIntro(argToken, tokenPos) && {
+            val okInfix = canBeLeadingInfixArg(argToken, tokenPos) && {
               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 &&
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 a7a6f9d27f..8da6c4345c 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
@@ -6,6 +6,7 @@ import scala.meta.classifiers._
 import scala.meta.inputs.Input
 import scala.meta.internal.classifiers.classifier
 import scala.meta.internal.tokenizers._
+import scala.meta.internal.trees._
 import scala.meta.prettyprinters._
 import scala.meta.tokenizers._
 import scala.meta.tokens.Token
@@ -74,17 +75,12 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
     }
   }
 
-  def isLeadingInfixOperator(index: Int): Boolean = {
-    val token = tokens(index)
-    token.is[Ident] && token.isIdentSymbolicInfixOperator && {
-      val nextSafeIndex = getNextSafeIndex(index)
-      tokens(nextSafeIndex).is[Whitespace] &&
-      (tokens(getStrictAfterSafe(nextSafeIndex)) match {
-        case _: Ident | _: Interpolation.Id | _: LeftParen | _: LeftBrace | _: Literal => true
-        case _ => false
-      })
-    }
-  }
+  // https://github.com/lampepfl/dotty/blob/4e7ab609/compiler/src/dotty/tools/dotc/parsing/Scanners.scala#L435
+  def canBeLeadingInfixArg(argToken: Token, argTokenPos: Int): Boolean =
+    isExprIntro(argToken, argTokenPos) && (argToken match {
+      case x: Ident => x.value.isUnaryOp || !x.isIdentSymbolicInfixOperator
+      case _ => true
+    })
 
   def getTokenAtLineStart(idx: Int): Token =
     getTokenAtLineStart(idx, tokens(idx))
@@ -554,7 +550,13 @@ final class ScannerTokens(val tokens: Tokens)(implicit dialect: Dialect) {
 
       def isLeadingInfix() =
         !newlines && dialect.allowInfixOperatorAfterNL &&
-          isLeadingInfixOperator(nextPos)
+          next.is[Ident] && next.isIdentSymbolicInfixOperator && {
+            val nextSafeIndex = getNextSafeIndex(nextPos)
+            tokens(nextSafeIndex).is[Whitespace] && {
+              val argPos = getStrictAfterSafe(nextSafeIndex)
+              canBeLeadingInfixArg(tokens(argPos), argPos)
+            }
+          }
 
       val resOpt =
         if (next == null || !hasLF) None


Test patch:
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 f74c53de15..68b844fbdf 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
@@ -419,7 +419,9 @@ class InfixSuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|{
-           |  println("hello") ??? ??? match {
+           |  println("hello")
+           |  ???
+           |  ??? match {
            |    case 0 => 1
            |  }
            |}
@@ -427,22 +429,21 @@ class InfixSuite extends BaseDottySuite {
       )
     )(
       Term.Block(
-        Term.Match(
-          Term.ApplyInfix(
-            Term.Apply(tname("println"), List(str("hello"))),
+        List(
+          Term.Apply(tname("println"), List(str("hello"))),
+          tname("???"),
+          Term.Match(
             tname("???"),
-            Nil,
-            List(tname("???"))
-          ),
-          List(Case(int(0), None, int(1))),
-          Nil
-        ) :: Nil
+            List(Case(int(0), None, int(1))),
+            Nil
+          )
+        )
       )
     )
   }
 
   test("scala3 infix syntax 5.2") {
-    runTestAssert[Stat](
+    runTestError[Stat](
       """|{
          |  println("hello")
          |    ???
@@ -451,27 +452,9 @@ class InfixSuite extends BaseDottySuite {
          |    }
          |}
          |""".stripMargin,
-      Some(
-        """|{
-           |  println("hello") ??? ??? match {
-           |    case 0 => 1
-           |  }
-           |}
-           |""".stripMargin
-      )
-    )(
-      Term.Block(
-        Term.Match(
-          Term.ApplyInfix(
-            Term.Apply(tname("println"), List(str("hello"))),
-            tname("???"),
-            Nil,
-            List(tname("???"))
-          ),
-          List(Case(int(0), None, int(1))),
-          Nil
-        ) :: Nil
-      )
+      """|error: Invalid indented leading infix operator found
+         |    ???
+         |    ^""".stripMargin
     )
   }
 
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 2dca35a396..3b89b23597 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
@@ -437,7 +437,9 @@ class MinorDottySuite extends BaseDottySuite {
          |""".stripMargin,
       Some(
         """|object X {
-           |  println("hello") ??? ??? match {
+           |  println("hello")
+           |  ???
+           |  ??? match {
            |    case 0 => 1
            |  }
            |}
@@ -452,16 +454,11 @@ class MinorDottySuite extends BaseDottySuite {
             Nil,
             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,
+            List(
+              Term.Apply(tname("println"), List(str("hello"))),
+              tname("???"),
+              Term.Match(tname("???"), List(Case(int(0), None, int(1))), Nil)
+            ),
             Nil
           )
         ) :: Nil