From 35f06b312ceb05ecdc2bb91e53328ef833174718 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Tue, 28 Dec 2021 15:32:25 +0000 Subject: [PATCH] navigate to decompiled class if no source --- .../internal/metals/DefinitionProvider.scala | 81 +++++++++++++++---- .../internal/metals/FileDecoderProvider.scala | 74 ++++++++++------- .../internal/metals/MetalsEnrichments.scala | 30 ++++++- 3 files changed, 138 insertions(+), 47 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala index 55d628f84c3..fde0a2cb6a2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DefinitionProvider.scala @@ -1,5 +1,9 @@ package scala.meta.internal.metals +import java.net.URI +import java.nio.file.FileSystemNotFoundException +import java.nio.file.FileSystems +import java.nio.file.Paths import java.util.Collections import java.{util => ju} @@ -246,21 +250,36 @@ case class DefinitionDestination( * Converts snapshot position to dirty buffer position in the destination file */ def toResult: Option[DefinitionResult] = - for { - location <- snapshot.definition(uri, symbol) - revisedPosition = distance.toRevised( - location.getRange.getStart.getLine, - location.getRange.getStart.getCharacter + if (uri.endsWith(".class")) { + val decodeURI = s"metalsDecode:$uri.cfr" + val location = new Location( + decodeURI, + new org.eclipse.lsp4j.Range(new Position(0, 0), new Position(0, 0)) ) - result <- revisedPosition.toLocation(location) - } yield { - DefinitionResult( - Collections.singletonList(result), - symbol, - path, - Some(snapshot) + Some( + DefinitionResult( + Collections.singletonList(location), + symbol, + path, + Some(snapshot) + ) ) - } + } else + for { + location <- snapshot.definition(uri, symbol) + revisedPosition = distance.toRevised( + location.getRange.getStart.getLine, + location.getRange.getStart.getCharacter + ) + result <- revisedPosition.toLocation(location) + } yield { + DefinitionResult( + Collections.singletonList(result), + symbol, + path, + Some(snapshot) + ) + } } class DestinationProvider( @@ -310,11 +329,39 @@ class DestinationProvider( symbol: String, allowedBuildTargets: Set[BuildTargetIdentifier] ): Option[SymbolDefinition] = { - val definitions = index.definitions(Symbol(symbol)).filter(_.path.exists) + val querySymbol: Symbol = Symbol(symbol) + val definitions = index.definitions(querySymbol).filter(_.path.exists) + val extraDefinitions = if (definitions.isEmpty) { + // search in all classpath, i.e. jars with no source + buildTargets + .inferBuildTarget(List(querySymbol)) + .map(f => { + val uri = f.jar.toURI.toString() + val fullURI = + URI.create(s"jar:${uri}!/${symbol.stripSuffix("#")}.class") + val fullJarPath = + try { + AbsolutePath(Paths.get(fullURI)) + } catch { + case _: FileSystemNotFoundException => + FileSystems + .newFileSystem(fullURI, new ju.HashMap[String, Any]()) + AbsolutePath(Paths.get(fullURI)) + } + + SymbolDefinition( + querySymbol, + querySymbol, + fullJarPath, + scala.meta.dialects.Scala212Source3 + ) + }) + .toList + } else definitions if (allowedBuildTargets.isEmpty) - definitions.headOption + extraDefinitions.headOption else { - val matched = definitions.find { defn => + val matched = extraDefinitions.find { defn => sourceBuildTargets(defn.path).exists(id => allowedBuildTargets.contains(id) ) @@ -322,7 +369,7 @@ class DestinationProvider( // Fallback to any definition - it's needed for worksheets // They might have dynamic `import $dep` and these sources jars // aren't registered in buildTargets - matched.orElse(definitions.headOption) + matched.orElse(extraDefinitions.headOption) } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala index 401993df132..595f8456c4d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala @@ -3,9 +3,7 @@ package scala.meta.internal.metals import java.io.ByteArrayOutputStream import java.io.PrintStream import java.net.URI -import java.net.URLDecoder import java.nio.charset.StandardCharsets -import java.nio.file.Paths import javax.annotation.Nullable import scala.annotation.tailrec @@ -141,13 +139,18 @@ final class FileDecoderProvider( * * jar: * metalsDecode:jar:file:///somePath/someFile-sources.jar!/somePackage/someFile.java + * + * jar + cfr: + * metalsDecode:jar:file:///somePath/someFile.jar!/somePackage/someFile.class.cfr + * + * build target: + * metalsDecode:file:///workspacePath/buildTargetName.metals-buildtarget */ def decodedFileContents(uriAsStr: String): Future[DecoderResponse] = { Try(URI.create(uriAsStr)) match { case Success(uri) => uri.getScheme() match { - case "jar" => Future { decodeJar(uri) } - case "file" => decodeMetalsFile(uri) + case "jar" | "file" => decodeMetalsFile(uri) case "metalsDecode" => decodedFileContents(uri.getSchemeSpecificPart()) case _ => @@ -165,21 +168,6 @@ final class FileDecoderProvider( } } - private def decodeJar(uri: URI): DecoderResponse = { - Try { - // jar file system cannot cope with a heavily encoded uri - // hence the roundabout way of creating an AbsolutePath - // must have "jar:file:"" instead of "jar:file%3A" - val decodedUriStr = URLDecoder.decode(uri.toString(), "UTF-8") - val decodedUri = URI.create(decodedUriStr) - val path = AbsolutePath(Paths.get(decodedUri)) - FileIO.slurp(path, StandardCharsets.UTF_8) - } match { - case Failure(exception) => DecoderResponse.failed(uri, exception) - case Success(value) => DecoderResponse.success(uri, value) - } - } - private def decodeMetalsFile( uri: URI ): Future[DecoderResponse] = { @@ -213,20 +201,41 @@ final class FileDecoderProvider( } } } else - Future.successful(DecoderResponse.failed(uri, "Unsupported extension")) + additionalExtension match { + case "java" | "scala" => + Future.successful( + toFile(uri.toString).map(strippedURI => { + FileIO.slurp(strippedURI, StandardCharsets.UTF_8) + }) match { + case Left(value) => value + case Right(value) => DecoderResponse.success(uri, value) + } + ) + case _ => + Future.successful( + DecoderResponse.failed(uri, "Unsupported extension") + ) + } } private def toFile( uri: URI, suffixToRemove: String + ): Either[DecoderResponse, AbsolutePath] = + toFile(uri.toString.stripSuffix(suffixToRemove)) + + private def toFile( + uriAsStr: String ): Either[DecoderResponse, AbsolutePath] = { - val strippedURI = uri.toString.stripSuffix(suffixToRemove) - Try { - strippedURI.toAbsolutePath - }.filter(_.exists) + val uri = uriAsStr.toURI + val path = Try { + uri.toAbsolutePath + } + path + .filter(_.exists) .toOption .toRight( - DecoderResponse.failed(uri, s"File $strippedURI doesn't exist") + DecoderResponse.failed(uri, s"File $uriAsStr doesn't exist") ) } @@ -482,7 +491,18 @@ final class FileDecoderProvider( ): Future[DecoderResponse] = { val cfrDependency = Dependency.of("org.benf", "cfr", "0.151") val cfrMain = "org.benf.cfr.reader.Main" - val args = List("--analyseas", "CLASS", s"""${path.toNIO.toString}""") + val (parent, className) = + if (path.isJarFileSystem) + (workspace, path.toNIO.toString.stripPrefix("\\").stripPrefix("/")) + else (path.parent, path.toNIO.toString) + val extraClassPath = path.jarPath + .map(jarPath => List("--extraclasspath", jarPath.toString)) + .getOrElse(List.empty) + val args = extraClassPath ::: List( + "--analyseas", + "CLASS", + s"""$className""" + ) val sbOut = new StringBuilder() val sbErr = new StringBuilder() @@ -491,7 +511,7 @@ final class FileDecoderProvider( .runJava( cfrDependency, cfrMain, - path.parent, + parent, args, redirectErrorOutput = false, s => { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 4b5c6033186..e1253916f30 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -2,8 +2,11 @@ package scala.meta.internal.metals import java.io.IOException import java.net.URI +import java.net.URLDecoder import java.nio.charset.StandardCharsets import java.nio.file.FileAlreadyExistsException +import java.nio.file.FileSystemNotFoundException +import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -266,6 +269,19 @@ object MetalsEnrichments } } + implicit class XtensionURI(uri: URI) { + def toAbsolutePath: AbsolutePath = + if (uri.getScheme().equalsIgnoreCase("JAR")) + try { + AbsolutePath(Paths.get(uri)) + } catch { + case _: FileSystemNotFoundException => + FileSystems.newFileSystem(uri, new util.HashMap[String, Any]()) + AbsolutePath(Paths.get(uri)) + } + else AbsolutePath(Paths.get(uri)) + } + implicit class XtensionPath(path: Path) { def toUriInput: Input.VirtualFile = { val uri = path.toUri.toString @@ -302,6 +318,8 @@ object MetalsEnrichments def isLocalFileSystem(workspace: AbsolutePath): Boolean = workspace.toNIO.getFileSystem == path.toNIO.getFileSystem + def isJarFileSystem: Boolean = path.toURI.getScheme() == "jar" + def isInReadonlyDirectory(workspace: AbsolutePath): Boolean = path.toNIO.startsWith( workspace.resolve(Directories.readonly).toNIO @@ -582,13 +600,19 @@ object MetalsEnrichments case _ => None } + def toURI: URI = { + // jar file system cannot cope with a heavily encoded uri + // hence the roundabout way of creating an AbsolutePath + // must have "jar:file:"" instead of "jar:file%3A" + val decodedUriStr = URLDecoder.decode(value, "UTF-8") + URI.create(decodedUriStr) + } + def toAbsolutePathSafe: Option[AbsolutePath] = Try(toAbsolutePath).toOption def toAbsolutePath: AbsolutePath = toAbsolutePath(followSymlink = true) def toAbsolutePath(followSymlink: Boolean): AbsolutePath = { - val path = AbsolutePath( - Paths.get(URI.create(value.stripPrefix("metals:"))) - ) + val path = value.stripPrefix("metals:").toURI.toAbsolutePath if (followSymlink) path.dealias else