diff --git a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala index f3dc8d1658c4..378564d90bc1 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala @@ -19,12 +19,14 @@ import scala.meta.internal.metals.EmptyReportContext import scala.meta.internal.metals.ReportContext import scala.meta.internal.metals.ReportLevel import scala.meta.internal.metals.StdReportContext +import scala.meta.internal.mtags.CommonMtagsEnrichments.* import scala.meta.internal.pc.CompilerAccess import scala.meta.internal.pc.DefinitionResultImpl import scala.meta.internal.pc.EmptyCompletionList import scala.meta.internal.pc.EmptySymbolSearch import scala.meta.internal.pc.PresentationCompilerConfigImpl import scala.meta.pc.* +import scala.meta.pc.{PcSymbolInformation as IPcSymbolInformation} import dotty.tools.dotc.reporting.StoreReporter import dotty.tools.pc.completions.CompletionProvider @@ -34,6 +36,7 @@ import dotty.tools.pc.buildinfo.BuildInfo import org.eclipse.lsp4j.DocumentHighlight import org.eclipse.lsp4j.TextEdit import org.eclipse.lsp4j as l +import scala.meta.internal.pc.SymbolInformationProvider case class ScalaPresentationCompiler( buildTargetIdentifier: String = "", @@ -184,6 +187,21 @@ case class ScalaPresentationCompiler( def diagnosticsForDebuggingPurposes(): ju.List[String] = List[String]().asJava + override def info( + symbol: String + ): CompletableFuture[Optional[IPcSymbolInformation]] = + compilerAccess.withNonInterruptableCompiler[Optional[IPcSymbolInformation]]( + None + )( + Optional.empty(), + EmptyCancelToken, + ) { access => + SymbolInformationProvider(using access.compiler().currentCtx) + .info(symbol) + .map(_.asJava) + .asJava + } + def semanticdbTextDocument( filename: URI, code: String diff --git a/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala new file mode 100644 index 000000000000..aa1508f89313 --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala @@ -0,0 +1,122 @@ +package scala.meta.internal.pc + +import scala.util.control.NonFatal + +import scala.meta.pc.PcSymbolKind +import scala.meta.pc.PcSymbolProperty + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Denotations.Denotation +import dotty.tools.dotc.core.Denotations.MultiDenotation +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.pc.utils.MtagsEnrichments.metalsDealias +import dotty.tools.pc.SemanticdbSymbols +import dotty.tools.pc.utils.MtagsEnrichments.allSymbols + +class SymbolInformationProvider(using Context): + private def toSymbols( + pkg: String, + parts: List[(String, Boolean)], + ): List[Symbol] = + def loop( + owners: List[Symbol], + parts: List[(String, Boolean)], + ): List[Symbol] = + parts match + case (head, isClass) :: tl => + val foundSymbols = + owners.flatMap { owner => + val next = + if isClass then owner.info.member(typeName(head)) + else owner.info.member(termName(head)) + next.allSymbols + } + if foundSymbols.nonEmpty then loop(foundSymbols, tl) + else Nil + case Nil => owners + + val pkgSym = + if pkg == "_empty_" then requiredPackage(nme.EMPTY_PACKAGE) + else requiredPackage(pkg) + loop(List(pkgSym), parts) + end toSymbols + + def info(symbol: String): Option[PcSymbolInformation] = + val index = symbol.lastIndexOf("/") + val pkg = normalizePackage(symbol.take(index + 1)) + + def loop( + symbol: String, + acc: List[(String, Boolean)], + ): List[(String, Boolean)] = + if symbol.isEmpty() then acc.reverse + else + val newSymbol = symbol.takeWhile(c => c != '.' && c != '#') + val rest = symbol.drop(newSymbol.size) + loop(rest.drop(1), (newSymbol, rest.headOption.exists(_ == '#')) :: acc) + val names = + loop(symbol.drop(index + 1).takeWhile(_ != '('), List.empty) + + val foundSymbols = + try toSymbols(pkg, names) + catch case NonFatal(e) => Nil + + val (searchedSymbol, alternativeSymbols) = + foundSymbols.partition: compilerSymbol => + SemanticdbSymbols.symbolName(compilerSymbol) == symbol + + searchedSymbol match + case Nil => None + case sym :: _ => + val classSym = if sym.isClass then sym else sym.moduleClass + val parents = + if classSym.isClass + then classSym.asClass.parentSyms.map(SemanticdbSymbols.symbolName) + else Nil + val dealisedSymbol = + if sym.isAliasType then sym.info.metalsDealias.typeSymbol else sym + val classOwner = + sym.ownersIterator.drop(1).find(s => s.isClass || s.is(Flags.Module)) + val overridden = sym.denot.allOverriddenSymbols.toList + + val pcSymbolInformation = + PcSymbolInformation( + symbol = SemanticdbSymbols.symbolName(sym), + kind = getSymbolKind(sym), + parents = parents, + dealiasedSymbol = SemanticdbSymbols.symbolName(dealisedSymbol), + classOwner = classOwner.map(SemanticdbSymbols.symbolName), + overriddenSymbols = overridden.map(SemanticdbSymbols.symbolName), + alternativeSymbols = + alternativeSymbols.map(SemanticdbSymbols.symbolName), + properties = + if sym.is(Flags.Abstract) then List(PcSymbolProperty.ABSTRACT) + else Nil, + ) + + Some(pcSymbolInformation) + end match + end info + + private def getSymbolKind(sym: Symbol): PcSymbolKind = + if sym.isAllOf(Flags.JavaInterface) then PcSymbolKind.INTERFACE + else if sym.is(Flags.Trait) then PcSymbolKind.TRAIT + else if sym.isConstructor then PcSymbolKind.CONSTRUCTOR + else if sym.isPackageObject then PcSymbolKind.PACKAGE_OBJECT + else if sym.isClass then PcSymbolKind.CLASS + else if sym.is(Flags.Macro) then PcSymbolKind.MACRO + else if sym.is(Flags.Local) then PcSymbolKind.LOCAL + else if sym.is(Flags.Method) then PcSymbolKind.METHOD + else if sym.is(Flags.Param) then PcSymbolKind.PARAMETER + else if sym.is(Flags.Package) then PcSymbolKind.PACKAGE + else if sym.is(Flags.TypeParam) then PcSymbolKind.TYPE_PARAMETER + else if sym.isType then PcSymbolKind.TYPE + else PcSymbolKind.UNKNOWN_KIND + + private def normalizePackage(pkg: String): String = + pkg.replace("/", ".").nn.stripSuffix(".") + +end SymbolInformationProvider diff --git a/presentation-compiler/test/dotty/tools/pc/tests/info/InfoSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/info/InfoSuite.scala new file mode 100644 index 000000000000..8464c4d69da4 --- /dev/null +++ b/presentation-compiler/test/dotty/tools/pc/tests/info/InfoSuite.scala @@ -0,0 +1,56 @@ +package dotty.tools.pc.tests.info + +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.PcSymbolKind +import scala.meta.pc.PcSymbolProperty + +import scala.meta.pc.PcSymbolInformation +import dotty.tools.pc.base.BasePCSuite +import scala.language.unsafeNulls +import org.junit.Test + +class InfoSuite extends BasePCSuite { + + def getInfo(symbol: String): PcSymbolInformation = { + val result = presentationCompiler.info(symbol).get() + assertEquals(true, result.isPresent(), s"no info returned for symbol $symbol") + assertNoDiff(result.get().symbol(), symbol) + result.get() + } + + @Test def `list` = + val info = getInfo("scala/collection/immutable/List#") + assertEquals(true, info.properties().contains(PcSymbolProperty.ABSTRACT), s"class List should be abstract") + assertEquals( + true, + info.parents().contains("scala/collection/immutable/LinearSeq#"), + "class List should extend LinearSeq" + ) + + @Test def `empty-list-constructor` = + val info = getInfo("scala/collection/immutable/List.empty().") + assertNoDiff(info.classOwner(), "scala/collection/immutable/List.") + assertEquals(info.kind(), PcSymbolKind.METHOD, "List.empty() should be a method") + + @Test def `assert` = + val info = getInfo("scala/Predef.assert().") + assertEquals(info.kind(), PcSymbolKind.METHOD, "assert() should be a method") + assertNoDiff(info.classOwner(), "scala/Predef.") + assertEquals( + info.alternativeSymbols().asScala.mkString("\n"), + "scala/Predef.assert(+1).", + "there should be a single alternative symbol to assert()" + ) + + @Test def `flatMap` = + val info = getInfo("scala/collection/immutable/List#flatMap().") + assertEquals(info.kind(), PcSymbolKind.METHOD, "List.flatMap() should be a method") + assertNoDiff(info.classOwner(), "scala/collection/immutable/List#") + assertNoDiff( + info.overriddenSymbols().asScala.mkString("\n"), + """|scala/collection/StrictOptimizedIterableOps#flatMap(). + |scala/collection/IterableOps#flatMap(). + |scala/collection/IterableOnceOps#flatMap(). + |""".stripMargin + ) +} diff --git a/project/Build.scala b/project/Build.scala index ba6fe5a555fd..df418a7fbab5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1340,7 +1340,7 @@ object Build { BuildInfoPlugin.buildInfoDefaultSettings lazy val presentationCompilerSettings = { - val mtagsVersion = "1.2.2+25-bb9dfbb9-SNAPSHOT" + val mtagsVersion = "1.2.2+44-42e0515a-SNAPSHOT" Seq( resolvers ++= Resolver.sonatypeOssRepos("snapshots"),