diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index e3f8d18214f1..bd2a1af2ed1c 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -13,12 +13,13 @@ import dotty.tools.dotc.core.Flags._ import dotty.tools.dotc.core.Names.{Name, TermName} import dotty.tools.dotc.core.NameKinds.SimpleNameKind import dotty.tools.dotc.core.NameOps._ -import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn, newSymbol} -import dotty.tools.dotc.core.Scopes +import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, TermSymbol, defn, newSymbol} +import dotty.tools.dotc.core.Scopes.Scope import dotty.tools.dotc.core.StdNames.{nme, tpnme} +import dotty.tools.dotc.core.SymDenotations.SymDenotation import dotty.tools.dotc.core.TypeComparer import dotty.tools.dotc.core.TypeError -import dotty.tools.dotc.core.Types.{ExprType, MethodType, NameFilter, NamedType, NoType, PolyType, Type} +import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NamedType, NoType, PolyType, TermRef, Type} import dotty.tools.dotc.printing.Texts._ import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition} @@ -105,191 +106,255 @@ object Completion { case _ => 0 } - /** Create a new `CompletionBuffer` for completing at `pos`. */ - private def completionBuffer(path: List[Tree], pos: SourcePosition): CompletionBuffer = { + private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { val mode = completionMode(path, pos) val prefix = completionPrefix(path, pos) - new CompletionBuffer(mode, prefix, pos) - } - - private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { + val completer = new Completer(mode, prefix, pos) - val offset = completionOffset(path) - val buffer = completionBuffer(path, pos) - - if (buffer.mode != Mode.None) - path match { - case Select(qual, _) :: _ => buffer.addSelectionCompletions(path, qual) - case Import(expr, _) :: _ => buffer.addMemberCompletions(expr) // TODO: distinguish given from plain imports - case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => buffer.addMemberCompletions(expr) - case _ => buffer.addScopeCompletions + val completions = path match { + case Select(qual, _) :: _ => completer.selectionCompletions(qual) + case Import(expr, _) :: _ => completer.directMemberCompletions(expr) + case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) + case _ => completer.scopeCompletions } - val completionList = buffer.getCompletions + val describedCompletions = describeCompletions(completions) + val offset = completionOffset(path) interactiv.println(i"""completion with pos = $pos, - | prefix = ${buffer.prefix}, - | term = ${buffer.mode.is(Mode.Term)}, - | type = ${buffer.mode.is(Mode.Type)} - | results = $completionList%, %""") - (offset, completionList) + | prefix = ${completer.prefix}, + | term = ${completer.mode.is(Mode.Term)}, + | type = ${completer.mode.is(Mode.Type)} + | results = $describedCompletions%, %""") + (offset, describedCompletions) } - private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) { + /** + * Return the list of code completions with descriptions based on a mapping from names to the denotations they refer to. + * If several denotations share the same name, the type denotations appear before term denotations inside + * the same `Completion`. + */ + private def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = { + completions + .toList.groupBy(_._1.toTermName) // don't distinguish between names of terms and types + .toList.map { (name, namedDenots) => + val denots = namedDenots.flatMap(_._2) + val typesFirst = denots.sortWith((d1, d2) => d1.isType && !d2.isType) + val desc = description(typesFirst) + Completion(name.show, desc, typesFirst.map(_.symbol)) + } + } - private val completions = new RenameAwareScope + /** + * A description for completion result that represents `symbols`. + * + * If `denots` contains a single denotation, show its full name in case it's a type, or its type if + * it's a term. + * + * When there are multiple denotations, show their kinds. + */ + private def description(denots: List[SingleDenotation])(using Context): String = + denots match { + case denot :: Nil => + if (denot.isType) denot.symbol.showFullName + else denot.info.widenTermRefExpr.show - /** - * Return the list of symbols that should be included in completion results. - * - * If several symbols share the same name, the type symbols appear before term symbols inside - * the same `Completion`. - */ - def getCompletions(using Context): List[Completion] = { - val nameToSymbols = completions.mappings.toList - nameToSymbols.map { case (name, symbols) => - val typesFirst = symbols.sortWith((s1, s2) => s1.isType && !s2.isType) - val desc = description(typesFirst) - Completion(name.show, desc, typesFirst) - } + case denot :: _ => + denots.map(den => ctx.printer.kindString(den.symbol)).distinct.mkString("", " and ", s" ${denot.name.show}") + + case Nil => + "" } - /** - * A description for completion result that represents `symbols`. - * - * If `symbols` contains a single symbol, show its full name in case it's a type, or its type if - * it's a term. - * - * When there are multiple symbols, show their kinds. + /** Computes code completions depending on the context in which completion is requested + * @param mode Should complete names of terms, types or both + * @param prefix The prefix that all suggested completions should start with + * @param pos Cursor position where completion was requested + * + * For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map + * and they never conflict with each other. + */ + private class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) { + /** Completions for terms and types that are currently in scope: + * the members of the current class, local definitions and the symbols that have been imported, + * recursively adding completions from outer scopes. + * In case a name is ambiguous, no completions are returned for it. + * This mimics the logic for deciding what is ambiguous used by the compiler. + * In general in case of a name clash symbols introduced in more deeply nested scopes + * have higher priority and shadow previous definitions with the same name although: + * - imports with the same level of nesting cause an ambiguity + * - members and local definitions with the same level of nesting are allowed for overloading + * - an import is ignored if there is a local definition or a member introduced in the same scope + * (even if the import follows it syntactically) + * - a more deeply nested import shadowing a member or a local definition causes an ambiguity */ - private def description(symbols: List[Symbol])(using Context): String = - symbols match { - case sym :: Nil => - if (sym.isType) sym.showFullName - else sym.info.widenTermRefExpr.show + def scopeCompletions(using context: Context): CompletionMap = { + val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) + def addMapping(name: Name, denots: ScopedDenotations) = + mappings(name) = mappings(name) :+ denots + var ctx = context + + while ctx ne NoContext do + if ctx.isImportContext then + importedCompletions(using ctx).foreach { (name, denots) => + addMapping(name, ScopedDenotations(denots, ctx)) + } + else if ctx.owner.isClass then + accessibleMembers(ctx.owner.thisType) + .groupByName.foreach { (name, denots) => + addMapping(name, ScopedDenotations(denots, ctx)) + } + else if ctx.scope != null then + ctx.scope.toList.filter(symbol => include(symbol, symbol.name)) + .flatMap(_.alternatives) + .groupByName.foreach { (name, denots) => + addMapping(name, ScopedDenotations(denots, ctx)) + } - case sym :: _ => - symbols.map(ctx.printer.kindString).mkString("", " and ", s" ${sym.name.show}") + ctx = ctx.outer + end while - case Nil => - "" - } + var resultMappings = Map.empty[Name, Seq[SingleDenotation]] + + mappings.foreach { (name, denotss) => + val first = denotss.head + denotss.find(!_.ctx.isImportContext) match { + // most deeply nested member or local definition if not shadowed by an import + case Some(local) if local.ctx.scope == first.ctx.scope => + resultMappings += name -> local.denots + + // most deeply nested import if not shadowed by another import + case None if denotss.length < 2 || (denotss(1).ctx.scope ne first.ctx.scope) => + resultMappings += name -> first.denots - /** - * Add symbols that are currently in scope to `info`: the members of the current class and the - * symbols that have been imported. - */ - def addScopeCompletions(using Context): Unit = { - if (ctx.owner.isClass) { - addAccessibleMembers(ctx.owner.thisType) - ctx.owner.asClass.classInfo.selfInfo match { - case selfSym: Symbol => add(selfSym, selfSym.name) case _ => } } - else if (ctx.scope != null) ctx.scope.foreach(s => add(s, s.name)) - - addImportCompletions - var outer = ctx.outer - while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) { - addImportCompletions(using outer) - outer = outer.outer - } - if (outer `ne` NoContext) addScopeCompletions(using outer) + resultMappings } - /** - * Find all the members of `qual` and add the ones that pass the include filters to `info`. - * - * If `info.mode` is `Import`, the members added via implicit conversion on `qual` are not - * considered. + /** Completions for selections from a term. + * Direct members take priority over members from extensions + * and so do members from extensions over members from implicit conversions */ - def addMemberCompletions(qual: Tree)(using Context): Unit = - if (!qual.tpe.widenDealias.isExactlyNothing) { - addAccessibleMembers(qual.tpe) - if (!mode.is(Mode.Import) && !qual.tpe.isNullType) - // Implicit conversions do not kick in when importing - // and for `NullClass` they produce unapplicable completions (for unclear reasons) - implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState()) - .foreach(addAccessibleMembers) - } + def selectionCompletions(qual: Tree)(using Context): CompletionMap = + implicitConversionMemberCompletions(qual) ++ + extensionCompletions(qual) ++ + directMemberCompletions(qual) - def addExtensionCompletions(path: List[Tree], qual: Tree)(using Context): Unit = - def applyExtensionReceiver(methodSymbol: Symbol, methodName: TermName): Symbol = { - val newMethodType = methodSymbol.info match { - case mt: MethodType => - mt.resultType match { - case resType: MethodType => resType - case resType => ExprType(resType) - } - case pt: PolyType => - PolyType(pt.paramNames)(_ => pt.paramInfos, _ => pt.resultType.resultType) - } - - newSymbol(owner = qual.symbol, methodName, methodSymbol.flags, newMethodType) - } + /** Completions for members of `qual`'s type. + * These include inherited definitions but not members added by extensions or implicit conversions + */ + def directMemberCompletions(qual: Tree)(using Context): CompletionMap = + if qual.tpe.widenDealias.isExactlyNothing then + Map.empty + else + accessibleMembers(qual.tpe).groupByName + + /** Completions introduced by imports directly in this context. + * Completions from outer contexts are not included. + */ + private def importedCompletions(using Context): CompletionMap = { + val imp = ctx.importInfo - val matchingNamePrefix = completionPrefix(path, pos) + def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = + imp.site.member(name).alternatives + .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } + + if imp == null then + Map.empty + else + val givenImports = imp.importedImplicits + .map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) } + .filter((name, denot) => include(denot, name)) + .groupByName + + val wildcardMembers = + if imp.selectors.exists(_.imported.name == nme.WILDCARD) then + val denots = accessibleMembers(imp.site) + .filter(mbr => !mbr.symbol.is(Given) && !imp.excluded.contains(mbr.name.toTermName)) + denots.groupByName + else + Map.empty + + val explicitMembers = + val importNamesInScope = imp.forwardMapping.toList.map(_._2) + val duplicatedNames = importNamesInScope.diff(importNamesInScope.distinct) + val discardedNames = duplicatedNames ++ imp.excluded + imp.reverseMapping.toList + .filter { (nameInScope, _) => !discardedNames.contains(nameInScope) } + .flatMap { (nameInScope, original) => + fromImport(original, nameInScope) ++ + fromImport(original.toTypeName, nameInScope.toTypeName) + }.toSeq.groupByName + + givenImports ++ wildcardMembers ++ explicitMembers + } - def extractDefinedExtensionMethods(types: Seq[Type]) = - types - .flatMap(_.membersBasedOnFlags(required = ExtensionMethod, excluded = EmptyFlags)) - .collect{ denot => - denot.name.toTermName match { - case name if name.startsWith(matchingNamePrefix) => (denot.symbol, name) - } + /** Completions from implicit conversions including old style extensions using implicit classes */ + private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap = + if qual.tpe.widenDealias.isExactlyNothing || qual.tpe.isNullType then + Map.empty + else + val membersFromConversion = + implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState()).flatMap(accessibleMembers) + membersFromConversion.toSeq.groupByName + + /** Completions from extension methods */ + private def extensionCompletions(qual: Tree)(using Context): CompletionMap = + def asDefLikeType(tpe: Type): Type = tpe match + case _: MethodOrPoly => tpe + case _ => ExprType(tpe) + + def tryApplyingReceiverToExtension(termRef: TermRef): Option[SingleDenotation] = + ctx.typer.tryApplyingExtensionMethod(termRef, qual) + .map { tree => + val tpe = asDefLikeType(tree.tpe.dealias) + termRef.denot.asSingleDenotation.mapInfo(_ => tpe) } + def extractMemberExtensionMethods(types: Seq[Type]): Seq[(TermRef, TermName)] = + object DenotWithMatchingName: + def unapply(denot: SingleDenotation): Option[(SingleDenotation, TermName)] = + denot.name match + case name: TermName if include(denot, name) => Some((denot, name)) + case _ => None + + types.flatMap { tpe => + tpe.membersBasedOnFlags(required = ExtensionMethod, excluded = EmptyFlags) + .collect { case DenotWithMatchingName(denot, name) => TermRef(tpe, denot.symbol) -> name } + } + // There are four possible ways for an extension method to be applicable // 1. The extension method is visible under a simple name, by being defined or inherited or imported in a scope enclosing the reference. - val extMethodsInScope = - val buf = completionBuffer(path, pos) - buf.addScopeCompletions - buf.completions.mappings.toList.flatMap { - case (termName, symbols) => symbols.map(s => (s, termName)) - } + val termCompleter = new Completer(Mode.Term, prefix, pos) + val extMethodsInScope = termCompleter.scopeCompletions.toList.flatMap { + case (name, denots) => denots.collect { case d: SymDenotation => (d.termRef, name.asTermName) } + } // 2. The extension method is a member of some given instance that is visible at the point of the reference. val givensInScope = ctx.implicits.eligible(defn.AnyType).map(_.implicitRef.underlyingRef) - val extMethodsFromGivensInScope = extractDefinedExtensionMethods(givensInScope) + val extMethodsFromGivensInScope = extractMemberExtensionMethods(givensInScope) // 3. The reference is of the form r.m and the extension method is defined in the implicit scope of the type of r. val implicitScopeCompanions = ctx.run.implicitScope(qual.tpe).companionRefs.showAsList - val extMethodsFromImplicitScope = extractDefinedExtensionMethods(implicitScopeCompanions) + val extMethodsFromImplicitScope = extractMemberExtensionMethods(implicitScopeCompanions) // 4. The reference is of the form r.m and the extension method is defined in some given instance in the implicit scope of the type of r. - val givensInImplicitScope = implicitScopeCompanions.flatMap(_.membersBasedOnFlags(required = Given, excluded = EmptyFlags)).map(_.symbol.info) - val extMethodsFromGivensInImplicitScope = extractDefinedExtensionMethods(givensInImplicitScope) + val givensInImplicitScope = implicitScopeCompanions.flatMap(_.membersBasedOnFlags(required = Given, excluded = EmptyFlags)).map(_.info) + val extMethodsFromGivensInImplicitScope = extractMemberExtensionMethods(givensInImplicitScope) val availableExtMethods = extMethodsFromGivensInImplicitScope ++ extMethodsFromImplicitScope ++ extMethodsFromGivensInScope ++ extMethodsInScope - val extMethodsWithAppliedReceiver = availableExtMethods.collect { - case (symbol, termName) if ctx.typer.isApplicableExtensionMethod(symbol.termRef, qual.tpe) => - applyExtensionReceiver(symbol, termName) + val extMethodsWithAppliedReceiver = availableExtMethods.flatMap { + case (termRef, termName) => + if termRef.symbol.is(ExtensionMethod) && !qual.tpe.isBottomType then + tryApplyingReceiverToExtension(termRef) + .map(denot => termName -> denot) + else None } - - for (symbol <- extMethodsWithAppliedReceiver) do add(symbol, symbol.name) - - def addSelectionCompletions(path: List[Tree], qual: Tree)(using Context): Unit = - addExtensionCompletions(path, qual) - addMemberCompletions(qual) - - /** - * If `sym` exists, no symbol with the same name is already included, and it satisfies the - * inclusion filter, then add it to the completions. - */ - private def add(sym: Symbol, nameInScope: Name)(using Context) = - if (sym.exists && - completionsFilter(NoType, nameInScope) && - !completions.lookup(nameInScope).exists && - include(sym, nameInScope)) - completions.enter(sym, nameInScope) - - /** Lookup members `name` from `site`, and try to add them to the completion list. */ - private def addMember(site: Type, name: Name, nameInScope: Name)(using Context) = - if (!completions.lookup(nameInScope).exists) - for (alt <- site.member(name).alternatives) add(alt.symbol, nameInScope) + extMethodsWithAppliedReceiver.groupByName /** Include in completion sets only symbols that * 1. start with given name prefix, and @@ -302,8 +367,12 @@ object Completion { * 8. symbol is not an artifact of the compiler * 9. have same term/type kind as name prefix given so far */ - private def include(sym: Symbol, nameInScope: Name)(using Context): Boolean = + private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean = + val sym = denot.symbol + nameInScope.startsWith(prefix) && + sym.exists && + completionsFilter(NoType, nameInScope) && !sym.isAbsent() && !sym.isPrimaryConstructor && sym.sourceSymbol.exists && @@ -316,52 +385,19 @@ object Completion { || (mode.is(Mode.Type) && (sym.isType || sym.isStableMember)) ) - /** - * Find all the members of `site` that are accessible and which should be included in `info`. - * - * @param site The type to inspect. - * @return The members of `site` that are accessible and pass the include filter of `info`. - */ - private def accessibleMembers(site: Type)(using Context): Seq[Symbol] = site match { - case site: NamedType if site.symbol.is(Package) => - extension (tpe: Type) - def accessibleSymbols = tpe.decls.toList.filter(sym => sym.isAccessibleFrom(site, superAccess = false)) - - val packageDecls = site.accessibleSymbols - val packageObjectsDecls = packageDecls.filter(_.isPackageObject).flatMap(_.thisType.accessibleSymbols) - packageDecls ++ packageObjectsDecls - case _ => - def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = - try buf ++= site.member(name).alternatives - catch { case ex: TypeError => } - site.memberDenots(completionsFilter, appendMemberSyms).collect { - case mbr if include(mbr.symbol, mbr.symbol.name) => mbr.accessibleFrom(site, superAccess = true).symbol - case _ => NoSymbol - }.filter(_.exists) - } - - /** Add all the accessible members of `site` in `info`. */ - private def addAccessibleMembers(site: Type)(using Context): Unit = - for (mbr <- accessibleMembers(site)) addMember(site, mbr.name, mbr.name) - - /** - * Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import, - * all the accessible members of the import's `site` are included. + /** @param site The type to inspect. + * @return The members of `site` that are accessible and pass the include filter. */ - private def addImportCompletions(using Context): Unit = { - val imp = ctx.importInfo - if (imp != null) { - def addImport(name: TermName, nameInScope: TermName) = { - addMember(imp.site, name, nameInScope) - addMember(imp.site, name.toTypeName, nameInScope.toTypeName) - } - imp.reverseMapping.foreachBinding { (nameInScope, original) => - if (original != nameInScope || !imp.excluded.contains(original)) - addImport(original, nameInScope) - } - if (imp.isWildcardImport) - for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName)) - addMember(imp.site, mbr.name, mbr.name) + private def accessibleMembers(site: Type)(using Context): Seq[SingleDenotation] = { + def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = + try + buf ++= site.member(name).alternatives + catch + case ex: TypeError => + + site.memberDenots(completionsFilter, appendMemberSyms).collect { + case mbr if include(mbr, mbr.name) + && mbr.symbol.isAccessibleFrom(site) => mbr } } @@ -386,8 +422,22 @@ object Completion { !name.isConstructorName && name.toTermName.info.kind == SimpleNameKind def isStable = true } + + extension (denotations: Seq[SingleDenotation]) + def groupByName(using Context): CompletionMap = denotations.groupBy(_.name) + + extension [N <: Name](namedDenotations: Seq[(N, SingleDenotation)]) + @annotation.targetName("groupByNameTupled") + def groupByName: CompletionMap = namedDenotations.groupMap((name, denot) => name)((name, denot) => denot) } + private type CompletionMap = Map[Name, Seq[SingleDenotation]] + + /** Temporary data structure representing denotations with the same name introduced in a given scope + * as a member of a type, by a local definition or by an import clause + */ + private case class ScopedDenotations(denots: Seq[SingleDenotation], ctx: Context) + /** * The completion mode: defines what kinds of symbols should be included in the completion * results. @@ -409,24 +459,5 @@ object Completion { /** Both term and type symbols are allowed */ val Import: Mode = new Mode(4) | Term | Type } - - /** A scope that tracks renames of the entered symbols. - * Useful for providing completions for renamed symbols - * in the REPL and the IDE. - */ - private class RenameAwareScope extends Scopes.MutableScope { - private val nameToSymbols: mutable.Map[TermName, List[Symbol]] = mutable.Map.empty - - /** Enter the symbol `sym` in this scope, recording a potential renaming. */ - def enter[T <: Symbol](sym: T, name: Name)(using Context): T = { - val termName = name.stripModuleClassSuffix.toTermName - nameToSymbols += termName -> (sym :: nameToSymbols.getOrElse(termName, Nil)) - newScopeEntry(name, sym) - sym - } - - /** Get the names that are known in this scope, along with the list of symbols they refer to. */ - def mappings: Map[TermName, List[Symbol]] = nameToSymbols.toMap - } } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index ec23b8fe77dc..20a17c464b35 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -38,6 +38,8 @@ import Constants.{Constant, IntTag, LongTag} import Denotations.SingleDenotation import annotation.{constructorOnly, threadUnsafe} +import scala.util.control.NonFatal + object Applications { import tpd._ @@ -2133,8 +2135,57 @@ trait Applications extends Compatibility { } } - def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) = - ref.symbol.is(ExtensionMethod) - && !receiver.isBottomType - && isApplicableMethodRef(ref, receiver :: Nil, WildcardType) + /** Assuming methodRef is a reference to an extension method defined e.g. as + * + * extension [T1, T2](using A)(using B, C)(receiver: R)(using D) + * def foo[T3](using E)(f: F): G = ??? + * + * return the tree representing methodRef partially applied to the receiver + * and all the implicit parameters preceding it (A, B, C) + * with the type parameters of the extension (T1, T2) inferred. + * None is returned if the implicit search fails for any of the leading implicit parameters + * or if the receiver has a wrong type (note that in general the type of the receiver + * might depend on the exact types of the found instances of the proceding implicits). + * No implicit search is tried for implicits following the receiver or for parameters of the def (D, E). + */ + def tryApplyingExtensionMethod(methodRef: TermRef, receiver: Tree)(using Context): Option[Tree] = + // Drop all parameters sections of an extension method following the receiver. + // The return type after truncation is not important + def truncateExtension(tp: Type)(using Context): Type = tp match + case poly: PolyType => + poly.newLikeThis(poly.paramNames, poly.paramInfos, truncateExtension(poly.resType)) + case meth: MethodType if meth.isContextualMethod => + meth.newLikeThis(meth.paramNames, meth.paramInfos, truncateExtension(meth.resType)) + case meth: MethodType => + meth.newLikeThis(meth.paramNames, meth.paramInfos, defn.AnyType) + + def replaceCallee(inTree: Tree, replacement: Tree)(using Context): Tree = inTree match + case Apply(fun, args) => Apply(replaceCallee(fun, replacement), args) + case TypeApply(fun, args) => TypeApply(replaceCallee(fun, replacement), args) + case _ => replacement + + val methodRefTree = ref(methodRef) + val truncatedSym = methodRef.symbol.asTerm.copy(info = truncateExtension(methodRef.info)) + val truncatedRefTree = untpd.TypedSplice(ref(truncatedSym)).withSpan(receiver.span) + val newCtx = ctx.fresh.setNewScope.setReporter(new reporting.ThrowingReporter(ctx.reporter)) + + try + val appliedTree = inContext(newCtx) { + // Introducing an auxiliary symbol in a temporary scope. + // Entering the symbol indirectly by `newCtx.enter` + // could instead add the symbol to the enclosing class + // which could break the REPL. + newCtx.scope.openForMutations.enter(truncatedSym) + newCtx.typer.extMethodApply(truncatedRefTree, receiver, WildcardType) + } + if appliedTree.tpe.exists && !appliedTree.tpe.isError then + Some(replaceCallee(appliedTree, methodRefTree)) + else + None + catch + case NonFatal(_) => None + + def isApplicableExtensionMethod(methodRef: TermRef, receiverType: Type)(using Context): Boolean = + methodRef.symbol.is(ExtensionMethod) && !receiverType.isBottomType && + tryApplyingExtensionMethod(methodRef, nullLiteral.asInstance(receiverType)).nonEmpty } diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 4e693b698351..cbf267e7c846 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -107,7 +107,7 @@ class TabcompleteTests extends ReplTest { @Test def `null` = fromInitialState { implicit s => val comp = tabComplete("null.") assertEquals( - List("!=", "##", "==", "asInstanceOf", "clone", "eq", "equals", "finalize", "getClass", "hashCode", + List("!=", "##", "==", "asInstanceOf", "eq", "equals", "getClass", "hashCode", "isInstanceOf", "ne", "notify", "notifyAll", "synchronized", "toString", "wait"), comp.distinct.sorted) } @@ -115,7 +115,7 @@ class TabcompleteTests extends ReplTest { @Test def anyRef = fromInitialState { implicit s => val comp = tabComplete("(null: AnyRef).") assertEquals( - List("!=", "##", "->", "==", "asInstanceOf", "clone", "ensuring", "eq", "equals", "finalize", "formatted", + List("!=", "##", "->", "==", "asInstanceOf", "ensuring", "eq", "equals", "formatted", "getClass", "hashCode", "isInstanceOf", "ne", "nn", "notify", "notifyAll", "synchronized", "toString", "wait", "→"), comp.distinct.sorted) } diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 24e9b6e4ea55..28e9f1a37882 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -19,7 +19,7 @@ class CompletionTest { .completion(m1, Set( ("print", Method, "(x: Any): Unit"), ("printf", Method, "(text: String, xs: Any*): Unit"), - ("println", Method, "(x: Any): Unit") + ("println", Method, "method println") )) } @@ -107,8 +107,7 @@ class CompletionTest { }""".withSource .completion( m1, - Set(("clone", Method, "(): Object"), - ("copy", Method, "(foobar: Int): MyCaseClass"), + Set(("copy", Method, "(foobar: Int): MyCaseClass"), ("canEqual", Method, "(that: Any): Boolean"))) } @@ -171,6 +170,14 @@ class CompletionTest { .completion(m1, Set(("out", Field, "java.io.PrintStream"))) } + @Test def importFromExplicitAndSyntheticPackageObject: Unit = { + withSources( + code"package foo.bar; trait XXXX1", + code"package foo; package object bar { trait XXXX2 }", + code"object Main { import foo.bar.XX${m1} }" + ) .completion(m1, Set(("XXXX1", Class, "foo.bar.XXXX1"), ("XXXX2", Class, "foo.bar.XXXX2"))) + } + @Test def completeJavaModuleClass: Unit = { code"""object O { val out = java.io.FileDesc${m1} @@ -183,6 +190,13 @@ class CompletionTest { .completion(m1, Set(("FileDescriptor", Class, "class and object FileDescriptor"))) } + @Test def importGivenByType: Unit = { + code"""trait Foo + object Bar + import Bar.{given Fo$m1}""".withSource + .completion(m1, Set(("Foo", Class, "Foo"))) + } + @Test def markDeprecatedSymbols: Unit = { code"""object Foo { @deprecated @@ -274,10 +288,198 @@ class CompletionTest { ("MyHashMap3", Class, "class and object HashMap"))) } + @Test def completeFromWildcardImports: Unit = { + code"""object Foo { + | val fooFloat: Float = 1.0 + | val fooLong: Long = 0L + | given fooInt: Int = 0 + | given fooString: String = "" + |} + |object Test1 { import Foo.{fooFloat => _, _}; foo$m1 } + |object Test2 { import Foo.given; foo$m2 } + |object Test3 { import Foo.{given String}; foo$m3 } + |object Test4 { import Foo.{_, given String}; foo$m4 } + |object Test5 { import Foo.{fooFloat, given}; foo$m5 } + |object Test6 { import Foo.{fooInt => _, fooString => fooStr, given}; foo$m6 } + |object Test7 { import Foo.{fooLong => fooInt, given Int}; foo$m7 } + """.withSource + .completion(m1, Set(("fooLong", Field, "Long"))) + .completion(m2, Set(("fooInt", Field, "Int"), + ("fooString", Field, "String"))) + .completion(m3, Set(("fooString", Field, "String"))) + .completion(m4, Set(("fooLong", Field, "Long"), + ("fooFloat", Field, "Float"), + ("fooString", Field, "String"))) + .completion(m5, Set(("fooFloat", Field, "Float"), + ("fooInt", Field, "Int"), + ("fooString", Field, "String"))) + .completion(m6, Set(("fooStr", Field, "String"))) + .completion(m7, Set(("fooInt", Field, "Long"))) + } + + @Test def dontCompleteFromAmbiguousImportsFromSameSite: Unit = { + code"""object Foo { + | val i = 0 + | val j = 1 + |} + |object Test { + | import Foo.{i => xxxx, j => xxxx} + | val x = xx$m1 + |}""".withSource + .completion(m1, Set()) + } + + @Test def collectNamesImportedInNestedScopes: Unit = { + code"""object Foo { + | val xxxx1 = 1 + |} + |object Bar { + | val xxxx2 = 2 + |} + |object Baz { + | val xxxx3 = 3 + |} + |object Test { + | import Foo.xxxx1 + | locally { + | import Bar.xxxx2 + | locally { + | import Baz.xxxx3 + | val x = xx$m1 + | } + | } + |}""".withSource + .completion(m1, Set(("xxxx1", Field, "Int"), ("xxxx2", Field, "Int"), ("xxxx3", Field, "Int"))) + } + + @Test def completeEnclosingObject: Unit = { + code"""object Test { + | def x = Tes$m1 + |}""".withSource + .completion(m1, Set(("Test", Module, "Test$"))) + } + + @Test def completeBothDefinitionsForEqualNestingLevels: Unit = { + code"""trait Foo { + | def xxxx(i: Int): Int = i + |} + |trait Bar { + | def xxxx(s: String): String = s + |} + |object Test extends Foo, Bar { + | val x = xx$m1 + |}""".withSource + .completion(m1, Set(("xxxx", Method, "method xxxx"))) // 2 different signatures are merged into one generic description + } + + @Test def dontCompleteFromAmbiguousImportsForEqualNestingLevels: Unit = { + code"""object Foo { + | def xxxx(i: Int): Int = i + |} + |object Bar { + | def xxxx(s: String): String = s + |} + |object Test { + | import Foo.xxxx + | import Bar.xxxx + | val x = xx$m1 + |}""".withSource + .completion(m1, Set()) + } + + @Test def preferLocalDefinitionToImportForEqualNestingLevels: Unit = { + code"""object Foo { + | val xxxx = 1 + |} + |object Test { + | def xxxx(s: String): String = s + | import Foo.xxxx + | val x = xx$m1 + |}""".withSource + .completion(m1, Set(("xxxx", Method, "(s: String): String"))) + } + + @Test def preferMoreDeeplyNestedDefinition: Unit = { + code"""object Test { + | def xxxx(i: Int): Int = i + | object Inner { + | def xxxx(s: String): String = s + | val x = xx$m1 + | } + |}""".withSource + .completion(m1, Set(("xxxx", Method, "(s: String): String"))) + } + + @Test def preferMoreDeeplyNestedImport: Unit = { + code"""object Foo { + | def xxxx(i: Int): Int = i + |} + |object Bar { + | def xxxx(s: String): String = s + |} + |object Test { + | import Foo.xxxx + | locally { + | import Bar.xxxx + | val x: String = xx$m1 + | } + |}""".withSource + .completion(m1, Set(("xxxx", Method, "(s: String): String"))) + } + + @Test def preferMoreDeeplyNestedLocalDefinitionToImport: Unit = { + code"""object Foo { + | def xxxx(i: Int): Int = i + |} + |object Test { + | import Foo.xxxx + | object Inner { + | def xxxx(s: String): String = s + | val x: String = xx$m1 + | } + |}""".withSource + .completion(m1, Set(("xxxx", Method, "(s: String): String"))) + } + + @Test def dontCompleteLocalDefinitionShadowedByImport: Unit = { + code"""object XXXX { + | val xxxx = 1 + |} + |object Test { + | locally { + | val xxxx = "" + | locally { + | import XXXX.xxxx // import conflicts with val from outer scope + | val y = xx$m1 + | } + | } + |}""".withSource + .completion(m1, Set()) + } + + @Test def completeFromLocalDefinitionIgnoringLessDeeplyNestedAmbiguities: Unit = { + code"""object XXXX { + | val xxxx = 1 + |} + |object Test { + | locally { + | val xxxx = "" + | locally { + | import XXXX.xxxx // import conflicts with val from outer scope + | locally { + | val xxxx = 'a' // shadows both the import and the val from outer scope + | val y = xx$m1 + | } + | } + | } + |}""".withSource + .completion(m1, Set(("xxxx", Field, "Char"))) + } + @Test def completionClassAndMethod: Unit = { code"""object Foo { | class bar - | def bar = 0 + | def bar(i: Int) = 0 |} |import Foo.b$m1""".withSource .completion(m1, Set(("bar", Class, "class and method bar"))) @@ -292,6 +494,47 @@ class CompletionTest { .completion(m1, Set(("bar", Field, "type and lazy value bar"))) } + @Test def keepTrackOfTermsAndTypesSeparately: Unit = { + code"""object XXXX { + | object YYYY + | type YYYY = YYYY.type + |} + |object Test { + | import XXXX._ + | val YYYY = Int + | val ZZZZ = YY$m1 + | type ZZZZ = YY$m2 + |}""".withSource + .completion(m1, Set(("YYYY", Field, "Int$"))) + .completion(m2, Set(("YYYY", Field, "type and value YYYY"))) + } + + @Test def completeRespectingAccessModifiers: Unit = { + code"""trait Foo { + | def xxxx1 = "" + | protected def xxxx2 = "" + | private def xxxx3 = "" + |} + |object Test1 extends Foo { + | xx$m1 + |} + |object Test2 { + | val foo = new Foo {} + | foo.xx$m2 + |}""".withSource + .completion(m1, Set(("xxxx1", Method, "=> String"), ("xxxx2", Method, "=> String"))) + .completion(m2, Set(("xxxx1", Method, "=> String"))) + } + + @Test def completeFromPackageObjectWithInheritance: Unit = { + code"""trait Foo[A] { def xxxx(a: A) = a } + |package object foo extends Foo[Int] {} + |object Test { + | foo.xx$m1 + |}""".withSource + .completion(m1, Set(("xxxx", Method, "(a: Int): Int"))) + } + @Test def completeExtensionMethodWithoutParameter: Unit = { code"""object Foo |extension (foo: Foo.type) def xxxx = 1 @@ -308,40 +551,140 @@ class CompletionTest { @Test def completeExtensionMethodWithTypeParameter: Unit = { code"""object Foo - |extension [A](foo: Foo.type) def xxxx: Int = 1 + |extension (foo: Foo.type) def xxxx[A]: Int = 1 |object Main { Foo.xx${m1} }""".withSource .completion(m1, Set(("xxxx", Method, "[A] => Int"))) } @Test def completeExtensionMethodWithParameterAndTypeParameter: Unit = { code"""object Foo - |extension [A](foo: Foo.type) def xxxx(a: A) = a + |extension (foo: Foo.type) def xxxx[A](a: A) = a |object Main { Foo.xx${m1} }""".withSource .completion(m1, Set(("xxxx", Method, "[A](a: A): A"))) } - @Test def completeExtensionMethodFromExtenionWithAUsingSection: Unit = { + @Test def completeExtensionMethodFromExtensionWithTypeParameter: Unit = { + code"""extension [A](a: A) def xxxx: A = a + |object Main { "abc".xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> String"))) + } + + @Test def completeExtensionMethodWithResultTypeDependantOnReceiver: Unit = { + code"""trait Foo { type Out; def get: Out} + |object Bar extends Foo { type Out = String; def get: Out = "abc"} + |extension (foo: Foo) def xxxx: foo.Out = foo.get + |object Main { Bar.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> String"))) + } + + @Test def completeExtensionMethodFromExtenionWithPrefixUsingSection: Unit = { + code"""object Foo + |trait Bar + |trait Baz + |given Bar with {} + |given Baz with {} + |extension (using Bar, Baz)(foo: Foo.type) def xxxx = 1 + |object Main { Foo.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> Int"))) + } + + @Test def completeExtensionMethodFromExtenionWithMultiplePrefixUsingSections: Unit = { + code"""object Foo + |trait Bar + |trait Baz + |given Bar with {} + |given Baz with {} + |extension (using Bar)(using Baz)(foo: Foo.type) def xxxx = 1 + |object Main { Foo.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> Int"))) + } + + @Test def dontCompleteExtensionMethodFromExtenionWithMissingImplicitFromPrefixUsingSection: Unit = { + code"""object Foo + |trait Bar + |trait Baz + |given Baz with {} + |extension (using Bar, Baz)(foo: Foo.type) def xxxx = 1 + |object Main { Foo.xx${m1} }""".withSource + .completion(m1, Set()) + } + + @Test def completeExtensionMethodForReceiverOfTypeDependentOnLeadingImplicits: Unit = { + code""" + |trait Foo: + | type Out <: Bar + | + |given Foo with + | type Out = Baz + | + |trait Bar: + | type Out + | + |trait Baz extends Bar + | + |given Baz with + | type Out = Quux + | + |class Quux + | + |object Quux: + | extension (using foo: Foo)(using fooOut: foo.Out)(fooOutOut: fooOut.Out) def xxxx = "abc" + | + |object Main { (new Quux).xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> String"))) + } + + @Test def completeExtensionMethodWithResultTypeDependentOnLeadingImplicit: Unit = { + code"""object Foo + |trait Bar { type Out; def get: Out } + |given Bar with { type Out = 123; def get: Out = 123 } + |extension (using bar: Bar)(foo: Foo.type) def xxxx: bar.Out = bar.get + |object Main { Foo.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> (123 : Int)"))) + } + + @Test def completeExtensionMethodFromExtenionWithPostfixUsingSection: Unit = { code"""object Foo |trait Bar |trait Baz - |given Bar = new Bar {} - |given Baz = new Baz {} + |given Bar with {} + |given Baz with {} |extension (foo: Foo.type)(using Bar, Baz) def xxxx = 1 |object Main { Foo.xx${m1} }""".withSource .completion(m1, Set(("xxxx", Method, "(using x$2: Bar, x$3: Baz): Int"))) } - @Test def completeExtensionMethodFromExtenionWithMultipleUsingSections: Unit = { + @Test def completeExtensionMethodFromExtenionWithMultiplePostfixUsingSections: Unit = { code"""object Foo |trait Bar |trait Baz - |given Bar = new Bar {} - |given Baz = new Baz {} + |given Bar with {} + |given Baz with {} |extension (foo: Foo.type)(using Bar)(using Baz) def xxxx = 1 |object Main { Foo.xx${m1} }""".withSource .completion(m1, Set(("xxxx", Method, "(using x$2: Bar)(using x$3: Baz): Int"))) } + @Test def completeExtensionMethodWithTypeParameterFromExtenionWithTypeParametersAndPrefixAndPostfixUsingSections: Unit = { + code"""trait Bar + |trait Baz + |given Bar with {} + |given Baz with {} + |extension [A](using bar: Bar)(a: A)(using baz: Baz) def xxxx[B]: Either[A, B] = Left(a) + |object Main { 123.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "(using baz: Baz): [B] => Either[Int, B]"))) + } + + @Test def completeExtensionMethodWithTypeBounds: Unit = { + code"""trait Foo + |trait Bar extends Foo + |given Bar with {} + |extension [A >: Bar](a: A) def xxxx[B <: a.type]: Either[A, B] = Left(a) + |val foo = new Foo {} + |object Main { foo.xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "[B <: (foo : Foo)] => Either[Foo, B]"))) + } + @Test def completeInheritedExtensionMethod: Unit = { code"""object Foo |trait FooOps { @@ -351,6 +694,15 @@ class CompletionTest { .completion(m1, Set(("xxxx", Method, "=> Int"))) } + @Test def completeExtensionMethodWithoutLosingTypeParametersFromGivenInstance: Unit = { + code"""trait ListOps[A] { + | extension (xs: List[A]) def xxxx = xs + |} + |given ListOps[Int] with {} + |object Main { List(1, 2, 3).xx${m1} }""".withSource + .completion(m1, Set(("xxxx", Method, "=> List[Int]"))) + } + @Test def completeRenamedExtensionMethod: Unit = { code"""object Foo |object FooOps { @@ -442,10 +794,9 @@ class CompletionTest { .completion(m1, Set(("xxxx", Method, "=> Int"))) } - @Test def dontCompleteInapplicableExtensionMethod: Unit = { - code"""case class Foo[A](a: A) - |extension (foo: Foo[Int]) def xxxx = foo.a - |object Main { Foo("abc").xx${m1} }""".withSource + @Test def dontCompleteExtensionMethodWithMismatchedReceiverType: Unit = { + code"""extension (i: Int) def xxxx = i + |object Main { "abc".xx${m1} }""".withSource .completion(m1, Set()) } } diff --git a/tests/neg/missing-implicit6.check b/tests/neg/missing-implicit6.check new file mode 100644 index 000000000000..8c4cb331808b --- /dev/null +++ b/tests/neg/missing-implicit6.check @@ -0,0 +1,39 @@ +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:34:8 ------------------------------------------------------ +34 | "a".xxx // error, no suggested import + | ^^^^^^^ + | value xxx is not a member of String +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:35:8 ------------------------------------------------------ +35 | 123.xxx // error, suggested import + | ^^^^^^^ + | value xxx is not a member of Int, but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import Test.Ops.xxx + | +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:36:8 ------------------------------------------------------ +36 | 123.yyy // error, suggested import + | ^^^^^^^ + | value yyy is not a member of Int, but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import Test.Ops.yyy + | +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:41:8 ------------------------------------------------------ +41 | 123.xxx // error, no suggested import + | ^^^^^^^ + | value xxx is not a member of Int +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:42:8 ------------------------------------------------------ +42 | 123.yyy // error, no suggested import + | ^^^^^^^ + | value yyy is not a member of Int +-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:43:8 ------------------------------------------------------ +43 | 123.zzz // error, suggested import even though there's no instance of Bar in scope + | ^^^^^^^ + | value zzz is not a member of Int, but could be made available as an extension method. + | + | The following import might fix the problem: + | + | import Test.Ops.zzz + | diff --git a/tests/neg/missing-implicit6.scala b/tests/neg/missing-implicit6.scala new file mode 100644 index 000000000000..ded6e5ba8fed --- /dev/null +++ b/tests/neg/missing-implicit6.scala @@ -0,0 +1,45 @@ +trait Foo { + type Out <: { type Out } +} + +trait Bar { + type Out +} + +object instances { + given foo: Foo with { + type Out = Bar + } + + given bar: Bar with { + type Out = Int + } +} + +object Test { + object Ops { + extension (using foo: Foo, bar: foo.Out)(i: Int) + def xxx = ??? + + extension (using foo: Foo, fooOut: foo.Out)(x: fooOut.Out) + def yyy = ??? + + extension (using foo: Foo)(i: Int)(using fooOut: foo.Out) + def zzz = ??? + } + + locally { + import instances.given + + "a".xxx // error, no suggested import + 123.xxx // error, suggested import + 123.yyy // error, suggested import + } + + locally { + import instances.foo + 123.xxx // error, no suggested import + 123.yyy // error, no suggested import + 123.zzz // error, suggested import even though there's no instance of Bar in scope + } +}