diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 139950888e31..51a1bce5461d 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -1188,6 +1188,7 @@ class Global(var currentSettings: Settings, reporter0: Reporter) import s.XsourceFeatures.contains def caseApplyCopyAccess = isScala3 && contains(o.caseApplyCopyAccess) def caseCompanionFunction = isScala3 && contains(o.caseCompanionFunction) + def caseCopyByName = isScala3 && contains(o.caseCopyByName) def inferOverride = isScala3 && contains(o.inferOverride) def any2StringAdd = isScala3 && contains(o.any2StringAdd) def unicodeEscapesRaw = isScala3 && contains(o.unicodeEscapesRaw) diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 7931cd7ec97e..654ca4ff3dcd 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -125,8 +125,8 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett val sourceReader = StringSetting ("-Xsource-reader", "classname", "Specify a custom method for reading source files.", "") val reporter = StringSetting ("-Xreporter", "classname", "Specify a custom subclass of FilteringReporter for compiler messages.", "scala.tools.nsc.reporters.ConsoleReporter") private val XsourceHelp = - sm"""|-Xsource:3 is for migrating a codebase, -Xsource-features can be added for - |cross-building to adopt certain Scala 3 behavior. + sm"""|-Xsource:3 is for migrating a codebase. -Xsource-features can be added for + |cross-building to adopt certain Scala 3 behaviors. | |See also "Scala 2 with -Xsource:3" on docs.scala-lang.org. | @@ -169,9 +169,10 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett // buffet of features available under -Xsource:3 object sourceFeatures extends MultiChoiceEnumeration { // Changes affecting binary encoding - val caseApplyCopyAccess = Choice("case-apply-copy-access", "Constructor modifiers are used for apply / copy methods of case classes. [bin]") + val caseApplyCopyAccess = Choice("case-apply-copy-access", "Constructor modifiers are used for apply / copy methods of case classes. [bin]") val caseCompanionFunction = Choice("case-companion-function", "Synthetic case companion objects no longer extend FunctionN. [bin]") - val inferOverride = Choice("infer-override", "Inferred type of member uses type of overridden member. [bin]") + val caseCopyByName = Choice("case-copy-by-name", "Synthesize case copy method with by-name parameters. [bin]") + val inferOverride = Choice("infer-override", "Inferred type of member uses type of overridden member. [bin]") // Other semantic changes val any2StringAdd = Choice("any2stringadd", "Implicit `any2stringadd` is never inferred.") @@ -203,10 +204,11 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett helpText = Some( sm"""Enable Scala 3 features under -Xsource:3. | - |Instead of `-Xsource-features:_`, it is recommended to enable specific features, for - |example `-Xsource-features:v2.13.14,-case-companion-function` (-x to exclude x). - |This way, new semantic changes in future Scala versions are not silently adopted; - |new features can be enabled after auditing the corresponding migration warnings. + |Instead of `-Xsource-features:_`, it is recommended to enable individual features. + |Features can also be removed from a feature group by prefixing with `-`; + |for example, `-Xsource-features:v2.13.14,-case-companion-function`. + |Listing features explicitly ensures new semantic changes in future Scala versions are + |not silently adopted; new features can be enabled after auditing migration warnings. | |`-Xsource:3-cross` is a shorthand for `-Xsource:3 -Xsource-features:_`. | diff --git a/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala b/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala index 232536f98a23..563fb1f61c40 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala @@ -27,8 +27,8 @@ trait Unapplies extends ast.TreeDSL { import global._ import definitions._ - import CODE.{ CASE => _, _ } - import treeInfo.{ isRepeatedParamType, isByNameParamType } + import CODE.{CASE => _, _} + import treeInfo.{isByNameParamType, isRepeatedParamType} private def unapplyParamName = nme.x_0 private def caseMods = Modifiers(SYNTHETIC | CASE) @@ -265,11 +265,16 @@ trait Unapplies extends ast.TreeDSL { * ClassDef of the case class. */ def caseClassCopyMeth(cdef: ClassDef): Option[DefDef] = { - def isDisallowed(vd: ValDef) = isRepeatedParamType(vd.tpt) || isByNameParamType(vd.tpt) - val classParamss = constrParamss(cdef) - - if (cdef.symbol.hasAbstractFlag || mexists(classParamss)(isDisallowed)) None - else { + val classParamss = constrParamss(cdef) + def copyOK = { + def warn() = if (currentRun.isScala3) runReporting.warning(cdef.namePos, "case `copy` method is allowed to have by-name parameters under Scala 3 (or with -Xsource-features:case-copy-by-name)", Scala3Migration, cdef.symbol) + def isAllowed(vd: ValDef) = { + def checkByName = currentRun.sourceFeatures.caseCopyByName || !isByNameParamType(vd.tpt).tap(if (_) warn()) + !isRepeatedParamType(vd.tpt) && checkByName + } + !cdef.symbol.hasAbstractFlag && mforall(classParamss)(isAllowed) + } + def synthesizeCopy = { def makeCopyParam(vd: ValDef, putDefault: Boolean) = { val rhs = if (putDefault) toIdent(vd) else EmptyTree val flags = PARAM | (vd.mods.flags & IMPLICIT) | (if (putDefault) DEFAULTPARAM else 0) @@ -277,14 +282,12 @@ trait Unapplies extends ast.TreeDSL { val tpt = atPos(vd.pos.focus)(TypeTree() setOriginal vd.tpt) treeCopy.ValDef(vd, Modifiers(flags), vd.name, tpt, rhs) } - val tparams = constrTparamsInvariant(cdef) val paramss = classParamss match { case Nil => Nil case ps :: pss => ps.map(makeCopyParam(_, putDefault = true)) :: mmap(pss)(makeCopyParam(_, putDefault = false)) } - val classTpe = classType(cdef, tparams) val argss = mmap(paramss)(toIdent) val body: Tree = New(classTpe, argss) @@ -301,10 +304,10 @@ trait Unapplies extends ast.TreeDSL { } } else synth - val copyDefDef = atPos(cdef.pos.focus)( + atPos(cdef.pos.focus)( DefDef(copyMods, nme.copy, tparams, paramss, TypeTree(), body) ) - Some(copyDefDef) } + if (copyOK) Some(synthesizeCopy) else None } } diff --git a/test/files/neg/t7879.check b/test/files/neg/t7879.check new file mode 100644 index 000000000000..d64d37be30b9 --- /dev/null +++ b/test/files/neg/t7879.check @@ -0,0 +1,6 @@ +t7879.scala:2: error: case `copy` method is allowed to have by-name parameters under Scala 3 (or with -Xsource-features:case-copy-by-name) +Scala 3 migration messages are issued as errors under -Xsource:3. Use -Wconf or @nowarn to demote them to warnings or suppress. +Applicable -Wconf / @nowarn filters for this fatal warning: msg=, cat=scala3-migration, site=C +case class C(i: Int)(j: => Int)(k: => Int) { def sum = i + j + k } + ^ +1 error diff --git a/test/files/neg/t7879.scala b/test/files/neg/t7879.scala new file mode 100644 index 000000000000..e873120e5ef2 --- /dev/null +++ b/test/files/neg/t7879.scala @@ -0,0 +1,2 @@ +//> using options -Xsource:3 +case class C(i: Int)(j: => Int)(k: => Int) { def sum = i + j + k } diff --git a/test/files/neg/t7879b.check b/test/files/neg/t7879b.check new file mode 100644 index 000000000000..89579285661d --- /dev/null +++ b/test/files/neg/t7879b.check @@ -0,0 +1,4 @@ +t7879b.scala:5: error: value copy is not a member of C + def f(c: C): C = c.copy(42)(Seq(9, 9, 9): _*) + ^ +1 error diff --git a/test/files/neg/t7879b.scala b/test/files/neg/t7879b.scala new file mode 100644 index 000000000000..d23e5dea0927 --- /dev/null +++ b/test/files/neg/t7879b.scala @@ -0,0 +1,6 @@ +//> using options -Xsource:3 -Xsource-features:case-copy-by-name +case class C(i: Int)(js: Int*) { def sum = i + js.sum } + +class Usage { + def f(c: C): C = c.copy(42)(Seq(9, 9, 9): _*) +} diff --git a/test/files/neg/t7879c.check b/test/files/neg/t7879c.check new file mode 100644 index 000000000000..29754cfca1b6 --- /dev/null +++ b/test/files/neg/t7879c.check @@ -0,0 +1,4 @@ +t7879c.scala:6: error: value copy is not a member of C + C(42)(27)(1).copy() + ^ +1 error diff --git a/test/files/neg/t7879c.scala b/test/files/neg/t7879c.scala new file mode 100644 index 000000000000..b16a74067350 --- /dev/null +++ b/test/files/neg/t7879c.scala @@ -0,0 +1,8 @@ +//> using options -Werror -Xlint +case class C(i: Int)(j: => Int)(k: => Int) { def sum = i + j + k } + +object Test extends App { + println { + C(42)(27)(1).copy() + } +} diff --git a/test/files/run/t7879.check b/test/files/run/t7879.check new file mode 100644 index 000000000000..d449c179a277 --- /dev/null +++ b/test/files/run/t7879.check @@ -0,0 +1,25 @@ + +scala> case class C(i: Int)(j: => Int) { def sum = i + j } +class C + +scala> def z = 27.tap(println) +def z: Int + +scala> val c = C(42)(z) +val c: C = C(42) + +scala> c.sum +27 +val res0: Int = 69 + +scala> def y = 28.tap(println) +def y: Int + +scala> c.copy()(y) +val res1: C = C(42) + +scala> c.copy()(y).sum +28 +val res2: Int = 70 + +scala> :quit diff --git a/test/files/run/t7879.scala b/test/files/run/t7879.scala new file mode 100644 index 000000000000..ae026b427ed7 --- /dev/null +++ b/test/files/run/t7879.scala @@ -0,0 +1,6 @@ + +import scala.tools.partest.SessionTest + +object Test extends SessionTest { + override def extraSettings = "-Yimports:java.lang,scala,scala.Predef,scala.util.chaining -Xsource:3 -Xsource-features:case-copy-by-name" +}