diff options
author | Brian McKenna <brian@simpleenergy.com> | 2014-09-07 12:33:52 -0600 |
---|---|---|
committer | Brian McKenna <brian@brianmckenna.org> | 2015-08-11 17:13:34 +1000 |
commit | 5bd8ea0edffe7b725e5fa665a82a5795d5dafe8f (patch) | |
tree | 1b41ee5ffa3a1be3fa7881d0fa9cd2665962260b | |
parent | 462e22d2f25bb9432c5a1e7bf20f391e6424f7a9 (diff) | |
download | scala-5bd8ea0edffe7b725e5fa665a82a5795d5dafe8f.tar.gz scala-5bd8ea0edffe7b725e5fa665a82a5795d5dafe8f.tar.bz2 scala-5bd8ea0edffe7b725e5fa665a82a5795d5dafe8f.zip |
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]
^
-rw-r--r-- | src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala | 19 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/typechecker/Implicits.scala | 74 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/typechecker/RefChecks.scala | 11 | ||||
-rw-r--r-- | src/library/scala/annotation/implicitAmbiguous.scala | 34 | ||||
-rw-r--r-- | src/reflect/scala/reflect/internal/Definitions.scala | 1 | ||||
-rw-r--r-- | src/reflect/scala/reflect/internal/Symbols.scala | 9 | ||||
-rw-r--r-- | src/reflect/scala/reflect/runtime/JavaUniverseForce.scala | 1 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous-2.check | 4 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous-2.scala | 11 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous-invalid.check | 7 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous-invalid.flags | 1 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous-invalid.scala | 6 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous.check | 4 | ||||
-rw-r--r-- | test/files/neg/implicit-ambiguous.scala | 11 |
14 files changed, 146 insertions, 47 deletions
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 diff --git a/test/files/neg/implicit-ambiguous-2.check b/test/files/neg/implicit-ambiguous-2.check new file mode 100644 index 0000000000..4a10b0dd65 --- /dev/null +++ b/test/files/neg/implicit-ambiguous-2.check @@ -0,0 +1,4 @@ +implicit-ambiguous-2.scala:10: error: Could not prove Int =!= Int + implicitly[Int =!= Int] + ^ +one error found diff --git a/test/files/neg/implicit-ambiguous-2.scala b/test/files/neg/implicit-ambiguous-2.scala new file mode 100644 index 0000000000..563c8c583f --- /dev/null +++ b/test/files/neg/implicit-ambiguous-2.scala @@ -0,0 +1,11 @@ +object Test { + trait =!=[C, D] + + implicit def neq[E, F] : E =!= F = null + + implicit def neqAmbig1[G, H, J] : J =!= J = null + @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}") + implicit def neqAmbig2[I] : I =!= I = null + + implicitly[Int =!= Int] +} diff --git a/test/files/neg/implicit-ambiguous-invalid.check b/test/files/neg/implicit-ambiguous-invalid.check new file mode 100644 index 0000000000..68b607c4c2 --- /dev/null +++ b/test/files/neg/implicit-ambiguous-invalid.check @@ -0,0 +1,7 @@ +implicit-ambiguous-invalid.scala:5: warning: Invalid implicitAmbiguous message for method neqAmbig1 in object Test: +The type parameter B referenced in the message of the @implicitAmbiguous annotation is not defined by method neqAmbig1. + implicit def neqAmbig1[A] : A =!= A = null + ^ +error: No warnings can be incurred under -Xfatal-warnings. +one warning found +one error found diff --git a/test/files/neg/implicit-ambiguous-invalid.flags b/test/files/neg/implicit-ambiguous-invalid.flags new file mode 100644 index 0000000000..85d8eb2ba2 --- /dev/null +++ b/test/files/neg/implicit-ambiguous-invalid.flags @@ -0,0 +1 @@ +-Xfatal-warnings diff --git a/test/files/neg/implicit-ambiguous-invalid.scala b/test/files/neg/implicit-ambiguous-invalid.scala new file mode 100644 index 0000000000..f8f9da655f --- /dev/null +++ b/test/files/neg/implicit-ambiguous-invalid.scala @@ -0,0 +1,6 @@ +object Test { + trait =!=[C, D] + + @annotation.implicitAmbiguous("Could not prove ${A} =!= ${B}") + implicit def neqAmbig1[A] : A =!= A = null +} diff --git a/test/files/neg/implicit-ambiguous.check b/test/files/neg/implicit-ambiguous.check new file mode 100644 index 0000000000..0b3cebcb6f --- /dev/null +++ b/test/files/neg/implicit-ambiguous.check @@ -0,0 +1,4 @@ +implicit-ambiguous.scala:10: error: Could not prove Int =!= Int + implicitly[Int =!= Int] + ^ +one error found diff --git a/test/files/neg/implicit-ambiguous.scala b/test/files/neg/implicit-ambiguous.scala new file mode 100644 index 0000000000..79b1297915 --- /dev/null +++ b/test/files/neg/implicit-ambiguous.scala @@ -0,0 +1,11 @@ +object Test { + 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] +} |