Skip to content

Commit 22d3382

Browse files
committed
Make code completions and import suggestions work correctly for extensions with leading using clauses
1 parent 6d9e101 commit 22d3382

File tree

5 files changed

+246
-37
lines changed

5 files changed

+246
-37
lines changed

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

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ 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, newSymbol}
16+
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, TermSymbol, defn, newSymbol}
1717
import dotty.tools.dotc.core.Scopes
1818
import dotty.tools.dotc.core.StdNames.{nme, tpnme}
1919
import dotty.tools.dotc.core.TypeComparer
2020
import dotty.tools.dotc.core.TypeError
21-
import dotty.tools.dotc.core.Types.{ExprType, MethodType, NameFilter, NamedType, NoType, PolyType, Type}
21+
import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NamedType, NoType, PolyType, Type}
2222
import dotty.tools.dotc.printing.Texts._
2323
import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition}
2424

@@ -216,28 +216,25 @@ object Completion {
216216
}
217217

218218
def addExtensionCompletions(path: List[Tree], qual: Tree)(using Context): Unit =
219-
def applyExtensionReceiver(methodSymbol: Symbol, methodName: TermName): Symbol = {
220-
val newMethodType = methodSymbol.info match {
221-
case mt: MethodType =>
222-
mt.resultType match {
223-
case resType: MethodType => resType
224-
case resType => ExprType(resType)
225-
}
226-
case pt: PolyType =>
227-
PolyType(pt.paramNames)(_ => pt.paramInfos, _ => pt.resultType.resultType)
228-
}
229-
230-
newSymbol(owner = qual.symbol, methodName, methodSymbol.flags, newMethodType)
231-
}
219+
def asDefLikeType(tpe: Type): Type = tpe match
220+
case _: MethodOrPoly => tpe
221+
case _ => ExprType(tpe)
222+
223+
def tryApplyingReceiver(methodSym: TermSymbol): Option[TermSymbol] =
224+
ctx.typer.tryApplyingReceiver(methodSym, qual)
225+
.map { tree =>
226+
val tpe = asDefLikeType(tree.tpe.dealias)
227+
newSymbol(owner = qual.symbol, methodSym.name, methodSym.flags, tpe)
228+
}
232229

233230
val matchingNamePrefix = completionPrefix(path, pos)
234231

235232
def extractDefinedExtensionMethods(types: Seq[Type]) =
236233
types
237234
.flatMap(_.membersBasedOnFlags(required = ExtensionMethod, excluded = EmptyFlags))
238235
.collect{ denot =>
239-
denot.name.toTermName match {
240-
case name if name.startsWith(matchingNamePrefix) => (denot.symbol, name)
236+
denot.name match {
237+
case name: TermName if name.startsWith(matchingNamePrefix) => (denot.symbol.asTerm, name)
241238
}
242239
}
243240

@@ -248,7 +245,7 @@ object Completion {
248245
val buf = completionBuffer(path, pos)
249246
buf.addScopeCompletions
250247
buf.completions.mappings.toList.flatMap {
251-
case (termName, symbols) => symbols.map(s => (s, termName))
248+
case (termName, symbols) => symbols.map(s => (s.asTerm, termName))
252249
}
253250

254251
// 2. The extension method is a member of some given instance that is visible at the point of the reference.
@@ -264,9 +261,12 @@ object Completion {
264261
val extMethodsFromGivensInImplicitScope = extractDefinedExtensionMethods(givensInImplicitScope)
265262

266263
val availableExtMethods = extMethodsFromGivensInImplicitScope ++ extMethodsFromImplicitScope ++ extMethodsFromGivensInScope ++ extMethodsInScope
267-
val extMethodsWithAppliedReceiver = availableExtMethods.collect {
268-
case (symbol, termName) if ctx.typer.isApplicableExtensionMethod(symbol.termRef, qual.tpe) =>
269-
applyExtensionReceiver(symbol, termName)
264+
265+
val extMethodsWithAppliedReceiver = availableExtMethods.flatMap {
266+
case (symbol, termName) =>
267+
if symbol.is(ExtensionMethod) && !qual.tpe.isBottomType then
268+
tryApplyingReceiver(symbol).map(_.copy(name = termName))
269+
else None
270270
}
271271

272272
for (symbol <- extMethodsWithAppliedReceiver) do add(symbol, symbol.name)

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,8 +2133,34 @@ trait Applications extends Compatibility {
21332133
}
21342134
}
21352135

2136-
def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =
2137-
ref.symbol.is(ExtensionMethod)
2138-
&& !receiver.isBottomType
2139-
&& isApplicableMethodRef(ref, receiver :: Nil, WildcardType)
2136+
private def tryApplyingReceiverToTruncatedExtMethod(methodSym: TermSymbol, receiver: Tree)(using Context): scala.util.Try[Tree] =
2137+
// Drop all parameters sections of an extension method following the receiver to prevent them from being inferred by the typer
2138+
def truncateExtension(tp: Type): Type = tp match
2139+
case poly: PolyType => poly.newLikeThis(poly.paramNames, poly.paramInfos, truncateExtension(poly.resType))
2140+
case meth: MethodType if meth.isContextualMethod => meth.newLikeThis(meth.paramNames, meth.paramInfos, truncateExtension(meth.resType))
2141+
case meth: MethodType => meth.newLikeThis(meth.paramNames, meth.paramInfos, defn.AnyType)
2142+
2143+
val truncatedSym = methodSym.copy(owner = defn.RootPackage, name = Names.termName(""), info = truncateExtension(methodSym.info))
2144+
val truncatedRef = ref(truncatedSym).withSpan(Span(0, 0)) // Fake span needed to make this work in REPL
2145+
val newCtx = ctx.fresh.setNewScope.setReporter(new reporting.ThrowingReporter(ctx.reporter))
2146+
scala.util.Try {
2147+
inContext(newCtx) {
2148+
ctx.enter(truncatedSym)
2149+
ctx.typer.extMethodApply(truncatedRef, receiver, WildcardType)
2150+
}
2151+
}.filter(tree => tree.tpe.exists && !tree.tpe.isError)
2152+
2153+
def tryApplyingReceiver(methodSym: TermSymbol, receiver: Tree)(using Context): Option[Tree] =
2154+
def replaceAppliedRef(inTree: Tree, replacement: Tree)(using Context): Tree = inTree match
2155+
case Apply(fun, args) => Apply(replaceAppliedRef(fun, replacement), args)
2156+
case TypeApply(fun, args) => TypeApply(replaceAppliedRef(fun, replacement), args)
2157+
case _: Ident => replacement
2158+
2159+
tryApplyingReceiverToTruncatedExtMethod(methodSym, receiver)
2160+
.toOption
2161+
.map(tree => replaceAppliedRef(tree, ref(methodSym)))
2162+
2163+
def isApplicableExtensionMethod(ref: TermRef, receiverType: Type)(using Context) =
2164+
ref.symbol.is(ExtensionMethod) && !receiverType.isBottomType &&
2165+
tryApplyingReceiverToTruncatedExtMethod(ref.symbol.asTerm, Typed(EmptyTree, TypeTree(receiverType))).isSuccess
21402166
}

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

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,40 +308,140 @@ class CompletionTest {
308308

309309
@Test def completeExtensionMethodWithTypeParameter: Unit = {
310310
code"""object Foo
311-
|extension [A](foo: Foo.type) def xxxx: Int = 1
311+
|extension (foo: Foo.type) def xxxx[A]: Int = 1
312312
|object Main { Foo.xx${m1} }""".withSource
313313
.completion(m1, Set(("xxxx", Method, "[A] => Int")))
314314
}
315315

316316
@Test def completeExtensionMethodWithParameterAndTypeParameter: Unit = {
317317
code"""object Foo
318-
|extension [A](foo: Foo.type) def xxxx(a: A) = a
318+
|extension (foo: Foo.type) def xxxx[A](a: A) = a
319319
|object Main { Foo.xx${m1} }""".withSource
320320
.completion(m1, Set(("xxxx", Method, "[A](a: A): A")))
321321
}
322322

323-
@Test def completeExtensionMethodFromExtenionWithAUsingSection: Unit = {
323+
@Test def completeExtensionMethodFromExtensionWithTypeParameter: Unit = {
324+
code"""extension [A](a: A) def xxxx: A = a
325+
|object Main { "abc".xx${m1} }""".withSource
326+
.completion(m1, Set(("xxxx", Method, "=> String")))
327+
}
328+
329+
@Test def completeExtensionMethodWithResultTypeDependantOnReceiver: Unit = {
330+
code"""trait Foo { type Out; def get: Out}
331+
|object Bar extends Foo { type Out = String; def get: Out = "abc"}
332+
|extension (foo: Foo) def xxxx: foo.Out = foo.get
333+
|object Main { Bar.xx${m1} }""".withSource
334+
.completion(m1, Set(("xxxx", Method, "=> String")))
335+
}
336+
337+
@Test def completeExtensionMethodFromExtenionWithPrefixUsingSection: Unit = {
324338
code"""object Foo
325339
|trait Bar
326340
|trait Baz
327-
|given Bar = new Bar {}
328-
|given Baz = new Baz {}
341+
|given Bar with {}
342+
|given Baz with {}
343+
|extension (using Bar, Baz)(foo: Foo.type) def xxxx = 1
344+
|object Main { Foo.xx${m1} }""".withSource
345+
.completion(m1, Set(("xxxx", Method, "=> Int")))
346+
}
347+
348+
@Test def completeExtensionMethodFromExtenionWithMultiplePrefixUsingSections: Unit = {
349+
code"""object Foo
350+
|trait Bar
351+
|trait Baz
352+
|given Bar with {}
353+
|given Baz with {}
354+
|extension (using Bar)(using Baz)(foo: Foo.type) def xxxx = 1
355+
|object Main { Foo.xx${m1} }""".withSource
356+
.completion(m1, Set(("xxxx", Method, "=> Int")))
357+
}
358+
359+
@Test def dontCompleteExtensionMethodFromExtenionWithMissingImplicitFromPrefixUsingSection: Unit = {
360+
code"""object Foo
361+
|trait Bar
362+
|trait Baz
363+
|given Baz with {}
364+
|extension (using Bar, Baz)(foo: Foo.type) def xxxx = 1
365+
|object Main { Foo.xx${m1} }""".withSource
366+
.completion(m1, Set())
367+
}
368+
369+
@Test def completeExtensionMethodForReceiverOfTypeDependentOnLeadingImplicits: Unit = {
370+
code"""
371+
|trait Foo:
372+
| type Out <: Bar
373+
|
374+
|given Foo with
375+
| type Out = Baz
376+
|
377+
|trait Bar:
378+
| type Out
379+
|
380+
|trait Baz extends Bar
381+
|
382+
|given Baz with
383+
| type Out = Quux
384+
|
385+
|class Quux
386+
|
387+
|object Quux:
388+
| extension (using foo: Foo)(using fooOut: foo.Out)(fooOutOut: fooOut.Out) def xxxx = "abc"
389+
|
390+
|object Main { (new Quux).xx${m1} }""".withSource
391+
.completion(m1, Set(("xxxx", Method, "=> String")))
392+
}
393+
394+
@Test def completeExtensionMethodWithResultTypeDependentOnLeadingImplicit: Unit = {
395+
code"""object Foo
396+
|trait Bar { type Out; def get: Out }
397+
|given Bar with { type Out = 123; def get: Out = 123 }
398+
|extension (using bar: Bar)(foo: Foo.type) def xxxx: bar.Out = bar.get
399+
|object Main { Foo.xx${m1} }""".withSource
400+
.completion(m1, Set(("xxxx", Method, "=> (123 : Int)")))
401+
}
402+
403+
@Test def completeExtensionMethodFromExtenionWithPostfixUsingSection: Unit = {
404+
code"""object Foo
405+
|trait Bar
406+
|trait Baz
407+
|given Bar with {}
408+
|given Baz with {}
329409
|extension (foo: Foo.type)(using Bar, Baz) def xxxx = 1
330410
|object Main { Foo.xx${m1} }""".withSource
331411
.completion(m1, Set(("xxxx", Method, "(using x$2: Bar, x$3: Baz): Int")))
332412
}
333413

334-
@Test def completeExtensionMethodFromExtenionWithMultipleUsingSections: Unit = {
414+
@Test def completeExtensionMethodFromExtenionWithMultiplePostfixUsingSections: Unit = {
335415
code"""object Foo
336416
|trait Bar
337417
|trait Baz
338-
|given Bar = new Bar {}
339-
|given Baz = new Baz {}
418+
|given Bar with {}
419+
|given Baz with {}
340420
|extension (foo: Foo.type)(using Bar)(using Baz) def xxxx = 1
341421
|object Main { Foo.xx${m1} }""".withSource
342422
.completion(m1, Set(("xxxx", Method, "(using x$2: Bar)(using x$3: Baz): Int")))
343423
}
344424

425+
@Test def completeExtensionMethodWithTypeParameterFromExtenionWithTypeParametersAndPrefixAndPostfixUsingSections: Unit = {
426+
code"""trait Bar
427+
|trait Baz
428+
|given Bar with {}
429+
|given Baz with {}
430+
|extension [A](using bar: Bar)(a: A)(using baz: Baz) def xxxx[B]: Either[A, B] = Left(a)
431+
|object Main { 123.xx${m1} }""".withSource
432+
.completion(m1, Set(("xxxx", Method, "(using baz: Baz): [B] => Either[Int, B]")))
433+
}
434+
435+
@Test def completeExtensionMethodWithTypeBounds: Unit = {
436+
code"""trait Foo
437+
|trait Bar extends Foo
438+
|given Bar with {}
439+
|extension [A >: Bar](a: A) def xxxx[B <: a.type]: Either[A, B] = Left(a)
440+
|val foo = new Foo {}
441+
|object Main { foo.xx${m1} }""".withSource
442+
.completion(m1, Set(("xxxx", Method, "[B <: (foo : Foo)] => Either[Foo, B]")))
443+
}
444+
345445
@Test def completeInheritedExtensionMethod: Unit = {
346446
code"""object Foo
347447
|trait FooOps {
@@ -442,10 +542,9 @@ class CompletionTest {
442542
.completion(m1, Set(("xxxx", Method, "=> Int")))
443543
}
444544

445-
@Test def dontCompleteInapplicableExtensionMethod: Unit = {
446-
code"""case class Foo[A](a: A)
447-
|extension (foo: Foo[Int]) def xxxx = foo.a
448-
|object Main { Foo("abc").xx${m1} }""".withSource
545+
@Test def dontCompleteExtensionMethodWithMismatchedReceiverType: Unit = {
546+
code"""extension (i: Int) def xxxx = i
547+
|object Main { "abc".xx${m1} }""".withSource
449548
.completion(m1, Set())
450549
}
451550
}

tests/neg/missing-implicit6.check

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:34:8 ------------------------------------------------------
2+
34 | "a".xxx // error, no suggested import
3+
| ^^^^^^^
4+
| value xxx is not a member of String
5+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:35:8 ------------------------------------------------------
6+
35 | 123.xxx // error, suggested import
7+
| ^^^^^^^
8+
| value xxx is not a member of Int, but could be made available as an extension method.
9+
|
10+
| The following import might fix the problem:
11+
|
12+
| import Test.Ops.xxx
13+
|
14+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:36:8 ------------------------------------------------------
15+
36 | 123.yyy // error, suggested import
16+
| ^^^^^^^
17+
| value yyy is not a member of Int, but could be made available as an extension method.
18+
|
19+
| The following import might fix the problem:
20+
|
21+
| import Test.Ops.yyy
22+
|
23+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:41:8 ------------------------------------------------------
24+
41 | 123.xxx // error, no suggested import
25+
| ^^^^^^^
26+
| value xxx is not a member of Int
27+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:42:8 ------------------------------------------------------
28+
42 | 123.yyy // error, no suggested import
29+
| ^^^^^^^
30+
| value yyy is not a member of Int
31+
-- [E008] Not Found Error: tests/neg/missing-implicit6.scala:43:8 ------------------------------------------------------
32+
43 | 123.zzz // error, suggested import even though there's no instance of Bar in scope
33+
| ^^^^^^^
34+
| value zzz is not a member of Int, but could be made available as an extension method.
35+
|
36+
| The following import might fix the problem:
37+
|
38+
| import Test.Ops.zzz
39+
|

tests/neg/missing-implicit6.scala

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
trait Foo {
2+
type Out <: { type Out }
3+
}
4+
5+
trait Bar {
6+
type Out
7+
}
8+
9+
object instances {
10+
given foo: Foo with {
11+
type Out = Bar
12+
}
13+
14+
given bar: Bar with {
15+
type Out = Int
16+
}
17+
}
18+
19+
object Test {
20+
object Ops {
21+
extension (using foo: Foo, bar: foo.Out)(i: Int)
22+
def xxx = ???
23+
24+
extension (using foo: Foo, fooOut: foo.Out)(x: fooOut.Out)
25+
def yyy = ???
26+
27+
extension (using foo: Foo)(i: Int)(using fooOut: foo.Out)
28+
def zzz = ???
29+
}
30+
31+
locally {
32+
import instances.given
33+
34+
"a".xxx // error, no suggested import
35+
123.xxx // error, suggested import
36+
123.yyy // error, suggested import
37+
}
38+
39+
locally {
40+
import instances.foo
41+
123.xxx // error, no suggested import
42+
123.yyy // error, no suggested import
43+
123.zzz // error, suggested import even though there's no instance of Bar in scope
44+
}
45+
}

0 commit comments

Comments
 (0)