Skip to content

Commit

Permalink
[kotlin] Reimplement evaluatable getters filtering using metadata helper
Browse files Browse the repository at this point in the history
Previously, filtering of evaluatable getters in variables view relied on bytecode inspections. Because of this, in some cases incorrect getters were shown, and also filtering was disabled completely in Android Studio.
  • Loading branch information
nikita-nazarov committed Feb 26, 2025
1 parent df14703 commit acb6170
Show file tree
Hide file tree
Showing 16 changed files with 341 additions and 31 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 @@ -36,8 +38,14 @@ public class MetadataDebugHelper {
* the debugger side. Any JDWP communication can be very expensive when debugging remotely,
* especially when debugging Android applications on a phone.
*/
@SuppressWarnings("unchecked")
public static String getDebugMetadataAsJson(Class<?> cls) {
StringBuilder sb = new StringBuilder();
appendDebugMetadataAsJson(sb, cls);
return sb.toString();
}

@SuppressWarnings("unchecked")
private static void appendDebugMetadataAsJson(StringBuilder sb, Class<?> cls) {
try {
Class<?> metadataClass = Class.forName(METADATA_CLASS_NAME, false, cls.getClassLoader());
Method kindMethod = metadataClass.getDeclaredMethod(KIND_METHOD_NAME);
Expand All @@ -50,7 +58,6 @@ public static String getDebugMetadataAsJson(Class<?> cls) {

Object metadata = cls.getAnnotation((Class<Annotation>)metadataClass);

StringBuilder sb = new StringBuilder();
sb.append("{");

int kind = (int)kindMethod.invoke(metadata);
Expand All @@ -75,11 +82,26 @@ public static String getDebugMetadataAsJson(Class<?> cls) {
appendAsJsonValueNoComma(sb, EXTRA_INT, extraInt);

sb.append("}");
} catch (Exception ignored) {
}
}

return sb.toString();
} catch (Exception ex) {
return null;
/*
* 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) {
if (sb.length() != 0) {
sb.append(METADATA_SEPARATOR);
}
appendDebugMetadataAsJson(sb, cls);
}
return sb.toString();
}

private static void appendAsJsonValue(StringBuilder sb, String name, Object value) {
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,142 @@
// 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.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.getOrCreateUserData
import com.intellij.rt.debugger.MetadataDebugHelper
import com.intellij.rt.debugger.JsonUtils
import com.sun.jdi.ReferenceType
import com.sun.jdi.StringReference
import com.sun.jdi.Value
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

object KotlinMetadataCacheService {
private val METADATA_CACHE_KEY = Key.create<KotlinMetadataCache?>("METADATA_CACHE_KEY")

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? {
val vmProxy = (context as? EvaluationContextImpl)?.virtualMachineProxy ?: return null
return vmProxy.getOrCreateUserData(METADATA_CACHE_KEY) {
KotlinMetadataCache()
}
}
}

private class KotlinMetadataCache {
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,
JsonUtils::class.java.name
)
(value as? StringReference)?.value()
}
}
}

private inline 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.KotlinMetadataCacheService
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,46 @@ class KotlinClassRenderer : ClassRenderer() {
}
}

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

private fun calculateGettersToShowUsingMetadata(
methods: List<Method>,
context: EvaluationContext
): Set<String>? {
val uniqueKotlinDeclaringTypes = methods
.map { it.declaringType() }
.filter { it.isInKotlinSources() }
.toSet()
val metadataList = KotlinMetadataCacheService.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 acb6170

Please sign in to comment.