From 6a353782aa135bcaf98fc08cb86f25d5fb0ed35f Mon Sep 17 00:00:00 2001 From: Hamza REMMAL Date: Thu, 1 Feb 2024 12:12:00 +0100 Subject: [PATCH] Add support for @deprecatedName --- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/transform/FirstTransform.scala | 2 +- .../dotty/tools/dotc/typer/Applications.scala | 125 ++++++++++++------ tests/warn/i19077.check | 12 ++ tests/warn/i19077.scala | 11 ++ 5 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 tests/warn/i19077.check create mode 100644 tests/warn/i19077.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4bc427ee0687..96c2be595f4a 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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") diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index b5bc43ee762c..f84f2b0d0eae 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -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)) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 452c6d197310..235b1fd0faad 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -529,50 +529,91 @@ 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 + + println(s"$names - $args - $nameToArg - $toDrop - $missingArgs") + + 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" + 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 + + println(i"=============${methRef.symbol.id}==============") + val t = handlePositional(names, args) + println("===============================") + t } /** Is `sym` a constructor of a Java-defined annotation? */ diff --git a/tests/warn/i19077.check b/tests/warn/i19077.check new file mode 100644 index 000000000000..c4aee98f8532 --- /dev/null +++ b/tests/warn/i19077.check @@ -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 diff --git a/tests/warn/i19077.scala b/tests/warn/i19077.scala new file mode 100644 index 000000000000..33aaddfe01c0 --- /dev/null +++ b/tests/warn/i19077.scala @@ -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 \ No newline at end of file