Skip to content

Named arg may be deprecatedName #21588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
143 changes: 98 additions & 45 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand All @@ -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 =
Expand Down
12 changes: 6 additions & 6 deletions tests/neg/i18122.check
Original file line number Diff line number Diff line change
Expand Up @@ -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
| ^
Expand Down
17 changes: 17 additions & 0 deletions tests/neg/i19077.check
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions tests/neg/i19077.scala
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions tests/warn/i19077.check
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions tests/warn/i19077.scala
Original file line number Diff line number Diff line change
@@ -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
Loading