Skip to content

Commit 1760da3

Browse files
committed
Fix #4986: Support implicitNotFound on parameters
Also fixes some complex cases of resolving type variables in `implicitNotFound` message for annotations put on type definitions
1 parent 1ab76c1 commit 1760da3

21 files changed

+511
-236
lines changed

compiler/src/dotty/tools/dotc/reporting/messages.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -2447,8 +2447,8 @@ import transform.SymUtils._
24472447

24482448
class InvalidReferenceInImplicitNotFoundAnnotation(typeVar: String, owner: String)(using Context)
24492449
extends ReferenceMsg(InvalidReferenceInImplicitNotFoundAnnotationID) {
2450-
def msg = em"""|Invalid reference to a type variable "${hl(typeVar)}" found in the annotation argument.
2451-
|The variable does not occur in the signature of ${hl(owner)}.
2450+
def msg = em"""|Invalid reference to a type variable ${hl(typeVar)} found in the annotation argument.
2451+
|The variable does not occur as a parameter in the scope of ${hl(owner)}.
24522452
|""".stripMargin
24532453
def explain = ""
24542454
}

compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala

+175-8
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import ast._
66
import core._
77
import Types._, ProtoTypes._, Contexts._, Decorators._, Denotations._, Symbols._
88
import Implicits._, Flags._, Constants.Constant
9+
import Trees._
10+
import NameOps._
911
import util.Spans._
1012
import util.SrcPos
1113
import config.Feature
1214
import java.util.regex.Matcher.quoteReplacement
1315
import reporting._
1416

17+
import scala.util.matching.Regex
18+
1519
object ErrorReporting {
1620

1721
import tpd._
@@ -131,14 +135,6 @@ object ErrorReporting {
131135
* all occurrences of `${X}` where `X` is in `paramNames` with the
132136
* corresponding shown type in `args`.
133137
*/
134-
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
135-
def translate(name: String): Option[String] = {
136-
assert(paramNames.length == args.length)
137-
val idx = paramNames.indexOf(name)
138-
if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None
139-
}
140-
"""\$\{\w*\}""".r.replaceSomeIn(raw, m => translate(m.matched.drop(2).init))
141-
}
142138

143139
def rewriteNotice: String =
144140
if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite."
@@ -180,9 +176,180 @@ object ErrorReporting {
180176
end selectErrorAddendum
181177
}
182178

179+
def substitutableTypeSymbolsInScope(sym: Symbol)(using Context): List[Symbol] =
180+
sym.ownersIterator.takeWhile(!_.is(Flags.Package)).flatMap { ownerSym =>
181+
ownerSym.paramSymss.flatten.filter(_.isType) ++
182+
ownerSym.typeRef.nonClassTypeMembers.map(_.symbol)
183+
}.toList
184+
183185
def dependentStr =
184186
"""Term-dependent types are experimental,
185187
|they must be enabled with a `experimental.dependent` language import or setting""".stripMargin
186188

187189
def err(using Context): Errors = new Errors
188190
}
191+
192+
193+
class ImplicitSearchError(
194+
arg: tpd.Tree,
195+
pt: Type,
196+
where: String,
197+
paramSymWithMethodCallTree: Option[(Symbol, tpd.Tree)] = None,
198+
ignoredInstanceNormalImport: => Option[SearchSuccess],
199+
importSuggestionAddendum: => String
200+
)(using ctx: Context) {
201+
def missingArgMsg = arg.tpe match {
202+
case ambi: AmbiguousImplicits =>
203+
(ambi.alt1, ambi.alt2) match {
204+
case (alt @ AmbiguousImplicitMsg(msg), _) =>
205+
userDefinedAmbiguousImplicitMsg(alt, msg)
206+
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
207+
userDefinedAmbiguousImplicitMsg(alt, msg)
208+
case _ =>
209+
defaultAmbiguousImplicitMsg(ambi)
210+
}
211+
case _ =>
212+
val shortMessage = userDefinedImplicitNotFoundParamMessage
213+
.orElse(userDefinedImplicitNotFoundTypeMessage)
214+
.getOrElse(defaultImplicitNotFoundMessage)
215+
formatMsg(shortMessage)() ++ hiddenImplicitsAddendum
216+
}
217+
218+
private def formatMsg(shortForm: String)(headline: String = shortForm) = arg match {
219+
case arg: Trees.SearchFailureIdent[?] =>
220+
shortForm
221+
case _ =>
222+
arg.tpe match {
223+
case tpe: SearchFailureType =>
224+
val original = arg match
225+
case Inlined(call, _, _) => call
226+
case _ => arg
227+
228+
i"""$headline.
229+
|I found:
230+
|
231+
| ${original.show.replace("\n", "\n ")}
232+
|
233+
|But ${tpe.explanation}."""
234+
}
235+
}
236+
237+
private def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
238+
def translate(name: String): Option[String] = {
239+
val idx = paramNames.indexOf(name)
240+
if (idx >= 0) Some(ex"${args(idx)}") else None
241+
}
242+
243+
"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match {
244+
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse(""))
245+
})
246+
}
247+
248+
/** Extract a user defined error message from a symbol `sym`
249+
* with an annotation matching the given class symbol `cls`.
250+
*/
251+
private def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
252+
ann <- sym.getAnnotation(cls)
253+
Trees.Literal(Constant(msg: String)) <- ann.argument(0)
254+
} yield msg
255+
256+
private def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"
257+
258+
private def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) = {
259+
formatMsg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
260+
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}"
261+
)
262+
}
263+
264+
private def defaultImplicitNotFoundMessage = {
265+
em"no implicit argument of type $pt was found${location("for")}"
266+
}
267+
268+
/** Construct a custom error message given an ambiguous implicit
269+
* candidate `alt` and a user defined message `raw`.
270+
*/
271+
private def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = {
272+
val params = alt.ref.underlying match {
273+
case p: PolyType => p.paramNames.map(_.toString)
274+
case _ => Nil
275+
}
276+
def resolveTypes(targs: List[tpd.Tree])(using Context) =
277+
targs.map(a => Inferencing.fullyDefinedType(a.tpe, "type argument", a.span))
278+
279+
// We can extract type arguments from:
280+
// - a function call:
281+
// @implicitAmbiguous("msg A=${A}")
282+
// implicit def f[A](): String = ...
283+
// implicitly[String] // found: f[Any]()
284+
//
285+
// - an eta-expanded function:
286+
// @implicitAmbiguous("msg A=${A}")
287+
// implicit def f[A](x: Int): String = ...
288+
// implicitly[Int => String] // found: x => f[Any](x)
289+
290+
val call = tpd.closureBody(alt.tree) // the tree itself if not a closure
291+
val (_, targs, _) = tpd.decomposeCall(call)
292+
val args = resolveTypes(targs)(using ctx.fresh.setTyperState(alt.tstate))
293+
userDefinedErrorString(raw, params, args)
294+
}
295+
296+
/** @param rawMsg Message template with variables, e.g. "Variable A is ${A}"
297+
* @param sym Symbol of the annotated type or of the method whose parameter was annotated
298+
* @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int
299+
*/
300+
private def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = {
301+
val substitutableTypesSymbols = ErrorReporting.substitutableTypeSymbolsInScope(sym)
302+
303+
userDefinedErrorString(
304+
rawMsg,
305+
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
306+
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
307+
)
308+
}
309+
310+
/** Extracting the message from a method parameter, e.g. in
311+
*
312+
* trait Foo
313+
*
314+
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
315+
*/
316+
private def userDefinedImplicitNotFoundParamMessage = paramSymWithMethodCallTree.flatMap { (sym, applTree) =>
317+
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
318+
val (fn, targs, _) = tpd.decomposeCall(applTree)
319+
val methodOwner = fn.symbol.owner
320+
val methodOwnerType = tpd.qualifier(fn).tpe
321+
val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType)
322+
val methodTypeArgs = targs.map(_.tpe)
323+
val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs)
324+
formatAnnotationMessage(rawMsg, sym.owner, substituteType)
325+
}
326+
}
327+
328+
/** Extracting the message from a type, e.g. in
329+
*
330+
* @annotation.implicitNotFound("Foo is missing")
331+
* trait Foo
332+
*
333+
* def foo(implicit foo: Foo): Any = ???
334+
*/
335+
private def userDefinedImplicitNotFoundTypeMessage =
336+
val classSym = pt.classSymbol
337+
userDefinedMsg(classSym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
338+
val substituteType = (_: Type).asSeenFrom(pt, classSym)
339+
formatAnnotationMessage(rawMsg, classSym, substituteType)
340+
}
341+
342+
private def hiddenImplicitsAddendum: String =
343+
def hiddenImplicitNote(s: SearchSuccess) =
344+
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."
345+
346+
val normalImports = ignoredInstanceNormalImport.map(hiddenImplicitNote)
347+
348+
normalImports.getOrElse(importSuggestionAddendum)
349+
end hiddenImplicitsAddendum
350+
351+
private object AmbiguousImplicitMsg {
352+
def unapply(search: SearchSuccess): Option[String] =
353+
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
354+
}
355+
}

compiler/src/dotty/tools/dotc/typer/Implicits.scala

+33-118
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ object Implicits:
8282
def strictEquality(using Context): Boolean =
8383
ctx.mode.is(Mode.StrictEquality) || Feature.enabled(nme.strictEquality)
8484

85+
8586
/** A common base class of contextual implicits and of-type implicits which
8687
* represents a set of references to implicit definitions.
8788
*/
@@ -832,126 +833,40 @@ trait Implicits:
832833
arg
833834
}
834835

835-
def missingArgMsg(arg: Tree, pt: Type, where: String)(using Context): String = {
836-
837-
def msg(shortForm: String)(headline: String = shortForm) = arg match {
838-
case arg: Trees.SearchFailureIdent[?] =>
839-
shortForm
840-
case _ =>
841-
arg.tpe match {
842-
case tpe: SearchFailureType =>
843-
val original = arg match
844-
case Inlined(call, _, _) => call
845-
case _ => arg
846-
847-
i"""$headline.
848-
|I found:
849-
|
850-
| ${original.show.replace("\n", "\n ")}
851-
|
852-
|But ${tpe.explanation}."""
853-
}
854-
}
855-
856-
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"
857-
858-
/** Extract a user defined error message from a symbol `sym`
859-
* with an annotation matching the given class symbol `cls`.
860-
*/
861-
def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
862-
ann <- sym.getAnnotation(cls)
863-
Trees.Literal(Constant(msg: String)) <- ann.argument(0)
864-
}
865-
yield msg
866-
867-
868-
arg.tpe match {
869-
case ambi: AmbiguousImplicits =>
870-
object AmbiguousImplicitMsg {
871-
def unapply(search: SearchSuccess): Option[String] =
872-
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
873-
}
874-
875-
/** Construct a custom error message given an ambiguous implicit
876-
* candidate `alt` and a user defined message `raw`.
877-
*/
878-
def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = {
879-
val params = alt.ref.underlying match {
880-
case p: PolyType => p.paramNames.map(_.toString)
881-
case _ => Nil
836+
/** @param arg Tree representing a failed result of implicit search
837+
* @param pt Type for which an implicit value was searched
838+
* @param where Description of where the search was performed. Might be empty
839+
* @param paramSymWithMethodCallTree Symbol of the parameter for which the implicit was searched and tree of the method call that triggered the implicit search
840+
*/
841+
def missingArgMsg(
842+
arg: Tree,
843+
pt: Type,
844+
where: String,
845+
paramSymWithMethodCallTree: Option[(Symbol, Tree)] = None
846+
)(using Context): String = {
847+
def findHiddenImplicitsCtx(c: Context): Context =
848+
if c == NoContext then c
849+
else c.freshOver(findHiddenImplicitsCtx(c.outer)).addMode(Mode.FindHiddenImplicits)
850+
851+
def ignoredInstanceNormalImport = arg.tpe match
852+
case fail: SearchFailureType =>
853+
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
854+
inferImplicit(fail.expectedType, fail.argument, arg.span) match {
855+
case s: SearchSuccess => Some(s)
856+
case f: SearchFailure =>
857+
f.reason match {
858+
case ambi: AmbiguousImplicits => Some(ambi.alt1)
859+
case r => None
860+
}
882861
}
883-
def resolveTypes(targs: List[Tree])(using Context) =
884-
targs.map(a => fullyDefinedType(a.tpe, "type argument", a.span))
885-
886-
// We can extract type arguments from:
887-
// - a function call:
888-
// @implicitAmbiguous("msg A=${A}")
889-
// implicit def f[A](): String = ...
890-
// implicitly[String] // found: f[Any]()
891-
//
892-
// - an eta-expanded function:
893-
// @implicitAmbiguous("msg A=${A}")
894-
// implicit def f[A](x: Int): String = ...
895-
// implicitly[Int => String] // found: x => f[Any](x)
896-
897-
val call = closureBody(alt.tree) // the tree itself if not a closure
898-
val (_, targs, _) = decomposeCall(call)
899-
val args = resolveTypes(targs)(using ctx.fresh.setTyperState(alt.tstate))
900-
err.userDefinedErrorString(raw, params, args)
901-
}
902-
903-
(ambi.alt1, ambi.alt2) match {
904-
case (alt @ AmbiguousImplicitMsg(msg), _) =>
905-
userDefinedAmbiguousImplicitMsg(alt, msg)
906-
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
907-
userDefinedAmbiguousImplicitMsg(alt, msg)
908-
case _ =>
909-
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
910-
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
911-
}
862+
else
863+
// It's unsafe to search for parts of the expected type if they are not fully defined,
864+
// since these come with nested contexts that are lost at this point. See #7249 for an
865+
// example where searching for a nested type causes an infinite loop.
866+
None
912867

913-
case _ =>
914-
val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw =>
915-
err.userDefinedErrorString(
916-
raw,
917-
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
918-
pt.widenExpr.dropDependentRefinement.argInfos))
919-
920-
def hiddenImplicitsAddendum: String =
921-
922-
def hiddenImplicitNote(s: SearchSuccess) =
923-
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."
924-
925-
def findHiddenImplicitsCtx(c: Context): Context =
926-
if c == NoContext then c
927-
else c.freshOver(findHiddenImplicitsCtx(c.outer)).addMode(Mode.FindHiddenImplicits)
928-
929-
val normalImports = arg.tpe match
930-
case fail: SearchFailureType =>
931-
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
932-
inferImplicit(fail.expectedType, fail.argument, arg.span)(
933-
using findHiddenImplicitsCtx(ctx)) match {
934-
case s: SearchSuccess => hiddenImplicitNote(s)
935-
case f: SearchFailure =>
936-
f.reason match {
937-
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1)
938-
case r => ""
939-
}
940-
}
941-
else
942-
// It's unsafe to search for parts of the expected type if they are not fully defined,
943-
// since these come with nested contexts that are lost at this point. See #7249 for an
944-
// example where searching for a nested type causes an infinite loop.
945-
""
946-
947-
def suggestedImports = importSuggestionAddendum(pt)
948-
if normalImports.isEmpty then suggestedImports else normalImports
949-
end hiddenImplicitsAddendum
950-
951-
msg(userDefined.getOrElse(
952-
em"no implicit argument of type $pt was found${location("for")}"))() ++
953-
hiddenImplicitsAddendum
954-
}
868+
val error = new ImplicitSearchError(arg, pt, where, paramSymWithMethodCallTree, ignoredInstanceNormalImport, importSuggestionAddendum(pt))
869+
error.missingArgMsg
955870
}
956871

957872
/** A string indicating the formal parameter corresponding to a missing argument */

0 commit comments

Comments
 (0)