diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 6aecb1791903..d1e91799f31a 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -9,6 +9,8 @@ import util.HashSet import typer.ConstFold import reporting.trace +import scala.annotation.tailrec + trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => import TreeInfo._ @@ -512,17 +514,22 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => } /** Decompose a call fn[targs](vargs_1)...(vargs_n) - * into its constituents (where targs, vargss may be empty) + * into its constituents (fn, targs, vargss). + * + * Note: targ and vargss may be empty */ - def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = tree match { - case Apply(fn, args) => - val (meth, targs, argss) = decomposeCall(fn) - (meth, targs, argss :+ args) - case TypeApply(fn, targs) => - val (meth, targss, args) = decomposeCall(fn) - (meth, targs ++ targss, args) - case _ => - (tree, Nil, Nil) + def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = { + @tailrec + def loop(tree: Tree, targss: List[Tree], argss: List[List[Tree]]): (Tree, List[Tree], List[List[Tree]]) = + tree match { + case Apply(fn, args) => + loop(fn, targss, args :: argss) + case TypeApply(fn, targs) => + loop(fn, targs ::: targss, argss) + case _ => + (tree, targss, argss) + } + loop(tree, Nil, Nil) } /** An extractor for closures, either contained in a block or standalone. diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f876f7a70c77..75a7b560945c 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -679,6 +679,8 @@ class Definitions { def ContravariantBetweenAnnot(implicit ctx: Context) = ContravariantBetweenAnnotType.symbol.asClass lazy val DeprecatedAnnotType = ctx.requiredClassRef("scala.deprecated") def DeprecatedAnnot(implicit ctx: Context) = DeprecatedAnnotType.symbol.asClass + lazy val ImplicitAmbiguousAnnotType = ctx.requiredClassRef("scala.annotation.implicitAmbiguous") + def ImplicitAmbiguousAnnot(implicit ctx: Context) = ImplicitAmbiguousAnnotType.symbol.asClass lazy val ImplicitNotFoundAnnotType = ctx.requiredClassRef("scala.annotation.implicitNotFound") def ImplicitNotFoundAnnot(implicit ctx: Context) = ImplicitNotFoundAnnotType.symbol.asClass lazy val InlineAnnotType = ctx.requiredClassRef("scala.inline") diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 3b8f18001c15..6e03384e31e1 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -160,11 +160,11 @@ object ErrorReporting { TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript) } - /** Format `raw` implicitNotFound argument, replacing all - * occurrences of `${X}` where `X` is in `paramNames` with the + /** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing + * all occurrences of `${X}` where `X` is in `paramNames` with the * corresponding shown type in `args`. */ - def implicitNotFoundString(raw: String, paramNames: List[String], args: List[Type]): String = { + def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = { def translate(name: String): Option[String] = { val idx = paramNames.indexOf(name) if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 31f23a3327e5..f11f75b704c4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -26,6 +26,7 @@ import Constants._ import Applications._ import ProtoTypes._ import ErrorReporting._ +import Annotations.Annotation import reporting.diagnostic.{Message, MessageContainer} import Inferencing.fullyDefinedType import Trees._ @@ -545,7 +546,7 @@ trait Implicits { self: Typer => /** If `formal` is of the form ClassTag[T], where `T` is a class type, * synthesize a class tag for `T`. - */ + */ def synthesizedClassTag(formal: Type): Tree = formal.argInfos match { case arg :: Nil => fullyDefinedType(arg, "ClassTag argument", pos) match { @@ -590,7 +591,7 @@ trait Implicits { self: Typer => /** If `formal` is of the form Eq[T, U], where no `Eq` instance exists for * either `T` or `U`, synthesize `Eq.eqAny[T, U]` as solution. - */ + */ def synthesizedEq(formal: Type)(implicit ctx: Context): Tree = { //println(i"synth eq $formal / ${formal.argTypes}%, %") formal.argTypes match { @@ -685,22 +686,67 @@ trait Implicits { self: Typer => } } def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where" + + /** Extract a user defined error message from a symbol `sym` + * with an annotation matching the given class symbol `cls`. + */ + def userDefinedMsg(sym: Symbol, cls: Symbol) = for { + ann <- sym.getAnnotation(cls) + Trees.Literal(Constant(msg: String)) <- ann.argument(0) + } yield msg + + arg.tpe match { case ambi: AmbiguousImplicits => - msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")( - s"ambiguous implicit arguments of type ${pt.show} found${location("for")}") - case _ => - val userDefined = - for { - notFound <- pt.typeSymbol.getAnnotation(defn.ImplicitNotFoundAnnot) - Trees.Literal(Constant(raw: String)) <- notFound.argument(0) - } - yield { - err.implicitNotFoundString( - raw, - pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), - pt.argInfos) + object AmbiguousImplicitMsg { + def unapply(search: SearchSuccess): Option[String] = + userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot) + } + + /** Construct a custom error message given an ambiguous implicit + * candidate `alt` and a user defined message `raw`. + */ + def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = { + val params = alt.ref.underlying match { + case p: PolyType => p.paramNames.map(_.toString) + case _ => Nil } + def resolveTypes(targs: List[Tree])(implicit ctx: Context) = + targs.map(a => fullyDefinedType(a.tpe, "type argument", a.pos)) + + // We can extract type arguments from: + // - a function call: + // @implicitAmbiguous("msg A=${A}") + // implicit def f[A](): String = ... + // implicitly[String] // found: f[Any]() + // + // - an eta-expanded function: + // @implicitAmbiguous("msg A=${A}") + // implicit def f[A](x: Int): String = ... + // implicitly[Int => String] // found: x => f[Any](x) + + val call = closureBody(alt.tree) // the tree itself if not a closure + val (_, targs, _) = decomposeCall(call) + val args = resolveTypes(targs)(ctx.fresh.setTyperState(alt.tstate)) + err.userDefinedErrorString(raw, params, args) + } + + (ambi.alt1, ambi.alt2) match { + case (alt @ AmbiguousImplicitMsg(msg), _) => + userDefinedAmbiguousImplicitMsg(alt, msg) + case (_, alt @ AmbiguousImplicitMsg(msg)) => + userDefinedAmbiguousImplicitMsg(alt, msg) + case _ => + msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")( + s"ambiguous implicit arguments of type ${pt.show} found${location("for")}") + } + + case _ => + val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw => + err.userDefinedErrorString( + raw, + pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), + pt.argInfos)) msg(userDefined.getOrElse(em"no implicit argument of type $pt was found${location("for")}"))() } } diff --git a/compiler/test/dotty/tools/dotc/reporting/UserDefinedErrorMessages.scala b/compiler/test/dotty/tools/dotc/reporting/UserDefinedErrorMessages.scala new file mode 100644 index 000000000000..bb8db2aff35c --- /dev/null +++ b/compiler/test/dotty/tools/dotc/reporting/UserDefinedErrorMessages.scala @@ -0,0 +1,131 @@ +package dotty.tools +package dotc +package reporting + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.reporting.diagnostic.messages._ +import org.junit.Assert._ +import org.junit.Test + +class UserDefinedErrorMessages extends ErrorMessagesTest { + @Test def userDefinedImplicitAmbiguous1 = + checkMessagesAfter("frontend") { + """ + |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] + |} + """.stripMargin + }.expect { (itcx, messages) => + import diagnostic.NoExplanation + implicit val ctx: Context = itcx + + assertMessageCount(1, messages) + val (m: NoExplanation) :: Nil = messages + + assertEquals(m.msg, "Could not prove Int =!= Int") + } + + @Test def userDefinedImplicitAmbiguous2 = + checkMessagesAfter("frontend") { + """ + |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] + |} + """.stripMargin + }.expect { (itcx, messages) => + import diagnostic.NoExplanation + implicit val ctx: Context = itcx + + assertMessageCount(1, messages) + val (m: NoExplanation) :: Nil = messages + + assertEquals(m.msg, "Could not prove Int =!= Int") + } + + @Test def userDefinedImplicitAmbiguous3 = + checkMessagesAfter("frontend") { + """ + |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 + | @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}") + | implicit def neqAmbig2[I] : I =!= I = null + | + | implicitly[Int =!= Int] + |} + """.stripMargin + }.expect { (itcx, messages) => + import diagnostic.NoExplanation + implicit val ctx: Context = itcx + + assertMessageCount(1, messages) + val (m: NoExplanation) :: Nil = messages + + assertEquals(m.msg, "Could not prove Int =!= Int") + } + + @Test def userDefinedImplicitAmbiguous4 = + checkMessagesAfter("frontend") { + """ + |class C { + | @annotation.implicitAmbiguous("msg A=${A}") + | implicit def f[A](x: Int): String = "f was here" + | implicit def g(x: Int): String = "f was here" + | def test: Unit = { + | implicitly[Int => String] + | } + |} + """.stripMargin + }.expect { (itcx, messages) => + import diagnostic.NoExplanation + implicit val ctx: Context = itcx + + assertMessageCount(1, messages) + val (m: NoExplanation) :: Nil = messages + + assertEquals(m.msg, "msg A=Any") + } + + @Test def userDefinedImplicitAmbiguous5 = + checkMessagesAfter("frontend") { + """ + |class C { + | @annotation.implicitAmbiguous("msg A=${A}") + | implicit def f[A](implicit x: String): Int = 1 + | implicit def g: Int = 2 + | def test: Unit = { + | implicit val s: String = "Hello" + | implicitly[Int] + | } + |} + """.stripMargin + }.expect { (itcx, messages) => + import diagnostic.NoExplanation + implicit val ctx: Context = itcx + + assertMessageCount(1, messages) + val (m: NoExplanation) :: Nil = messages + + assertEquals(m.msg, "msg A=Any") + } +}