diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 91dd4896fdfb..a6f99c21f635 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 9ae1b38c8a41..127e6647f204 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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") diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 62a408209c20..8729abe877a8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala index e4ae285ce3c1..adf0f1a75583 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala @@ -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) } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 8542caa8e637..0f5ba5d027d6 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -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) @@ -438,6 +438,7 @@ class PlainPrinter(_ctx: Context) extends Printer { (kindString(sym) ~~ { if (sym.isAnonymousClass) toTextParents(sym.info.parents) ~~ "{...}" else if (hasMeaninglessName(sym) && !printDebug) simpleNameString(sym.owner) + idString(sym) + else if sym.is(Package) then fullNameString(sym) else nameString(sym) }).close diff --git a/compiler/src/dotty/tools/dotc/printing/Printer.scala b/compiler/src/dotty/tools/dotc/printing/Printer.scala index e5a2a510436d..9d301a869f8d 100644 --- a/compiler/src/dotty/tools/dotc/printing/Printer.scala +++ b/compiler/src/dotty/tools/dotc/printing/Printer.scala @@ -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 @@ -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 diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 83c0912a325e..e5ed7031b071 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -900,6 +900,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def name = if (printDebug) nameString(sym) + else if sym.is(Package) then + fullNameString(sym) else if (sym.is(ModuleClass) && sym.isPackageObject && sym.name.stripModuleClassSuffix == tpnme.PACKAGE) nameString(sym.owner.name) else if (sym.is(ModuleClass)) diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index 7db48670e03a..5afbb123143c 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -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 } diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index 9a64593c614f..ae360fbd9a0f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -343,33 +343,35 @@ object messages { } // Get closest match in `site` - val closest = + def closest: List[String] = 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 min n.length) => + (n, dist, sym) + } .groupBy(_._2).toList .sortBy(_._1) .headOption.map(_._2).getOrElse(Nil) .map(incorrectChars).toList .sortBy(_._3) - .take(1).map { case (n, sym, _) => (n, sym) } - - val siteName = site match { - case site: NamedType => site.name.show - case site => i"$site" - } - - val closeMember = closest match { - case (n, sym) :: Nil => - s" - did you mean $siteName.$n?" - case Nil => "" - case _ => assert( - false, - "Could not single out one distinct member to match on input with" - ) - } + .map(_._1) + // [Martin] Note: I have no idea what this does. This shows the + // pitfalls of not naming things, functional or not. + + val finalAddendum = + if addendum.nonEmpty then addendum + else closest match { + case n :: _ => + val siteName = site match + case site: NamedType => site.name.show + case site => i"$site" + s" - did you mean $siteName.$n?" + case Nil => "" + } - ex"$selected $name is not a member of ${site.widen}$closeMember$addendum" + ex"$selected $name is not a member of ${site.widen}$finalAddendum" } val explanation: String = "" diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index b74ad17a1311..f83159c69b6d 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -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.importSuggestionAddendum(ViewProto(found.widen, expected)) + TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript1) } /** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 0d8111fbd642..58456da5dcaf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -14,7 +14,7 @@ import Flags._ import TypeErasure.{erasure, hasStableErasure} import Mode.ImplicitsEnabled import NameOps._ -import NameKinds.LazyImplicitName +import NameKinds.{LazyImplicitName, EvidenceParamName} import Symbols._ import Denotations._ import Types._ @@ -67,6 +67,14 @@ object Implicits { final val Extension = 4 } + /** If `expected` is a selection prototype, does `tp` have an extension + * method with the selecting name? False otherwise. + */ + def hasExtMethod(tp: Type, expected: Type)(given Context) = expected match + case SelectionProto(name, _, _, _) => + tp.memberBasedOnFlags(name, required = ExtensionMethod).exists + case _ => false + /** A common base class of contextual implicits and of-type implicits which * represents a set of references to implicit definitions. */ @@ -147,11 +155,7 @@ object Implicits { val isImplicitConversion = tpw.derivesFrom(defn.ConversionClass) // An implementation of <:< counts as a view val isConforms = tpw.derivesFrom(defn.SubTypeClass) - val hasExtensions = resType match { - case SelectionProto(name, _, _, _) => - tpw.memberBasedOnFlags(name, required = ExtensionMethod).exists - case _ => false - } + val hasExtensions = hasExtMethod(tpw, resType) val conversionKind = if (isFunctionInS2 || isImplicitConversion || isConforms) Candidate.Conversion else Candidate.None @@ -1215,32 +1219,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 = importSuggestionAddendum(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 @@ -1256,7 +1265,8 @@ trait Implicits { self: Typer => def addendum = if (qt1 eq qt) "" else (i"\nwhich is an alias of: $qt1") em"parameter of ${qual.tpe.widen}$addendum" case _ => - em"parameter ${paramName} of $methodStr" + em"${ if paramName.is(EvidenceParamName) then "an implicit parameter" + else s"parameter $paramName" } of $methodStr" } private def strictEquality(implicit ctx: Context): Boolean = @@ -1352,32 +1362,10 @@ trait Implicits { self: Typer => ctx.searchHistory.emitDictionary(span, result) } - /** An implicit search; parameters as in `inferImplicit` */ - class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(implicit ctx: Context) { - assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType], - em"found: $argument: ${argument.tpe}, expected: $pt") - - private def nestedContext() = - ctx.fresh.setMode(ctx.mode &~ Mode.ImplicitsEnabled) - - private def implicitProto(resultType: Type, f: Type => Type) = - if (argument.isEmpty) f(resultType) else ViewProto(f(argument.tpe.widen), f(resultType)) - // Not clear whether we need to drop the `.widen` here. All tests pass with it in place, though. - - private def isCoherent = pt.isRef(defn.EqlClass) - - /** The expected type for the searched implicit */ - @threadUnsafe lazy val fullProto: Type = implicitProto(pt, identity) - - /** The expected type where parameters and uninstantiated typevars are replaced by wildcard types */ - val wildProto: Type = implicitProto(pt, wildApprox(_)) - - val isNot: Boolean = wildProto.classSymbol == defn.NotClass - - //println(i"search implicits $pt / ${eligible.map(_.ref)}") - - /** 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) { + /** Try to typecheck an implicit reference */ + def typedImplicit(cand: Candidate, pt: Type, argument: Tree, span: Span)(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 NoMatchingImplicitsFailure + else record("typedImplicit") val ref = cand.ref val generated: Tree = tpd.ref(ref).withSpan(span.startPos) @@ -1431,6 +1419,30 @@ trait Implicits { self: Typer => } } + /** An implicit search; parameters as in `inferImplicit` */ + class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(implicit ctx: Context) { + assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType], + em"found: $argument: ${argument.tpe}, expected: $pt") + + private def nestedContext() = + ctx.fresh.setMode(ctx.mode &~ Mode.ImplicitsEnabled) + + private def implicitProto(resultType: Type, f: Type => Type) = + if (argument.isEmpty) f(resultType) else ViewProto(f(argument.tpe.widen), f(resultType)) + // Not clear whether we need to drop the `.widen` here. All tests pass with it in place, though. + + private def isCoherent = pt.isRef(defn.EqlClass) + + /** The expected type for the searched implicit */ + @threadUnsafe lazy val fullProto: Type = implicitProto(pt, identity) + + /** The expected type where parameters and uninstantiated typevars are replaced by wildcard types */ + val wildProto: Type = implicitProto(pt, wildApprox(_)) + + val isNot: Boolean = wildProto.classSymbol == defn.NotClass + + //println(i"search implicits $pt / ${eligible.map(_.ref)}") + /** Try to type-check implicit reference, after checking that this is not * a diverging search */ @@ -1440,7 +1452,7 @@ trait Implicits { self: Typer => else { val history = ctx.searchHistory.nest(cand, pt) val result = - typedImplicit(cand, contextual)(nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)) + typedImplicit(cand, pt, argument, span)(nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)) result match { case res: SearchSuccess => ctx.searchHistory.defineBynameImplicit(pt.widenExpr, res) diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala new file mode 100644 index 000000000000..bd8641bc49fa --- /dev/null +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -0,0 +1,263 @@ +package dotty.tools +package dotc +package typer + +import core._ +import Contexts._, Types._, Symbols._, Names._, Decorators._, ProtoTypes._ +import Flags._ +import NameKinds.FlatName +import config.Printers.implicits +import util.Spans.Span +import ast.{untpd, tpd} +import Implicits.{hasExtMethod, Candidate} +import java.util.{Timer, TimerTask} +import collection.mutable + +/** This trait defines the method `importSuggestionAddendum` that adds an addendum + * to error messages suggesting additional imports. + */ +trait ImportSuggestions with + this: Typer => + + import tpd._ + + /** Timeout to test a single implicit value as a suggestion, in ms */ + private inline val testOneImplicitTimeOut = 500 + + /** Global timeout to stop looking for further implicit suggestions, in ms */ + private inline val suggestImplicitTimeOut = 10000 + + /** A list of TermRefs referring to the roots where suggestions for + * imports of givens or extension methods that might fix a type error + * are searched. + * + * These roots are 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. + * + * Excluded from the root set are: + * + * - Objects that contain `$`s in their name. These have to + * be omitted since they might be inner Java class files which + * cannot be read by the ClassfileParser without crashing. + * - Any members of static parts of Java classes. + * - Any members of the empty package. These should be + * skipped since the empty package often contains unrelated junk files + * that should not be used for suggestions. + * - Any members of the java or java.lang packages. These are + * skipped as an optimization, since they won't contain implicits anyway. + */ + private def suggestionRoots(given Context) = + val seen = mutable.Set[TermRef]() + + 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 } + } + } + + 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 + + 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) + + def rootsOnPath(tp: Type)(given Context): List[TermRef] = tp match + case ref: TermRef => rootsIn(ref) ::: rootsOnPath(ref.prefix) + case _ => Nil + + def recur(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 ++ recur(given ctx.outer) + else Nil + + recur + end suggestionRoots + + /** Given an expected type `pt`, return two lists of TermRefs: + * + * 1. The _fully matching_ given instances that can be completed + * to a full synthesized given term that matches the expected type `pt`. + * + * 2. The _head matching_ given instances, that conform to the + * expected type `pt`, ignoring any dependent implicit arguments. + * + * If there are no fully matching given instances under (1), and `pt` is + * a view prototype of a selection of the form `T ?=>? { name: ... }`, + * return instead a list of all possible references to extension methods named + * `name` that are applicable to `T`. + */ + private def importSuggestions(pt: Type)(given ctx: Context): (List[TermRef], List[TermRef]) = + val timer = new Timer() + val deadLine = System.currentTimeMillis() + suggestImplicitTimeOut + + /** Test whether the head of a given instance matches the expected type `pt`, + * ignoring any dependent implicit arguments. + */ + def shallowTest(ref: TermRef): Boolean = + System.currentTimeMillis < deadLine + && { + given Context = ctx.fresh.setExploreTyperState() + pt match + case pt: ViewProto => pt.isMatchedBy(ref) + case _ => normalize(ref, pt) <:< pt + } + + /** Test whether a full given term can be synthesized that matches + * the expected type `pt`. + */ + def deepTest(ref: TermRef): Boolean = + System.currentTimeMillis < deadLine + && { + val task = new TimerTask with + def run() = + println(i"Cancelling test of $ref when making suggestions for error in ${ctx.source}") + ctx.run.isCancelled = true + val span = ctx.owner.sourcePos.span + val (expectedType, argument, kind) = pt match + case ViewProto(argType, resType) => + (resType, + untpd.Ident(ref.name).withSpan(span).withType(argType), + if hasExtMethod(ref, resType) then Candidate.Extension + else Candidate.Conversion) + case _ => + (pt, EmptyTree, Candidate.Value) + val candidate = Candidate(ref, kind, 0) + try + timer.schedule(task, testOneImplicitTimeOut) + typedImplicit(candidate, expectedType, argument, span)( + given ctx.fresh.setExploreTyperState()).isSuccess + finally + task.cancel() + ctx.run.isCancelled = false + } + end deepTest + + /** Optionally, an extension method reference `site.name` that is + * applicable to `argType`. + */ + def extensionMethod(site: TermRef, name: TermName, argType: Type): Option[TermRef] = + site.member(name) + .alternatives + .map(mbr => TermRef(site, mbr.symbol)) + .filter(ref => + ref.symbol.is(Extension) + && isApplicableMethodRef(ref, argType :: Nil, WildcardType)) + .headOption + + try + val roots = suggestionRoots + .filterNot(root => defn.RootImportTypes.exists(_.symbol == root.symbol)) + // don't suggest things that are imported by default + + def extensionImports = pt match + case ViewProto(argType, SelectionProto(name: TermName, _, _, _)) => + roots.flatMap(extensionMethod(_, name, argType)) + case _ => + Nil + + roots + .flatMap(_.implicitMembers.filter(shallowTest)) + // filter whether the head of the implicit can match + .partition(deepTest) + // partition into full matches and head matches + match + case (Nil, partials) => (extensionImports, partials) + case givenImports => givenImports + catch + case ex: Throwable => + if ctx.settings.Ydebug.value then + println("caught exception when searching for suggestions") + ex.printStackTrace() + (Nil, Nil) + finally timer.cancel() + end importSuggestions + + /** 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 importSuggestionAddendum(pt: Type)(given ctx: Context): String = + val (fullMatches, headMatches) = + importSuggestions(pt)(given ctx.fresh.setExploreTyperState()) + implicits.println(i"suggestions for $pt in ${ctx.owner} = ($fullMatches%, %, $headMatches%, %)") + val (suggestedRefs, help) = + if fullMatches.nonEmpty then (fullMatches, "fix") + else (headMatches, "make progress towards fixing") + def importString(ref: TermRef): String = + s" import ${ctx.printer.toTextRef(ref).show}" + val suggestions = suggestedRefs + .zip(suggestedRefs.map(importString)) + .filter((ref, str) => str.contains('.')) + .sortWith { (x, y) => + // sort by specificity first, alphabetically second + val ((ref1, str1), (ref2, str2)) = (x, y) + val diff = compare(ref1, ref2) + diff > 0 || diff == 0 && str1 < str2 + } + .map((ref, str) => str) + .distinct // TermRefs might be different but generate the same strings + if suggestions.isEmpty then "" + else + val fix = + if suggestions.tail.isEmpty then "The following import" + else "One of the following imports" + i""" + | + |$fix might $help the problem: + | + |$suggestions%\n% + """ + end importSuggestionAddendum +end ImportSuggestions diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index d31e2b6c1bfd..3fafcef1bace 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -11,6 +11,7 @@ import util.SourcePosition import config.Printers.typr import ast.Trees._ import NameOps._ +import ProtoTypes._ import collection.mutable import reporting.diagnostic.messages._ import Checking.{checkNoPrivateLeaks, checkNoWildcard} @@ -266,10 +267,10 @@ trait TypeAssigner { errorType(ex"$qualType does not have a constructor", tree.sourcePos) else { val kind = if (name.isTypeName) "type" else "value" - val addendum = + def addendum = if (qualType.derivesFrom(defn.DynamicClass)) "\npossible cause: maybe a wrong Dynamic method signature?" - else qual1.getAttachment(Typer.HiddenSearchFailure) match { + else qual1.getAttachment(Typer.HiddenSearchFailure) match case Some(failure) if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits] => i""". |An extension method was tried, but could not be fully constructed: @@ -281,12 +282,19 @@ trait TypeAssigner { |Note that `$name` is treated as an infix operator in Scala 3. |If you do not want that, insert a `;` or empty line in front |or drop any spaces behind the operator.""" - else "" - } + else + var add = importSuggestionAddendum( + ViewProto(qualType.widen, + SelectionProto(name, WildcardType, NoViewsAllowed, privateOK = false))) + if add.isEmpty then "" + else ", but could be made available as an extension method." ++ add + end addendum errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos) } } + def importSuggestionAddendum(pt: Type)(given Context): String = "" + /** The type of the selection in `tree`, where `qual1` is the typed qualifier part. * The selection type is additionally checked for accessibility. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 87c5c93db675..248706dc4db0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -84,6 +84,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits + with ImportSuggestions with Inferencing with Dynamic with Checking @@ -2210,7 +2211,9 @@ class Typer extends Namer if (tree.source != ctx.source && tree.source.exists) typed(tree, pt, locked)(ctx.withSource(tree.source)) else - try adapt(typedUnadapted(tree, pt, locked), pt, locked) + try + if ctx.run.isCancelled then tree.withType(WildcardType) + else adapt(typedUnadapted(tree, pt, locked), pt, locked) catch { case ex: TypeError => errorTree(tree, ex, tree.sourcePos.focus) diff --git a/compiler/test-resources/repl/1379 b/compiler/test-resources/repl/1379 index 241b57166b76..902d6251a62b 100644 --- a/compiler/test-resources/repl/1379 +++ b/compiler/test-resources/repl/1379 @@ -1,4 +1,4 @@ scala> object Foo { val bar = new Object { def baz = 1 }; bar.baz } 1 | object Foo { val bar = new Object { def baz = 1 }; bar.baz } | ^^^^^^^ - | value baz is not a member of Object - did you mean bar.eq? \ No newline at end of file + | value baz is not a member of Object \ No newline at end of file diff --git a/compiler/test-resources/repl/i6676 b/compiler/test-resources/repl/i6676 index f543c6ae2de3..9fc239ca9855 100644 --- a/compiler/test-resources/repl/i6676 +++ b/compiler/test-resources/repl/i6676 @@ -8,7 +8,7 @@ scala> xml" scala> xml"" 1 | xml"" | ^ - |value xml is not a member of StringContext - did you mean StringContext.s? + | value xml is not a member of StringContext scala> xml""" 1 | xml""" | ^ diff --git a/compiler/test-resources/repl/importFromObj b/compiler/test-resources/repl/importFromObj index 31753dfbd821..4192e6d8d4c5 100644 --- a/compiler/test-resources/repl/importFromObj +++ b/compiler/test-resources/repl/importFromObj @@ -14,8 +14,8 @@ val res0: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3) scala> import util.foo 1 | import util.foo | ^^^ - | value foo is not a member of util - did you mean util.Try? + | value foo is not a member of util scala> import util.foo.bar 1 | import util.foo.bar | ^^^^^^^^ - | value foo is not a member of util - did you mean util.Try? + | value foo is not a member of util diff --git a/tests/neg/missing-implicit.check b/tests/neg/missing-implicit.check new file mode 100644 index 000000000000..8ac23fee4918 --- /dev/null +++ b/tests/neg/missing-implicit.check @@ -0,0 +1,72 @@ +-- [E008] Member Not Found Error: tests/neg/missing-implicit.scala:5:25 ------------------------------------------------ +5 | case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error + | ^^^^^^^ + | value > is not a member of T, but could be made available as an extension method. + | + | One of the following imports might fix the problem: + | + | import math.Ordered.orderingToOrdered + | import math.Ordering.Implicits.infixOrderingOps + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit.scala:5:51 ------------------------------------------------ +5 | case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error + | ^^^^^^^ + | value - is not a member of T, but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import math.Numeric.Implicits.infixNumericOps + | +-- Error: tests/neg/missing-implicit.scala:10:24 ----------------------------------------------------------------------- +10 |val f = Future[Unit] { } // error + | ^ + | Cannot find an implicit ExecutionContext. You might pass + | an (implicit ec: ExecutionContext) parameter to your method. + | + | The ExecutionContext is used to configure how and on which + | thread pools Futures will run, so the specific ExecutionContext + | that is selected is important. + | + | If your application does not define an ExecutionContext elsewhere, + | consider using Scala's global ExecutionContext by defining + | the following: + | + | implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global + | + | The following import might fix the problem: + | + | import concurrent.ExecutionContext.Implicits.global + | +-- [E007] Type Mismatch Error: tests/neg/missing-implicit.scala:12:25 -------------------------------------------------- +12 |val b: java.lang.Byte = (1: Byte) // error, but no hint + | ^^^^^^^ + | Found: Byte + | Required: Byte² + | + | where: Byte is a class in package scala + | Byte² is a class in package java.lang +-- [E007] Type Mismatch Error: tests/neg/missing-implicit.scala:16:44 -------------------------------------------------- +16 |val d: scala.concurrent.duration.Duration = (10, DAYS) // error + | ^^^^^^^^^^ + | Found: (Int, java.util.concurrent.TimeUnit) + | Required: concurrent².duration.Duration + | + | where: concurrent is a package in package java.util + | concurrent² is a package in package scala + | + | + | The following import might fix the problem: + | + | import concurrent.duration.pairIntToDuration + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit.scala:18:48 ----------------------------------------------- +18 |val d2: scala.concurrent.duration.Duration = 10.days // error + | ^^^^^^^ + | value days is not a member of Int, but could be made available as an extension method. + | + | One of the following imports might fix the problem: + | + | import concurrent.duration.DurationInt + | import concurrent.duration.DurationLong + | import concurrent.duration.DurationDouble + | diff --git a/tests/neg/missing-implicit.scala b/tests/neg/missing-implicit.scala new file mode 100644 index 000000000000..34d88f0e74eb --- /dev/null +++ b/tests/neg/missing-implicit.scala @@ -0,0 +1,18 @@ +import Predef.{byte2Byte => _, _} +import math.Numeric + +def consume[T: Numeric](xs: List[T], limit: T): List[T] = xs match + case x :: xs1 if limit > 0 => consume(xs1, limit - x) // error // error + case _ => xs + +import scala.concurrent.Future + +val f = Future[Unit] { } // error + +val b: java.lang.Byte = (1: Byte) // error, but no hint + +val DAYS = scala.concurrent.duration.DAYS + +val d: scala.concurrent.duration.Duration = (10, DAYS) // error + +val d2: scala.concurrent.duration.Duration = 10.days // error diff --git a/tests/neg/missing-implicit1.check b/tests/neg/missing-implicit1.check new file mode 100644 index 000000000000..f04a2c248b69 --- /dev/null +++ b/tests/neg/missing-implicit1.check @@ -0,0 +1,76 @@ +-- Error: tests/neg/missing-implicit1.scala:17:4 ----------------------------------------------------------------------- +17 | ff // error + | ^ + |no implicit argument of type testObjectInstance.Zip[Option] was found for parameter xs of method ff in object testObjectInstance + | + |The following import might fix the problem: + | + | import testObjectInstance.instances.zipOption + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit1.scala:19:16 ---------------------------------------------- +19 | List(1, 2, 3).traverse(x => Option(x)) // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | value traverse is not a member of List[Int], but could be made available as an extension method. + | + | The following import might make progress towards fixing the problem: + | + | import testObjectInstance.instances.traverseList + | +-- Error: tests/neg/missing-implicit1.scala:23:42 ---------------------------------------------------------------------- +23 | List(1, 2, 3).traverse(x => Option(x)) // error + | ^ + |no implicit argument of type testObjectInstance.Zip[Option] was found for an implicit parameter of method traverse in trait Traverse + | + |The following import might fix the problem: + | + | import testObjectInstance.instances.zipOption + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit1.scala:26:16 ---------------------------------------------- +26 | List(1, 2, 3).first // error + | ^^^^^^^^^^^^^^^^^^^ + | value first is not a member of List[Int], but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import testObjectInstance.instances.first + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit1.scala:27:16 ---------------------------------------------- +27 | List(1, 2, 3).second // error + | ^^^^^^^^^^^^^^^^^^^^ + | value second is not a member of List[Int], but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import testObjectInstance.instances.listExtension + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit1.scala:28:17 ---------------------------------------------- +28 | Array(1, 2, 3).first // error, no hint + | ^^^^^^^^^^^^^^^^^^^^ + | value first is not a member of Array[Int] +-- Error: tests/neg/missing-implicit1.scala:44:4 ----------------------------------------------------------------------- +44 | ff // error + | ^ + | no implicit argument of type Zip[Option] was found for parameter xs of method ff + | + | The following import might fix the problem: + | + | import instances.zipOption + | +-- [E008] Member Not Found Error: tests/neg/missing-implicit1.scala:46:16 ---------------------------------------------- +46 | List(1, 2, 3).traverse(x => Option(x)) // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | value traverse is not a member of List[Int], but could be made available as an extension method. + | + | The following import might make progress towards fixing the problem: + | + | import instances.traverseList + | +-- Error: tests/neg/missing-implicit1.scala:50:42 ---------------------------------------------------------------------- +50 | List(1, 2, 3).traverse(x => Option(x)) // error + | ^ + |no implicit argument of type Zip[Option] was found for an implicit parameter of method traverse in trait Traverse + | + |The following import might fix the problem: + | + | import instances.zipOption + | diff --git a/tests/neg/missing-implicit1.scala b/tests/neg/missing-implicit1.scala new file mode 100644 index 000000000000..88d5bb23a51d --- /dev/null +++ b/tests/neg/missing-implicit1.scala @@ -0,0 +1,52 @@ +object testObjectInstance with + trait Zip[F[_]] + trait Traverse[F[_]] { + def [A, B, G[_] : Zip](fa: F[A]) traverse(f: A => G[B]): G[F[B]] + } + + object instances { + given zipOption: Zip[Option] = ??? + given traverseList: Traverse[List] = ??? + given listExtension: [T](xs: List[T]) extended with + def second: T = xs.tail.head + def [T](xs: List[T]) first: T = xs.head + } + + def ff(given xs: Zip[Option]) = ??? + + ff // error + + List(1, 2, 3).traverse(x => Option(x)) // error + + locally { + import instances.traverseList + List(1, 2, 3).traverse(x => Option(x)) // error + } + + List(1, 2, 3).first // error + List(1, 2, 3).second // error + Array(1, 2, 3).first // error, no hint +end testObjectInstance + +def testLocalInstance = + trait Zip[F[_]] + trait Traverse[F[_]] { + def [A, B, G[_] : Zip](fa: F[A]) traverse(f: A => G[B]): G[F[B]] + } + + object instances { + given zipOption: Zip[Option] = ??? + given traverseList: Traverse[List] = ??? + } + + def ff(given xs: Zip[Option]) = ??? + + ff // error + + List(1, 2, 3).traverse(x => Option(x)) // error + + locally { + import instances.traverseList + List(1, 2, 3).traverse(x => Option(x)) // error + } +end testLocalInstance \ No newline at end of file diff --git a/tests/neg/missing-implicit2.check b/tests/neg/missing-implicit2.check new file mode 100644 index 000000000000..59106a0042e4 --- /dev/null +++ b/tests/neg/missing-implicit2.check @@ -0,0 +1,18 @@ +-- Error: tests/neg/missing-implicit2.scala:10:18 ---------------------------------------------------------------------- +10 | f(given xFromY) // error + | ^ + | no implicit argument of type Y was found for parameter y of method xFromY + | + | The following import might fix the problem: + | + | import test.instances.y + | +-- Error: tests/neg/missing-implicit2.scala:16:5 ----------------------------------------------------------------------- +16 | f // error + | ^ + | no implicit argument of type X was found for parameter x of method f in object test + | + | The following import might make progress towards fixing the problem: + | + | import instances2.xFromY + | diff --git a/tests/neg/missing-implicit2.scala b/tests/neg/missing-implicit2.scala new file mode 100644 index 000000000000..e9ad60a2d368 --- /dev/null +++ b/tests/neg/missing-implicit2.scala @@ -0,0 +1,17 @@ +trait X +trait Y +object test with + def f(given x: X) = ??? + object instances { + given y: Y = ??? + } + locally { + given xFromY(given y: Y): X = ??? + f(given xFromY) // error + } + locally { + object instances2 { + given xFromY: Y => X = ??? + } + f // error + } diff --git a/tests/pos/i7700.scala b/tests/pos/i7700.scala index 913285822c31..bdca41ff87ce 100644 --- a/tests/pos/i7700.scala +++ b/tests/pos/i7700.scala @@ -1,12 +1,12 @@ package test -trait Show[-A] +trait Show[-A] with def show(a: A): String -object Macros +object Macros with inline def (sc: StringContext) show(args: =>Any*): String = ??? -object Show +object Show with def[A] (a: A) show(given S: Show[A]): String = S.show(a) export Macros.show \ No newline at end of file