Skip to content

Commit 3bfc89c

Browse files
committed
Fix #10264: Add code completion for extension methods
1 parent 7bdf89e commit 3bfc89c

File tree

4 files changed

+192
-9
lines changed

4 files changed

+192
-9
lines changed

compiler/src/dotty/tools/dotc/interactive/Completion.scala

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import dotty.tools.dotc.core.Flags._
1313
import dotty.tools.dotc.core.Names.{Name, TermName}
1414
import dotty.tools.dotc.core.NameKinds.SimpleNameKind
1515
import dotty.tools.dotc.core.NameOps._
16-
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn}
16+
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn, newSymbol}
1717
import dotty.tools.dotc.core.Scopes
1818
import dotty.tools.dotc.core.StdNames.{nme, tpnme}
1919
import dotty.tools.dotc.core.TypeError
20-
import dotty.tools.dotc.core.Types.{NameFilter, NamedType, NoType, Type}
20+
import dotty.tools.dotc.core.Types.{ExprType, MethodType, NameFilter, NamedType, NoType, PolyType, Type}
2121
import dotty.tools.dotc.printing.Texts._
2222
import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition}
2323

@@ -118,10 +118,15 @@ object Completion {
118118

119119
if (buffer.mode != Mode.None)
120120
path match {
121-
case Select(qual, _) :: _ => buffer.addMemberCompletions(qual)
122-
case Import(expr, _) :: _ => buffer.addMemberCompletions(expr) // TODO: distinguish given from plain imports
123-
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => buffer.addMemberCompletions(expr)
124-
case _ => buffer.addScopeCompletions
121+
case Select(qual, _) :: _ =>
122+
buffer.addExtensionCompletions(path, qual)
123+
buffer.addMemberCompletions(qual)
124+
case Import(expr, _) :: _ =>
125+
buffer.addMemberCompletions(expr) // TODO: distinguish given from plain imports
126+
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ =>
127+
buffer.addMemberCompletions(expr)
128+
case _ =>
129+
buffer.addScopeCompletions
125130
}
126131

127132
val completionList = buffer.getCompletions
@@ -214,6 +219,53 @@ object Completion {
214219
.foreach(addAccessibleMembers)
215220
}
216221

222+
def addExtensionCompletions(path: List[Tree], qual: Tree)(using Context): Unit =
223+
def applyExtensionReceiver(methodSymbol: Symbol, methodName: TermName): Symbol = {
224+
val newMethodType = methodSymbol.info match {
225+
case mt: MethodType =>
226+
mt.resultType match {
227+
case resType: MethodType => resType
228+
case resType => ExprType(resType)
229+
}
230+
case pt: PolyType =>
231+
val resType = pt.resultType
232+
PolyType(pt.paramNames)(_ => pt.paramInfos, _ => resType.resultType)
233+
}
234+
235+
newSymbol(owner = qual.symbol, methodName, methodSymbol.flags, newMethodType)
236+
}
237+
238+
val extensionMethodsWithMatchingNameInScope =
239+
val buf = completionBuffer(path, pos)
240+
buf.addScopeCompletions
241+
buf.completions.mappings.toList.flatMap {
242+
case (termName, symbols) => symbols.map(s => (s, termName))
243+
}
244+
245+
val givensInScope = ctx.implicits.eligible(defn.AnyType).map(_.implicitRef.underlyingRef)
246+
val implicitScopeCompanions = ctx.run.implicitScope(qual.tpe).companionRefs.showAsList
247+
val givensInImplicitScope = implicitScopeCompanions.flatMap(_.membersBasedOnFlags(required = Given, excluded = EmptyFlags)).map(_.symbol.info)
248+
val implicitExtensionSources = givensInImplicitScope ++ implicitScopeCompanions ++ givensInScope
249+
250+
val matchingNamePrefix = completionPrefix(path, pos)
251+
val extensionMethodsWithMatchingNameFromImplicits =
252+
implicitExtensionSources
253+
.flatMap(_.membersBasedOnFlags(required = ExtensionMethod, excluded = EmptyFlags))
254+
.collect{ denot =>
255+
denot.name.toTermName match {
256+
case name if name.startsWith(matchingNamePrefix) => (denot.symbol, name)
257+
}
258+
}
259+
260+
val extensionMethodsWithAppliedReceiver =
261+
(extensionMethodsWithMatchingNameFromImplicits ++ extensionMethodsWithMatchingNameInScope)
262+
.collect {
263+
case (symbol, termName) if ctx.typer.isApplicableExtensionMethod(symbol.termRef, qual.tpe) =>
264+
applyExtensionReceiver(symbol, termName)
265+
}
266+
267+
for (symbol <- extensionMethodsWithAppliedReceiver) do add(symbol, symbol.name)
268+
217269
/**
218270
* If `sym` exists, no symbol with the same name is already included, and it satisfies the
219271
* inclusion filter, then add it to the completions.

compiler/src/dotty/tools/dotc/typer/Applications.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,4 +2176,8 @@ trait Applications extends Compatibility {
21762176
report.error(em"not an extension method: $methodRef", receiver.srcPos)
21772177
app
21782178
}
2179+
2180+
def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =
2181+
ref.symbol.is(ExtensionMethod)
2182+
&& isApplicableMethodRef(ref, receiver :: Nil, WildcardType)
21792183
}

compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,7 @@ trait ImportSuggestions:
222222
site.member(name)
223223
.alternatives
224224
.map(mbr => TermRef(site, mbr.symbol))
225-
.filter(ref =>
226-
ref.symbol.is(ExtensionMethod)
227-
&& isApplicableMethodRef(ref, argType :: Nil, WildcardType))
225+
.filter(ref => ctx.typer.isApplicableExtensionMethod(ref, argType))
228226
.headOption
229227

230228
try

language-server/test/dotty/tools/languageserver/CompletionTest.scala

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,133 @@ class CompletionTest {
288288
|import Foo.b$m1""".withSource
289289
.completion(m1, Set(("bar", Field, "type and lazy value bar")))
290290
}
291+
292+
@Test def completeExtensionMethodWithoutParameter: Unit = {
293+
code"""object Foo
294+
|extension (foo: Foo.type) def xxxx = 1
295+
|object Main { Foo.xx${m1} }""".withSource
296+
.completion(m1, Set(("xxxx", Method, "=> Int")))
297+
}
298+
299+
@Test def completeExtensionMethodWithParameter: Unit = {
300+
code"""object Foo
301+
|extension (foo: Foo.type) def xxxx(i: Int) = i
302+
|object Main { Foo.xx${m1} }""".withSource
303+
.completion(m1, Set(("xxxx", Method, "(i: Int): Int")))
304+
}
305+
306+
@Test def completeExtensionMethodWithTypeParameter: Unit = {
307+
code"""object Foo
308+
|extension [A](foo: Foo.type) def xxxx: Int = 1
309+
|object Main { Foo.xx${m1} }""".withSource
310+
.completion(m1, Set(("xxxx", Method, "[A] => Int")))
311+
}
312+
313+
@Test def completeExtensionMethodWithParameterAndTypeParameter: Unit = {
314+
code"""object Foo
315+
|extension [A](foo: Foo.type) def xxxx(a: A) = a
316+
|object Main { Foo.xx${m1} }""".withSource
317+
.completion(m1, Set(("xxxx", Method, "[A](a: A): A")))
318+
}
319+
320+
@Test def completeExtensionMethodFromExtenionWithAUsingSection: Unit = {
321+
code"""object Foo
322+
|trait Bar
323+
|trait Baz
324+
|given Bar = new Bar {}
325+
|given Baz = new Baz {}
326+
|extension (foo: Foo.type)(using Bar, Baz) def xxxx = 1
327+
|object Main { Foo.xx${m1} }""".withSource
328+
.completion(m1, Set(("xxxx", Method, "(using x$1: Bar, x$2: Baz): Int")))
329+
}
330+
331+
@Test def completeExtensionMethodFromExtenionWithMultipleUsingSections: Unit = {
332+
code"""object Foo
333+
|trait Bar
334+
|trait Baz
335+
|given Bar = new Bar {}
336+
|given Baz = new Baz {}
337+
|extension (foo: Foo.type)(using Bar)(using Baz) def xxxx = 1
338+
|object Main { Foo.xx${m1} }""".withSource
339+
.completion(m1, Set(("xxxx", Method, "(using x$1: Bar)(using x$2: Baz): Int")))
340+
}
341+
342+
@Test def completeRenamedExtensionMethod: Unit = {
343+
code"""object Foo
344+
|object FooOps {
345+
| extension (foo: Foo.type) def xxxx = 1
346+
|}
347+
|import FooOps.{xxxx => yyyy}
348+
|object Main { Foo.yy${m1} }""".withSource
349+
.completion(m1, Set(("yyyy", Method, "=> Int")))
350+
}
351+
352+
@Test def completeExtensionMethodFromGivenInstanceDefinedInScope: Unit = {
353+
code"""object Foo
354+
|trait FooOps
355+
|given FooOps {
356+
| extension (foo: Foo.type) def xxxx = 1
357+
|}
358+
|object Main { Foo.xx${m1} }""".withSource
359+
.completion(m1, Set(("xxxx", Method, "=> Int")))
360+
}
361+
362+
@Test def completeExtensionMethodFromImportedGivenInstance: Unit = {
363+
code"""object Foo
364+
|trait FooOps
365+
|object Bar {
366+
| given FooOps {
367+
| extension (foo: Foo.type) def xxxx = 1
368+
| }
369+
|}
370+
|import Bar.given
371+
|object Main { Foo.xx${m1} }""".withSource
372+
.completion(m1, Set(("xxxx", Method, "=> Int")))
373+
}
374+
375+
@Test def completeExtensionMethodFromImplicitScope: Unit = {
376+
code"""case class Foo(i: Int)
377+
|object Foo {
378+
| extension (foo: Foo) def xxxx = foo.i
379+
|}
380+
|object Main { Foo(123).xx${m1} }""".withSource
381+
.completion(m1, Set(("xxxx", Method, "=> Int")))
382+
}
383+
384+
@Test def completeExtensionMethodFromGivenInImplicitScope: Unit = {
385+
code"""trait Bar
386+
|case class Foo(i: Int)
387+
|object Foo {
388+
| given Bar {
389+
| extension (foo: Foo) def xxxx = foo.i
390+
| }
391+
|}
392+
|object Main { Foo(123).xx${m1} }""".withSource
393+
.completion(m1, Set(("xxxx", Method, "=> Int")))
394+
}
395+
396+
@Test def dontCompleteExtensionMethodWithMismatchedName: Unit = {
397+
code"""object Foo
398+
|extension (foo: Foo.type) def xxxx = 1
399+
|object Main { Foo.yy${m1} }""".withSource
400+
.completion(m1, Set())
401+
}
402+
403+
@Test def dontCompleteShadowedExtensionMethod: Unit = {
404+
code"""object Foo {
405+
| def xxxx = "abcd"
406+
|}
407+
|object FooOps {
408+
| extension (foo: Foo.type) def xxxx = 1
409+
|}
410+
|object Main { Foo.xx${m1} }""".withSource
411+
.completion(m1, Set(("xxxx", Method, "=> String")))
412+
}
413+
414+
@Test def dontCompleteInapplicableExtensionMethod: Unit = {
415+
code"""case class Foo[A](a: A)
416+
|extension (foo: Foo[Int]) def xxxx = foo.a
417+
|object Main { Foo("abc").xx${m1} }""".withSource
418+
.completion(m1, Set())
419+
}
291420
}

0 commit comments

Comments
 (0)