KT-11678 Support navigation from Java source to Kotlin binaries

Introduce LightMemberOriginForCompiledElement which holds logic for finding corresponding decompiled declaration
ByJvmSignatureIndexer stores the required information while building decompiled text

Some are unsolved in this commit:
   - constructors
   - @Jvm* and other unregular generated members
   - annotation methods

 #KT-11678 Fixed
This commit is contained in:
Pavel V. Talanov
2016-03-29 19:32:58 +03:00
parent f9fa365059
commit e19cc04c63
14 changed files with 339 additions and 12 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
// The purpose of this class is to hold a unique signature of either a method or a field, so that annotations on a member can be put
// into a map indexed by these signatures
internal data class MemberSignature private constructor(internal val signature: String) {
data class MemberSignature private constructor(internal val signature: String) {
companion object {
@JvmStatic
fun fromMethod(nameResolver: NameResolver, signature: JvmProtoBuf.JvmMethodSignature): MemberSignature {
@@ -17,7 +17,11 @@
package org.jetbrains.kotlin.idea.caches.resolve
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.compiled.ClsClassImpl
import org.jetbrains.kotlin.asJava.KtLightFieldImpl
import org.jetbrains.kotlin.asJava.KtLightMethodImpl
import org.jetbrains.kotlin.asJava.KtWrappingLightClass
import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile
import org.jetbrains.kotlin.name.FqName
@@ -40,6 +44,14 @@ class KtLightClassForDecompiledDeclaration(
}
}
override fun getOwnFields(): List<PsiField> {
return clsDelegate.ownFields.map { KtLightFieldImpl.create(LightMemberOriginForCompiledField(it, file), it, this) }
}
override fun getOwnMethods(): List<PsiMethod> {
return clsDelegate.ownMethods.map { KtLightMethodImpl.create(it, LightMemberOriginForCompiledMethod(it, file), this) }
}
override fun getNavigationElement() = kotlinOrigin?.navigationElement ?: file
override fun getFqName() = fqName
@@ -0,0 +1,144 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.caches.resolve
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMember
import com.intellij.psi.PsiMethod
import org.jetbrains.kotlin.asJava.LightMemberOrigin
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile
import org.jetbrains.kotlin.idea.decompiler.textBuilder.DecompiledTextIndexer
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.load.kotlin.MemberSignature
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.resolve.descriptorUtil.classId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOriginKind
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedSimpleFunctionDescriptor
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBufUtil
interface LightMemberOriginForCompiledElement : LightMemberOrigin {
override val originKind: JvmDeclarationOriginKind
get() = JvmDeclarationOriginKind.OTHER
}
data class LightMemberOriginForCompiledField(val psiField: PsiField, val file: KtClsFile) : LightMemberOriginForCompiledElement {
override fun copy(): LightMemberOrigin {
return LightMemberOriginForCompiledField(psiField.copy() as PsiField, file)
}
override val originalElement: KtDeclaration? by lazy(LazyThreadSafetyMode.PUBLICATION) {
val desc = MapPsiToAsmDesc.typeDesc(psiField.type)
val signature = MemberSignature.fromFieldNameAndDesc(psiField.name!!, desc)
file.getDeclaration(ByJvmSignatureIndexer, ClassNameAndSignature(psiField.relativeClassName(), signature))
}
}
data class LightMemberOriginForCompiledMethod(val psiMethod: PsiMethod, val file: KtClsFile) : LightMemberOriginForCompiledElement {
override fun copy(): LightMemberOrigin {
return LightMemberOriginForCompiledMethod(psiMethod.copy() as PsiMethod, file)
}
override val originalElement: KtDeclaration? by lazy(LazyThreadSafetyMode.PUBLICATION) {
val desc = MapPsiToAsmDesc.methodDesc(psiMethod)
val signature = MemberSignature.fromMethodNameAndDesc(psiMethod.name, desc)
file.getDeclaration(ByJvmSignatureIndexer, ClassNameAndSignature(psiMethod.relativeClassName(), signature))
}
}
// this is convenient data structure for this purpose and is not supposed to be used outside this file
// every member is represented by its jvm signature and relative class name (which is easy to obtain from descriptors or cls psi)
// relative class name is a path containing inner/nested class names from top level class to the class containing this member (excluding top level class name)
// Examples: for top level function or function in a top level class relativeClassName is empty
// For: class TopLevel { class A { class B { fun f() } } }
// relativeClassName for function 'f' will be [A, B]
private data class ClassNameAndSignature(val relativeClassName: List<Name>, val memberSignature: MemberSignature)
private fun PsiMember.relativeClassName(): List<Name> {
return generateSequence(this.containingClass) { it.containingClass }.toList().dropLast(1).reversed().map { Name.identifier(it.name!!) }
}
private fun ClassDescriptor.relativeClassName(): List<Name> {
return classId.relativeClassName.pathSegments().drop(1).orEmpty()
}
private fun ClassDescriptor.desc(): String = "L" + JvmClassName.byClassId(classId).internalName + ";"
private object ByJvmSignatureIndexer : DecompiledTextIndexer<ClassNameAndSignature> {
override fun indexDescriptor(descriptor: DeclarationDescriptor): Collection<ClassNameAndSignature> {
val signatures = arrayListOf<ClassNameAndSignature>()
fun save(id: List<Name>, signature: MemberSignature) {
signatures.add(ClassNameAndSignature(id, signature))
}
if (descriptor is ClassDescriptor) {
@Suppress("NON_EXHAUSTIVE_WHEN")
when (descriptor.kind) {
ClassKind.ENUM_ENTRY -> {
val enumClass = descriptor.containingDeclaration as ClassDescriptor
val signature = MemberSignature.fromFieldNameAndDesc(descriptor.name.asString(), enumClass.desc())
save(enumClass.relativeClassName(), signature)
}
ClassKind.OBJECT -> {
val instanceFieldSignature = MemberSignature.fromFieldNameAndDesc(JvmAbi.INSTANCE_FIELD, descriptor.desc())
save(descriptor.relativeClassName(), instanceFieldSignature)
if (descriptor.isCompanionObject) {
val signature = MemberSignature.fromFieldNameAndDesc(descriptor.name.asString(), descriptor.desc())
save((descriptor.containingDeclaration as? ClassDescriptor)?.relativeClassName().orEmpty(), signature)
}
}
}
}
if (descriptor is DeserializedSimpleFunctionDescriptor) {
JvmProtoBufUtil.getJvmMethodSignature(descriptor.proto, descriptor.nameResolver, descriptor.typeTable)?.let {
val signature = MemberSignature.fromMethodNameAndDesc(it)
save((descriptor.containingDeclaration as? ClassDescriptor)?.relativeClassName().orEmpty(), signature)
}
}
if (descriptor is DeserializedPropertyDescriptor) {
val proto = descriptor.proto
val className = (descriptor.containingDeclaration as? ClassDescriptor)?.relativeClassName().orEmpty()
if (proto.hasExtension(JvmProtoBuf.propertySignature)) {
val signature = proto.getExtension(JvmProtoBuf.propertySignature)
val fieldSignature = JvmProtoBufUtil.getJvmFieldSignature(proto, descriptor.nameResolver, descriptor.typeTable)
if (fieldSignature != null) {
save(className, MemberSignature.fromFieldNameAndDesc(fieldSignature.name, fieldSignature.desc))
}
if (signature.hasGetter()) {
save(className, MemberSignature.fromMethod(descriptor.nameResolver, signature.getter))
}
if (signature.hasSetter()) {
save(className, MemberSignature.fromMethod(descriptor.nameResolver, signature.setter))
}
}
}
return signatures
}
}
// expose with different type
val BySignatureIndexer: DecompiledTextIndexer<*> = ByJvmSignatureIndexer
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.caches.resolve
import com.intellij.openapi.diagnostic.Logger
import com.intellij.psi.*
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.Type.*
object MapPsiToAsmDesc {
fun typeDesc(type: PsiType): String = when (type) {
PsiType.VOID -> primitive(VOID_TYPE)
PsiType.BOOLEAN -> primitive(BOOLEAN_TYPE)
PsiType.CHAR -> primitive(CHAR_TYPE)
PsiType.INT -> primitive(INT_TYPE)
PsiType.BYTE -> primitive(BYTE_TYPE)
PsiType.SHORT -> primitive(SHORT_TYPE)
PsiType.LONG -> primitive(LONG_TYPE)
PsiType.FLOAT -> primitive(FLOAT_TYPE)
PsiType.DOUBLE -> primitive(DOUBLE_TYPE)
is PsiArrayType -> "[" + typeDesc(type.componentType)
is PsiClassType -> {
val resolved = type.resolve()
when (resolved) {
is PsiTypeParameter -> resolved.superTypes.firstOrNull()?.let { typeDesc(it) } ?: "Ljava/lang/Object;"
is PsiClass -> classDesc(resolved)
null -> unknownSignature()
else -> error("Resolved to unexpected $resolved of class ${resolved.javaClass}" )
}
}
else -> error("Unexpected type $type of class ${type.javaClass}")
}
private fun classDesc(psiClass: PsiClass) = buildString {
append("L")
val classes = generateSequence(psiClass) { it.containingClass }.toList().reversed()
append(classes.first().qualifiedName!!.replace(".", "/"))
classes.drop(1).forEach {
append("$")
append(it.name!!)
}
append(";")
}
fun methodDesc(psiMethod: PsiMethod): String = buildString {
append("(")
psiMethod.parameterList.parameters.forEach {
append(typeDesc(it.type))
}
append(")")
psiMethod.returnType?.let {
append(typeDesc(it))
}
?: return unknownSignature() // TODO: support constructors, there seems to be additional logic in java that doesn't work correctly for compiled kotlin
}
private fun unknownSignature() = ""
private fun error(message: String): String {
LOG.error(message)
return unknownSignature()
}
private fun primitive(asmType: Type) = asmType.descriptor
private val LOG = Logger.getInstance(javaClass)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,10 +21,13 @@ import com.intellij.openapi.roots.FileIndexFacade
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.psi.compiled.ClassFileDecompilers
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.caches.IDEKotlinBinaryClassCache
import org.jetbrains.kotlin.idea.caches.resolve.BySignatureIndexer
import org.jetbrains.kotlin.idea.decompiler.KotlinDecompiledFileViewProvider
import org.jetbrains.kotlin.idea.decompiler.KtDecompiledFile
import org.jetbrains.kotlin.idea.decompiler.common.createIncompatibleAbiVersionDecompiledText
import org.jetbrains.kotlin.idea.decompiler.navigation.ByDescriptorIndexer
import org.jetbrains.kotlin.idea.decompiler.textBuilder.DecompiledText
import org.jetbrains.kotlin.idea.decompiler.textBuilder.ResolverForDecompiler
import org.jetbrains.kotlin.idea.decompiler.textBuilder.buildDecompiledText
@@ -34,7 +37,6 @@ import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.types.flexibility
import org.jetbrains.kotlin.types.isFlexible
import java.util.*
class KotlinClassFileDecompiler : ClassFileDecompilers.Full() {
private val stubBuilder = KotlinClsStubBuilder()
@@ -75,19 +77,21 @@ fun buildDecompiledTextForClassFile(
return createIncompatibleAbiVersionDecompiledText(JvmMetadataVersion.INSTANCE, classHeader.metadataVersion)
}
fun buildText(declarations: List<DeclarationDescriptor>) =
buildDecompiledText(classId.packageFqName, declarations, decompilerRendererForClassFiles, listOf(ByDescriptorIndexer, BySignatureIndexer))
return when (classHeader.kind) {
KotlinClassHeader.Kind.FILE_FACADE ->
buildDecompiledText(classId.packageFqName, ArrayList(resolver.resolveDeclarationsInFacade(classId.asSingleFqName())),
decompilerRendererForClassFiles)
KotlinClassHeader.Kind.CLASS ->
buildDecompiledText(classId.packageFqName, listOfNotNull(resolver.resolveTopLevelClass(classId)),
decompilerRendererForClassFiles)
buildText(resolver.resolveDeclarationsInFacade(classId.asSingleFqName()))
KotlinClassHeader.Kind.CLASS -> {
buildText(listOfNotNull(resolver.resolveTopLevelClass(classId)))
}
KotlinClassHeader.Kind.MULTIFILE_CLASS -> {
val partClasses = findMultifileClassParts(classFile, classId, classHeader)
val partMembers = partClasses.flatMap { partClass ->
resolver.resolveDeclarationsInFacade(partClass.classId.asSingleFqName())
}
buildDecompiledText(classId.packageFqName, partMembers, decompilerRendererForClassFiles)
buildText(partMembers)
}
else ->
throw UnsupportedOperationException("Unknown header kind: $classHeader, class $classId")
@@ -1,2 +1,3 @@
[LibraryCompanionObjectUsages.0.kt] Companion object (10: 15) val a = A.Companion
[LibraryCompanionObjectUsages.1.java] Local variable declaration (7: 11) A.Companion o = A.Companion;
[LibraryCompanionObjectUsages.1.java] Local variable declaration (7: 11) A.Companion o = A.Companion;
[LibraryCompanionObjectUsages.1.java] Unclassified usage (7: 27) A.Companion o = A.Companion;
@@ -1,4 +1,5 @@
[LibraryMemberFunctionUsages.0.kt] Callable reference (10: 16) val f = A::foo
[LibraryMemberFunctionUsages.0.kt] Function call (11: 9) A().foo(1)
[LibraryMemberFunctionUsages.1.java] Unclassified usage (11: 17) new A().foo(1);
[library.kt] Callable reference (61: 17) val ff = A::foo
[library.kt] Function call (60: 7) a.foo(2)
@@ -0,0 +1,8 @@
public class Field {
public static void foo() {
(new k.Class()).p<caret>rop;
}
}
// REF: (in k.Class).prop
// CLS_REF: (in k.Class).prop
@@ -5,4 +5,4 @@ public class PackageFacade {
}
// REF: /src/<test dir>/resolve/referenceInJava/dependency.dependencies.kt
// CLS_REF: (k).DependenciesKt
// CLS_REF: <jar>!/k.DependenciesKt.class
@@ -0,0 +1,8 @@
public class MethodOfDeeplyNested {
public static void foo() {
(new k.Class.F.F()).f<caret>unction();
}
}
// REF: (in k.Class.F.F).function()
// CLS_REF: (in k.Class.F.F).function()
@@ -0,0 +1,8 @@
public class MethodWithParameters {
public static void foo() {
(new k.Class()).f<caret>unction2();
}
}
// REF: (in k.Class).function2(Byte, Char, Short, Int, Long, Boolean, Float, Double, ByteArray, CharArray, IntArray, LongArray, BooleanArray, FloatArray, DoubleArray, Array<String>, Array<ByteArray>, Array<Array<String>>, T, G, String, Class.F, Class.G, Class.F.F)
// CLS_REF: (in k.Class).function2(kotlin.Byte, kotlin.Char, kotlin.Short, kotlin.Int, kotlin.Long, kotlin.Boolean, kotlin.Float, kotlin.Double, kotlin.ByteArray, kotlin.CharArray, kotlin.IntArray, kotlin.LongArray, kotlin.BooleanArray, kotlin.FloatArray, kotlin.DoubleArray, kotlin.Array<kotlin.String>, kotlin.Array<kotlin.ByteArray>, kotlin.Array<kotlin.Array<kotlin.String>>, T, G, kotlin.String, k.Class.F, k.Class.G, k.Class.F.F)
@@ -3,6 +3,26 @@ package k
public class Class() {
public val prop: Int = 0
fun function() = 1
fun <T : Number, G> function2(
b: Byte, c: Char, s: Short, i: Int, l: Long, bool: Boolean, f: Float, d: Double,
ba: ByteArray, ca: CharArray, ia: IntArray, la: LongArray, boola: BooleanArray, fa: FloatArray, da: DoubleArray, sa: Array<String>,
baa: Array<ByteArray>, saa: Array<Array<String>>,
t: T, g: G, str: String, nestedClass: Class.F, innerClass: Class.G, nestedNested: Class.F.F
) {
}
class F {
fun function() = 1
class F {
fun function() = 1
}
}
inner class G {
fun function() = 5
}
}
public enum class EnumClass {
@@ -61,6 +61,12 @@ public class ReferenceResolveInJavaTestGenerated extends AbstractReferenceResolv
doTest(fileName);
}
@TestMetadata("Field.java")
public void testField() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/Field.java");
doTest(fileName);
}
@TestMetadata("FileFacade.java")
public void testFileFacade() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/FileFacade.java");
@@ -79,6 +85,18 @@ public class ReferenceResolveInJavaTestGenerated extends AbstractReferenceResolv
doTest(fileName);
}
@TestMetadata("MethodOfDeeplyNested.java")
public void testMethodOfDeeplyNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/MethodOfDeeplyNested.java");
doTest(fileName);
}
@TestMetadata("MethodWithParameters.java")
public void testMethodWithParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/MethodWithParameters.java");
doTest(fileName);
}
@TestMetadata("ObjectInstance.java")
public void testObjectInstance() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/ObjectInstance.java");
@@ -59,6 +59,12 @@ public class ReferenceToCompiledKotlinResolveInJavaTestGenerated extends Abstrac
doTest(fileName);
}
@TestMetadata("Field.java")
public void testField() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/Field.java");
doTest(fileName);
}
@TestMetadata("FileFacade.java")
public void testFileFacade() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/FileFacade.java");
@@ -77,6 +83,18 @@ public class ReferenceToCompiledKotlinResolveInJavaTestGenerated extends Abstrac
doTest(fileName);
}
@TestMetadata("MethodOfDeeplyNested.java")
public void testMethodOfDeeplyNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/MethodOfDeeplyNested.java");
doTest(fileName);
}
@TestMetadata("MethodWithParameters.java")
public void testMethodWithParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/MethodWithParameters.java");
doTest(fileName);
}
@TestMetadata("ObjectInstance.java")
public void testObjectInstance() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/resolve/referenceInJava/binaryAndSource/ObjectInstance.java");