Skip to content

Commit

Permalink
[kotlin] Implement evaluatable getters filtering using Kotlin class m…
Browse files Browse the repository at this point in the history
…etadata

Previously, evaluatable getters in the variables view of the Kotlin debugger relied on a filtering heuristic that inspected bytecode of each getter. Unfortunately, such filtering was incorrect in some cases. Moreover the filtering was disabled completely in the Android Studio debugger, because method bytecode is not available there. This commit reimplements filtering of evaluatable getters by using Kotlin class metadata, fetched by `MetadataDebugHelper`.
  • Loading branch information
nikita-nazarov committed Feb 20, 2025
1 parent 46f94c7 commit 681d8bd
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.Arrays;

public class MetadataDebugHelper {
public static final String METADATA_SEPARATOR = "\n";

private static final String METADATA_CLASS_NAME = "kotlin.Metadata";

private static final String KIND = "kind";
Expand Down Expand Up @@ -82,6 +84,29 @@ public static String getDebugMetadataAsJson(Class<?> cls) {
}
}

/*
* This function is used similarly to `getDebugMetadataAsJson`, when there is a need to
* fetch metadata for multiple classes.
*
* The return value is a concatenation of metadata JSON representations of given classes
* separated by MetadataDebugHelper.METADATA_SEPARATOR.
*/
public static String getDebugMetadataListAsJson(Class<?>... classes) {
StringBuilder sb = new StringBuilder();
for (Class<?> cls : classes) {
String metadataAsJson = getDebugMetadataAsJson(cls);
if (metadataAsJson == null) {
return null;
}

if (sb.length() != 0) {
sb.append(METADATA_SEPARATOR);
}
sb.append(metadataAsJson);
}
return sb.toString();
}

private static void appendAsJsonValue(StringBuilder sb, String name, Object value) {
appendAsJsonValueNoComma(sb, name, value).append(',');
}
Expand Down
3 changes: 3 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 @@ -58,7 +58,10 @@
<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="module" module-name="intellij.java.debugger.impl.shared" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.java.rt" />
<orderEntry type="library" name="kotlin-metadata" level="project" />
</component>
</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright 2000-2025 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.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerManagerListener
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.rt.debugger.MetadataDebugHelper
import com.sun.jdi.ReferenceType
import com.sun.jdi.StringReference
import com.sun.jdi.Value
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
import kotlin.metadata.jvm.KotlinClassMetadata

@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? {
return getCache(context)?.fetchKotlinMetadata(refType, context)
}

fun getKotlinMetadataList(refTypes: Collection<ReferenceType>, context: EvaluationContext): List<KotlinClassMetadata>? {
return getCache(context)?.fetchKotlinMetadataList(refTypes, context)
}

private fun getCache(context: EvaluationContext): KotlinMetadataCache? {
return caches.firstOrNull { it.debugProcess == context.debugProcess }
}

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) {
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>()

fun fetchKotlinMetadata(refType: ReferenceType, context: EvaluationContext): KotlinClassMetadata? {
cache[refType]?.let { return it }
val metadataAsJson = callMethodFromHelper(
context, "getDebugMetadataAsJson", listOf(refType.classObject())
) ?: return null
val metadata = parseMetadataFromJson(metadataAsJson) ?: return null
cache[refType] = metadata
return metadata
}

fun fetchKotlinMetadataList(refTypes: Collection<ReferenceType>, context: EvaluationContext): List<KotlinClassMetadata>? {
val result = mutableListOf<KotlinClassMetadata>()
val toFetch = mutableListOf<ReferenceType>()
for (refType in refTypes) {
val metadata = cache[refType]
if (metadata != null) {
result.add(metadata)
} else {
toFetch.add(refType)
}
}

if (toFetch.isEmpty()) {
return result
}

val classObjects = toFetch.map { it.classObject() }
val concatenatedJsonMetadatas = callMethodFromHelper(
context, "getDebugMetadataListAsJson", classObjects
) ?: return null
val splitJsonMetadatas = concatenatedJsonMetadatas.split(MetadataDebugHelper.METADATA_SEPARATOR)
if (splitJsonMetadatas.size != toFetch.size) {
return null
}

for ((metadataAsJson, refType) in splitJsonMetadatas.zip(toFetch)) {
val metadata = parseMetadataFromJson(metadataAsJson) ?: return null
cache[refType] = metadata
result.add(metadata)
}
return result
}

private fun parseMetadataFromJson(metadataAsJson: String): KotlinClassMetadata? {
val metadata = wrapJsonSyntaxException {
Gson().fromJson(metadataAsJson, MetadataAdapter::class.java).toMetadata()
} ?: return null
val parsedMetadata = wrapIllegalArgumentException {
KotlinClassMetadata.readStrict(metadata)
} ?: return null
return parsedMetadata
}

private fun callMethodFromHelper(context: EvaluationContext, methodName: String, args: List<Value>): String? {
return wrapEvaluateException {
val value = DebuggerUtilsImpl.invokeHelperMethod(
context as EvaluationContextImpl,
MetadataDebugHelper::class.java,
methodName,
args,
true
)
(value as? StringReference)?.value()
}
}
}

private fun <T> wrapJsonSyntaxException(block: () -> T): T? {
return try {
block()
} catch (_: JsonSyntaxException) {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ 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 org.jetbrains.kotlin.idea.debugger.base.util.isLateinitVariableGetter
import org.jetbrains.kotlin.idea.debugger.base.util.isSimpleGetter
import org.jetbrains.kotlin.idea.debugger.core.render.GetterDescriptor
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.KotlinDebuggerCoreBundle
import org.jetbrains.kotlin.idea.debugger.core.KotlinMetadataDebuggerCacheService
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSources
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSourcesAsync
import org.jetbrains.kotlin.idea.debugger.core.render.GetterDescriptor
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.concurrent.CompletableFuture
import java.util.function.Function
import kotlin.metadata.KmProperty
import kotlin.metadata.isNotDefault
import kotlin.metadata.jvm.KotlinClassMetadata
import kotlin.metadata.jvm.getterSignature

class KotlinClassRenderer : ClassRenderer() {
init {
Expand All @@ -53,7 +59,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, methods) ?: methods.getters()
getters.createNodes(value, parentDescriptor.project, evaluationContext, nodeManager)
}
DebuggerUtilsAsync.allFields(refType).thenCombine(gettersFuture) { fields, getterNodes ->
if (fields.none { FieldVisibilityProvider.shouldDisplayField(it) } && getterNodes.isEmpty()) {
builder.setChildren(listOf(nodeManager.createMessageNode(KotlinDebuggerCoreBundle.message("message.class.has.no.properties"))))
Expand All @@ -78,6 +87,42 @@ class KotlinClassRenderer : ClassRenderer() {
}
}

private fun fetchGettersUsingMetadata(
context: EvaluationContext,
methods: List<Method>
): List<Method>? {
val gettersToShow = calculateGettersToShow(methods, context) ?: return null
return methods
.filter { it.name() in gettersToShow }
.distinctBy { it.name() }
}

private fun calculateGettersToShow(methods: List<Method>, context: EvaluationContext): Set<String>? {
val uniqueKotlinDeclaringTypes = methods
.map { it.declaringType() }
.filter { it.isInKotlinSources() }
.toSet()
val metadataCache = KotlinMetadataDebuggerCacheService.getInstance(context.project)
val metadataList = metadataCache.getKotlinMetadataList(uniqueKotlinDeclaringTypes, context) ?: return null
val gettersToShow = mutableSetOf<String>()
for (metadata in metadataList) {
if (metadata !is KotlinClassMetadata.Class) {
continue
}

for (property in metadata.kmClass.properties) {
if (property.shouldBeVisibleInVariablesView()) {
gettersToShow.addIfNotNull(property.getterSignature?.name)
}
}
}
return gettersToShow
}

private fun KmProperty.shouldBeVisibleInVariablesView(): Boolean {
return getter.isNotDefault
}

override fun calcLabel(
descriptor: ValueDescriptor,
evaluationContext: EvaluationContext?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("../testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public void testDelegatedPropertyInClassWoRenderer() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/delegatedPropertyInClassWoRenderer.kt");
}

@TestMetadata("evaluatableGetters.kt")
public void testEvaluatableGetters() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/evaluatableGetters.kt");
}

@TestMetadata("frameAnonymousObject.kt")
public void testFrameAnonymousObject() throws Exception {
runTest("testData/evaluation/singleBreakpoint/frame/frameAnonymousObject.kt");
Expand Down
Loading

0 comments on commit 681d8bd

Please sign in to comment.