Skip to content

Make code completions and import suggestions work correctly for extensions with leading using clauses #11187

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
Show file tree
Hide file tree
Changes from 1 commit
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
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] =
Copy link
Contributor

Choose a reason for hiding this comment

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

Usage of TermSymbol cannot handle prefix types, it's better to use TermRef.

The name of the method is long, but not informative. I'd suggest make it shorter and add documentation to make its semantics clear.

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: the lines are too long here (and in some other places), might be good to write the body of case in a new line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


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
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use the position from receiver?

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")))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it an accidenal change? The same question for the change below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is intentional. This is actually a new test case. The old one got copied below and renamed to completeExtensionMethodFromExtensionWithTypeParameter because now we need to distinguish between the two

}

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