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 Oct 2, 2024
1 parent 2421cdc commit ea15856
Show file tree
Hide file tree
Showing 4 changed files with 193 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,25 @@ class KotlinClassRenderer : ClassRenderer() {
}
}

private fun fetchGettersUsingMetadata(
context: EvaluationContext,
refType: ReferenceType,
methods: List<Method>
): List<Method>? {
val metadata = KotlinMetadataDebuggerCacheService
.getInstance(context.project)
.getKotlinMetadata(refType, 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,159 @@
// 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.DebugProcess
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.impl.DebuggerManagerListener
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.sun.jdi.*
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.annotations.ApiStatus
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

@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class KotlinMetadataDebuggerCacheService private constructor(project: Project) {
companion object {
@JvmStatic
fun getInstance(project: Project): KotlinMetadataDebuggerCacheService = project.service()
}

private class KotlinMetadataCacheListener(private val project: Project) : DebuggerManagerListener {
override fun sessionCreated(session: DebuggerSession) {
getInstance(project).createCache(session.process)
}

override fun sessionRemoved(session: DebuggerSession) {
getInstance(project).removeCache(session.process)
}
}

// There is one cache per debug process. The size of the list will almost always be 1 when debugging.
private val caches = mutableListOf<KotlinMetadataCache>()

fun getKotlinMetadata(refType: ReferenceType, context: EvaluationContext): KotlinClassMetadata? {
for (cache in caches) {
if (context.debugProcess === cache.debugProcess) {
return cache.fetchKotlinMetadata(refType, context)
}
}
return null
}

private fun createCache(debugProcess: DebugProcess) {
caches.add(KotlinMetadataCache(debugProcess))
}

private fun removeCache(debugProcess: DebugProcess) {
caches.removeIf { it.debugProcess === debugProcess }
}
}

private class KotlinMetadataCache(val debugProcess: DebugProcess) {
// The purpose of this class is to prevent searching for
// the MetadataUtilKt class and `getDebugMetadataAsJson` method
// multiple times.
private sealed class MetadataJdiFetcher {
companion object {
private const val METADATA_UTILS_CLASS_NAME = "kotlin.jvm.internal.MetadataDebugUtilKt"
private const val GET_DEBUG_METADATA_AS_JSON = "getDebugMetadataAsJson"

fun getInstance(context: EvaluationContext): MetadataJdiFetcher {
val metadataUtilClass = wrapEvaluateException {
context.debugProcess.findClass(context, METADATA_UTILS_CLASS_NAME, null)
} as? ClassType ?: return FailedToInitialize
val getDebugMetadataAsJsonMethod = metadataUtilClass.methodsByName(GET_DEBUG_METADATA_AS_JSON).singleOrNull()
?: return FailedToInitialize
return Initialized(metadataUtilClass, getDebugMetadataAsJsonMethod)
}
}

data object FailedToInitialize : MetadataJdiFetcher()
class Initialized(
private val metadataUtilClass: ClassType,
private val getDebugMetadataAsJsonMethod: Method
) : MetadataJdiFetcher() {
fun fetchMetadataAsJson(refType: ReferenceType, context: EvaluationContext): String? {
val classObject = refType.classObject() ?: return null
val stringRef = wrapEvaluateException {
context.debugProcess.invokeMethod(
context, metadataUtilClass, getDebugMetadataAsJsonMethod, listOf(classObject)
)
} as? StringReference
return stringRef?.value()
}
}
}

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 val cache = mutableMapOf<ReferenceType, KotlinClassMetadata>()
private lateinit var metadataJdiFetcher: MetadataJdiFetcher

fun fetchKotlinMetadata(refType: ReferenceType, context: EvaluationContext): KotlinClassMetadata? {
if (context.debugProcess !== debugProcess) {
return null
}

if (!::metadataJdiFetcher.isInitialized) {
metadataJdiFetcher = MetadataJdiFetcher.getInstance(context)
}

when (val fetcher = metadataJdiFetcher) {
is MetadataJdiFetcher.FailedToInitialize -> return null
is MetadataJdiFetcher.Initialized -> {
cache[refType]?.let { return it }

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

val parsedMetadata = wrapIllegalArgumentException {
KotlinClassMetadata.readStrict(metadata)
} ?: return null

cache[refType] = parsedMetadata
return parsedMetadata
}
}
}
}

private fun <T> wrapJsonSyntaxException(block: () -> T): T? {
return try {
block()
} catch (e: JsonSyntaxException) {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<listener class="org.jetbrains.kotlin.idea.debugger.evaluate.DropCodeFragmentCacheOnRefreshListener" topic="com.intellij.debugger.impl.DebuggerManagerListener"/>
<listener class="org.jetbrains.kotlin.idea.debugger.coroutine.DisableCoroutineViewListener"
topic="com.intellij.debugger.impl.DebuggerManagerListener"/>
<listener class="org.jetbrains.kotlin.idea.debugger.core.KotlinMetadataDebuggerCacheService$KotlinMetadataCacheListener"
topic="com.intellij.debugger.impl.DebuggerManagerListener"/>
</projectListeners>

<extensionPoints>
Expand Down

0 comments on commit ea15856

Please sign in to comment.