All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.dotc.interactive.Completion.scala Maven / Gradle / Ivy

There is a newer version: 3.6.4-RC1-bin-20241220-0bfa1af-NIGHTLY
Show newest version
package dotty.tools.dotc.interactive

import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.NavigateAST
import dotty.tools.dotc.config.Printers.interactiv
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Decorators.*
import dotty.tools.dotc.core.Denotations.SingleDenotation
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.Phases
import dotty.tools.dotc.core.Scopes.*
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn, newSymbol}
import dotty.tools.dotc.core.StdNames.nme
import dotty.tools.dotc.core.SymDenotations.SymDenotation
import dotty.tools.dotc.core.TypeError
import dotty.tools.dotc.core.Phases
import dotty.tools.dotc.core.Types.{AppliedType, ExprType, MethodOrPoly, NameFilter, NoType, RefinedType, TermRef, Type, TypeProxy}
import dotty.tools.dotc.parsing.Tokens
import dotty.tools.dotc.typer.Implicits.SearchSuccess
import dotty.tools.dotc.typer.Inferencing
import dotty.tools.dotc.util.Chars
import dotty.tools.dotc.util.SourcePosition

import scala.collection.mutable
import scala.util.control.NonFatal
import dotty.tools.dotc.core.ContextOps.localContext
import dotty.tools.dotc.core.Names
import dotty.tools.dotc.core.Types
import dotty.tools.dotc.core.Symbols
import dotty.tools.dotc.core.Constants
import dotty.tools.dotc.core.TypeOps
import dotty.tools.dotc.core.StdNames

/**
 * One of the results of a completion query.
 *
 * @param label         The label of this completion result, or the text that this completion result
 *                      should insert in the scope where the completion request happened.
 * @param description   The description of this completion result: the fully qualified name for
 *                      types, or the type for terms.
 * @param symbols       The symbols that are matched by this completion result.
 */
case class Completion(label: String, description: String, symbols: List[Symbol])

object Completion:

  /** Get possible completions from tree at `pos`
   *
   *  @return offset and list of symbols for possible completions
   */
  def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
    val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
    val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
    inContext(completionContext):
      val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos)
      val mode = completionMode(untpdPath, pos)
      val rawPrefix = completionPrefix(untpdPath, pos)
      val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath)

      postProcessCompletions(untpdPath, completions, rawPrefix)

  /** Get possible completions from tree at `pos`
   *  This method requires manually computing the mode, prefix and paths.
   *
   *  @return completion map of name to list of denotations
   */
  def rawCompletions(
    pos: SourcePosition,
    mode: Mode,
    rawPrefix: String,
    tpdPath: List[tpd.Tree],
    untpdPath: List[untpd.Tree],
    customMatcher: Option[Name => Boolean] = None
  )(using Context): CompletionMap =
    val adjustedPath = typeCheckExtensionConstructPath(untpdPath, tpdPath, pos)
    computeCompletions(pos, mode, rawPrefix, adjustedPath, untpdPath, customMatcher)

  /**
   * Inspect `path` to determine what kinds of symbols should be considered.
   *
   * If the path starts with:
   *  - a `RefTree`, then accept symbols of the same kind as its name;
   *  - a renaming import, and the cursor is on the renamee, accept both terms and types;
   *  - an import, accept both terms and types;
   *
   * Otherwise, provide no completion suggestion.
   */
  def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match
    case GenericImportSelector(sel) =>
      if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
      else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
      else Mode.None // import scala.{util => u@@}
    case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
    case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
    case (ref: untpd.RefTree) :: _ =>
      val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope

      if (ref.name.isTermName) Mode.Term | maybeSelectMembers
      else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
      else Mode.None

    case _ => Mode.None

  /** When dealing with  in varios palces we check to see if they are
   *  due to incomplete backticks. If so, we ensure we get the full prefix
   *  including the backtick.
   *
   * @param content The source content that we'll check the positions for the prefix
   * @param start The start position we'll start to look for the prefix at
   * @param end The end position we'll look for the prefix at
   * @return Either the full prefix including the ` or an empty string
   */
  private def checkBacktickPrefix(content: Array[Char], start: Int, end: Int): String =
    content.lift(start) match
      case Some(char) if char == '`' =>
        content.slice(start, end).mkString
      case _ =>
        ""

  def naiveCompletionPrefix(text: String, offset: Int): String =
    var i = offset - 1
    while i >= 0 && text(i).isUnicodeIdentifierPart do i -= 1
    i += 1 // move to first character
    text.slice(i, offset)

  /**
   * Inspect `path` to determine the completion prefix. Only symbols whose name start with the
   * returned prefix should be considered.
   */
  def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
    path match
      case GenericImportSelector(sel) =>
        if sel.isGiven then completionPrefix(sel.bound :: Nil, pos)
        else if sel.isWildcard then pos.source.content()(pos.point - 1).toString
        else completionPrefix(sel.imported :: Nil, pos)

      // Foo.`se will result in Select(Ident(Foo), )
      case (select: untpd.Select) :: _ if select.name == nme.ERROR =>
        checkBacktickPrefix(select.source.content(), select.nameSpan.start, select.span.end)

      // import scala.util.chaining.`s will result in a Ident()
      case (ident: untpd.Ident) :: _ if ident.name == nme.ERROR =>
        checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end)

      case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR =>
        tree.name.toString.take(pos.span.point - tree.span.point)

      case _ => naiveCompletionPrefix(pos.source.content().mkString, pos.point)


  end completionPrefix

  private object GenericImportSelector:
    def unapply(path: List[untpd.Tree]): Option[untpd.ImportSelector] =
      path match
        case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ => Some(sel)
        case (sel: untpd.ImportSelector) :: _ => Some(sel)
        case _ => None

  private object GenericImportOrExport:
    def unapply(path: List[untpd.Tree]): Option[untpd.ImportOrExport] =
      path match
        case untpd.Ident(_) :: (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
        case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
        case _ => None

  /** Inspect `path` to determine the offset where the completion result should be inserted. */
  def completionOffset(untpdPath: List[untpd.Tree]): Int =
    untpdPath match
      case (ref: untpd.RefTree) :: _ => ref.span.point
      case _ => 0

  /** Handle case when cursor position is inside extension method construct.
   *  The extension method construct is then desugared into methods, and consturct parameters
   *  are no longer a part of a typed tree, but instead are prepended to method parameters.
   *
   *  @param untpdPath The typed or untyped path to the tree that is being completed
   *  @param tpdPath The typed path that will be returned if no extension method construct is found
   *  @param pos The cursor position
   *
   *  @return Typed path to the parameter of the extension construct if found or tpdPath
   */
  private def typeCheckExtensionConstructPath(
    untpdPath: List[untpd.Tree], tpdPath: List[tpd.Tree], pos: SourcePosition
  )(using Context): List[tpd.Tree] =
    untpdPath.collectFirst:
      case untpd.ExtMethods(paramss, _) =>
        val enclosingParam = paramss.flatten
          .find(_.span.contains(pos.span))
          .flatMap:
            case untpd.TypeDef(_, bounds: untpd.ContextBounds) => bounds.cxBounds.find(_.span.contains(pos.span))
            case other => Some(other)

        enclosingParam.map: param =>
          ctx.typer.index(paramss.flatten)
          val typedEnclosingParam = ctx.typer.typed(param)
          Interactive.pathTo(typedEnclosingParam, pos.span)
    .flatten.getOrElse(tpdPath)

  private def computeCompletions(
    pos: SourcePosition,
    mode: Mode,
    rawPrefix: String,
    adjustedPath: List[tpd.Tree],
    untpdPath: List[untpd.Tree],
    matches: Option[Name => Boolean]
  )(using Context): CompletionMap =
    val hasBackTick = rawPrefix.headOption.contains('`')
    val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
    val matches0 = matches.getOrElse(_.startsWith(prefix))
    val completer = new Completer(mode, pos, untpdPath, matches0)

    val result = adjustedPath match
      // Ignore synthetic select from `This` because in code it was `Ident`
      // See example in dotty.tools.languageserver.CompletionTest.syntheticThis
      case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic      => completer.scopeCompletions
      case tpd.Select(qual, _) :: _               if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual)
      case tpd.Select(qual, _) :: _                                             => Map.empty
      case (tree: tpd.ImportOrExport) :: _                                      => completer.directMemberCompletions(tree.expr)
      case _                                                                    => completer.scopeCompletions

    interactiv.println(i"""completion info with pos    = $pos,
                          |                     term   = ${completer.mode.is(Mode.Term)},
                          |                     type   = ${completer.mode.is(Mode.Type)},
                          |                     scope  = ${completer.mode.is(Mode.Scope)},
                          |                     member = ${completer.mode.is(Mode.Member)}""")

    result

  def postProcessCompletions(path: List[untpd.Tree], completions: CompletionMap, rawPrefix: String)(using Context): (Int, List[Completion]) =
    val describedCompletions = describeCompletions(completions)
    val hasBackTick = rawPrefix.headOption.contains('`')
    val backtickedCompletions =
      describedCompletions.map(completion => backtickCompletions(completion, hasBackTick))

    interactiv.println(i"""completion resutls = $backtickedCompletions%, %""")

    val offset = completionOffset(path)
    (offset, backtickedCompletions)

  def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
    if hasBackTick || needsBacktick(completion.label) then
      completion.copy(label = s"`${completion.label}`")
    else
      completion

  // This borrows from Metals, which itself borrows from Ammonite. This uses
  // the same approach, but some of the utils that already exist in Dotty.
  // https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala
  // https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala
  private def needsBacktick(s: String) =
    val chunks = s.split("_", -1).nn

    val validChunks = chunks.zipWithIndex.forall { case (chunk, index) =>
      chunk.nn.forall(Chars.isIdentifierPart) ||
      (chunk.nn.forall(Chars.isOperatorPart) &&
        index == chunks.length - 1 &&
        !(chunks.lift(index - 1).contains("") && index - 1 == 0))
    }

    val validStart =
      Chars.isIdentifierStart(s(0)) || chunks(0).nn.forall(Chars.isOperatorPart)

    val valid = validChunks && validStart && !keywords.contains(s)

    !valid
  end needsBacktick

  private lazy val keywords = Tokens.keywords.map(Tokens.tokenString)

  /**
   * 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, each denotation will be transformed into a separate completion item.
   */
  def describeCompletions(completions: CompletionMap)(using Context): List[Completion] =
    for
      (name, denots) <- completions.toList
      denot <- denots
    yield
      Completion(name.show, description(denot), List(denot.symbol))

  def description(denot: SingleDenotation)(using Context): String =
    if denot.isType then denot.symbol.showFullName
    else denot.info.widenTermRefExpr.show

  def isInNewContext(untpdPath: List[untpd.Tree]): Boolean =
    untpdPath match
      case _ :: untpd.New(selectOrIdent: (untpd.Select | untpd.Ident)) :: _ => true
      case _ => false

  /** Include in completion sets only symbols that
   *   1. is not absent (info is not NoType)
   *   2. are not a primary constructor,
   *   3. have an existing source symbol,
   *   4. are the module class in case of packages,
   *   5. are mutable accessors, to exclude setters for `var`,
   *   6. symbol is not a package object
   *   7. symbol is not an artifact of the compiler
   *   8. symbol is not a constructor proxy module when in type completion mode
   *   9. have same term/type kind as name prefix given so far
   */
  def isValidCompletionSymbol(sym: Symbol, completionMode: Mode, isNew: Boolean)(using Context): Boolean =

    lazy val isEnum = sym.is(Enum) ||
      (sym.companionClass.exists && sym.companionClass.is(Enum))

    sym.exists &&
    !sym.isAbsent() &&
    !sym.isPrimaryConstructor &&
    sym.sourceSymbol.exists &&
    (!sym.is(Package) || sym.is(ModuleClass)) &&
    !sym.isAllOf(Mutable | Accessor) &&
    !sym.isPackageObject &&
    !sym.is(Artifact) &&
    !(completionMode.is(Mode.Type) && sym.isAllOf(ConstructorProxyModule)) &&
    !(isNew && isEnum) &&
    (
         (completionMode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass))
      || (completionMode.is(Mode.Type) && (sym.isType || sym.isStableMember)))
    )

  given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with
    val order =
      List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass)

    override def compare(x: Seq[SingleDenotation], y: Seq[SingleDenotation]): Int =
      val owner0 = x.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol)
      val owner1 = y.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol)

      order.indexOf(owner0) - order.indexOf(owner1)

  /** Computes code completions depending on the context in which completion is requested
   *  @param mode    Should complete names of terms, types or both
   *  @param pos     Cursor position where completion was requested
   *  @param matches Function taking name used to filter completions
   *
   *  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.
   */
  class Completer(val mode: Mode, pos: SourcePosition, untpdPath: List[untpd.Tree], matches: Name => Boolean):
    /** 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 if they are in the same name space
     *  - 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
     */
    def scopeCompletions(using context: Context): CompletionMap =

      /** 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
       */
      case class ScopedDenotations private (denots: Seq[SingleDenotation], ctx: Context)
      object ScopedDenotations:
        def apply(denots: Seq[SingleDenotation], ctx: Context, includeFn: SingleDenotation => Boolean): ScopedDenotations =
          ScopedDenotations(denots.filter(includeFn), ctx)

      val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty)
      def addMapping(name: Name, denots: ScopedDenotations) =
        mappings(name) = mappings(name) :+ denots

      ctx.outersIterator.foreach { case ctx @ given Context =>
        if ctx.isImportContext then
          importedCompletions.foreach { (name, denots) =>
            addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
          }
        else if ctx.owner.isClass then
          accessibleMembers(ctx.owner.thisType)
            .groupByName.foreach { (name, denots) =>
              addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
            }
        else if ctx.scope ne EmptyScope then
          ctx.scope.toList.filter(symbol => include(symbol, symbol.name))
            .flatMap(_.alternatives)
            .groupByName.foreach { (name, denots) =>
              addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
            }
      }

      var resultMappings = Map.empty[Name, Seq[SingleDenotation]]

      mappings.foreach { (name, denotss) =>
        val first = denotss.head

        // import a.c
        def isSingleImport =  denotss.length < 2
        // import a.C
        // locally {  import b.C }
        def isImportedInDifferentScope = first.ctx.scope ne denotss(1).ctx.scope
        // import a.C
        // import a.C
        def isSameSymbolImportedDouble = denotss.forall(_.denots == first.denots)

        // https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html
        // import java.lang.*
        // {
        //   import scala.*
        //   {
        //     import Predef.*
        //     { /* source */ }
        //   }
        // }
        def notConflictingWithDefaults = // is imported symbol
          denotss.filterNot(_.denots.exists(denot => Interactive.isImportedByDefault(denot.symbol))).size <= 1

        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

          case None if isSingleImport || isImportedInDifferentScope || isSameSymbolImportedDouble =>
            resultMappings += name -> first.denots
          case None if notConflictingWithDefaults =>
            val ordered = denotss.map(_.denots).sorted
            resultMappings += name -> ordered.head
          case _ =>
        }
      }

      resultMappings
    end scopeCompletions

    /** Widen only those types which are applied or are exactly nothing
     */
    def widenQualifier(qual: tpd.Tree)(using Context): tpd.Tree =
      qual.typeOpt.widenDealias match
        case widenedType if widenedType.isExactlyNothing => qual.withType(widenedType)
        case appliedType: AppliedType => qual.withType(appliedType)
        case _ => qual

    /** 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 selectionCompletions(qual: tpd.Tree)(using Context): CompletionMap =
      val adjustedQual = widenQualifier(qual)

      val implicitConversionMembers = implicitConversionMemberCompletions(adjustedQual)
      val extensionMembers = extensionCompletions(adjustedQual)
      val directMembers = directMemberCompletions(adjustedQual)
      val namedTupleMembers = namedTupleCompletions(adjustedQual)

      List(
        implicitConversionMembers,
        extensionMembers,
        directMembers,
        namedTupleMembers
      ).reduce(_ ++ _)

    /** Completions for members of `qual`'s type.
     *  These include inherited definitions but not members added by extensions or implicit conversions
     */
    def directMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap =
      if qual.typeOpt.isExactlyNothing then
        Map.empty
      else
        accessibleMembers(qual.typeOpt).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

      if imp == null then
        Map.empty
      else
        def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] =
          imp.site.member(name).alternatives
            .collect { case denot if include(denot, nameInScope) => nameInScope -> denot }

        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
    end importedCompletions

    /** Completions from implicit conversions including old style extensions using implicit classes */
    private def implicitConversionMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap =

      def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type =
        try
          val typingCtx = ctx.fresh
          inContext(typingCtx):
            val methodRefTree = tpd.ref(conversionTarget.ref, needLoad = false)
            val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil))
            Inferencing.fullyDefinedType(convertedTree.tpe, "", pos)
        catch
          case error => conversionTarget.tree.tpe // fallback to not fully defined type

      if qual.typeOpt.isExactlyNothing || qual.typeOpt.isNullType then
        Map.empty
      else
        implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState())
          .flatMap { conversionTarget => accessibleMembers(tryToInstantiateTypeVars(conversionTarget)) }
          .toSeq
          .groupByName

    /** Completions for named tuples */
    private def namedTupleCompletions(qual: tpd.Tree)(using Context): CompletionMap =
      def namedTupleCompletionsFromType(tpe: Type): CompletionMap =
        val freshCtx = ctx.fresh.setExploreTyperState()
        inContext(freshCtx):
          tpe.namedTupleElementTypes
            .map { (name, tpe) =>
              val symbol = newSymbol(owner = NoSymbol, name, EmptyFlags, tpe)
              val denot = SymDenotation(symbol, NoSymbol, name, EmptyFlags, tpe)
              name -> denot
            }
            .toSeq
            .filter((name, denot) => include(denot, name))
            .groupByName

      val qualTpe = qual.typeOpt
      if qualTpe.isNamedTupleType then
        namedTupleCompletionsFromType(qualTpe)
      else if qualTpe.derivesFrom(defn.SelectableClass) then
        val pre = if !TypeOps.isLegalPrefix(qualTpe) then Types.SkolemType(qualTpe) else qualTpe
        val fieldsType = pre.select(StdNames.tpnme.Fields).dealias.simplified
        namedTupleCompletionsFromType(fieldsType)
      else Map.empty

    /** Completions from extension methods */
    private def extensionCompletions(qual: tpd.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.typeOpt.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 { tp =>
          val tpe = tp.widenExpr
          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 termCompleter = new Completer(Mode.Term, pos, untpdPath, matches)
      val extMethodsInScope = termCompleter.scopeCompletions.toList.flatMap:
        case (name, denots) => denots.collect:
          case d: SymDenotation if d.isTerm && d.termRef.symbol.is(Extension) => (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 = 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.nn.implicitScope(qual.typeOpt).companionRefs.showAsList
      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 = GivenVal, excluded = EmptyFlags)).map(_.info)
      val extMethodsFromGivensInImplicitScope = extractMemberExtensionMethods(givensInImplicitScope)

      val availableExtMethods = extMethodsFromGivensInImplicitScope ++ extMethodsFromImplicitScope ++ extMethodsFromGivensInScope ++ extMethodsInScope
      val extMethodsWithAppliedReceiver = availableExtMethods.flatMap {
        case (termRef, termName) =>
          if termRef.symbol.is(ExtensionMethod) && !qual.typeOpt.isBottomType then
            tryApplyingReceiverToExtension(termRef)
              .map(denot => termName -> denot)
          else None
      }
      extMethodsWithAppliedReceiver.groupByName

    lazy val isNew: Boolean = isInNewContext(untpdPath)

    /** Include in completion sets only symbols that
     *   1. match the filter method,
     *   2. satisfy [[Completion.isValidCompletionSymbol]]
     */
    private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean =
      matches(nameInScope) &&
      completionsFilter(NoType, nameInScope) &&
      isValidCompletionSymbol(denot.symbol, mode, isNew)

    private def extractRefinements(site: Type)(using Context): Seq[SingleDenotation] =
      site match
        case RefinedType(parent, name, info) =>
          val flags = info match
            case _: (ExprType | MethodOrPoly) => Method
            case _ => EmptyFlags
          val symbol = newSymbol(owner = NoSymbol, name, flags, info)
          val denot = SymDenotation(symbol, NoSymbol, name, flags, info)
          denot +: extractRefinements(parent)
        case tp: TypeProxy => extractRefinements(tp.superType)
        case _ => List.empty

    /** @param site The type to inspect.
     *  @return The members of `site` that are accessible and pass the include filter.
     */
    private def accessibleMembers(site: Type)(using Context): Seq[SingleDenotation] = {
      def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
        try
          val member = site.member(name)
          if member.symbol.is(ParamAccessor) && !member.symbol.isAccessibleFrom(site) then
            buf ++= site.nonPrivateMember(name).alternatives
          else
            buf ++= member.alternatives
        catch
          case ex: TypeError =>

      val members = site.memberDenots(completionsFilter, appendMemberSyms).collect {
        case mbr if include(mbr, mbr.name)
                    && mbr.symbol.isAccessibleFrom(site) => mbr
      }
      val refinements = extractRefinements(site).filter(mbr => include(mbr, mbr.name))

      members ++ refinements
    }

    /**
     * Given `qual` of type T, finds all the types S such that there exists an implicit conversion
     * from T to S. It then applies conversion method for proper type parameter resolution.
     *
     * @param qual The argument to which the implicit conversion should be applied.
     * @return The set of types after `qual` implicit conversion.
     */
    private def implicitConversionTargets(qual: tpd.Tree)(using Context): Set[SearchSuccess] = {
      val typer = ctx.typer
      val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits

      interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %")
      conversions
    }

    /** Filter for names that should appear when looking for completions. */
    private object completionsFilter extends NameFilter:
      def apply(pre: Type, name: Name)(using Context): Boolean =
        !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]]

  /**
   * The completion mode: defines what kinds of symbols should be included in the completion
   * results.
   */
  class Mode(val bits: Int) extends AnyVal:
    def is(other: Mode): Boolean = (bits & other.bits) == other.bits
    def |(other: Mode): Mode = new Mode(bits | other.bits)

  object Mode:
    /** No symbol should be included */
    val None: Mode = new Mode(0)

    /** Term symbols are allowed */
    val Term: Mode = new Mode(1)

    /** Type and stable term symbols are allowed */
    val Type: Mode = new Mode(2)

    /** Both term and type symbols are allowed */
    val ImportOrExport: Mode = new Mode(4) | Term | Type

    val Scope: Mode = new Mode(8)

    val Member: Mode = new Mode(16)





© 2015 - 2025 Weber Informatics LLC | Privacy Policy