Skip to content
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

navigate to decompiled class if no source #3411

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -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}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -310,19 +329,47 @@ 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

This might not work for some more complex Scala symbols such as inner classes 🤔

In that case we might want to use ClasspathSymbols and find the class that contains the symbol.

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)
)
}
// 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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 _ =>
Expand All @@ -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] = {
Expand Down Expand Up @@ -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")
)
}

Expand Down Expand Up @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No - although it does look it. The options is meant to mean ANALYSE_AS. Either jar or class

"CLASS",
s"""$className"""
)
val sbOut = new StringBuilder()
val sbErr = new StringBuilder()

Expand All @@ -491,7 +511,7 @@ final class FileDecoderProvider(
.runJava(
cfrDependency,
cfrMain,
path.parent,
parent,
args,
redirectErrorOutput = false,
s => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down