Skip to content

Commit

Permalink
Make code completions and import suggestions work correctly for exten…
Browse files Browse the repository at this point in the history
…sions with leading using clauses
  • Loading branch information
prolativ committed Jan 21, 2021
1 parent 6d9e101 commit 22d3382
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 37 deletions.
42 changes: 21 additions & 21 deletions compiler/src/dotty/tools/dotc/interactive/Completion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +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, newSymbol}
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, TermSymbol, 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.{ExprType, MethodType, NameFilter, NamedType, NoType, PolyType, Type}
import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NamedType, NoType, PolyType, Type}
import dotty.tools.dotc.printing.Texts._
import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition}

Expand Down Expand Up @@ -216,28 +216,25 @@ object Completion {
}

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)
}
def asDefLikeType(tpe: Type): Type = tpe match
case _: MethodOrPoly => tpe
case _ => ExprType(tpe)

def tryApplyingReceiver(methodSym: TermSymbol): Option[TermSymbol] =
ctx.typer.tryApplyingReceiver(methodSym, qual)
.map { tree =>
val tpe = asDefLikeType(tree.tpe.dealias)
newSymbol(owner = qual.symbol, methodSym.name, methodSym.flags, tpe)
}

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)
denot.name match {
case name: TermName if name.startsWith(matchingNamePrefix) => (denot.symbol.asTerm, name)
}
}

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

// 2. The extension method is a member of some given instance that is visible at the point of the reference.
Expand All @@ -264,9 +261,12 @@ object Completion {
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)

val extMethodsWithAppliedReceiver = availableExtMethods.flatMap {
case (symbol, termName) =>
if symbol.is(ExtensionMethod) && !qual.tpe.isBottomType then
tryApplyingReceiver(symbol).map(_.copy(name = termName))
else None
}

for (symbol <- extMethodsWithAppliedReceiver) do add(symbol, symbol.name)
Expand Down
34 changes: 30 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2133,8 +2133,34 @@ trait Applications extends Compatibility {
}
}

def isApplicableExtensionMethod(ref: TermRef, receiver: Type)(using Context) =
ref.symbol.is(ExtensionMethod)
&& !receiver.isBottomType
&& isApplicableMethodRef(ref, receiver :: Nil, WildcardType)
private def tryApplyingReceiverToTruncatedExtMethod(methodSym: TermSymbol, receiver: Tree)(using Context): scala.util.Try[Tree] =
// Drop all parameters sections of an extension method following the receiver to prevent them from being inferred by the typer
def truncateExtension(tp: Type): 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)

val truncatedSym = methodSym.copy(owner = defn.RootPackage, name = Names.termName(""), info = truncateExtension(methodSym.info))
val truncatedRef = ref(truncatedSym).withSpan(Span(0, 0)) // Fake span needed to make this work in REPL
val newCtx = ctx.fresh.setNewScope.setReporter(new reporting.ThrowingReporter(ctx.reporter))
scala.util.Try {
inContext(newCtx) {
ctx.enter(truncatedSym)
ctx.typer.extMethodApply(truncatedRef, receiver, WildcardType)
}
}.filter(tree => tree.tpe.exists && !tree.tpe.isError)

def tryApplyingReceiver(methodSym: TermSymbol, receiver: Tree)(using Context): Option[Tree] =
def replaceAppliedRef(inTree: Tree, replacement: Tree)(using Context): Tree = inTree match
case Apply(fun, args) => Apply(replaceAppliedRef(fun, replacement), args)
case TypeApply(fun, args) => TypeApply(replaceAppliedRef(fun, replacement), args)
case _: Ident => replacement

tryApplyingReceiverToTruncatedExtMethod(methodSym, receiver)
.toOption
.map(tree => replaceAppliedRef(tree, ref(methodSym)))

def isApplicableExtensionMethod(ref: TermRef, receiverType: Type)(using Context) =
ref.symbol.is(ExtensionMethod) && !receiverType.isBottomType &&
tryApplyingReceiverToTruncatedExtMethod(ref.symbol.asTerm, Typed(EmptyTree, TypeTree(receiverType))).isSuccess
}
123 changes: 111 additions & 12 deletions language-server/test/dotty/tools/languageserver/CompletionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -308,40 +308,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 = new Bar {}
|given Baz = new 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 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 {
Expand Down Expand Up @@ -442,10 +542,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())
}
}
39 changes: 39 additions & 0 deletions tests/neg/missing-implicit6.check
Original file line number Diff line number Diff line change
@@ -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
|
45 changes: 45 additions & 0 deletions tests/neg/missing-implicit6.scala
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 22d3382

Please sign in to comment.