Skip to content

Commit

Permalink
[kotlin] Add capability to fetch @kotlin.Metadata in the debugger
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-nazarov committed Sep 20, 2024
1 parent 3106110 commit 1dc6149
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 7 deletions.
2 changes: 2 additions & 0 deletions plugins/kotlin/jvm-debugger/core/kotlin.jvm-debugger.core.iml
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="kotlin.base.resources" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="library" name="jetbrains.kotlinx.metadata.jvm" level="project" />
</component>
</module>
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import com.intellij.debugger.ui.tree.render.ClassRenderer
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.openapi.project.Project
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.base.util.safeFields
import org.jetbrains.kotlin.idea.debugger.base.util.safeType
import kotlinx.metadata.isDelegated
import kotlinx.metadata.isNotDefault
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.idea.debugger.base.util.isLateinitVariableGetter
import org.jetbrains.kotlin.idea.debugger.base.util.isSimpleGetter
import org.jetbrains.kotlin.idea.debugger.core.GetterDescriptor
import org.jetbrains.kotlin.idea.debugger.core.KotlinDebuggerCoreBundle
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSources
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSourcesAsync
import org.jetbrains.kotlin.idea.debugger.base.util.safeFields
import org.jetbrains.kotlin.idea.debugger.base.util.safeType
import org.jetbrains.kotlin.idea.debugger.core.*
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import java.util.concurrent.CompletableFuture
import java.util.function.Function

Expand All @@ -52,7 +53,10 @@ class KotlinClassRenderer : ClassRenderer() {
val nodeDescriptorFactory = builder.descriptorManager
val refType = value.referenceType()
val gettersFuture = DebuggerUtilsAsync.allMethods(refType)
.thenApply { methods -> methods.getters().createNodes(value, parentDescriptor.project, evaluationContext, nodeManager) }
.thenApply { methods ->
val getters = fetchGettersUsingMetadata(evaluationContext, refType, methods) ?: methods.getters()
getters.createNodes(value, parentDescriptor.project, evaluationContext, nodeManager)
}
DebuggerUtilsAsync.allFields(refType).thenCombine(gettersFuture) { fields, getterNodes ->
if (fields.isEmpty() && getterNodes.isEmpty()) {
builder.setChildren(listOf(nodeManager.createMessageNode(KotlinDebuggerCoreBundle.message("message.class.has.no.properties"))))
Expand All @@ -77,6 +81,22 @@ class KotlinClassRenderer : ClassRenderer() {
}
}

private fun fetchGettersUsingMetadata(
context: EvaluationContext,
refType: ReferenceType,
methods: List<Method>
): List<Method>? {
val metadata = refType.fetchKotlinMetadata(context) as? KotlinClassMetadata.Class ?: return null
val gettersToShow = metadata.kmClass.properties.mapNotNull {
if (!it.isDelegated && it.getter.isNotDefault) {
"get${it.name.capitalizeAsciiOnly()}"
} else {
null
}
}.toSet()
return methods.filter { it.name() in gettersToShow }
}

override fun calcLabel(
descriptor: ValueDescriptor,
evaluationContext: EvaluationContext?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package org.jetbrains.kotlin.idea.debugger.core

import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.sun.jdi.*
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.idea.debugger.base.util.wrapEvaluateException
import org.jetbrains.kotlin.idea.debugger.base.util.wrapIllegalArgumentException
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

fun ReferenceType.fetchKotlinMetadata(context: EvaluationContext): KotlinClassMetadata? {
val classObject = classObject() ?: return null

val debugProcess = context.debugProcess
val metadataUtilClass = wrapEvaluateException {
debugProcess.findClass(context, METADATA_UTILS_CLASS_NAME, null)
} as? ClassType ?: return null
val getDebugMetadataAsJsonMethod = metadataUtilClass.methodsByName(GET_DEBUG_METADATA_AS_JSON).singleOrNull()
?: return null
val metadataAsJson = wrapEvaluateException {
debugProcess.invokeMethod(context, metadataUtilClass, getDebugMetadataAsJsonMethod, listOf(classObject))
} as? StringReference ?: return null

val metadata = wrapJsonSyntaxException {
Gson().fromJson(metadataAsJson.value(), MetadataAdapter::class.java).toMetadata()
} ?: return null

return wrapIllegalArgumentException {
KotlinClassMetadata.readStrict(metadata)
}
}

private const val METADATA_UTILS_CLASS_NAME = "kotlin.jvm.internal.MetadataDebugUtilKt"
private const val GET_DEBUG_METADATA_AS_JSON = "getDebugMetadataAsJson"

private class MetadataAdapter(
val kind: Int,
val metadataVersion: Array<Int>,
val data1: Array<String>,
val data2: Array<String>,
val extraString: String,
val packageName: String,
val extraInt: Int,
) {
@OptIn(ExperimentalEncodingApi::class)
fun toMetadata(): Metadata {
return Metadata(
kind = kind,
metadataVersion = metadataVersion.toIntArray(),
data1 = data1.map { String(Base64.Default.decode(it)) }.toTypedArray(),
data2 = data2,
extraString = extraString,
packageName = packageName,
extraInt = extraInt
)
}
}

private fun <T> wrapJsonSyntaxException(block: () -> T): T? {
return try {
block()
} catch (e: JsonSyntaxException) {
null
}
}

0 comments on commit 1dc6149

Please sign in to comment.