-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Make suggestions of missing implicits imports on type errors #7862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
7cea875
9a3c33a
53a6098
b848c57
13c1839
e1360eb
5e3f11c
540f69a
2ccec1f
51f19ec
55b1a31
9c67ee4
b3a4966
717cb7d
ad5055b
848dd0a
72df855
0501bfa
1124b35
c601a72
02d7594
9e72e6c
aabcd81
427da51
5ac8b80
8289d51
6f94a83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ import Flags._ | |
import TypeErasure.{erasure, hasStableErasure} | ||
import Mode.ImplicitsEnabled | ||
import NameOps._ | ||
import NameKinds.LazyImplicitName | ||
import NameKinds.{LazyImplicitName, FlatName} | ||
import Symbols._ | ||
import Denotations._ | ||
import Types._ | ||
|
@@ -37,6 +37,8 @@ import config.Printers.{implicits, implicitsDetailed} | |
import collection.mutable | ||
import reporting.trace | ||
import annotation.tailrec | ||
import scala.util.control.NonFatal | ||
import java.util.{Timer, TimerTask} | ||
|
||
import scala.annotation.internal.sharable | ||
import scala.annotation.threadUnsafe | ||
|
@@ -67,6 +69,12 @@ object Implicits { | |
final val Extension = 4 | ||
} | ||
|
||
/** Timeout to test a single implicit value as a suggestion, in ms */ | ||
val testOneImplicitTimeOut = 500 | ||
|
||
/** Global timeout to stop looking for further implicit suggestions, in ms */ | ||
val suggestImplicitTimeOut = 10000 | ||
|
||
/** A common base class of contextual implicits and of-type implicits which | ||
* represents a set of references to implicit definitions. | ||
*/ | ||
|
@@ -462,6 +470,125 @@ object Implicits { | |
def explanation(implicit ctx: Context): String = | ||
em"${err.refStr(ref)} produces a diverging implicit search when trying to $qualify" | ||
} | ||
|
||
/** A helper class to find imports of givens that might fix a type error. | ||
* | ||
* suggestions(p).search | ||
* | ||
* returns a list of TermRefs that refer to implicits or givens | ||
* that satisfy predicate `p`. | ||
* | ||
* The search algorithm looks for givens in the smallest set of objects | ||
* and packages that includes | ||
* | ||
* - any object that is a defined in an enclosing scope, | ||
* - any object that is a member of an enclosing class, | ||
* - any enclosing package (including the root package), | ||
* - any object that is a member of a searched object or package, | ||
* - any object or package from which something is imported in an enclosing scope, | ||
* - any package that is nested in a searched package, provided | ||
* the package was accessed in some way previously. | ||
*/ | ||
class suggestions(qualifies: TermRef => Boolean) with | ||
private val seen = mutable.Set[TermRef]() | ||
|
||
private def lookInside(root: Symbol)(given Context): Boolean = | ||
if root.is(Package) then root.isTerm && root.isCompleted | ||
else !root.name.is(FlatName) | ||
&& !root.name.lastPart.contains('$') | ||
&& root.is(ModuleVal, butNot = JavaDefined) | ||
|
||
def nestedRoots(site: Type)(given Context): List[Symbol] = | ||
val seenNames = mutable.Set[Name]() | ||
site.baseClasses.flatMap { bc => | ||
bc.info.decls.filter { dcl => | ||
lookInside(dcl) | ||
&& !seenNames.contains(dcl.name) | ||
&& { seenNames += dcl.name; true } | ||
} | ||
} | ||
|
||
private def rootsStrictlyIn(ref: Type)(given Context): List[TermRef] = | ||
val site = ref.widen | ||
val refSym = site.typeSymbol | ||
val nested = | ||
if refSym.is(Package) then | ||
if refSym == defn.EmptyPackageClass // Don't search the empty package | ||
|| refSym == defn.JavaPackageClass // As an optimization, don't search java... | ||
|| refSym == defn.JavaLangPackageClass // ... or java.lang. | ||
then Nil | ||
else refSym.info.decls.filter(lookInside) | ||
else | ||
if !refSym.is(Touched) then refSym.ensureCompleted() // JavaDefined is reliably known only after completion | ||
if refSym.is(JavaDefined) then Nil | ||
else nestedRoots(site) | ||
nested | ||
.map(mbr => TermRef(ref, mbr.asTerm)) | ||
.flatMap(rootsIn) | ||
.toList | ||
|
||
private def rootsIn(ref: TermRef)(given Context): List[TermRef] = | ||
if seen.contains(ref) then Nil | ||
else | ||
implicits.println(i"search for suggestions in ${ref.symbol.fullName}") | ||
seen += ref | ||
ref :: rootsStrictlyIn(ref) | ||
|
||
private def rootsOnPath(tp: Type)(given Context): List[TermRef] = tp match | ||
case ref: TermRef => rootsIn(ref) ::: rootsOnPath(ref.prefix) | ||
case _ => Nil | ||
|
||
private def roots(given ctx: Context): List[TermRef] = | ||
if ctx.owner.exists then | ||
val defined = | ||
if ctx.owner.isClass then | ||
if ctx.owner eq ctx.outer.owner then Nil | ||
else rootsStrictlyIn(ctx.owner.thisType) | ||
else | ||
if ctx.scope eq ctx.outer.scope then Nil | ||
else ctx.scope | ||
.filter(lookInside(_)) | ||
.flatMap(sym => rootsIn(sym.termRef)) | ||
val imported = | ||
if ctx.importInfo eq ctx.outer.importInfo then Nil | ||
else ctx.importInfo.sym.info match | ||
case ImportType(expr) => rootsOnPath(expr.tpe) | ||
case _ => Nil | ||
defined ++ imported ++ roots(given ctx.outer) | ||
else Nil | ||
|
||
def search(given ctx: Context): List[TermRef] = | ||
val timer = new Timer() | ||
val deadLine = System.currentTimeMillis() + suggestImplicitTimeOut | ||
|
||
def test(ref: TermRef)(given Context): Boolean = | ||
System.currentTimeMillis < deadLine | ||
&& { | ||
val task = new TimerTask with | ||
def run() = | ||
implicits.println(i"Cancelling test of $ref when making suggestions for error in ${ctx.source}") | ||
ctx.run.isCancelled = true | ||
timer.schedule(task, testOneImplicitTimeOut) | ||
try qualifies(ref) | ||
finally | ||
task.cancel() | ||
ctx.run.isCancelled = false | ||
} | ||
|
||
try | ||
roots | ||
.filterNot(root => defn.RootImportTypes.exists(_.symbol == root.symbol)) | ||
// don't suggest things that are imported by default | ||
.flatMap(_.implicitMembers.filter(test)) | ||
catch | ||
case ex: Throwable => | ||
if ctx.settings.Ydebug.value then | ||
println("caught exception when searching for suggestions") | ||
ex.printStackTrace() | ||
Nil | ||
finally timer.cancel() | ||
end search | ||
end suggestions | ||
} | ||
|
||
import Implicits._ | ||
|
@@ -683,6 +810,33 @@ trait Implicits { self: Typer => | |
} | ||
} | ||
|
||
/** An addendum to an error message where the error might be fixed | ||
* by some implicit value of type `pt` that is however not found. | ||
* The addendum suggests given imports that might fix the problem. | ||
* If there's nothing to suggest, an empty string is returned. | ||
*/ | ||
override def implicitSuggestionsFor(pt: Type)(given ctx: Context): String = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This signature looks error-prone to me as there is no way to distinguish between the presence and the absence of a suggestion (in any case, a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact an empty string is preferable, since it's easier to consume. Generally, the compiler always prefers sentinels over optional values, as long as feasible. |
||
val suggestedRefs = | ||
Implicits.suggestions(_ <:< pt).search(given ctx.fresh.setExploreTyperState()) | ||
def importString(ref: TermRef): String = | ||
s" import ${ctx.printer.toTextRef(ref).show}" | ||
val suggestions = suggestedRefs.map(importString) | ||
.filter(_.contains('.')) | ||
.distinct // TermRefs might be different but generate the same strings | ||
.sorted // To get test stability. TODO: Find more useful sorting criteria | ||
if suggestions.isEmpty then "" | ||
else | ||
val fix = | ||
if suggestions.tail.isEmpty then "The following import" | ||
else "One of the following imports" | ||
i""" | ||
| | ||
|$fix might fix the problem: | ||
| | ||
|$suggestions%\n% | ||
""" | ||
end implicitSuggestionsFor | ||
|
||
/** Handlers to synthesize implicits for special types */ | ||
type SpecialHandler = (Type, Span) => Context => Tree | ||
type SpecialHandlers = List[(ClassSymbol, SpecialHandler)] | ||
|
@@ -1215,32 +1369,37 @@ trait Implicits { self: Typer => | |
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), | ||
pt.widenExpr.argInfos)) | ||
|
||
def hiddenImplicitsAddendum: String = arg.tpe match { | ||
case fail: SearchFailureType => | ||
|
||
def hiddenImplicitNote(s: SearchSuccess) = | ||
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`." | ||
def hiddenImplicitsAddendum: String = | ||
|
||
def hiddenImplicitNote(s: SearchSuccess) = | ||
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`." | ||
|
||
def FindHiddenImplicitsCtx(ctx: Context): Context = | ||
if (ctx == NoContext) ctx | ||
else ctx.freshOver(FindHiddenImplicitsCtx(ctx.outer)).addMode(Mode.FindHiddenImplicits) | ||
|
||
val normalImports = arg.tpe match | ||
case fail: SearchFailureType => | ||
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then | ||
inferImplicit(fail.expectedType, fail.argument, arg.span)( | ||
FindHiddenImplicitsCtx(ctx)) match { | ||
case s: SearchSuccess => hiddenImplicitNote(s) | ||
case f: SearchFailure => | ||
f.reason match { | ||
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1) | ||
case r => "" | ||
} | ||
} | ||
else | ||
// It's unsafe to search for parts of the expected type if they are not fully defined, | ||
// since these come with nested contexts that are lost at this point. See #7249 for an | ||
// example where searching for a nested type causes an infinite loop. | ||
"" | ||
|
||
def FindHiddenImplicitsCtx(ctx: Context): Context = | ||
if (ctx == NoContext) ctx | ||
else ctx.freshOver(FindHiddenImplicitsCtx(ctx.outer)).addMode(Mode.FindHiddenImplicits) | ||
def suggestedImports = implicitSuggestionsFor(pt) | ||
if normalImports.isEmpty then suggestedImports else normalImports | ||
end hiddenImplicitsAddendum | ||
|
||
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then | ||
inferImplicit(fail.expectedType, fail.argument, arg.span)( | ||
FindHiddenImplicitsCtx(ctx)) match { | ||
case s: SearchSuccess => hiddenImplicitNote(s) | ||
case f: SearchFailure => | ||
f.reason match { | ||
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1) | ||
case r => "" | ||
} | ||
} | ||
else | ||
// It's unsafe to search for parts of the expected type if they are not fully defined, | ||
// since these come with nested contexts that are lost at this point. See #7249 for an | ||
// example where searching for a nested type causes an infinite loop. | ||
"" | ||
} | ||
msg(userDefined.getOrElse( | ||
em"no implicit argument of type $pt was found${location("for")}"))() ++ | ||
hiddenImplicitsAddendum | ||
|
@@ -1378,6 +1537,7 @@ trait Implicits { self: Typer => | |
|
||
/** Try to typecheck an implicit reference */ | ||
def typedImplicit(cand: Candidate, contextual: Boolean)(implicit ctx: Context): SearchResult = trace(i"typed implicit ${cand.ref}, pt = $pt, implicitsEnabled == ${ctx.mode is ImplicitsEnabled}", implicits, show = true) { | ||
if ctx.run.isCancelled then return NoMatchingImplicitsFailure | ||
record("typedImplicit") | ||
val ref = cand.ref | ||
val generated: Tree = tpd.ref(ref).withSpan(span.startPos) | ||
|
Uh oh!
There was an error while loading. Please reload this page.