From 5bd8ea0edffe7b725e5fa665a82a5795d5dafe8f Mon Sep 17 00:00:00 2001 From: Brian McKenna Date: Sun, 7 Sep 2014 12:33:52 -0600 Subject: SI-6806 Add an @implicitAmbiguous annotation Example usage: trait =!=[C, D] implicit def neq[E, F] : E =!= F = null @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}") implicit def neqAmbig1[G, H, J] : J =!= J = null implicit def neqAmbig2[I] : I =!= I = null implicitly[Int =!= Int] Which gives the following error: implicit-ambiguous.scala:9: error: Could not prove Int =!= Int implicitly[Int =!= Int] ^ Better than what was previously given: implicit-ambiguous.scala:9: error: ambiguous implicit values: both method neqAmbig1 in object Test of type [G, H, J]=> Main.$anon.Test.=!=[J,J] and method neqAmbig2 in object Test of type [I]=> Main.$anon.Test.=!=[I,I] match expected type Main.$anon.Test.=!=[Int,Int] implicitly[Int =!= Int] ^ --- .../tools/nsc/typechecker/ContextErrors.scala | 19 ++++-- .../scala/tools/nsc/typechecker/Implicits.scala | 74 ++++++++++++---------- .../scala/tools/nsc/typechecker/RefChecks.scala | 11 ++-- .../scala/annotation/implicitAmbiguous.scala | 34 ++++++++++ .../scala/reflect/internal/Definitions.scala | 1 + src/reflect/scala/reflect/internal/Symbols.scala | 9 +-- .../scala/reflect/runtime/JavaUniverseForce.scala | 1 + 7 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 src/library/scala/annotation/implicitAmbiguous.scala (limited to 'src') diff --git a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala index b0bd9977a8..94e56a8e52 100644 --- a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala +++ b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala @@ -1212,7 +1212,8 @@ trait ContextErrors { import definitions._ - def AmbiguousImplicitError(info1: ImplicitInfo, info2: ImplicitInfo, + def AmbiguousImplicitError(info1: ImplicitInfo, tree1: Tree, + info2: ImplicitInfo, tree2: Tree, pre1: String, pre2: String, trailer: String) (isView: Boolean, pt: Type, tree: Tree)(implicit context0: Context) = { if (!info1.tpe.isErroneous && !info2.tpe.isErroneous) { @@ -1248,10 +1249,20 @@ trait ContextErrors { if (explanation == "") "" else "\n" + explanation ) } + + def treeTypeArgs(annotatedTree: Tree) = annotatedTree match { + case TypeApply(_, args) => args.map(_.toString) + case _ => Nil + } + context.issueAmbiguousError(AmbiguousImplicitTypeError(tree, - if (isView) viewMsg - else s"ambiguous implicit values:\n${coreMsg}match expected type $pt") - ) + (tree1.symbol, tree2.symbol) match { + case (ImplicitAmbiguousMsg(msg), _) => msg.format(treeTypeArgs(tree1)) + case (_, ImplicitAmbiguousMsg(msg)) => msg.format(treeTypeArgs(tree2)) + case (_, _) if isView => viewMsg + case (_, _) => s"ambiguous implicit values:\n${coreMsg}match expected type $pt" + } + )) } } diff --git a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala index 196b137a3e..0eb697f749 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Implicits.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Implicits.scala @@ -885,7 +885,7 @@ trait Implicits { * - find the most likely one * - if it matches, forget about all others it improves upon */ - @tailrec private def rankImplicits(pending: Infos, acc: Infos): Infos = pending match { + @tailrec private def rankImplicits(pending: Infos, acc: List[(SearchResult, ImplicitInfo)]): List[(SearchResult, ImplicitInfo)] = pending match { case Nil => acc case firstPending :: otherPending => def firstPendingImproves(alt: ImplicitInfo) = @@ -912,7 +912,7 @@ trait Implicits { val pendingImprovingBest = undoLog undo { otherPending filterNot firstPendingImproves } - rankImplicits(pendingImprovingBest, firstPending :: acc) + rankImplicits(pendingImprovingBest, (newBest, firstPending) :: acc) } } @@ -928,14 +928,14 @@ trait Implicits { // So if there is any element not improved upon by the first it is an error. rankImplicits(eligible, Nil) match { case Nil => () - case chosen :: rest => - rest find (alt => !improves(chosen, alt)) match { - case Some(competing) => - AmbiguousImplicitError(chosen, competing, "both", "and", "")(isView, pt, tree)(context) + case (chosenResult, chosenInfo) :: rest => + rest find { case (_, alt) => !improves(chosenInfo, alt) } match { + case Some((competingResult, competingInfo)) => + AmbiguousImplicitError(chosenInfo, chosenResult.tree, competingInfo, competingResult.tree, "both", "and", "")(isView, pt, tree)(context) return AmbiguousSearchFailure // Stop the search once ambiguity is encountered, see t4457_2.scala case _ => - if (isView) chosen.useCountView += 1 - else chosen.useCountArg += 1 + if (isView) chosenInfo.useCountView += 1 + else chosenInfo.useCountArg += 1 } } @@ -1445,9 +1445,9 @@ trait Implicits { } } - object ImplicitNotFoundMsg { - def unapply(sym: Symbol): Option[(Message)] = sym.implicitNotFoundMsg match { - case Some(m) => Some(new Message(sym, m)) + class ImplicitAnnotationMsg(f: Symbol => Option[String], clazz: Symbol, annotationName: String) { + def unapply(sym: Symbol): Option[(Message)] = f(sym) match { + case Some(m) => Some(new Message(sym, m, annotationName)) case None if sym.isAliasType => // perform exactly one step of dealiasing // this is necessary because ClassManifests are now aliased to ClassTags @@ -1459,41 +1459,45 @@ trait Implicits { // check the message's syntax: should be a string literal that may contain occurrences of the string "${X}", // where `X` refers to a type parameter of `sym` def check(sym: Symbol): Option[String] = - sym.getAnnotation(ImplicitNotFoundClass).flatMap(_.stringArg(0) match { - case Some(m) => new Message(sym, m).validate - case None => Some("Missing argument `msg` on implicitNotFound annotation.") + sym.getAnnotation(clazz).flatMap(_.stringArg(0) match { + case Some(m) => new Message(sym, m, annotationName).validate + case None => Some(s"Missing argument `msg` on $annotationName annotation.") }) + } + + object ImplicitNotFoundMsg extends ImplicitAnnotationMsg(_.implicitNotFoundMsg, ImplicitNotFoundClass, "implicitNotFound") + + object ImplicitAmbiguousMsg extends ImplicitAnnotationMsg(_.implicitAmbiguousMsg, ImplicitAmbiguousClass, "implicitAmbiguous") + class Message(sym: Symbol, msg: String, annotationName: String) { // http://dcsobral.blogspot.com/2010/01/string-interpolation-in-scala-with.html private val Intersobralator = """\$\{\s*([^}\s]+)\s*\}""".r - class Message(sym: Symbol, msg: String) { - private def interpolate(text: String, vars: Map[String, String]) = - Intersobralator.replaceAllIn(text, (_: Regex.Match) match { - case Regex.Groups(v) => Regex quoteReplacement vars.getOrElse(v, "") + private def interpolate(text: String, vars: Map[String, String]) = + Intersobralator.replaceAllIn(text, (_: Regex.Match) match { + case Regex.Groups(v) => Regex quoteReplacement vars.getOrElse(v, "") // #3915: need to quote replacement string since it may include $'s (such as the interpreter's $iw) - }) + }) - private lazy val typeParamNames: List[String] = sym.typeParams.map(_.decodedName) - private def typeArgsAtSym(paramTp: Type) = paramTp.baseType(sym).typeArgs + private lazy val typeParamNames: List[String] = sym.typeParams.map(_.decodedName) + private def typeArgsAtSym(paramTp: Type) = paramTp.baseType(sym).typeArgs - def format(paramName: Name, paramTp: Type): String = format(typeArgsAtSym(paramTp) map (_.toString)) + def format(paramName: Name, paramTp: Type): String = format(typeArgsAtSym(paramTp) map (_.toString)) - def format(typeArgs: List[String]): String = - interpolate(msg, Map((typeParamNames zip typeArgs): _*)) // TODO: give access to the name and type of the implicit argument, etc? + def format(typeArgs: List[String]): String = + interpolate(msg, Map((typeParamNames zip typeArgs): _*)) // TODO: give access to the name and type of the implicit argument, etc? - def validate: Option[String] = { - val refs = Intersobralator.findAllMatchIn(msg).map(_ group 1).toSet - val decls = typeParamNames.toSet + def validate: Option[String] = { + val refs = Intersobralator.findAllMatchIn(msg).map(_ group 1).toSet + val decls = typeParamNames.toSet - (refs &~ decls) match { - case s if s.isEmpty => None - case unboundNames => - val singular = unboundNames.size == 1 - val ess = if (singular) "" else "s" - val bee = if (singular) "is" else "are" - Some(s"The type parameter$ess ${unboundNames mkString ", "} referenced in the message of the @implicitNotFound annotation $bee not defined by $sym.") - } + (refs &~ decls) match { + case s if s.isEmpty => None + case unboundNames => + val singular = unboundNames.size == 1 + val ess = if (singular) "" else "s" + val bee = if (singular) "is" else "are" + Some(s"The type parameter$ess ${unboundNames mkString ", "} referenced in the message of the @$annotationName annotation $bee not defined by $sym.") } } } diff --git a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala index 0198529ef7..ae5c07a76d 100644 --- a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala +++ b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala @@ -1468,10 +1468,13 @@ abstract class RefChecks extends InfoTransform with scala.reflect.internal.trans case m: MemberDef => val sym = m.symbol applyChecks(sym.annotations) - // validate implicitNotFoundMessage - analyzer.ImplicitNotFoundMsg.check(sym) foreach { warn => - reporter.warning(tree.pos, f"Invalid implicitNotFound message for ${sym}%s${sym.locationString}%s:%n$warn") - } + + def messageWarning(name: String)(warn: String) = + reporter.warning(tree.pos, f"Invalid $name message for ${sym}%s${sym.locationString}%s:%n$warn") + + // validate implicitNotFoundMessage and implicitAmbiguousMessage + analyzer.ImplicitNotFoundMsg.check(sym) foreach messageWarning("implicitNotFound") + analyzer.ImplicitAmbiguousMsg.check(sym) foreach messageWarning("implicitAmbiguous") case tpt@TypeTree() => if(tpt.original != null) { diff --git a/src/library/scala/annotation/implicitAmbiguous.scala b/src/library/scala/annotation/implicitAmbiguous.scala new file mode 100644 index 0000000000..46eab9ae8f --- /dev/null +++ b/src/library/scala/annotation/implicitAmbiguous.scala @@ -0,0 +1,34 @@ +package scala.annotation + +import scala.annotation.meta._ + +/** + * To customize the error message that's emitted when an implicit of type + * C[T1,..., TN] is found more than once, annotate the class C + * with @implicitAmbiguous. Assuming C has type parameters X1,..., XN, the + * error message will be the result of replacing all occurrences of ${Xi} in + * the string msg with the string representation of the corresponding type + * argument Ti. * + * + * If more than one @implicitAmbiguous annotation is collected, the compiler is + * free to pick any of them to display. + * + * Nice errors can direct users to fix imports or even tell them why code + * intentionally doesn't compile. + * + * {{{ + * trait =!=[C, D] + * + * implicit def neq[E, F] : E =!= F = null + * + * @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}") + * implicit def neqAmbig1[G, H, J] : J =!= J = null + * implicit def neqAmbig2[I] : I =!= I = null + * + * implicitly[Int =!= Int] + * }}} + * + * @author Brian McKenna + * @since 2.12.0 + */ +final class implicitAmbiguous(msg: String) extends scala.annotation.StaticAnnotation {} diff --git a/src/reflect/scala/reflect/internal/Definitions.scala b/src/reflect/scala/reflect/internal/Definitions.scala index 02fa3c882b..231b6a8a66 100644 --- a/src/reflect/scala/reflect/internal/Definitions.scala +++ b/src/reflect/scala/reflect/internal/Definitions.scala @@ -1103,6 +1103,7 @@ trait Definitions extends api.StandardDefinitions { lazy val BridgeClass = requiredClass[scala.annotation.bridge] lazy val ElidableMethodClass = requiredClass[scala.annotation.elidable] lazy val ImplicitNotFoundClass = requiredClass[scala.annotation.implicitNotFound] + lazy val ImplicitAmbiguousClass = getClassIfDefined("scala.annotation.implicitAmbiguous") lazy val MigrationAnnotationClass = requiredClass[scala.annotation.migration] lazy val ScalaStrictFPAttr = requiredClass[scala.annotation.strictfp] lazy val SwitchClass = requiredClass[scala.annotation.switch] diff --git a/src/reflect/scala/reflect/internal/Symbols.scala b/src/reflect/scala/reflect/internal/Symbols.scala index 8a52f0b9d8..1113da2eff 100644 --- a/src/reflect/scala/reflect/internal/Symbols.scala +++ b/src/reflect/scala/reflect/internal/Symbols.scala @@ -884,10 +884,11 @@ trait Symbols extends api.Symbols { self: SymbolTable => // string. So this needs attention. For now the fact that migration is // private[scala] ought to provide enough protection. def hasMigrationAnnotation = hasAnnotation(MigrationAnnotationClass) - def migrationMessage = getAnnotation(MigrationAnnotationClass) flatMap { _.stringArg(0) } - def migrationVersion = getAnnotation(MigrationAnnotationClass) flatMap { _.stringArg(1) } - def elisionLevel = getAnnotation(ElidableMethodClass) flatMap { _.intArg(0) } - def implicitNotFoundMsg = getAnnotation(ImplicitNotFoundClass) flatMap { _.stringArg(0) } + def migrationMessage = getAnnotation(MigrationAnnotationClass) flatMap { _.stringArg(0) } + def migrationVersion = getAnnotation(MigrationAnnotationClass) flatMap { _.stringArg(1) } + def elisionLevel = getAnnotation(ElidableMethodClass) flatMap { _.intArg(0) } + def implicitNotFoundMsg = getAnnotation(ImplicitNotFoundClass) flatMap { _.stringArg(0) } + def implicitAmbiguousMsg = getAnnotation(ImplicitAmbiguousClass) flatMap { _.stringArg(0) } def isCompileTimeOnly = hasAnnotation(CompileTimeOnlyAttr) def compileTimeOnlyMessage = getAnnotation(CompileTimeOnlyAttr) flatMap (_ stringArg 0) diff --git a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala index ea213cadd9..a2232d1963 100644 --- a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala +++ b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala @@ -370,6 +370,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse => definitions.BridgeClass definitions.ElidableMethodClass definitions.ImplicitNotFoundClass + definitions.ImplicitAmbiguousClass definitions.MigrationAnnotationClass definitions.ScalaStrictFPAttr definitions.SwitchClass -- cgit v1.2.3