From 3c5e21636f50db1afaa192b42f643b42b5807ee3 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 20 Dec 2023 18:00:26 +0100 Subject: [PATCH] improvement: Support completions for implicit classes Previously, we would only support extension methods and not the old style implicit classes. Now, those are supported also. Introduced in https://github.com/scalameta/metals/pull/5904/ --- .../tools/pc/CompilerSearchVisitor.scala | 9 +- .../dotty/tools/pc/SemanticdbSymbols.scala | 11 ++ .../pc/completions/CompletionProvider.scala | 4 +- .../pc/completions/CompletionValue.scala | 14 ++ .../tools/pc/completions/Completions.scala | 28 ++- .../completion/CompletionExtensionSuite.scala | 170 +++++++++++++++++- 6 files changed, 230 insertions(+), 6 deletions(-) diff --git a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala index d217a0acd9b1..d20537f0239e 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala @@ -9,6 +9,7 @@ import scala.meta.pc.* import scala.util.control.NonFatal import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Symbols.* @@ -19,8 +20,14 @@ class CompilerSearchVisitor( val logger: Logger = Logger.getLogger(classOf[CompilerSearchVisitor].getName().nn).nn + private def isAccessibleImplicitClass(sym: Symbol) = + val owner = sym.maybeOwner + owner != NoSymbol && owner.isClass && + owner.is(Flags.Implicit) && + owner.isStatic && owner.isPublic + private def isAccessible(sym: Symbol): Boolean = try - sym != NoSymbol && sym.isPublic && sym.isStatic + sym != NoSymbol && sym.isPublic && sym.isStatic || isAccessibleImplicitClass(sym) catch case err: AssertionError => logger.log(Level.WARNING, err.getMessage()) diff --git a/presentation-compiler/src/main/dotty/tools/pc/SemanticdbSymbols.scala b/presentation-compiler/src/main/dotty/tools/pc/SemanticdbSymbols.scala index c6e43186cef2..da8add9df327 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/SemanticdbSymbols.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/SemanticdbSymbols.scala @@ -49,7 +49,16 @@ object SemanticdbSymbols: // however in scalac this method is defined only in `module Files` if typeSym.is(JavaDefined) then typeSym :: owner.info.decl(termName(value)).symbol :: Nil + /** + * Looks like decl doesn't work for: + * package a: + * implicit class <> (i: Int): + * def inc = i + 1 + */ + else if typeSym == NoSymbol then + owner.info.member(typeName(value)).symbol :: Nil else typeSym :: Nil + end if case Descriptor.Term(value) => val outSymbol = owner.info.decl(termName(value)).symbol if outSymbol.exists @@ -92,6 +101,8 @@ object SemanticdbSymbols: .map(_.symbol) .filter(sym => symbolName(sym) == s) .toList + end match + end tryMember parentSymbol.flatMap(tryMember) try diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala index f008ea5a9dbb..b3b7d1fde07d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala @@ -215,7 +215,7 @@ class CompletionProvider( def mkItemWithImports( v: CompletionValue.Workspace | CompletionValue.Extension | - CompletionValue.Interpolator + CompletionValue.Interpolator | CompletionValue.ImplicitClass ) = val sym = v.symbol path match @@ -260,7 +260,7 @@ class CompletionProvider( end mkItemWithImports completion match - case v: (CompletionValue.Workspace | CompletionValue.Extension) => + case v: (CompletionValue.Workspace | CompletionValue.Extension | CompletionValue.ImplicitClass) => mkItemWithImports(v) case v: CompletionValue.Interpolator if v.isWorkspace || v.isExtension => mkItemWithImports(v) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala index a5b2fb40f418..a890afa3cb67 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala @@ -143,6 +143,20 @@ object CompletionValue: override def isFromWorkspace: Boolean = true override def completionItemDataKind: Integer = CompletionSource.WorkspaceKind.ordinal + /** + * CompletionValue for old implicit classes methods via SymbolSearch + */ + case class ImplicitClass( + label: String, + symbol: Symbol, + override val snippetSuffix: CompletionSuffix, + override val importSymbol: Symbol, + ) extends Symbolic: + override def completionItemKind(using Context): CompletionItemKind = + CompletionItemKind.Method + override def description(printer: ShortenedTypePrinter)(using Context): String = + s"${printer.completionSymbol(symbol)} (implicit)" + /** * CompletionValue for extension methods via SymbolSearch */ diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 1e2dbb5cdaa1..118a9edae816 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -520,14 +520,38 @@ class Completions( Some(search.search(query, buildTargetIdentifier, visitor).nn) case CompletionKind.Members => val visitor = new CompilerSearchVisitor(sym => - if sym.is(ExtensionMethod) && + def isExtensionMethod = sym.is(ExtensionMethod) && qualType.widenDealias <:< sym.extensionParam.info.widenDealias - then + def isImplicitClass(owner: Symbol) = + val constructorParam = + owner.info + .membersBasedOnFlags( + Flags.ParamAccessor, + Flags.EmptyFlags, + ) + .headOption + .map(_.info) + owner.isClass && owner.is(Flags.Implicit) && + constructorParam.exists(p => + qualType.widenDealias <:< p.widenDealias + ) + end isImplicitClass + + def isImplicitClassMethod = sym.is(Flags.Method) && !sym.isConstructor && + isImplicitClass(sym.maybeOwner) + + if isExtensionMethod then completionsWithSuffix( sym, sym.decodedName, CompletionValue.Extension(_, _, _) ).map(visit).forall(_ == true) + else if isImplicitClassMethod then + completionsWithSuffix( + sym, + sym.decodedName, + CompletionValue.ImplicitClass(_, _, _, sym.maybeOwner), + ).map(visit).forall(_ == true) else false, ) Some(search.searchMethods(query, buildTargetIdentifier, visitor).nn) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala index a260fe8b6e52..c01b38e7cf61 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala @@ -20,6 +20,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `simple-old-syntax` = + check( + """|package example + | + |object Test: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.test@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin + ) + @Test def `simple2` = check( """|package example @@ -35,6 +49,21 @@ class CompletionExtensionSuite extends BaseCompletionSuite: filter = _.contains("(extension)") ) + @Test def `simple2-old-syntax` = + check( + """|package example + | + |object enrichments: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.t@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + @Test def `filter-by-type` = check( """|package example @@ -52,6 +81,22 @@ class CompletionExtensionSuite extends BaseCompletionSuite: filter = _.contains("(extension)") ) + @Test def `filter-by-type-old` = + check( + """|package example + | + |object enrichments: + | implicit class A(num: Int): + | def identity2: Int = num + 1 + | implicit class B(str: String): + | def identity: String = str + | + |def main = "foo".iden@@ + |""".stripMargin, + """|identity: String (implicit) + |""".stripMargin // identity2 won't be available + ) + @Test def `filter-by-type-subtype` = check( """|package example @@ -70,6 +115,24 @@ class CompletionExtensionSuite extends BaseCompletionSuite: filter = _.contains("(extension)") ) + @Test def `filter-by-type-subtype-old` = + check( + """|package example + | + |class A + |class B extends A + | + |object enrichments: + | implicit class Test(a: A): + | def doSomething: A = a + | + |def main = (new B).do@@ + |""".stripMargin, + """|doSomething: A (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + @Test def `simple-edit` = checkEdit( """|package example @@ -92,6 +155,28 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `simple-edit-old` = + checkEdit( + """|package example + | + |object enrichments: + | implicit class A (num: Int): + | def incr: Int = num + 1 + | + |def main = 100.inc@@ + |""".stripMargin, + """|package example + | + |import example.enrichments.A + | + |object enrichments: + | implicit class A (num: Int): + | def incr: Int = num + 1 + | + |def main = 100.incr + |""".stripMargin + ) + @Test def `simple-edit-suffix` = checkEdit( """|package example @@ -114,6 +199,28 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `simple-edit-suffix-old` = + checkEdit( + """|package example + | + |object enrichments: + | implicit class A (val num: Int): + | def plus(other: Int): Int = num + other + | + |def main = 100.pl@@ + |""".stripMargin, + """|package example + | + |import example.enrichments.A + | + |object enrichments: + | implicit class A (val num: Int): + | def plus(other: Int): Int = num + other + | + |def main = 100.plus($0) + |""".stripMargin + ) + @Test def `simple-empty` = check( """|package example @@ -129,6 +236,21 @@ class CompletionExtensionSuite extends BaseCompletionSuite: filter = _.contains("(extension)") ) + @Test def `simple-empty-old` = + check( + """|package example + | + |object enrichments: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + @Test def `directly-in-pkg1` = check( """| @@ -143,6 +265,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `directly-in-pkg1-old` = + check( + """| + |package examples: + | implicit class A(num: Int): + | def incr: Int = num + 1 + | + |package examples2: + | def main = 100.inc@@ + |""".stripMargin, + """|incr: Int (implicit) + |""".stripMargin + ) + @Test def `directly-in-pkg2` = check( """|package example: @@ -157,6 +293,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `directly-in-pkg2-old` = + check( + """|package examples: + | object X: + | def fooBar(num: Int) = num + 1 + | implicit class A (num: Int) { def incr: Int = num + 1 } + | + |package examples2: + | def main = 100.inc@@ + |""".stripMargin, + """|incr: Int (implicit) + |""".stripMargin + ) + @Test def `nested-pkg` = check( """|package a: // some comment @@ -175,7 +325,25 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin ) - @Test def `name-conflict` = + @Test def `nested-pkg-old` = + check( + """|package aa: // some comment + | package cc: + | implicit class A (num: Int): + | def increment2 = num + 2 + | implicit class A (num: Int): + | def increment = num + 1 + | + | + |package bb: + | def main: Unit = 123.incre@@ + |""".stripMargin, + """|increment: Int (implicit) + |increment2: Int (implicit) + |""".stripMargin + ) + + @Test def `name-conflict` = checkEdit( """ |package example