Debugger: Provide fixed implementation of 'toString()' for lambdas (KT-32691)

This commit is contained in:
Yan Zhulanow
2019-09-10 16:13:09 +03:00
parent 82a1750d26
commit ab0f841642
4 changed files with 76 additions and 16 deletions
@@ -23,10 +23,9 @@ import com.sun.jdi.ClassLoaderReference
import com.sun.jdi.ClassType
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.isDexDebug
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import kotlin.jvm.internal.Lambda
class OrdinaryClassLoadingAdapter : ClassLoadingAdapter {
private companion object {
@@ -37,29 +36,54 @@ class OrdinaryClassLoadingAdapter : ClassLoadingAdapter {
private val LAMBDA_SUPERCLASSES = listOf(ClassBytes("kotlin.jvm.internal.Lambda"))
// Copied from com.intellij.debugger.ui.impl.watch.CompilingEvaluator.changeSuperToMagicAccessor
fun changeSuperToMagicAccessor(bytes: ByteArray): ByteArray {
fun patchClass(bytes: ByteArray, useMagicAccessor: Boolean): ByteArray {
val classWriter = ClassWriter(0)
val classVisitor = object : ClassVisitor(Opcodes.API_VERSION, classWriter) {
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array<String>?
) {
private var superName: String? = null
override fun visit(version: Int, access: Int, name: String, sig: String?, superName: String?, interfaces: Array<String>?) {
var newSuperName = superName
if ("java/lang/Object" == newSuperName) {
if (useMagicAccessor && "java/lang/Object" == newSuperName) {
newSuperName = "sun/reflect/MagicAccessorImpl"
}
super.visit(version, access, name, signature, newSuperName, interfaces)
this.superName = newSuperName
super.visit(version, access, name, sig, newSuperName, interfaces)
}
override fun visitEnd() {
if (superName == "kotlin/jvm/internal/Lambda") {
generateLambdaToString(this)
}
}
}
ClassReader(bytes).accept(classVisitor, 0)
return classWriter.toByteArray()
}
private fun generateLambdaToString(classVisitor: ClassVisitor) {
val stringBuilderType = Type.getType(StringBuilder::class.java)
val lambdaType = Type.getType(Lambda::class.java)
val methodVisitor = classVisitor.visitMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null)
InstructionAdapter(methodVisitor).apply {
visitCode()
anew(stringBuilderType)
dup()
invokespecial(stringBuilderType.internalName, "<init>", "()V", false)
aconst(lambdaType.className + "#")
invokevirtual(stringBuilderType.internalName, "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
load(0, lambdaType)
invokevirtual("java/lang/Object", "hashCode", "()I", false)
invokevirtual(stringBuilderType.internalName, "append", "(I)Ljava/lang/StringBuilder;", false)
invokevirtual(stringBuilderType.internalName, "toString", "()Ljava/lang/String;", false)
areturn(Type.getType(String::class.java))
visitMaxs(2, 1)
visitEnd()
}
}
fun useMagicAccessor(context: ExecutionContext): Boolean {
val rawVersion = context.vm.version()?.substringBefore('_') ?: return false
val javaVersion = JavaSdkVersion.fromVersionString(rawVersion) ?: return false
@@ -105,7 +129,7 @@ class OrdinaryClassLoadingAdapter : ClassLoadingAdapter {
}
for ((className, _, bytes) in classesToLoad) {
val patchedBytes = if (useMagicAccessor(context)) changeSuperToMagicAccessor(bytes) else bytes
val patchedBytes = patchClass(bytes, useMagicAccessor(context))
defineClass(className, patchedBytes, context, classLoader)
}
}
@@ -315,6 +315,11 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/kt7046localVarInInline.kt");
}
@TestMetadata("lambdaToString.kt")
public void testLambdaToString() throws Exception {
runTest("idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.kt");
}
@TestMetadata("localClass.kt")
public void testLocalClass() throws Exception {
runTest("idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/localClass.kt");
@@ -0,0 +1,20 @@
fun main() {
//Breakpoint!
val a = 5
}
private fun String.removeId(): String {
return substringBeforeLast('#') + "#id"
}
// EXPRESSION: {}.toString().removeId()
// RESULT: "kotlin.jvm.internal.Lambda#id": Ljava/lang/String;
// EXPRESSION: { 1 + 2 }.toString().removeId()
// RESULT: "kotlin.jvm.internal.Lambda#id": Ljava/lang/String;
// EXPRESSION: { s: String -> s.toString() }.toString().removeId()
// RESULT: "kotlin.jvm.internal.Lambda#id": Ljava/lang/String;
// EXPRESSION: { l: List<String> -> l.first() }.toString().removeId()
// RESULT: "kotlin.jvm.internal.Lambda#id": Ljava/lang/String;
@@ -0,0 +1,11 @@
LineBreakpoint created at lambdaToString.kt:3
Run Java
Connected to the target VM
lambdaToString.kt:3
Compile bytecode for {}.toString().removeId()
Compile bytecode for { 1 + 2 }.toString().removeId()
Compile bytecode for { s: String -> s.toString() }.toString().removeId()
Compile bytecode for { l: List<String> -> l.first() }.toString().removeId()
Disconnected from the target VM
Process finished with exit code 0