diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 41b6ac43c714..171616ec7031 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -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} @@ -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 @@ -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. diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 3960ac1a37bb..8ff7a16c4a27 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -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) = + ref.symbol.is(ExtensionMethod) + && !receiver.isBottomType + && isApplicableMethodRef(ref, receiver :: Nil, WildcardType) } diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index ecee8265d038..4c3e8b0980dd 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -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 diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 4e6bfbddf102..4e693b698351 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -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) } diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index a6d9246a7bee..5383186c3593 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -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()) + } }