Skip to content

Add support for @deprecatedName #19086

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 1 commit 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 @@ -1011,6 +1011,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
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase =>

override def transformTypeApply(tree: TypeApply)(using Context): Tree =
constToLiteral(tree)

override def transformApply(tree: Apply)(using Context): Tree =
constToLiteral(foldCondition(tree))

Expand Down
120 changes: 78 additions & 42 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -529,50 +529,86 @@ trait Applications extends Compatibility {
* 1. `(args diff toDrop)` can be reordered to match `pnames`
* 2. For every `(name -> arg)` in `nameToArg`, `arg` is an element of `args`
*/
def handleNamed(pnames: List[Name], args: List[Trees.Tree[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)
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)
def handleNamed(names: List[(Name, Option[(Name, Annotation)])], args: List[Trees.Tree[T]],
nameToArg: Map[Name, Trees.NamedArg[T]], toDrop: Set[Name], missingArgs: Boolean): List[Trees.Tree[T]] =

/** Returns the corresponding name parameter with the name that helped find it */
def findNameArgument(pname: (Name, Option[(Name, Annotation)])): Option[(Name, Trees.NamedArg[T])] =
pname._2 match
// This parameter has a deprecated name, looking for this first
case Some(name, annot) => nameToArg.get(name) match
// Argument with a deprecated name was found: emit a warning and change the parameter name
case Some(arg) =>
val sinceMsg = annot.argumentConstantString(1).map(version => s" (since: $version)").getOrElse("")
report.deprecationWarning(em"naming parameter $name is deprecated$sinceMsg", arg.srcPos)
Some((name, arg))
// fallback to the primary name
case None => nameToArg.get(pname._1).map((pname._1, _))
// no-deprecated name, we can look for the argument with the main name
case None => nameToArg.get(pname._1).map((pname._1, _))
end findNameArgument

def namesRest = if names.isEmpty then Nil else names.tail

names match
case pname :: pnames if findNameArgument(pname).nonEmpty => findNameArgument(pname) match
case Some((name, arg)) => // named argument was found, move on to the next parameter
val namesToDrop = pname._2.map(_._1).toSet + pname._1 // drop both the deprecated and the primary name
arg :: handleNamed(pnames, args, nameToArg - name, toDrop ++ namesToDrop, missingArgs)
case _ => throw new IllegalStateException
case _ => args match
case (arg @ NamedArg(name, _)) :: args1 =>
if toDrop.contains(name) then // argument is already passed
handleNamed(names, args1, nameToArg, toDrop - name, missingArgs)
else if (nameToArg.contains(name) && names.nonEmpty) // argument is missing, pass an empty tree
genericEmptyTree :: handleNamed(names.tail, args, nameToArg, toDrop, missingArgs = true)
else // name not (or no longer) available for named arg
def msg =
if methodType.paramNames.contains(name) then
em"parameter $name of $methString is already instantiated"
else
em"$methString does not have a parameter $name"
Copy link
Contributor

@odersky odersky Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had another idea (not what we discussed this afternoon). We could keep the logic as before but at this point, instead of failing, we do an addiitonal check. Say our argument is x = e. We check whether the method symbol has a deprecatedName("x") annotation on a parameter with new name y. If that's the case we change the NamedArg name to y and try again. If that succeeds with issue the deprecated name warning.

fail(msg, arg.asInstanceOf[Arg])
arg :: handleNamed(namesRest, args1, nameToArg, toDrop, missingArgs)
case arg :: args1 =>
if toDrop.nonEmpty || missingArgs then
report.error(em"positional after named argument", arg.srcPos)
arg :: handleNamed(namesRest, args1, nameToArg, toDrop, missingArgs) // unnamed argument; pick it
case Nil => // no more args, continue to pick up any preceding named args
if names.isEmpty then Nil
else handleNamed(names.tail, args, nameToArg, toDrop, missingArgs)
end handleNamed

def handlePositional(names: List[(Name, Option[(Name, Annotation)])], args: List[Trees.Tree[T]]): List[Trees.Tree[T]] =
args match
// Named argument, switch to handleNamed processing
case (_: NamedArg @unchecked) :: _ =>
val nameAssocs = for case arg @ NamedArg(name, _) <- args yield (name, arg)
handleNamed(names, args, nameAssocs.toMap, toDrop = Set(), missingArgs = false)
// Positional argument, skip it and move to the next one
case arg :: args1 =>
arg :: handlePositional(if (pnames.isEmpty) Nil else pnames.tail, args1)
arg :: handlePositional(if names.isEmpty then Nil else names.tail, args1)
case Nil => Nil
}

handlePositional(methodType.paramNames, args)
end handlePositional

// format: (primary_name, (deprecated_name, annotation): Option)
// NB: deprecated_name might be different from (or the same as) primary_name
// names also has the same order as the parameter list
val names =
for p <- methRef.symbol.paramSymss.flatten
name = p.name
if methodType.paramNames.contains(name)
yield
p.getAnnotation(defn.DeprecatedNameAnnot) match
case Some(annot) =>
// Get the name of the deprecated
val deprecated = annot.argumentConstantString(0).map(_.toTermName).getOrElse(name)
(name, Some(deprecated, annot))
case None =>
(name, None)
end names

handlePositional(names, args)
}

/** Is `sym` a constructor of a Java-defined annotation? */
Expand Down
12 changes: 12 additions & 0 deletions tests/warn/i19077.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Deprecation Warning: tests/warn/i19077.scala:8:6 ----------------------------
8 | f(x = 2) // warn
| ^^^^^
| naming parameter x is deprecated
-- Deprecation Warning: tests/warn/i19077.scala:9:6 ----------------------------
9 | g(x = 2) // warn
| ^^^^^
| naming parameter x is deprecated
-- Deprecation Warning: tests/warn/i19077.scala:10:6 ---------------------------
10 | h(x = 2) // warn
| ^^^^^
| naming parameter x is deprecated
11 changes: 11 additions & 0 deletions tests/warn/i19077.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//> using options -deprecation

def f(@deprecatedName x: Int) = x * 2
def g(@deprecatedName("x") x: Int) = x * 2
def h(@deprecatedName("x") y: Int) = y * 2

@main def Test =
f(x = 2) // warn
g(x = 2) // warn
h(x = 2) // warn
h(y = 2) // no-warn