From f085145e56b77e9bc2fde7dfa4c42a44cadad66a Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Thu, 14 Nov 2024 16:47:56 +0100 Subject: [PATCH] Implement automatic rewrites for named infix arguments interpreted as named tuples --- .../tools/dotc/config/MigrationVersion.scala | 1 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 27 ++++++++++----- .../tools/dotc/reporting/ErrorMessageID.scala | 2 +- .../dotty/tools/dotc/reporting/messages.scala | 9 +++-- .../dotty/tools/dotc/CompilationTests.scala | 1 + tests/neg/infix-named-args.check | 34 +++++++++++++++---- tests/neg/infix-named-args.scala | 11 +++--- tests/neg/named-tuples.check | 1 - tests/rewrites/infix-named-args.check | 15 ++++++++ tests/rewrites/infix-named-args.scala | 15 ++++++++ tests/warn/infix-named-args-migration.scala | 14 ++++++++ tests/warn/infix-named-args.scala | 7 ---- 13 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 tests/rewrites/infix-named-args.check create mode 100644 tests/rewrites/infix-named-args.scala create mode 100644 tests/warn/infix-named-args-migration.scala delete mode 100644 tests/warn/infix-named-args.scala diff --git a/compiler/src/dotty/tools/dotc/config/MigrationVersion.scala b/compiler/src/dotty/tools/dotc/config/MigrationVersion.scala index 3da716abbc40..247e3f62a98d 100644 --- a/compiler/src/dotty/tools/dotc/config/MigrationVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/MigrationVersion.scala @@ -26,6 +26,7 @@ enum MigrationVersion(val warnFrom: SourceVersion, val errorFrom: SourceVersion) case WithOperator extends MigrationVersion(`3.4`, future) case FunctionUnderscore extends MigrationVersion(`3.4`, future) case NonNamedArgumentInJavaAnnotation extends MigrationVersion(`3.6`, `3.6`) + case AmbiguousNamedTupleInfixApply extends MigrationVersion(`3.6`, never) case ImportWildcard extends MigrationVersion(future, future) case ImportRename extends MigrationVersion(future, future) case ParameterEnclosedByParenthesis extends MigrationVersion(future, future) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index d3e198a7e7a7..56d71c7fb57e 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -668,6 +668,7 @@ object StdNames { val readResolve: N = "readResolve" val zero: N = "zero" val zip: N = "zip" + val `++` : N = "++" val nothingRuntimeClass: N = "scala.runtime.Nothing$" val nullRuntimeClass: N = "scala.runtime.Null$" diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 87aa27c120c8..5541663c1722 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1120,14 +1120,9 @@ object Parsers { if (prec < opPrec || leftAssoc && prec == opPrec) { opStack = opStack.tail recur { - atSpan(opInfo.operator.span union opInfo.operand.span union top.span): - def deprecateInfixNamedArg(t: Tree): Unit = t match - case Tuple(ts) => ts.foreach(deprecateInfixNamedArg) - case Parens(t) => deprecateInfixNamedArg(t) - case t: NamedArg => report.deprecationWarning(InfixNamedArgDeprecation(), t.srcPos) - case _ => - deprecateInfixNamedArg(top) - InfixOp(opInfo.operand, opInfo.operator, top) + migrateInfixOp(opInfo, isType): + atSpan(opInfo.operator.span union opInfo.operand.span union top.span): + InfixOp(opInfo.operand, opInfo.operator, top) } } else top @@ -1135,6 +1130,22 @@ object Parsers { recur(top) } + private def migrateInfixOp(opInfo: OpInfo, isType: Boolean)(infixOp: InfixOp): Tree = { + def isNamedTupleOperator = opInfo.operator.name match + case nme.EQ | nme.NE | nme.eq | nme.ne | nme.`++` | nme.zip => true + case _ => false + if isType then infixOp + else infixOp.right match + case Tuple(args) if args.exists(_.isInstanceOf[NamedArg]) && !isNamedTupleOperator => + report.errorOrMigrationWarning(AmbiguousNamedTupleInfixApply(), infixOp.right.srcPos, MigrationVersion.AmbiguousNamedTupleInfixApply) + if MigrationVersion.AmbiguousNamedTupleInfixApply.needsPatch then + val asApply = cpy.Apply(infixOp)(Select(opInfo.operand, opInfo.operator.name), args) + patch(source, infixOp.span, asApply.show) + asApply // allow to use pre-3.6 syntax in migration mode + else infixOp + case _ => infixOp + } + /** True if we are seeing a lambda argument after a colon of the form: * : (params) => * body diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index c959028a880f..35c170858bbf 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -217,7 +217,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case NonNamedArgumentInJavaAnnotationID // errorNumber: 201 case QuotedTypeMissingID // errorNumber: 202 case AmbiguousNamedTupleAssignmentID // errorNumber: 203 - case DeprecatedNamedInfixArgID // errorNumber: 204 + case AmbiguousNamedTupleInfixApplyID // errorNumber: 204 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index c678dd95beaf..328ec122f848 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3353,9 +3353,12 @@ final class AmbiguousNamedTupleAssignment(key: Name, value: untpd.Tree)(using Co override protected def explain(using Context): String = "" -class InfixNamedArgDeprecation()(using Context) extends SyntaxMsg(DeprecatedNamedInfixArgID): - def msg(using Context) = "Named argument syntax is deprecated for infix application" +class AmbiguousNamedTupleInfixApply()(using Context) extends SyntaxMsg(AmbiguousNamedTupleInfixApplyID): + def msg(using Context) = + "Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list." + + Message.rewriteNotice("This", version = SourceVersion.`3.6-migration`) + def explain(using Context) = - i"""The argument will be parsed as a named tuple in future. + i"""Starting with Scala 3.6 infix named arguments are interpretted as Named Tuple. | |To avoid this warning, either remove the argument names or use dotted selection.""" diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index de00faa86406..3bd3b5138fad 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -79,6 +79,7 @@ class CompilationTests { compileFile("tests/rewrites/i20002.scala", defaultOptions.and("-indent", "-rewrite")), compileDir("tests/rewrites/annotation-named-pararamters", defaultOptions.and("-rewrite", "-source:3.6-migration")), compileFile("tests/rewrites/i21418.scala", unindentOptions.and("-rewrite", "-source:3.5-migration")), + compileFile("tests/rewrites/infix-named-args.scala", defaultOptions.and("-rewrite", "-source:3.6-migration")), ).checkRewrites() } diff --git a/tests/neg/infix-named-args.check b/tests/neg/infix-named-args.check index 86a98bf9b3d6..0cfbbaef73a3 100644 --- a/tests/neg/infix-named-args.check +++ b/tests/neg/infix-named-args.check @@ -1,5 +1,5 @@ -- [E134] Type Error: tests/neg/infix-named-args.scala:2:13 ------------------------------------------------------------ -2 | def f = 42 + (x = 1) // error // a named tuple! +2 | def f = 42 + (x = 1) // error // werror | ^^^^ | None of the overloaded alternatives of method + in class Int with types | (x: Double): Double @@ -11,11 +11,31 @@ | (x: Byte): Int | (x: String): String | match arguments ((x : Int)) (a named tuple) --- [E007] Type Mismatch Error: tests/neg/infix-named-args.scala:13:18 -------------------------------------------------- -13 | def g = this ** 2 // error - | ^ - | Found: (2 : Int) - | Required: X +-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:2:15 -------------------------------------------------------- +2 | def f = 42 + (x = 1) // error // werror + | ^^^^^^^ + |Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list. + |This can be rewritten automatically under -rewrite -source 3.6-migration. + | + | longer explanation available when compiling with `-explain` +-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:5:26 -------------------------------------------------------- +5 | def g = new C() `multi` (x = 42, y = 27) // werror + | ^^^^^^^^^^^^^^^^ + |Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list. + |This can be rewritten automatically under -rewrite -source 3.6-migration. + | + | longer explanation available when compiling with `-explain` +-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:6:21 -------------------------------------------------------- +6 | def h = new C() ** (x = 42, y = 27) // werror + | ^^^^^^^^^^^^^^^^ + |Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list. + |This can be rewritten automatically under -rewrite -source 3.6-migration. + | + | longer explanation available when compiling with `-explain` +-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:13:18 ------------------------------------------------------- +13 | def f = this ** (x = 2) // werror + | ^^^^^^^ + |Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list. + |This can be rewritten automatically under -rewrite -source 3.6-migration. | | longer explanation available when compiling with `-explain` -there were 6 deprecation warnings; re-run with -deprecation for details diff --git a/tests/neg/infix-named-args.scala b/tests/neg/infix-named-args.scala index 2cec30a5d0ff..d8616899540c 100644 --- a/tests/neg/infix-named-args.scala +++ b/tests/neg/infix-named-args.scala @@ -1,13 +1,14 @@ class C: - def f = 42 + (x = 1) // error // a named tuple! + def f = 42 + (x = 1) // error // werror def multi(x: Int, y: Int): Int = x + y def **(x: Int, y: Int): Int = x + y - def g = new C() `multi` (x = 42, y = 27) // werror // werror // not actually a tuple! appearances to the contrary - def h = new C() ** (x = 42, y = 27) // werror // werror + def g = new C() `multi` (x = 42, y = 27) // werror + def h = new C() ** (x = 42, y = 27) // werror type X = (x: Int) class D(d: Int): + def **(x: Int): Int = d * x def **(x: X): Int = d * x.x - def f = this ** (x = 2) - def g = this ** 2 // error + def f = this ** (x = 2) // werror + def g = this ** 2 diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index a66bd3e52039..8ec958b6a75d 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -101,4 +101,3 @@ | Required: (name : ?, age : ?) | | longer explanation available when compiling with `-explain` -there were 2 deprecation warnings; re-run with -deprecation for details diff --git a/tests/rewrites/infix-named-args.check b/tests/rewrites/infix-named-args.check new file mode 100644 index 000000000000..a50593ef18a8 --- /dev/null +++ b/tests/rewrites/infix-named-args.check @@ -0,0 +1,15 @@ +class C: + def multi(x: Int, y: Int): Int = x + y + def **(x: Int, y: Int): Int = x + y + def g = new C().multi(x = 42, y = 27) + def h = new C().**(x = 42, y = 27) + +type X = (x: Int) + +class D(d: Int): + def **(x: Int): Int = d * x + def **(x: X): Int = d * x.x + def f = this.**(x = 2) + def g = this ** 2 + def h = this ** ((x = 2)) + def i = this.**(x = (1 + 1)) \ No newline at end of file diff --git a/tests/rewrites/infix-named-args.scala b/tests/rewrites/infix-named-args.scala new file mode 100644 index 000000000000..bcdf4a21a9d2 --- /dev/null +++ b/tests/rewrites/infix-named-args.scala @@ -0,0 +1,15 @@ +class C: + def multi(x: Int, y: Int): Int = x + y + def **(x: Int, y: Int): Int = x + y + def g = new C() `multi` (x = 42, y = 27) + def h = new C() ** (x = 42, y = 27) + +type X = (x: Int) + +class D(d: Int): + def **(x: Int): Int = d * x + def **(x: X): Int = d * x.x + def f = this ** (x = 2) + def g = this ** 2 + def h = this ** ((x = 2)) + def i = this ** (x = (1 + 1)) diff --git a/tests/warn/infix-named-args-migration.scala b/tests/warn/infix-named-args-migration.scala new file mode 100644 index 000000000000..df4bfb50271c --- /dev/null +++ b/tests/warn/infix-named-args-migration.scala @@ -0,0 +1,14 @@ +//> using options -source:3.6-migration +class C: + def f = 42 + (x = 1) // warn // interpreted as 42.+(x = 1) under migration, x is a valid synthetic parameter name + def multi(x: Int, y: Int): Int = x + y + def **(x: Int, y: Int): Int = x + y + def g = new C() `multi` (x = 42, y = 27) // warn + def h = new C() ** (x = 42, y = 27) // warn + +type X = (x: Int) + +class D(d: Int): + def **(x: Int): Int = d * x + def f = this ** (x = 2) // warn + def g = this ** 2 diff --git a/tests/warn/infix-named-args.scala b/tests/warn/infix-named-args.scala deleted file mode 100644 index 2e7803cf2720..000000000000 --- a/tests/warn/infix-named-args.scala +++ /dev/null @@ -1,7 +0,0 @@ -//> using options -deprecation -type X = (x: Int) - -class E(e: Int): - def **(x: Int): Int = e * x - def **(x: X): Int = e * x.x - def f = this ** (x = 2) // warn