Skip to content

Fix #10264: Add code completion for extension methods #10473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions compiler/src/dotty/tools/dotc/interactive/Completion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ 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}
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn, newSymbol}
import dotty.tools.dotc.core.Scopes
import dotty.tools.dotc.core.StdNames.{nme, tpnme}
import dotty.tools.dotc.core.TypeComparer
import dotty.tools.dotc.core.TypeError
import dotty.tools.dotc.core.Types.{NameFilter, NamedType, NoType, Type}
import dotty.tools.dotc.core.Types.{ExprType, MethodType, NameFilter, NamedType, NoType, PolyType, Type}
import dotty.tools.dotc.printing.Texts._
import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition}

Expand Down Expand Up @@ -118,7 +119,7 @@ object Completion {

if (buffer.mode != Mode.None)
path match {
case Select(qual, _) :: _ => buffer.addMemberCompletions(qual)
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
Expand Down Expand Up @@ -214,6 +215,66 @@ object Completion {
.foreach(addAccessibleMembers)
}

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)
}

val matchingNamePrefix = completionPrefix(path, pos)

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)
}
}

// 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))
}

// 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)

// 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)

// 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 availableExtMethods = extMethodsFromGivensInImplicitScope ++ extMethodsFromImplicitScope ++ extMethodsFromGivensInScope ++ extMethodsInScope
val extMethodsWithAppliedReceiver = availableExtMethods.collect {
case (symbol, termName) if ctx.typer.isApplicableExtensionMethod(symbol.termRef, qual.tpe) =>
applyExtensionReceiver(symbol, termName)
}

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.
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2176,4 +2176,9 @@ trait Applications extends Compatibility {
report.error(em"not an extension method: $methodRef", receiver.srcPos)
app
}

def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could check !receiver.isBottomType here to exclude Nothing and Null so you don't have to exclude them in the caller.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I can move the check here but not sure we should exclude Null. The compiler still lets you define an extension on Null so I think for consistency this should be displayed in a completion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler still lets you define an extension on Null so I think for consistency this should be displayed in a completion

I don't think it's a big deal either way, but because by default Null is a subtype of every reference type, one should be careful to not include all the extension methods that apply to reference types in the list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking isBottomType then

ref.symbol.is(ExtensionMethod)
&& !receiver.isBottomType
&& isApplicableMethodRef(ref, receiver :: Nil, WildcardType)
}
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,7 @@ trait ImportSuggestions:
site.member(name)
.alternatives
.map(mbr => TermRef(site, mbr.symbol))
.filter(ref =>
ref.symbol.is(ExtensionMethod)
&& isApplicableMethodRef(ref, argType :: Nil, WildcardType))
.filter(ref => ctx.typer.isApplicableExtensionMethod(ref, argType))
.headOption

try
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/repl/TabcompleteTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class TabcompleteTests extends ReplTest {
val comp = tabComplete("(null: AnyRef).")
assertEquals(
List("!=", "##", "->", "==", "asInstanceOf", "clone", "ensuring", "eq", "equals", "finalize", "formatted",
"getClass", "hashCode", "isInstanceOf", "ne", "notify", "notifyAll", "synchronized", "toString", "wait", "→"),
"getClass", "hashCode", "isInstanceOf", "ne", "nn", "notify", "notifyAll", "synchronized", "toString", "wait", "→"),
comp.distinct.sorted)
}

Expand Down
157 changes: 157 additions & 0 deletions language-server/test/dotty/tools/languageserver/CompletionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,161 @@ class CompletionTest {
|import Foo.b$m1""".withSource
.completion(m1, Set(("bar", Field, "type and lazy value bar")))
}

@Test def completeExtensionMethodWithoutParameter: Unit = {
code"""object Foo
|extension (foo: Foo.type) def xxxx = 1
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeExtensionMethodWithParameter: Unit = {
code"""object Foo
|extension (foo: Foo.type) def xxxx(i: Int) = i
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "(i: Int): Int")))
}

@Test def completeExtensionMethodWithTypeParameter: Unit = {
code"""object Foo
|extension [A](foo: Foo.type) def xxxx: 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
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "[A](a: A): A")))
}

@Test def completeExtensionMethodFromExtenionWithAUsingSection: Unit = {
code"""object Foo
|trait Bar
|trait Baz
|given Bar = new Bar {}
|given Baz = new Baz {}
|extension (foo: Foo.type)(using Bar, Baz) def xxxx = 1
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "(using x$1: Bar, x$2: Baz): Int")))
}

@Test def completeExtensionMethodFromExtenionWithMultipleUsingSections: Unit = {
code"""object Foo
|trait Bar
|trait Baz
|given Bar = new Bar {}
|given Baz = new Baz {}
|extension (foo: Foo.type)(using Bar)(using Baz) def xxxx = 1
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "(using x$1: Bar)(using x$2: Baz): Int")))
}

@Test def completeInheritedExtensionMethod: Unit = {
code"""object Foo
|trait FooOps {
| extension (foo: Foo.type) def xxxx = 1
|}
|object Main extends FooOps { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeRenamedExtensionMethod: Unit = {
code"""object Foo
|object FooOps {
| extension (foo: Foo.type) def xxxx = 1
|}
|import FooOps.{xxxx => yyyy}
|object Main { Foo.yy${m1} }""".withSource
.completion(m1, Set(("yyyy", Method, "=> Int")))
}

@Test def completeExtensionMethodFromGivenInstanceDefinedInScope: Unit = {
code"""object Foo
|trait FooOps
|given FooOps {
| extension (foo: Foo.type) def xxxx = 1
|}
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeExtensionMethodFromImportedGivenInstance: Unit = {
code"""object Foo
|trait FooOps
|object Bar {
| given FooOps {
| extension (foo: Foo.type) def xxxx = 1
| }
|}
|import Bar.given
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeExtensionMethodFromImplicitScope: Unit = {
code"""case class Foo(i: Int)
|object Foo {
| extension (foo: Foo) def xxxx = foo.i
|}
|object Main { Foo(123).xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeExtensionMethodFromGivenInImplicitScope: Unit = {
code"""trait Bar
|case class Foo(i: Int)
|object Foo {
| given Bar {
| extension (foo: Foo) def xxxx = foo.i
| }
|}
|object Main { Foo(123).xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def completeExtensionMethodOnResultOfImplicitConversion: Unit = {
code"""import scala.language.implicitConversions
|case class Foo(i: Int)
|extension (foo: Foo) def xxxx = foo.i
|given Conversion[Int, Foo] = Foo(_)
|object Main { 123.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> Int")))
}

@Test def dontCompleteExtensionMethodWithMismatchedName: Unit = {
code"""object Foo
|extension (foo: Foo.type) def xxxx = 1
|object Main { Foo.yy${m1} }""".withSource
.completion(m1, Set())
}

@Test def preferNormalMethodToExtensionMethod: Unit = {
code"""object Foo {
| def xxxx = "abcd"
|}
|object FooOps {
| extension (foo: Foo.type) def xxxx = 1
|}
|object Main { Foo.xx${m1} }""".withSource
.completion(m1, Set(("xxxx", Method, "=> String")))
}

@Test def preferExtensionMethodFromExplicitScope: Unit = {
code"""object Foo
|extension (foo: Foo.type) def xxxx = 1
|object FooOps {
| extension (foo: Foo.type) def xxxx = "abcd"
|}
|object Main { Foo.xx${m1} }""".withSource
.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
.completion(m1, Set())
}
}