diff --git a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt index 66f6f1b7d59..990a910aef5 100644 --- a/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt +++ b/idea/jvm-debugger/jvm-debugger-evaluation/src/org/jetbrains/kotlin/idea/debugger/evaluate/classLoading/OrdinaryClassLoadingAdapter.kt @@ -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? - ) { + private var superName: String? = null + + override fun visit(version: Int, access: Int, name: String, sig: String?, superName: String?, interfaces: Array?) { 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, "", "()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) } } diff --git a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinEvaluateExpressionTestGenerated.java b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinEvaluateExpressionTestGenerated.java index 53ca105f2df..e054ae369a3 100644 --- a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinEvaluateExpressionTestGenerated.java +++ b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinEvaluateExpressionTestGenerated.java @@ -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"); diff --git a/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.kt b/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.kt new file mode 100644 index 00000000000..98bb623367e --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.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 -> l.first() }.toString().removeId() +// RESULT: "kotlin.jvm.internal.Lambda#id": Ljava/lang/String; \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.out b/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.out new file mode 100644 index 00000000000..469038de515 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-test/testData/evaluation/singleBreakpoint/lambdaToString.out @@ -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 -> l.first() }.toString().removeId() +Disconnected from the target VM + +Process finished with exit code 0