diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b88a3e6e986a..df2d73084b89 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1033,6 +1033,7 @@ class Definitions { @tu lazy val DeprecatedAnnot: ClassSymbol = requiredClass("scala.deprecated") @tu lazy val DeprecatedOverridingAnnot: ClassSymbol = requiredClass("scala.deprecatedOverriding") @tu lazy val DeprecatedInheritanceAnnot: ClassSymbol = requiredClass("scala.deprecatedInheritance") + @tu lazy val DeprecatedNameAnnot: ClassSymbol = requiredClass("scala.deprecatedName") @tu lazy val ImplicitAmbiguousAnnot: ClassSymbol = requiredClass("scala.annotation.implicitAmbiguous") @tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound") @tu lazy val InlineParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InlineParam") diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 71fc250d0710..27d7853e0b64 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -616,6 +616,8 @@ trait Applications extends Compatibility { def infoStr = if methType.isErroneous then "" else i": $methType" i"${err.refStr(methRef)}$infoStr" + private type TreeList[T <: Untyped] = List[Trees.Tree[T]] + /** Re-order arguments to correctly align named arguments * Issue errors in the following situations: * @@ -627,67 +629,118 @@ trait Applications extends Compatibility { * (either named or positional), or * - The formal parameter at the argument position is also mentioned * in a subsequent named parameter. - * - "parameter already instantiated" if a two named arguments have the same name. + * - "parameter already instantiated" if two named arguments have the same name or deprecated alias. * - "does not have parameter" if a named parameter does not mention a formal * parameter name. */ - def reorder[T <: Untyped](args: List[Trees.Tree[T]]): List[Trees.Tree[T]] = { + def reorder[T <: Untyped](args: TreeList[T]): TreeList[T] = + + extension [A](list: List[A]) inline def dropOne = if list.isEmpty then list else list.tail // aka list.drop(1) + + extension (dna: Annotation) + def deprecatedName: Name = + dna.argumentConstantString(0).map(_.toTermName).getOrElse(nme.NO_NAME) + def since: String = + val version = dna.argumentConstantString(1).filter(!_.isEmpty) + version.map(v => s" (since $v)").getOrElse("") - /** @param pnames The list of parameter names that are missing arguments + val deprecatedNames: Map[Name, Annotation] = + val sym = methRef.symbol + val paramss = + if sym.hasAnnotation(defn.MappedAlternativeAnnot) then sym.rawParamss + else sym.paramSymss + paramss.find(_.exists(_.isTerm)) match + case Some(ps) if ps.exists(_.hasAnnotation(defn.DeprecatedNameAnnot)) => + ps.flatMap: p => + p.getAnnotation(defn.DeprecatedNameAnnot).map(p.name -> _) + .toMap + case _ => Map.empty + + extension (name: Name) + def isMatchedBy(usage: Name): Boolean = + name == usage + || deprecatedNames.get(name).exists(_.deprecatedName == usage) + def checkDeprecationOf(usage: Name, pos: SrcPos): Unit = if !ctx.isAfterTyper then + for dna <- deprecatedNames.get(name) do + dna.deprecatedName match + case `name` | nme.NO_NAME if name == usage => + report.deprecationWarning(em"naming parameter $usage is deprecated${dna.since}", pos) + case `usage` => + report.deprecationWarning(em"the parameter name $usage is deprecated${dna.since}: use $name instead", pos) + case _ => + def alternative: Name = + deprecatedNames.get(name).map(_.deprecatedName).getOrElse(nme.NO_NAME) + + /** Reorder the suffix of named args per a list of required names. + * + * @param pnames The list of parameter names that are missing arguments * @param args The list of arguments that are not yet passed, or that are waiting to be dropped * @param nameToArg A map from as yet unseen names to named arguments - * @param toDrop A set of names that have already be passed as named arguments + * @param toDrop A set of names that have already been passed as named arguments + * @param missingArgs true if args were already missing, so error on positional * * For a well-typed application we have the invariants * * 1. `(args diff toDrop)` can be reordered to match `pnames` * 2. For every `(name -> arg)` in `nameToArg`, `arg` is an element of `args` + * + * Recurse over the parameter names looking for named args to use. + * If there are no more parameters or no args fit, process the next arg: + * a named arg may be previously used, or not yet used, or badly named. */ - def handleNamed(pnames: List[Name], args: List[Trees.Tree[T]], + def handleNamed(pnames: List[Name], args: TreeList[T], nameToArg: Map[Name, Trees.NamedArg[T]], toDrop: Set[Name], - missingArgs: Boolean): List[Trees.Tree[T]] = pnames match { - case pname :: pnames1 if nameToArg contains pname => - // there is a named argument for this parameter; pick it - nameToArg(pname) :: handleNamed(pnames1, args, nameToArg - pname, toDrop + pname, missingArgs) + missingArgs: Boolean): TreeList[T] = + pnames match + case pname :: pnames if nameToArg.contains(pname) => + val arg = nameToArg(pname) // use the named argument for this parameter + pname.checkDeprecationOf(pname, arg.srcPos) + arg :: handleNamed(pnames, args, nameToArg - pname, toDrop + pname, missingArgs) + case pname :: pnames if nameToArg.contains(pname.alternative) => + val alt = pname.alternative + val arg = nameToArg(alt) // use the named argument for this parameter + pname.checkDeprecationOf(alt, arg.srcPos) + arg :: handleNamed(pnames, args, nameToArg - alt, toDrop + alt, missingArgs) case _ => - def pnamesRest = if (pnames.isEmpty) pnames else pnames.tail - args match { - case (arg @ NamedArg(aname, _)) :: args1 => - if (toDrop contains aname) // argument is already passed - handleNamed(pnames, args1, nameToArg, toDrop - aname, missingArgs) - else if ((nameToArg contains aname) && pnames.nonEmpty) // argument is missing, pass an empty tree - genericEmptyTree :: handleNamed(pnames.tail, args, nameToArg, toDrop, missingArgs = true) - else { // name not (or no longer) available for named arg - def msg = - if (methodType.paramNames contains aname) - em"parameter $aname of $methString is already instantiated" - else - em"$methString does not have a parameter $aname" - fail(msg, arg.asInstanceOf[Arg]) - arg :: handleNamed(pnamesRest, args1, nameToArg, toDrop, missingArgs) - } - case arg :: args1 => - if toDrop.nonEmpty || missingArgs then - report.error(i"positional after named argument", arg.srcPos) - arg :: handleNamed(pnamesRest, args1, nameToArg, toDrop, missingArgs) // unnamed argument; pick it - case Nil => // no more args, continue to pick up any preceding named args - if (pnames.isEmpty) Nil - else handleNamed(pnamesRest, args, nameToArg, toDrop, missingArgs) - } - } - - def handlePositional(pnames: List[Name], args: List[Trees.Tree[T]]): List[Trees.Tree[T]] = - args match { - case (arg: NamedArg @unchecked) :: _ => - val nameAssocs = for (case arg @ NamedArg(name, _) <- args) yield (name, arg) - handleNamed(pnames, args, nameAssocs.toMap, toDrop = Set(), missingArgs = false) - case arg :: args1 => - arg :: handlePositional(if (pnames.isEmpty) Nil else pnames.tail, args1) - case Nil => Nil - } + args match + case allArgs @ (arg @ NamedArg(aname, _)) :: args => + if toDrop.contains(aname) then + // named argument was already picked (using aname), skip it + handleNamed(pnames, args, nameToArg, toDrop - aname, missingArgs) + else if pnames.nonEmpty && nameToArg.contains(aname) then + // argument for pname is missing, pass an empty tree; arg may be used later, so keep it + genericEmptyTree :: handleNamed(pnames.tail, allArgs, nameToArg, toDrop, missingArgs = true) + else // name not (or no longer) available for named arg + def msg = + if methodType.paramNames.exists(nm => nm == aname || nm.alternative == aname) then + em"parameter $aname of $methString is already instantiated" + else + em"$methString does not have a parameter $aname" + fail(msg, arg.asInstanceOf[Arg]) + arg :: handleNamed(pnames.dropOne, args, nameToArg, toDrop, missingArgs) + case arg :: args => + if toDrop.nonEmpty || missingArgs then + report.error(i"positional after named argument", arg.srcPos) + arg :: handleNamed(pnames.dropOne, args, nameToArg, toDrop, missingArgs) // unnamed argument; pick it + case nil => // no more args, continue to pick up any preceding named args + if pnames.isEmpty then nil + else handleNamed(pnames.dropOne, args = nil, nameToArg, toDrop, missingArgs) + + // Skip prefix of positional args, then handleNamed + def handlePositional(pnames: List[Name], args: TreeList[T]): TreeList[T] = + args match + case (arg @ NamedArg(name, _)) :: args if !pnames.isEmpty && pnames.head.isMatchedBy(name) => + pnames.head.checkDeprecationOf(name, arg.srcPos) + arg :: handlePositional(pnames.tail, args) + case (_: NamedArg) :: _ => + val nameAssocs = args.collect { case arg @ NamedArg(name, _) => name -> arg } + handleNamed(pnames, args, nameAssocs.toMap, toDrop = Set.empty, missingArgs = false) + case arg :: args => + arg :: handlePositional(pnames.dropOne, args) + case nil => nil handlePositional(methodType.paramNames, args) - } + end reorder /** Is `sym` a constructor of a Java-defined annotation? */ def isJavaAnnotConstr(sym: Symbol): Boolean = diff --git a/tests/neg/i18122.check b/tests/neg/i18122.check index 0d08dc33c52a..22d9f2427df0 100644 --- a/tests/neg/i18122.check +++ b/tests/neg/i18122.check @@ -30,18 +30,18 @@ 23 | bar1(ys = 1) // error: missing arg | ^^^^^^^^^^^^ | missing argument for parameter x of method bar1 in object Test: (x: Int, ys: Int*): Unit --- Error: tests/neg/i18122.scala:43:16 --------------------------------------------------------------------------------- +-- Error: tests/neg/i18122.scala:43:22 --------------------------------------------------------------------------------- 43 | bar1(x = 1, 2, ys = 3) // error: positional after named - | ^ - | positional after named argument + | ^^^^^^ + | parameter ys of method bar1 in object Test: (x: Int, ys: Int*): Unit is already instantiated -- Error: tests/neg/i18122.scala:44:18 --------------------------------------------------------------------------------- 44 | bar1(1, 2, ys = 3) // error: parameter ys is already instantiated | ^^^^^^ | parameter ys of method bar1 in object Test: (x: Int, ys: Int*): Unit is already instantiated --- Error: tests/neg/i18122.scala:45:16 --------------------------------------------------------------------------------- +-- Error: tests/neg/i18122.scala:45:22 --------------------------------------------------------------------------------- 45 | bar2(x = 1, 2, ys = 3) // error: positional after named - | ^ - | positional after named argument + | ^^^^^^ + | parameter ys of method bar2 in object Test: (x: Int, ys: Int*): Unit is already instantiated -- Error: tests/neg/i18122.scala:46:17 --------------------------------------------------------------------------------- 46 | bar1(ys = 1, 2, x = 3) // error: positional after named | ^ diff --git a/tests/neg/i19077.check b/tests/neg/i19077.check new file mode 100644 index 000000000000..4a95821a1c82 --- /dev/null +++ b/tests/neg/i19077.check @@ -0,0 +1,17 @@ +-- Error: tests/neg/i19077.scala:5:15 ---------------------------------------------------------------------------------- +5 | f(1, 2, 3, x = 42) // error + | ^^^^^^ + | parameter x of method f: (x: Int, y: Int, z: Int): Int is already instantiated +-- Error: tests/neg/i19077.scala:6:15 ---------------------------------------------------------------------------------- +6 | f(1, 2, 3, w = 42) // error + | ^^^^^^ + | parameter w of method f: (x: Int, y: Int, z: Int): Int is already instantiated +-- Error: tests/neg/i19077.scala:7:20 ---------------------------------------------------------------------------------- +7 | f(1, 2, w = 42, z = 27) // error + | ^^^^^^ + | parameter z of method f: (x: Int, y: Int, z: Int): Int is already instantiated +-- Error: tests/neg/i19077.scala:8:20 ---------------------------------------------------------------------------------- +8 | f(1, 2, z = 42, w = 27) // error + | ^^^^^^ + | parameter w of method f: (x: Int, y: Int, z: Int): Int is already instantiated +there was 1 deprecation warning; re-run with -deprecation for details diff --git a/tests/neg/i19077.scala b/tests/neg/i19077.scala new file mode 100644 index 000000000000..cbfb33e6dfe7 --- /dev/null +++ b/tests/neg/i19077.scala @@ -0,0 +1,13 @@ + +def f(@deprecatedName("x") x: Int, @deprecatedName y: Int, @deprecatedName("w") z: Int) = x+y+z + +@main def Test = + f(1, 2, 3, x = 42) // error + f(1, 2, 3, w = 42) // error + f(1, 2, w = 42, z = 27) // error + f(1, 2, z = 42, w = 27) // error + +object X { def m[T](i: Int)(s: String) = s*i; def m[T](i: Int)(d: Double) = d*i } + +object Y: + def f = X.m(42)(s = "") // overloading resolution objects to methRef.symbol.paramSymss diff --git a/tests/warn/i19077.check b/tests/warn/i19077.check new file mode 100644 index 000000000000..646e529c38f3 --- /dev/null +++ b/tests/warn/i19077.check @@ -0,0 +1,44 @@ +-- Deprecation Warning: tests/warn/i19077.scala:26:14 ------------------------------------------------------------------ +26 | def g = f(y = 42) // warn but omit empty since + | ^^^^^^ + | the parameter name y is deprecated: use x instead +-- Deprecation Warning: tests/warn/i19077.scala:12:6 ------------------------------------------------------------------- +12 | f(x = 1, 2, 3) // warn + | ^^^^^ + | naming parameter x is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:13:9 ------------------------------------------------------------------- +13 | f(1, y = 2, 3) // warn + | ^^^^^ + | naming parameter y is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:14:12 ------------------------------------------------------------------ +14 | f(1, 2, w = 3) // warn + | ^^^^^ + | the parameter name w is deprecated: use z instead +-- Deprecation Warning: tests/warn/i19077.scala:15:6 ------------------------------------------------------------------- +15 | f(x = 1, w = 3, y = 2) // warn // warn // warn + | ^^^^^ + | naming parameter x is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:15:20 ------------------------------------------------------------------ +15 | f(x = 1, w = 3, y = 2) // warn // warn // warn + | ^^^^^ + | naming parameter y is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:15:13 ------------------------------------------------------------------ +15 | f(x = 1, w = 3, y = 2) // warn // warn // warn + | ^^^^^ + | the parameter name w is deprecated: use z instead +-- Deprecation Warning: tests/warn/i19077.scala:16:13 ------------------------------------------------------------------ +16 | f(w = 3, x = 1, y = 2) // warn // warn // warn + | ^^^^^ + | naming parameter x is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:16:20 ------------------------------------------------------------------ +16 | f(w = 3, x = 1, y = 2) // warn // warn // warn + | ^^^^^ + | naming parameter y is deprecated +-- Deprecation Warning: tests/warn/i19077.scala:16:6 ------------------------------------------------------------------- +16 | f(w = 3, x = 1, y = 2) // warn // warn // warn + | ^^^^^ + | the parameter name w is deprecated: use z instead +-- Deprecation Warning: tests/warn/i19077.scala:20:8 ------------------------------------------------------------------- +20 | X.f(v = 42) // warn + | ^^^^^^ + | the parameter name v is deprecated (since 3.3): use x instead diff --git a/tests/warn/i19077.scala b/tests/warn/i19077.scala new file mode 100644 index 000000000000..0c60cfb0d9ff --- /dev/null +++ b/tests/warn/i19077.scala @@ -0,0 +1,26 @@ +//> using options -deprecation + +def f(@deprecatedName("x") x: Int, @deprecatedName y: Int, @deprecatedName("w") z: Int) = x+y+z + +object X: + def f(@deprecatedName("v", since="3.3") x: Int) = x + def f(@deprecatedName("v") x: Int, y: Int) = x+y + +@main def Test = + f(1, 2, 3) // nowarn + f(1, 2, z = 3) // nowarn + f(x = 1, 2, 3) // warn + f(1, y = 2, 3) // warn + f(1, 2, w = 3) // warn + f(x = 1, w = 3, y = 2) // warn // warn // warn + f(w = 3, x = 1, y = 2) // warn // warn // warn + + X.f(42) + X.f(x = 42) + X.f(v = 42) // warn + X.f(x = 42, y = 27) + X.f(y = 42, x = 27) + +object empty: + def f(@deprecatedName("y", since="") x: Int) = x + def g = f(y = 42) // warn but omit empty since