Repository: scalameta/scalameta (scala)

Issue:
infix with `Unit` literal
### reproduce steps

`build.sbt`

```scala
libraryDependencies += "org.scalameta" %% "scalameta" % "4.11.0"

scalaVersion := "2.13.15"
```

`Main.scala`

```scala
import scala.meta._

object Main {
  def main(args: Array[String]): Unit = {
    val src = Input.String("""a b ()""")
    val p = implicitly[parsers.Parse[Term]]
    println(p.apply(src, dialects.Scala3).get.structure)
  }
}
```

#### actual:

```scala
Term.ApplyInfix(
  Term.Name("a"),
  Term.Name("b"),
  Type.ArgClause(Nil),
  Term.ArgClause(Nil)
)
```

#### expect if dialects is **2.13 or higher**:

```scala
Term.ApplyInfix(
  Term.Name("a"),
  Term.Name("b"),
  Type.ArgClause(Nil),
  Term.ArgClause(List(Lit.Unit()), None)
)
```

### why current implementation is wrong

#### Scala 2.13.x or 3.x

```
Welcome to Scala 2.13.15 (OpenJDK 64-Bit Server VM, Java 21.0.5).
Type in expressions for evaluation. Or try :help.

scala> object a { def b(x: Any): Unit = () }
object a

scala> a b ()

scala> object a { def b(): Unit = () }
object a

scala> a b ()
         ^
       error: can't supply unit value with infix notation because nullary method b: (): Unit takes no arguments; use dotted invocation instead: a.b()
```

#### Scala 2.12

```
Welcome to Scala 2.12.20 (OpenJDK 64-Bit Server VM, Java 21.0.5).
Type in expressions for evaluation. Or try :help.

scala> object a { def b(x: Any): Unit = () }
defined object a

scala> a b ()
<console>:13: warning: Adaptation of argument list by inserting () is deprecated: leaky (Object-receiving) target makes this especially dangerous.
        signature: a.b(x: Any): Unit
  given arguments: <none>
 after adaptation: a.b((): Unit)
       a b ()
         ^

scala> object a { def b(): Unit = () }
defined object a

scala> a b ()
```

Pull request description:
Fixes #4034.

Patch:
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
index 708a16b9e2..c853e0046e 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
@@ -22,6 +22,11 @@ final class Dialect private[meta] (
     val allowCaseClassWithoutParameterList: Boolean,
     // Are extractor varargs specified using colons, i.e. is `case Extractor(xs: _*)` legal or not?
     val allowColonForExtractorVarargs: Boolean,
+    // treatment of "a b ()" changed from 2.12 (empty args, aka "nullary") to 2.13 (a Unit arg)
+    val allowEmptyInfixArgs: Boolean,
+
+    // *** scala3 flags below ***
+
     // Are enums allowed?
     // They are in Dotty, but not in Scala 2.13 or older.
     val allowEnums: Boolean,
@@ -210,6 +215,7 @@ final class Dialect private[meta] (
     allowAtForExtractorVarargs = allowAtForExtractorVarargs,
     allowCaseClassWithoutParameterList = allowCaseClassWithoutParameterList,
     allowColonForExtractorVarargs = allowColonForExtractorVarargs,
+    allowEmptyInfixArgs = true,
     allowEnums = allowEnums,
     allowImplicitByNameParameters = allowImplicitByNameParameters,
     allowInlineIdents = allowInlineIdents,
@@ -290,6 +296,9 @@ final class Dialect private[meta] (
     privateCopy(allowCaseClassWithoutParameterList = newValue)
   def withAllowColonForExtractorVarargs(newValue: Boolean): Dialect =
     privateCopy(allowColonForExtractorVarargs = newValue)
+  def withAllowEmptyInfixArgs(newValue: Boolean): Dialect =
+    privateCopy(allowEmptyInfixArgs = newValue)
+
   def withAllowEnums(newValue: Boolean): Dialect = privateCopy(allowEnums = newValue)
   def withAllowImplicitByNameParameters(newValue: Boolean): Dialect =
     privateCopy(allowImplicitByNameParameters = newValue)
@@ -433,6 +442,7 @@ final class Dialect private[meta] (
       allowAtForExtractorVarargs: Boolean = this.allowAtForExtractorVarargs,
       allowCaseClassWithoutParameterList: Boolean = this.allowCaseClassWithoutParameterList,
       allowColonForExtractorVarargs: Boolean = this.allowColonForExtractorVarargs,
+      allowEmptyInfixArgs: Boolean = this.allowEmptyInfixArgs,
       allowEnums: Boolean = this.allowEnums,
       allowImplicitByNameParameters: Boolean = this.allowImplicitByNameParameters,
       allowInlineIdents: Boolean = this.allowInlineIdents,
@@ -501,6 +511,7 @@ final class Dialect private[meta] (
       allowAtForExtractorVarargs = allowAtForExtractorVarargs,
       allowCaseClassWithoutParameterList = allowCaseClassWithoutParameterList,
       allowColonForExtractorVarargs = allowColonForExtractorVarargs,
+      allowEmptyInfixArgs = allowEmptyInfixArgs,
       allowEnums = allowEnums,
       allowImplicitByNameParameters = allowImplicitByNameParameters,
       allowInlineIdents = allowInlineIdents,
@@ -568,6 +579,7 @@ final class Dialect private[meta] (
       allowAtForExtractorVarargs = allowAtForExtractorVarargs,
       allowCaseClassWithoutParameterList = allowCaseClassWithoutParameterList,
       allowColonForExtractorVarargs = allowColonForExtractorVarargs,
+      allowEmptyInfixArgs = allowEmptyInfixArgs,
       allowEnums = allowEnums,
       allowImplicitByNameParameters = allowImplicitByNameParameters,
       allowInlineIdents = allowInlineIdents,
@@ -663,6 +675,7 @@ final class Dialect private[meta] (
       allowAtForExtractorVarargs: Boolean,
       allowCaseClassWithoutParameterList: Boolean,
       allowColonForExtractorVarargs: Boolean,
+      allowEmptyInfixArgs: Boolean,
       allowEnums: Boolean,
       allowImplicitByNameParameters: Boolean,
       allowInlineIdents: Boolean,
@@ -728,7 +741,7 @@ final class Dialect private[meta] (
     this.allowAtForExtractorVarargs == allowAtForExtractorVarargs &&
       this.allowCaseClassWithoutParameterList == allowCaseClassWithoutParameterList &&
       this.allowColonForExtractorVarargs == allowColonForExtractorVarargs &&
-      this.allowEnums == allowEnums &&
+      this.allowEmptyInfixArgs == allowEmptyInfixArgs && this.allowEnums == allowEnums &&
       this.allowImplicitByNameParameters == allowImplicitByNameParameters &&
       this.allowInlineIdents == allowInlineIdents && this.allowInlineMods == allowInlineMods &&
       this.allowLiteralTypes == allowLiteralTypes &&
@@ -787,6 +800,7 @@ final class Dialect private[meta] (
       allowAtForExtractorVarargs = that.allowAtForExtractorVarargs,
       allowCaseClassWithoutParameterList = that.allowCaseClassWithoutParameterList,
       allowColonForExtractorVarargs = that.allowColonForExtractorVarargs,
+      allowEmptyInfixArgs = that.allowEmptyInfixArgs,
       allowEnums = that.allowEnums,
       allowImplicitByNameParameters = that.allowImplicitByNameParameters,
       allowInlineIdents = that.allowInlineIdents,
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
index b785fe1b26..1433b2426a 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
@@ -39,7 +39,7 @@ package object dialects {
 
   implicit val Scala213: Dialect = Scala212.withAllowImplicitByNameParameters(true)
     .withAllowLiteralTypes(true).withAllowNumericLiteralUnderscoreSeparators(true)
-    .withAllowTryWithAnyExpr(true).withAllowBinaryLiterals(true)
+    .withAllowTryWithAnyExpr(true).withAllowBinaryLiterals(true).withAllowEmptyInfixArgs(false)
 
   /**
    * Dialect starting with Scala 2.13.6 for `-Xsource:3` option
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 eb6545e09f..a61b6592f1 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
@@ -1875,7 +1875,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
 
     def toArgClause(rhs: Typ): Term.ArgClause = copyPos(rhs)(
       (rhs match {
-        case _: Lit.Unit => Nil
+        case _: Lit.Unit if dialect.allowEmptyInfixArgs => Nil
         case t: Term.Tuple => t.args
         case _ => rhs :: Nil
       }).reduceWith(Term.ArgClause(_))
@@ -1910,7 +1910,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       }
       val args = copyPos(rhs)(
         (rhs match {
-          case _: Lit.Unit => Nil
+          case _: Lit.Unit if dialect.allowEmptyInfixArgs => Nil
           case t: Pat.Tuple => t.args
           case _ => rhs :: Nil
         }).reduceWith(Pat.ArgClause.apply)
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
index ea8ced6b19..a9e8ed79d8 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
@@ -731,19 +731,13 @@ object TreeSyntax {
       case t: Pat.Tuple => m(SimplePattern, s("(", r(t.args, ", "), ")"))
       case t: Pat.ArgClause => m(SimplePattern, s("(", r(t.values, ", "), ")"))
       case t: Pat.Extract => m(SimplePattern, s(t.fun, t.argClause))
-      case t: Pat.ExtractInfix => m(
-          Pattern3(t.op.value),
-          s(
-            p(Pattern3(t.op.value), t.lhs, left = true),
-            " ",
-            t.op,
-            " ",
-            t.argClause match {
-              case Pat.ArgClause(pat :: Nil) => s(p(Pattern3(t.op.value), pat, right = true))
-              case pats => s(pats)
-            }
-          )
-        )
+      case t: Pat.ExtractInfix =>
+        val pop = Pattern3(t.op.value)
+        val rhs = t.argClause match {
+          case Pat.ArgClause(pat :: Nil) if !pat.is[Lit.Unit] => s(p(pop, pat, right = true))
+          case pats => s(pats)
+        }
+        m(pop, s(p(pop, t.lhs, left = true), " ", t.op, " ", rhs))
       case t: Pat.Interpolate =>
         /** @see LegacyScanner.getStringPart, when ch == '$' */
         def needBraces(id: String): Boolean = !Character.isUnicodeIdentifierStart(id.head)


Test patch:
diff --git a/tests/shared/src/test/scala-2.13/scala/meta/tests/tokenizers/TokensPositionSuite.scala b/tests/shared/src/test/scala-2.13/scala/meta/tests/tokenizers/TokensPositionSuite.scala
index 159c58eab5..88804488ce 100644
--- a/tests/shared/src/test/scala-2.13/scala/meta/tests/tokenizers/TokensPositionSuite.scala
+++ b/tests/shared/src/test/scala-2.13/scala/meta/tests/tokenizers/TokensPositionSuite.scala
@@ -46,7 +46,6 @@ class TokensPositionSuite extends BasePositionSuite(dialects.Scala213) {
     "case x `.y` () =>",
     """|Pat.ExtractInfix x `.y` ()
        |Term.Name `.y`
-       |Pat.ArgClause ()
        |Term.Block case x `.y` () =>@@
        |""".stripMargin
   )
@@ -240,7 +239,6 @@ class TokensPositionSuite extends BasePositionSuite(dialects.Scala213) {
   checkPositions[Stat](
     "a f ()",
     """|Type.ArgClause a f @@()
-       |Term.ArgClause ()
        |""".stripMargin
   )
   checkPositions[Stat](
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/BasicPositionSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/BasicPositionSuite.scala
index 8ed1c8bd26..06db86c0d6 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/BasicPositionSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/BasicPositionSuite.scala
@@ -48,7 +48,6 @@ class BasicPositionSuite extends BasePositionSuite(dialects.Scala213) {
   checkPositions[Term](
     "a f ()",
     """|Type.ArgClause a f @@()
-       |Term.ArgClause ()
        |""".stripMargin
   )
 
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/CommonTrees.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/CommonTrees.scala
index a51a439350..7b6453eccc 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/CommonTrees.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/CommonTrees.scala
@@ -96,8 +96,9 @@ trait CommonTrees {
   final def tparamInline(name: String, tpe: String) = tparam(List(Mod.Inline()), name, tpe)
   final def tparamUsing(name: String, tpe: String) = tparam(List(Mod.Using()), name, tpe)
 
-  final def tinfix(lt: Term, op: Term.Name, rt: List[Term], ta: List[Type] = Nil): Term.ApplyInfix =
-    Term.ApplyInfix(lt, op, ta, rt)
+  final def tinfix(lt: Term, op: Term.Name, ta: List[Type], rt: Term*): Term.ApplyInfix = Term
+    .ApplyInfix(lt, op, ta, rt.toList)
+  final def tinfix(lt: Term, op: Term.Name, rt: Term*): Term.ApplyInfix = tinfix(lt, op, Nil, rt: _*)
 
   final def pname(name: String): Type.Name = Type.Name(name)
   implicit def implicitStringToType(obj: String): Type.Name = pname(obj)
@@ -129,12 +130,19 @@ trait CommonTrees {
   final def purectxfunc(param: List[Type], res: Type): Type.PureContextFunction = Type
     .PureContextFunction(param, res)
 
+  final def patvar(name: Term.Name): Pat.Var = Pat.Var(name)
+  implicit def implicitStringToPatVar(obj: String): Pat.Var = patvar(obj)
+
+  final def patinfix(lt: Pat, op: Term.Name, rt: Pat*): Pat.ExtractInfix = Pat
+    .ExtractInfix(lt, op, rt.toList)
+
   final val noBounds = Type.Bounds(None, None)
   final def loBound(bound: Type): Type.Bounds = Type.Bounds(Some(bound), None)
   final def hiBound(bound: Type): Type.Bounds = Type.Bounds(None, Some(bound))
   final def bounds(lo: Type.Name = null, hi: Type.Name = null): Type.Bounds = Type
     .Bounds(Option(lo), Option(hi))
 
+  final def lit() = Lit.Unit()
   final def bool(v: Boolean) = Lit.Boolean(v)
   final def lit(v: Boolean) = Lit.Boolean(v)
   final def int(v: Int) = Lit.Int(v)
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/PatSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/PatSuite.scala
index 9cca3f879b..d1f730f0f7 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/PatSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/PatSuite.scala
@@ -118,14 +118,53 @@ class PatSuite extends ParseSuite {
     assertPat("a :: b")(ExtractInfix(Var(tname("a")), tname("::"), Var(tname("b")) :: Nil))
   }
 
-  test("a :: ()") {
-    assertPat("a :: ()")(ExtractInfix(Var(tname("a")), tname("::"), Nil))
+  test("a ::[T] ()") {
     val error = """|<input>:1: error: infix patterns cannot have type arguments: not expected `[`
                    |a ::[T] ()
                    |    ^""".stripMargin
     runTestError[Pat]("a ::[T] ()", error)
   }
 
+  test("a :: () [scala211]") {
+    implicit val dialect: Dialect = dialects.Scala211
+    runTestAssert[Pat]("a :: ()")(patinfix("a", "::"))
+  }
+
+  test("a :: () [scala212]") {
+    implicit val dialect: Dialect = dialects.Scala212
+    runTestAssert[Pat]("a :: ()")(patinfix("a", "::"))
+  }
+
+  test("a :: () [scala213]") {
+    implicit val dialect: Dialect = dialects.Scala213
+    runTestAssert[Pat]("a :: ()", "a :: (())")(patinfix("a", "::", lit()))
+  }
+
+  test("a :: () [scala3]") {
+    implicit val dialect: Dialect = dialects.Scala3
+    runTestAssert[Pat]("a :: ()", "a :: (())")(patinfix("a", "::", lit()))
+  }
+
+  test("a :: (()) [scala211]") {
+    implicit val dialect: Dialect = dialects.Scala211
+    runTestAssert[Pat]("a :: (())")(patinfix("a", "::", lit()))
+  }
+
+  test("a :: (()) [scala212]") {
+    implicit val dialect: Dialect = dialects.Scala212
+    runTestAssert[Pat]("a :: (())")(patinfix("a", "::", lit()))
+  }
+
+  test("a :: (()) [scala213]") {
+    implicit val dialect: Dialect = dialects.Scala213
+    runTestAssert[Pat]("a :: (())")(patinfix("a", "::", lit()))
+  }
+
+  test("a :: (()) [scala3]") {
+    implicit val dialect: Dialect = dialects.Scala3
+    runTestAssert[Pat]("a :: (())")(patinfix("a", "::", lit()))
+  }
+
   test("1 | 2 | 3") {
     assertPat("1 | 2 | 3")(Alternative(int(1), Alternative(int(2), int(3))))
     runTestAssert[Pat]("1 `|` 2")(ExtractInfix(lit(1), tname("|"), List(lit(2))))
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/TermSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/TermSuite.scala
index f69627abba..56b3b6511c 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/TermSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/TermSuite.scala
@@ -4,13 +4,15 @@ package parsers
 import scala.meta._
 import scala.meta.tokenizers.TokenizerOptions
 
+import scala.language.implicitConversions
+
 class TermSuite extends ParseSuite {
   import Name.Anonymous
   import Name.Indeterminate
   import Term.{Name => _, _}
 
   implicit val dialect: Dialect = dialects.Scala211
-  implicit val parseTerm: String => Term = term(_)
+  implicit def parseTerm(code: String)(implicit dialect: Dialect): Term = term(code)
 
   private def assertTerm(expr: String)(tree: Tree)(implicit loc: munit.Location): Unit =
     assertTree(term(expr))(tree)
@@ -90,7 +92,45 @@ class TermSuite extends ParseSuite {
     }
   }
 
-  test("a + ()")(assertTerm("a + ()")(ApplyInfix(tname("a"), tname("+"), Nil, Nil)))
+  test("a + () [scala211]") {
+    implicit val dialect: Dialect = dialects.Scala211
+    runTestAssert[Term]("a + ()")(tinfix("a", "+"))
+  }
+
+  test("a + () [scala212]") {
+    implicit val dialect: Dialect = dialects.Scala212
+    runTestAssert[Term]("a + ()")(tinfix("a", "+"))
+  }
+
+  test("a + () [scala213]") {
+    implicit val dialect: Dialect = dialects.Scala213
+    runTestAssert[Term]("a + ()", "a + (())")(tinfix("a", "+", lit()))
+  }
+
+  test("a + () [scala3]") {
+    implicit val dialect: Dialect = dialects.Scala3
+    runTestAssert[Term]("a + ()", "a + (())")(tinfix("a", "+", lit()))
+  }
+
+  test("a + (()) [scala211]") {
+    implicit val dialect: Dialect = dialects.Scala211
+    runTestAssert[Term]("a + (())")(tinfix("a", "+", lit()))
+  }
+
+  test("a + (()) [scala212]") {
+    implicit val dialect: Dialect = dialects.Scala212
+    runTestAssert[Term]("a + (())")(tinfix("a", "+", lit()))
+  }
+
+  test("a + (()) [scala213]") {
+    implicit val dialect: Dialect = dialects.Scala213
+    runTestAssert[Term]("a + (())")(tinfix("a", "+", lit()))
+  }
+
+  test("a + (()) [scala3]") {
+    implicit val dialect: Dialect = dialects.Scala3
+    runTestAssert[Term]("a + (())")(tinfix("a", "+", lit()))
+  }
 
   test("a + b")(assertTerm("a + b")(ApplyInfix(tname("a"), tname("+"), Nil, tname("b") :: Nil)))
 
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/PatSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/PatSuite.scala
index 5c336d7eb3..55aa01d6ba 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/PatSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/PatSuite.scala
@@ -132,8 +132,8 @@ class PatSuite extends ParseSuite {
 
   }
 
-  test("a :: ()") {
-    assertPat("a :: ()")(ExtractInfix(Var(tname("a")), tname("::"), Nil))
+  test("a ::[T] ()") {
+    // the "a :: ()" case is tested in parsers/PatSuite, with other dialects
     val error = """|<input>:1: error: infix patterns cannot have type arguments: not expected `[`
                    |a ::[T] ()
                    |    ^""".stripMargin