Skip to content

Commit 9a3c33a

Browse files
committed
Make suggestions of missing implicits imports on type errors
Add an addendum to an error message where the error might be fixed be some implicit argument or conversion or some extension method that is however not found. The addendum suggests suggests implicit imports that might fix the problem.
1 parent 7cea875 commit 9a3c33a

File tree

8 files changed

+204
-29
lines changed

8 files changed

+204
-29
lines changed

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
278278
}
279279

280280
/** The string representation of this type used as a prefix */
281-
protected def toTextRef(tp: SingletonType): Text = controlled {
281+
def toTextRef(tp: SingletonType): Text = controlled {
282282
tp match {
283283
case tp: TermRef =>
284284
toTextPrefix(tp.prefix) ~ selectionString(tp)

compiler/src/dotty/tools/dotc/printing/Printer.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package printing
44

55
import core._
66
import Texts._, ast.Trees._
7-
import Types.Type, Symbols.Symbol, Scopes.Scope, Constants.Constant,
7+
import Types.{Type, SingletonType}, Symbols.Symbol, Scopes.Scope, Constants.Constant,
88
Names.Name, Denotations._, Annotations.Annotation
99
import typer.Implicits.SearchResult
1010
import util.SourcePosition
@@ -97,6 +97,9 @@ abstract class Printer {
9797
*/
9898
def toText(sym: Symbol): Text
9999

100+
/** Textual representation of singeton type reference */
101+
def toTextRef(tp: SingletonType): Text
102+
100103
/** Textual representation of symbol's declaration */
101104
def dclText(sym: Symbol): Text
102105

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import diagnostic.messages._
1515
import diagnostic._
1616
import ast.{tpd, Trees}
1717
import Message._
18+
import core.Decorators._
1819

1920
import java.lang.System.currentTimeMillis
2021
import java.io.{ BufferedReader, PrintWriter }

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ object ErrorReporting {
150150
val expected1 = reported(expected)
151151
val (found2, expected2) =
152152
if (found1 frozen_<:< expected1) (found, expected) else (found1, expected1)
153-
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript)
153+
val postScript1 =
154+
if !postScript.isEmpty then postScript
155+
else ctx.typer.implicitSuggestionsFor(ViewProto(found.widen, expected))
156+
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript1)
154157
}
155158

156159
/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing

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

Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import config.Printers.{implicits, implicitsDetailed}
3737
import collection.mutable
3838
import reporting.trace
3939
import annotation.tailrec
40+
import scala.util.control.NonFatal
4041

4142
import scala.annotation.internal.sharable
4243
import scala.annotation.threadUnsafe
@@ -462,6 +463,66 @@ object Implicits {
462463
def explanation(implicit ctx: Context): String =
463464
em"${err.refStr(ref)} produces a diverging implicit search when trying to $qualify"
464465
}
466+
467+
/** A helper class to find imports of givens that might fix a type error.
468+
*
469+
* suggestions(p).search
470+
*
471+
* returns a list of TermRefs that refer to implicits or givens
472+
* that satisfy predicate `p`.
473+
*
474+
* The search algorithm looks for givens in the smallest set of objects
475+
* and packages that includes
476+
*
477+
* - any object that is a defined in an enclosing scope,
478+
* - any object that is a member of an enclosing class,
479+
* - any enclosing package (including the root package),
480+
* - any object that is a member of a searched object or package,
481+
* - any object or package from which something is imported in an enclosing scope,
482+
* - any package that is nested in a searched package, provided
483+
* the package was accessed in some way previously.
484+
*/
485+
class suggestions(qualifies: TermRef => Boolean) with
486+
private val seen = mutable.Set[TermRef]()
487+
488+
private def lookInside(root: Symbol)(given ctx: Context): Boolean =
489+
!root.name.lastPart.contains('$')
490+
&& root.is(ModuleVal, butNot = JavaDefined)
491+
&& (root.isCompleted || !root.is(Package))
492+
493+
private def rootsIn(ref: TermRef)(given ctx: Context): List[TermRef] =
494+
if seen.contains(ref) then Nil
495+
else
496+
implicitsDetailed.println(i"search in ${ref.symbol.fullName}")
497+
seen += ref
498+
ref :: ref.fields
499+
.filter(fld => lookInside(fld.symbol))
500+
.map(fld => TermRef(ref, fld.symbol.asTerm))
501+
.flatMap(rootsIn)
502+
.toList
503+
504+
private def rootsOnPath(tp: Type)(given ctx: Context): List[TermRef] = tp match
505+
case ref: TermRef => rootsIn(ref) ::: rootsOnPath(ref.prefix)
506+
case _ => Nil
507+
508+
private def roots(given ctx: Context): List[TermRef] =
509+
if ctx.owner.exists then
510+
val defined =
511+
if ctx.scope eq ctx.outer.scope then Nil
512+
else ctx.scope
513+
.filter(lookInside(_))
514+
.flatMap(sym => rootsIn(sym.termRef))
515+
val imported =
516+
if ctx.importInfo eq ctx.outer.importInfo then Nil
517+
else ctx.importInfo.sym.info match
518+
case ImportType(expr) => rootsOnPath(expr.tpe)
519+
case _ => Nil
520+
defined ++ imported ++ roots(given ctx.outer)
521+
else Nil
522+
523+
def search(given ctx: Context): List[TermRef] =
524+
roots.flatMap(_.implicitMembers.filter(qualifies))
525+
end suggestions
465526
}
466527

467528
import Implicits._
@@ -683,6 +744,35 @@ trait Implicits { self: Typer =>
683744
}
684745
}
685746

747+
/** An addendum to an error message where the error might be fixed
748+
* be some implicit value of type `pt` that is however not found.
749+
* The addendum suggests suggests implicit imports that might fix the problem.
750+
*/
751+
override def implicitSuggestionsFor(pt: Type)(given ctx: Context): String =
752+
val suggestedRefs =
753+
try Implicits.suggestions(_ <:< pt).search(given ctx.fresh.setExploreTyperState())
754+
catch case NonFatal(ex) => Nil
755+
def refToRawString(ref: TermRef) = ctx.printer.toTextRef(ref).show
756+
def refToString(ref: TermRef): String =
757+
val raw = refToRawString(ref)
758+
ref.prefix match
759+
case prefix: TermRef if !raw.contains(".") => s"${refToRawString(prefix)}.$raw"
760+
case _ => raw
761+
def suggestStr(ref: TermRef) = i" import ${refToString(ref)}"
762+
if suggestedRefs.isEmpty then ""
763+
else
764+
val suggestions = suggestedRefs.map(suggestStr).distinct
765+
// TermRefs might be different but generate the same strings
766+
val fix =
767+
if suggestions.tail.isEmpty then "The following import"
768+
else "One of the following imports"
769+
i"""
770+
|
771+
|$fix might fix the problem:
772+
|
773+
|$suggestions%\n%
774+
"""
775+
686776
/** Handlers to synthesize implicits for special types */
687777
type SpecialHandler = (Type, Span) => Context => Tree
688778
type SpecialHandlers = List[(ClassSymbol, SpecialHandler)]
@@ -1215,32 +1305,37 @@ trait Implicits { self: Typer =>
12151305
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
12161306
pt.widenExpr.argInfos))
12171307

1218-
def hiddenImplicitsAddendum: String = arg.tpe match {
1219-
case fail: SearchFailureType =>
1220-
1221-
def hiddenImplicitNote(s: SearchSuccess) =
1222-
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."
1308+
def hiddenImplicitsAddendum: String =
1309+
1310+
def hiddenImplicitNote(s: SearchSuccess) =
1311+
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."
1312+
1313+
def FindHiddenImplicitsCtx(ctx: Context): Context =
1314+
if (ctx == NoContext) ctx
1315+
else ctx.freshOver(FindHiddenImplicitsCtx(ctx.outer)).addMode(Mode.FindHiddenImplicits)
1316+
1317+
val normalImports = arg.tpe match
1318+
case fail: SearchFailureType =>
1319+
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
1320+
inferImplicit(fail.expectedType, fail.argument, arg.span)(
1321+
FindHiddenImplicitsCtx(ctx)) match {
1322+
case s: SearchSuccess => hiddenImplicitNote(s)
1323+
case f: SearchFailure =>
1324+
f.reason match {
1325+
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1)
1326+
case r => ""
1327+
}
1328+
}
1329+
else
1330+
// It's unsafe to search for parts of the expected type if they are not fully defined,
1331+
// since these come with nested contexts that are lost at this point. See #7249 for an
1332+
// example where searching for a nested type causes an infinite loop.
1333+
""
12231334

1224-
def FindHiddenImplicitsCtx(ctx: Context): Context =
1225-
if (ctx == NoContext) ctx
1226-
else ctx.freshOver(FindHiddenImplicitsCtx(ctx.outer)).addMode(Mode.FindHiddenImplicits)
1335+
def suggestedImports = implicitSuggestionsFor(pt)
1336+
if normalImports.isEmpty then suggestedImports else normalImports
1337+
end hiddenImplicitsAddendum
12271338

1228-
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
1229-
inferImplicit(fail.expectedType, fail.argument, arg.span)(
1230-
FindHiddenImplicitsCtx(ctx)) match {
1231-
case s: SearchSuccess => hiddenImplicitNote(s)
1232-
case f: SearchFailure =>
1233-
f.reason match {
1234-
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1)
1235-
case r => ""
1236-
}
1237-
}
1238-
else
1239-
// It's unsafe to search for parts of the expected type if they are not fully defined,
1240-
// since these come with nested contexts that are lost at this point. See #7249 for an
1241-
// example where searching for a nested type causes an infinite loop.
1242-
""
1243-
}
12441339
msg(userDefined.getOrElse(
12451340
em"no implicit argument of type $pt was found${location("for")}"))() ++
12461341
hiddenImplicitsAddendum

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import util.SourcePosition
1111
import config.Printers.typr
1212
import ast.Trees._
1313
import NameOps._
14+
import ProtoTypes._
1415
import collection.mutable
1516
import reporting.diagnostic.messages._
1617
import Checking.{checkNoPrivateLeaks, checkNoWildcard}
@@ -266,7 +267,7 @@ trait TypeAssigner {
266267
errorType(ex"$qualType does not have a constructor", tree.sourcePos)
267268
else {
268269
val kind = if (name.isTypeName) "type" else "value"
269-
val addendum =
270+
def addendum =
270271
if (qualType.derivesFrom(defn.DynamicClass))
271272
"\npossible cause: maybe a wrong Dynamic method signature?"
272273
else qual1.getAttachment(Typer.HiddenSearchFailure) match {
@@ -281,12 +282,17 @@ trait TypeAssigner {
281282
|Note that `$name` is treated as an infix operator in Scala 3.
282283
|If you do not want that, insert a `;` or empty line in front
283284
|or drop any spaces behind the operator."""
284-
else ""
285+
else
286+
implicitSuggestionsFor(
287+
ViewProto(qualType.widen,
288+
SelectionProto(name, WildcardType, NoViewsAllowed, privateOK = false)))
285289
}
286290
errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos)
287291
}
288292
}
289293

294+
def implicitSuggestionsFor(pt: Type)(given Context): String = ""
295+
290296
/** The type of the selection in `tree`, where `qual1` is the typed qualifier part.
291297
* The selection type is additionally checked for accessibility.
292298
*/

tests/neg/missing-implicit.check

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-- [E008] Member Not Found Error: tests/neg/missing-implicit.scala:5:25 ------------------------------------------------
2+
5 | case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error
3+
| ^^^^^^^
4+
| value > is not a member of T
5+
|
6+
| One of the following imports might fix the problem:
7+
|
8+
| import math.Ordering.Implicits.infixOrderingOps
9+
| import math.Ordered.orderingToOrdered
10+
|
11+
-- [E008] Member Not Found Error: tests/neg/missing-implicit.scala:5:51 ------------------------------------------------
12+
5 | case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error
13+
| ^^^^^^^
14+
| value - is not a member of T
15+
|
16+
| One of the following imports might fix the problem:
17+
|
18+
| import math.Fractional.Implicits.infixFractionalOps
19+
| import math.Numeric.Implicits.infixNumericOps
20+
| import math.Integral.Implicits.infixIntegralOps
21+
|
22+
-- Error: tests/neg/missing-implicit.scala:10:24 -----------------------------------------------------------------------
23+
10 |val f = Future[Unit] { } // error
24+
| ^
25+
| Cannot find an implicit ExecutionContext. You might pass
26+
| an (implicit ec: ExecutionContext) parameter to your method.
27+
|
28+
| The ExecutionContext is used to configure how and on which
29+
| thread pools Futures will run, so the specific ExecutionContext
30+
| that is selected is important.
31+
|
32+
| If your application does not define an ExecutionContext elsewhere,
33+
| consider using Scala's global ExecutionContext by defining
34+
| the following:
35+
|
36+
| implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
37+
|
38+
| The following import might fix the problem:
39+
|
40+
| import concurrent.ExecutionContext.Implicits.global
41+
|
42+
-- [E007] Type Mismatch Error: tests/neg/missing-implicit.scala:12:25 --------------------------------------------------
43+
12 |val b: java.lang.Byte = (1: Byte) // error
44+
| ^^^^^^^
45+
| Found: Byte
46+
| Required: Byte²
47+
|
48+
| where: Byte is a class in package scala
49+
| Byte² is a class in package lang
50+
|
51+
|
52+
| The following import might fix the problem:
53+
|
54+
| import Predef.byte2Byte
55+
|

tests/neg/missing-implicit.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Predef.{byte2Byte => _}
2+
import math.Numeric
3+
4+
def consume[T: Numeric](xs: List[T], limit: T): List[T] = xs match
5+
case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error
6+
case _ => xs
7+
8+
import scala.concurrent.Future
9+
10+
val f = Future[Unit] { } // error
11+
12+
val b: java.lang.Byte = (1: Byte) // error

0 commit comments

Comments
 (0)