Skip to content

Commit 3e4d48d

Browse files
WojciechMazursom-snyttmbovel
committed
Migration rewrites for infix arguments interpreted as named tuples (#21949)
Best effort migration rewrites based on prior work in #21565 At this point, it's too late to deprecate named infix arguments. Let's produce warnings instead. We accept infix arguments that might be an argument of a named tuple, eg. `zip`, `++` or `==` - each of these takes a single argument with NamedTuple. --------- Co-authored-by: Som Snytt <[email protected]> Co-authored-by: Matt Bovel <[email protected]> [Cherry-picked 5d5a9e6]
1 parent 10c13da commit 3e4d48d

File tree

13 files changed

+140
-6
lines changed

13 files changed

+140
-6
lines changed

compiler/src/dotty/tools/dotc/config/MigrationVersion.scala

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ enum MigrationVersion(val warnFrom: SourceVersion, val errorFrom: SourceVersion)
2626
case WithOperator extends MigrationVersion(`3.4`, future)
2727
case FunctionUnderscore extends MigrationVersion(`3.4`, future)
2828
case NonNamedArgumentInJavaAnnotation extends MigrationVersion(`3.6`, `3.6`)
29+
case AmbiguousNamedTupleInfixApply extends MigrationVersion(`3.6`, never)
2930
case ImportWildcard extends MigrationVersion(future, future)
3031
case ImportRename extends MigrationVersion(future, future)
3132
case ParameterEnclosedByParenthesis extends MigrationVersion(future, future)

compiler/src/dotty/tools/dotc/core/StdNames.scala

+1
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ object StdNames {
668668
val readResolve: N = "readResolve"
669669
val zero: N = "zero"
670670
val zip: N = "zip"
671+
val `++` : N = "++"
671672
val nothingRuntimeClass: N = "scala.runtime.Nothing$"
672673
val nullRuntimeClass: N = "scala.runtime.Null$"
673674

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+19-3
Original file line numberDiff line numberDiff line change
@@ -1116,16 +1116,32 @@ object Parsers {
11161116
if (prec < opPrec || leftAssoc && prec == opPrec) {
11171117
opStack = opStack.tail
11181118
recur {
1119-
atSpan(opInfo.operator.span union opInfo.operand.span union top.span) {
1120-
InfixOp(opInfo.operand, opInfo.operator, top)
1121-
}
1119+
migrateInfixOp(opInfo, isType):
1120+
atSpan(opInfo.operator.span union opInfo.operand.span union top.span):
1121+
InfixOp(opInfo.operand, opInfo.operator, top)
11221122
}
11231123
}
11241124
else top
11251125
}
11261126
recur(top)
11271127
}
11281128

1129+
private def migrateInfixOp(opInfo: OpInfo, isType: Boolean)(infixOp: InfixOp): Tree = {
1130+
def isNamedTupleOperator = opInfo.operator.name match
1131+
case nme.EQ | nme.NE | nme.eq | nme.ne | nme.`++` | nme.zip => true
1132+
case _ => false
1133+
if isType then infixOp
1134+
else infixOp.right match
1135+
case Tuple(args) if args.exists(_.isInstanceOf[NamedArg]) && !isNamedTupleOperator =>
1136+
report.errorOrMigrationWarning(AmbiguousNamedTupleInfixApply(), infixOp.right.srcPos, MigrationVersion.AmbiguousNamedTupleInfixApply)
1137+
if MigrationVersion.AmbiguousNamedTupleInfixApply.needsPatch then
1138+
val asApply = cpy.Apply(infixOp)(Select(opInfo.operand, opInfo.operator.name), args)
1139+
patch(source, infixOp.span, asApply.show(using ctx.withoutColors))
1140+
asApply // allow to use pre-3.6 syntax in migration mode
1141+
else infixOp
1142+
case _ => infixOp
1143+
}
1144+
11291145
/** True if we are seeing a lambda argument after a colon of the form:
11301146
* : (params) =>
11311147
* body

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
217217
case NonNamedArgumentInJavaAnnotationID // errorNumber: 201
218218
case QuotedTypeMissingID // errorNumber: 202
219219
case AmbiguousNamedTupleAssignmentID // errorNumber: 203
220+
case AmbiguousNamedTupleInfixApplyID // errorNumber: 204
220221

221222
def errorNumber = ordinal - 1
222223

compiler/src/dotty/tools/dotc/reporting/messages.scala

+10
Original file line numberDiff line numberDiff line change
@@ -3352,3 +3352,13 @@ final class AmbiguousNamedTupleAssignment(key: Name, value: untpd.Tree)(using Co
33523352
|To assign a value, use curly braces: `{${key} = ${value}}`."""
33533353

33543354
override protected def explain(using Context): String = ""
3355+
3356+
class AmbiguousNamedTupleInfixApply()(using Context) extends SyntaxMsg(AmbiguousNamedTupleInfixApplyID):
3357+
def msg(using Context) =
3358+
"Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list."
3359+
+ Message.rewriteNotice("This", version = SourceVersion.`3.6-migration`)
3360+
3361+
def explain(using Context) =
3362+
i"""Starting with Scala 3.6 infix named arguments are interpretted as Named Tuple.
3363+
|
3364+
|To avoid this warning, either remove the argument names or use dotted selection."""

compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ object ErrorReporting {
110110
case tp => i" and expected result type $tp"
111111
}
112112
i"(${tp.typedArgs().tpes}%, %)$result"
113-
s"arguments ${argStr(tp)}"
113+
def hasNames = tp.args.exists:
114+
case tree: untpd.Tuple => tree.trees.exists(_.isInstanceOf[NamedArg])
115+
case _ => false
116+
val addendum = if hasNames then " (a named tuple)" else ""
117+
s"arguments ${argStr(tp)}$addendum"
114118
case _ =>
115119
i"expected type $tp"
116120
}

compiler/test/dotty/tools/dotc/CompilationTests.scala

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class CompilationTests {
7979
compileFile("tests/rewrites/i20002.scala", defaultOptions.and("-indent", "-rewrite")),
8080
compileDir("tests/rewrites/annotation-named-pararamters", defaultOptions.and("-rewrite", "-source:3.6-migration")),
8181
compileFile("tests/rewrites/i21418.scala", unindentOptions.and("-rewrite", "-source:3.5-migration")),
82+
compileFile("tests/rewrites/infix-named-args.scala", defaultOptions.and("-rewrite", "-source:3.6-migration")),
8283
).checkRewrites()
8384
}
8485

docs/_docs/reference/other-new-features/named-tuples.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ val persons: List[Person] = ...
1717
val minors = persons.filter: p =>
1818
p.age < 18
1919
```
20-
Named bindings in tuples are similar to function parameters and arguments. We use `name: Type` for element types and `name = value` for element values. It is illegal to mix named and unnamed elements in a tuple, or to use the same same
21-
name for two different elements.
20+
Named bindings in tuples are similar to function parameters and arguments.
21+
We use `name: Type` for element types and `name = value` for element values.
22+
It is illegal to mix named and unnamed elements in a tuple, or to use the same name for two different elements.
2223

2324
Fields of named tuples can be selected by their name, as in the line `p.age < 18` above.
2425

tests/neg/infix-named-args.check

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- [E134] Type Error: tests/neg/infix-named-args.scala:2:13 ------------------------------------------------------------
2+
2 | def f = 42 + (x = 1) // error // werror
3+
| ^^^^
4+
| None of the overloaded alternatives of method + in class Int with types
5+
| (x: Double): Double
6+
| (x: Float): Float
7+
| (x: Long): Long
8+
| (x: Int): Int
9+
| (x: Char): Int
10+
| (x: Short): Int
11+
| (x: Byte): Int
12+
| (x: String): String
13+
| match arguments ((x : Int)) (a named tuple)
14+
-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:2:15 --------------------------------------------------------
15+
2 | def f = 42 + (x = 1) // error // werror
16+
| ^^^^^^^
17+
|Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list.
18+
|This can be rewritten automatically under -rewrite -source 3.6-migration.
19+
|
20+
| longer explanation available when compiling with `-explain`
21+
-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:5:26 --------------------------------------------------------
22+
5 | def g = new C() `multi` (x = 42, y = 27) // werror
23+
| ^^^^^^^^^^^^^^^^
24+
|Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list.
25+
|This can be rewritten automatically under -rewrite -source 3.6-migration.
26+
|
27+
| longer explanation available when compiling with `-explain`
28+
-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:6:21 --------------------------------------------------------
29+
6 | def h = new C() ** (x = 42, y = 27) // werror
30+
| ^^^^^^^^^^^^^^^^
31+
|Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list.
32+
|This can be rewritten automatically under -rewrite -source 3.6-migration.
33+
|
34+
| longer explanation available when compiling with `-explain`
35+
-- [E204] Syntax Warning: tests/neg/infix-named-args.scala:13:18 -------------------------------------------------------
36+
13 | def f = this ** (x = 2) // werror
37+
| ^^^^^^^
38+
|Ambigious syntax: this infix call argument list is interpreted as single named tuple argument, not as an named arguments list.
39+
|This can be rewritten automatically under -rewrite -source 3.6-migration.
40+
|
41+
| longer explanation available when compiling with `-explain`

tests/neg/infix-named-args.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class C:
2+
def f = 42 + (x = 1) // error // werror
3+
def multi(x: Int, y: Int): Int = x + y
4+
def **(x: Int, y: Int): Int = x + y
5+
def g = new C() `multi` (x = 42, y = 27) // werror
6+
def h = new C() ** (x = 42, y = 27) // werror
7+
8+
type X = (x: Int)
9+
10+
class D(d: Int):
11+
def **(x: Int): Int = d * x
12+
def **(x: X): Int = d * x.x
13+
def f = this ** (x = 2) // werror
14+
def g = this ** 2

tests/rewrites/infix-named-args.check

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class C:
2+
def multi(x: Int, y: Int): Int = x + y
3+
def **(x: Int, y: Int): Int = x + y
4+
def g = new C().multi(x = 42, y = 27)
5+
def h = new C().**(x = 42, y = 27)
6+
7+
type X = (x: Int)
8+
9+
class D(d: Int):
10+
def **(x: Int): Int = d * x
11+
def **(x: X): Int = d * x.x
12+
def f = this.**(x = 2)
13+
def g = this ** 2
14+
def h = this ** ((x = 2))
15+
def i = this.**(x = (1 + 1))

tests/rewrites/infix-named-args.scala

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class C:
2+
def multi(x: Int, y: Int): Int = x + y
3+
def **(x: Int, y: Int): Int = x + y
4+
def g = new C() `multi` (x = 42, y = 27)
5+
def h = new C() ** (x = 42, y = 27)
6+
7+
type X = (x: Int)
8+
9+
class D(d: Int):
10+
def **(x: Int): Int = d * x
11+
def **(x: X): Int = d * x.x
12+
def f = this ** (x = 2)
13+
def g = this ** 2
14+
def h = this ** ((x = 2))
15+
def i = this ** (x = (1 + 1))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//> using options -source:3.6-migration
2+
class C:
3+
def f = 42 + (x = 1) // warn // interpreted as 42.+(x = 1) under migration, x is a valid synthetic parameter name
4+
def multi(x: Int, y: Int): Int = x + y
5+
def **(x: Int, y: Int): Int = x + y
6+
def g = new C() `multi` (x = 42, y = 27) // warn
7+
def h = new C() ** (x = 42, y = 27) // warn
8+
9+
type X = (x: Int)
10+
11+
class D(d: Int):
12+
def **(x: Int): Int = d * x
13+
def f = this ** (x = 2) // warn
14+
def g = this ** 2

0 commit comments

Comments
 (0)