[kotlin debugger] IDEA-357870 Introduce MetadataDebugHelper

Kotlin compiler stores meta information about every class in the `kotlin.Metadata` annotation. Unfortunately, there is no way to fetch this annotation using plain JDI interface, because unlike method information, class annotations are not exposed to the debugger. This commit adds a helper class that allows fetching `kotlin.Metadata` of a specific class as a JSON string.

GitOrigin-RevId: 4d463abcd21f75acf90a2358d5782ee1b8b524d2
This commit is contained in:
Nikita Nazarov
2025-03-25 11:25:03 +00:00
committed by intellij-monorepo-bot
parent d7d6d06351
commit 856194d31d
@@ -0,0 +1,132 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.rt.debugger;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class MetadataDebugHelper {
private static final String METADATA_CLASS_NAME = "kotlin.Metadata";
private static final String KIND = "kind";
private static final String METADATA_VERSION = "metadataVersion";
private static final String DATA_1 = "data1";
private static final String DATA_2 = "data2";
private static final String EXTRA_STRING = "extraString";
private static final String PACKAGE_NAME = "packageName";
private static final String EXTRA_INT = "extraInt";
private static final String KIND_METHOD_NAME = "k";
private static final String METADATA_VERSION_METHOD_NAME = "mv";
private static final String DATA_1_METHOD_NAME = "d1";
private static final String DATA_2_METHOD_NAME = "d2";
private static final String EXTRA_STRING_METHOD_NAME = "xs";
private static final String PACKAGE_NAME_METHOD_NAME = "pn";
private static final String EXTRA_INT_METHOD_NAME = "xi";
/*
* Kotlin debugger uses this function to fetch @kotlin.Metadata for a given class.
* Originally, reading annotations from the debugger is not supported by JVM.
*
* The metadata is returned as a string in JSON format, where the `data1` field is
* base64 encoded to keep the format valid.
*
* Returning metadata as a single string allows us to minimize the number of JDI calls from
* 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) {
try {
Class<?> metadataClass = Class.forName(METADATA_CLASS_NAME, false, cls.getClassLoader());
Method kindMethod = metadataClass.getDeclaredMethod(KIND_METHOD_NAME);
Method metadataVersionMethod = metadataClass.getDeclaredMethod(METADATA_VERSION_METHOD_NAME);
Method data1Method = metadataClass.getDeclaredMethod(DATA_1_METHOD_NAME);
Method data2Method = metadataClass.getDeclaredMethod(DATA_2_METHOD_NAME);
Method extraStringMethod = metadataClass.getDeclaredMethod(EXTRA_STRING_METHOD_NAME);
Method packageNameMethod = metadataClass.getDeclaredMethod(PACKAGE_NAME_METHOD_NAME);
Method extraIntMethod = metadataClass.getDeclaredMethod(EXTRA_INT_METHOD_NAME);
Object metadata = cls.getAnnotation((Class<Annotation>)metadataClass);
StringBuilder sb = new StringBuilder();
sb.append("{");
int kind = (int)kindMethod.invoke(metadata);
appendAsJsonValue(sb, KIND, kind);
int[] metadataVersion = (int[]) metadataVersionMethod.invoke(metadata);
appendAsJsonValue(sb, METADATA_VERSION, Arrays.toString(metadataVersion));
String[] data1 = (String[])data1Method.invoke(metadata);
appendAsJsonValue(sb, DATA_1, Arrays.toString(toJsonBase64Encoded(data1)));
String[] data2 = (String[])data2Method.invoke(metadata);
appendAsJsonValue(sb, DATA_2, Arrays.toString(toJson(data2)));
String extraString = (String) extraStringMethod.invoke(metadata);
appendAsJsonValue(sb, EXTRA_STRING, toJson(extraString));
String packageName = (String) packageNameMethod.invoke(metadata);
appendAsJsonValue(sb, PACKAGE_NAME, toJson(packageName));
int extraInt = (int)extraIntMethod.invoke(metadata);
appendAsJsonValueNoComma(sb, EXTRA_INT, extraInt);
sb.append("}");
return sb.toString();
} catch (Exception ex) {
return null;
}
}
private static void appendAsJsonValue(StringBuilder sb, String name, Object value) {
appendAsJsonValueNoComma(sb, name, value).append(',');
}
private static StringBuilder appendAsJsonValueNoComma(StringBuilder sb, String name, Object value) {
return sb.append(toJson(name)).append(':').append(value);
}
private static String toJson(String str) {
StringBuilder sb = new StringBuilder();
sb.append('"');
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '"') {
sb.append("\\\"");
} else {
sb.append(c);
}
}
sb.append('"');
return sb.toString();
}
private static String[] toJson(String[] array) {
String[] result = new String[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = toJson(array[i]);
}
return result;
}
@SuppressWarnings("PrimitiveArrayArgumentToVarargsMethod")
private static String[] toJsonBase64Encoded(String[] array) throws Exception {
Class<?> base64Class = Class.forName("java.util.Base64");
Method getEncoding = base64Class.getDeclaredMethod("getEncoder");
Object encoder = getEncoding.invoke(base64Class);
Class<?> encoderClass = Class.forName("java.util.Base64$Encoder");
Method encodeToString = encoderClass.getDeclaredMethod("encodeToString", byte[].class);
String[] result = new String[array.length];
for (int i = 0; i < array.length; i++) {
byte[] bytes = array[i].getBytes(StandardCharsets.UTF_8);
String encoded = (String)encodeToString.invoke(encoder, bytes);
result[i] = toJson(encoded);
}
return result;
}
}