Skip to content

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

Merged
merged 27 commits into from
Jan 5, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7cea875
Make "did you mean hints" less chatty
odersky Dec 28, 2019
9a3c33a
Make suggestions of missing implicits imports on type errors
odersky Dec 28, 2019
53a6098
Sort suggestions alphabetically
odersky Dec 29, 2019
b848c57
Don't treat package object's <init> methods as package members
odersky Dec 30, 2019
13c1839
Refine search and reporting criteria
odersky Dec 30, 2019
e1360eb
Don't suggest imports for conversions from Null or Nothing
odersky Dec 30, 2019
5e3f11c
Implement cancellation
odersky Dec 31, 2019
540f69a
Subject implicit suggestion searches to timeouts
odersky Dec 31, 2019
2ccec1f
Streamline suggestion printing
odersky Jan 1, 2020
51f19ec
Use java.util.Timer
odersky Jan 1, 2020
55b1a31
Drop Scheduler
odersky Jan 2, 2020
9c67ee4
Revise search logic
odersky Jan 2, 2020
b3a4966
Some refinements
odersky Jan 2, 2020
717cb7d
Refine implicit search criterion and prioritization
odersky Jan 4, 2020
ad5055b
Further refinement of "did you mean" hints
odersky Jan 4, 2020
848dd0a
Print package symbols always under their full names
odersky Jan 4, 2020
72df855
Fix repl test
odersky Jan 4, 2020
0501bfa
Refactorings and comments for better clarity
odersky Jan 4, 2020
1124b35
Fix two more repl tests
odersky Jan 4, 2020
c601a72
Issue a "did you mean" hint only if there's nothing else to say
odersky Jan 5, 2020
02d7594
Remove dead code and data in "did you mean" hints
odersky Jan 5, 2020
9e72e6c
Better explanation for missing members that have extension methods
odersky Jan 5, 2020
aabcd81
Also suggest imports of extension methods
odersky Jan 5, 2020
427da51
Factor out suggestions logic into its own trait
odersky Jan 5, 2020
5ac8b80
Also suggest partial matches for missing implicit arguments
odersky Jan 5, 2020
8289d51
Don't mention evidence$n parameters in error message
odersky Jan 5, 2020
6f94a83
Fix unrelated test
odersky Jan 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ import scala.util.control.NonFatal
/** A compiler run. Exports various methods to compile source files */
class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with ConstraintRunInfo {

/** If this variable is set to `true`, some core typer operations will
* return immediately. Currently these early abort operations are
* `Typer.typed` and `Implicits.typedImplicit`.
*/
@volatile var isCancelled = false

/** Produces the following contexts, from outermost to innermost
*
* bootStrap: A context with next available runId and a scope consisting of
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ class Definitions {
}
@tu lazy val ScalaPackageObject: Symbol = ctx.requiredModule("scala.package")
@tu lazy val JavaPackageVal: TermSymbol = ctx.requiredPackage(nme.java)
@tu lazy val JavaPackageClass: ClassSymbol = JavaPackageVal.moduleClass.asClass
@tu lazy val JavaLangPackageVal: TermSymbol = ctx.requiredPackage(jnme.JavaLang)
@tu lazy val JavaLangPackageClass: ClassSymbol = JavaLangPackageVal.moduleClass.asClass

// fundamental modules
@tu lazy val SysPackage : Symbol = ctx.requiredModule("scala.sys.package")
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2205,7 +2205,7 @@ object SymDenotations {

if (symbol `eq` defn.ScalaPackageClass) {
val denots = super.computeNPMembersNamed(name)
if (denots.exists) denots
if (denots.exists || name == nme.CONSTRUCTOR) denots
else recur(packageObjs, NoDenotation)
}
else recur(packageObjs, NoDenotation)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/TypeErrors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ object CyclicReference {
def apply(denot: SymDenotation)(implicit ctx: Context): CyclicReference = {
val ex = new CyclicReference(denot)
if (!(ctx.mode is Mode.CheckCyclic)) {
cyclicErrors.println(ex.getMessage)
cyclicErrors.println(s"Cyclic reference involving $denot")
for (elem <- ex.getStackTrace take 200)
cyclicErrors.println(elem.toString)
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
}

/** The string representation of this type used as a prefix */
protected def toTextRef(tp: SingletonType): Text = controlled {
def toTextRef(tp: SingletonType): Text = controlled {
tp match {
case tp: TermRef =>
toTextPrefix(tp.prefix) ~ selectionString(tp)
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/printing/Printer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package printing

import core._
import Texts._, ast.Trees._
import Types.Type, Symbols.Symbol, Scopes.Scope, Constants.Constant,
import Types.{Type, SingletonType}, Symbols.Symbol, Scopes.Scope, Constants.Constant,
Names.Name, Denotations._, Annotations.Annotation
import typer.Implicits.SearchResult
import util.SourcePosition
Expand Down Expand Up @@ -97,6 +97,9 @@ abstract class Printer {
*/
def toText(sym: Symbol): Text

/** Textual representation of singeton type reference */
def toTextRef(tp: SingletonType): Text

/** Textual representation of symbol's declaration */
def dclText(sym: Symbol): Text

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import diagnostic.messages._
import diagnostic._
import ast.{tpd, Trees}
import Message._
import core.Decorators._

import java.lang.System.currentTimeMillis
import java.io.{ BufferedReader, PrintWriter }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,10 @@ object messages {
// Get closest match in `site`
val closest =
decls
.map { case (n, sym) => (n, distance(n, name.show), sym) }
.collect { case (n, dist, sym) if dist <= maxDist => (n, dist, sym) }
.map { (n, sym) => (n, distance(n, name.show), sym) }
.collect {
case (n, dist, sym) if dist <= maxDist && dist < name.toString.length => (n, dist, sym)
}
.groupBy(_._2).toList
.sortBy(_._1)
.headOption.map(_._2).getOrElse(Nil)
Expand Down
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,15 @@ object ErrorReporting {
val expected1 = reported(expected)
val (found2, expected2) =
if (found1 frozen_<:< expected1) (found, expected) else (found1, expected1)
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript)
val postScript1 =
if !postScript.isEmpty
|| expected.isRef(defn.AnyClass)
|| expected.isRef(defn.AnyValClass)
|| expected.isRef(defn.ObjectClass)
|| defn.isBottomType(found)
then postScript
else ctx.typer.implicitSuggestionsFor(ViewProto(found.widen, expected))
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript1)
}

/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
Expand Down
210 changes: 185 additions & 25 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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._
Expand Down Expand Up @@ -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 =
Copy link
Contributor

Choose a reason for hiding this comment

The 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 String is returned). What do you think of returning an Option[String] instead? Returning an empty String to model the absence of suggestion does work here by chance because this method is the latest to be called in a chain of if/else if, but we should not have to look at how this method is called to be able to reason on its correctness (this is not the concern of this method, we should be able to locally reason about it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading