From 0a092d66130888a089fb332c2d0105152dcefc7d Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Thu, 1 Aug 2019 21:18:16 +0900 Subject: [PATCH] Debugger: Fix breakpoint firing in '$suspendImpl' (KT-27645) --- .../coroutines/coroutineCodegenUtil.kt | 4 +- .../jetbrains/kotlin/util/declarationUtil.kt | 19 +++ .../kotlin/idea/fir/FirResolutionApi.kt | 10 +- .../KotlinSyntheticTypeComponentProvider.kt | 134 ++---------------- ...otlinSyntheticTypeComponentProvider.kt.183 | 8 ++ ...otlinSyntheticTypeComponentProviderBase.kt | 131 +++++++++++++++++ .../src/stepping/stepOver/suspendImpl.kt | 16 +++ .../src/stepping/stepOver/suspendImpl.out | 10 ++ .../debugger/KotlinSteppingTestGenerated.java | 5 + 9 files changed, 208 insertions(+), 129 deletions(-) create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/util/declarationUtil.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt.183 create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProviderBase.kt create mode 100644 idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.kt create mode 100644 idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.out diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutineCodegenUtil.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutineCodegenUtil.kt index c29ab7ccda2..592678a7f84 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutineCodegenUtil.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutineCodegenUtil.kt @@ -498,8 +498,10 @@ fun InstructionAdapter.invokeInvokeSuspendWithUnit(thisName: String) { ) } +const val SUSPEND_IMPL_NAME_SUFFIX = "\$suspendImpl" + fun Method.getImplForOpenMethod(ownerInternalName: String) = - Method("$name\$suspendImpl", returnType, arrayOf(Type.getObjectType(ownerInternalName)) + argumentTypes) + Method(name + SUSPEND_IMPL_NAME_SUFFIX, returnType, arrayOf(Type.getObjectType(ownerInternalName)) + argumentTypes) fun FunctionDescriptor.isSuspendLambdaOrLocalFunction() = this.isSuspend && when (this) { is AnonymousFunctionDescriptor -> this.isSuspendLambda diff --git a/compiler/frontend/src/org/jetbrains/kotlin/util/declarationUtil.kt b/compiler/frontend/src/org/jetbrains/kotlin/util/declarationUtil.kt new file mode 100644 index 00000000000..09b6eef3a48 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/util/declarationUtil.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.util + +import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtPsiUtil + +fun KtElement.containingNonLocalDeclaration(): KtDeclaration? { + var container = this.containingDeclarationForPseudocode + while (container != null && KtPsiUtil.isLocal(container)) { + container = container.containingDeclarationForPseudocode + } + return container +} \ No newline at end of file diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/fir/FirResolutionApi.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/fir/FirResolutionApi.kt index bc5a2ea8ddb..79a76a153aa 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/fir/FirResolutionApi.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/fir/FirResolutionApi.kt @@ -6,7 +6,6 @@ package org.jetbrains.kotlin.idea.fir import com.intellij.psi.PsiElement -import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.FirReference import org.jetbrains.kotlin.fir.declarations.* @@ -24,6 +23,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.util.containingNonLocalDeclaration private val FirResolvePhase.stubMode: Boolean get() = this <= FirResolvePhase.DECLARATIONS @@ -145,14 +145,6 @@ private fun FirDeclaration.runResolve( } } -private fun KtElement.containingNonLocalDeclaration(): KtDeclaration? { - var container = this.containingDeclarationForPseudocode - while (container != null && KtPsiUtil.isLocal(container)) { - container = container.containingDeclarationForPseudocode - } - return container -} - fun KtElement.getOrBuildFir( state: FirResolveState, phase: FirResolvePhase = FirResolvePhase.BODY_RESOLVE diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt index 1586d9cc102..75c9ea15a5b 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt @@ -1,131 +1,27 @@ /* - * Copyright 2010-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package org.jetbrains.kotlin.idea.debugger.filter -import com.intellij.debugger.engine.SyntheticTypeComponentProvider -import com.sun.jdi.* -import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations -import org.jetbrains.kotlin.load.java.JvmAbi -import org.jetbrains.kotlin.name.FqNameUnsafe -import org.jetbrains.org.objectweb.asm.Opcodes -import kotlin.jvm.internal.FunctionReference -import kotlin.jvm.internal.PropertyReference +import com.sun.jdi.Method +import com.sun.jdi.TypeComponent +import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_IMPL_NAME_SUFFIX +import org.jetbrains.kotlin.idea.debugger.isInKotlinSources -class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvider { - override fun isSynthetic(typeComponent: TypeComponent?): Boolean { - if (typeComponent !is Method) return false - - val containingType = typeComponent.declaringType() - val typeName = containingType.name() - if (!FqNameUnsafe.isValid(typeName)) return false - - // TODO: this is most likely not necessary since KT-28453 is fixed, but still can be useful when debugging old compiled code - if (containingType.isCallableReferenceSyntheticClass()) { - return true - } - - try { - if (typeComponent.isDelegateToDefaultInterfaceImpl()) return true - - if (typeComponent.location()?.lineNumber() != 1) return false - - if (typeComponent.allLineLocations().any { it.lineNumber() != 1 }) { - return false - } - - return !typeComponent.declaringType().allLineLocations().any { it.lineNumber() != 1 } - } - catch(e: AbsentInformationException) { - return false - } - catch(e: UnsupportedOperationException) { - return false - } - } - - private tailrec fun ReferenceType?.isCallableReferenceSyntheticClass(): Boolean { - if (this !is ClassType) return false - val superClass = this.superclass() ?: return false - val superClassName = superClass.name() - if (superClassName == PropertyReference::class.java.name || superClassName == FunctionReference::class.java.name) { - return true - } - - // The direct supertype may be PropertyReference0 or something - return if (superClassName.startsWith("kotlin.jvm.internal.")) - superClass.isCallableReferenceSyntheticClass() - else - false - } - - private fun Method.isDelegateToDefaultInterfaceImpl(): Boolean { - if (safeAllLineLocations().size != 1) return false - if (!virtualMachine().canGetBytecodes()) return false - - if (!hasOnlyInvokeStatic(this)) return false - - return hasInterfaceWithImplementation(this) - } - - private val LOAD_INSTRUCTIONS_WITH_INDEX = Opcodes.ILOAD.toByte()..Opcodes.ALOAD.toByte() - private val LOAD_INSTRUCTIONS = (Opcodes.ALOAD + 1).toByte()..(Opcodes.IALOAD - 1).toByte() - - private val RETURN_INSTRUCTIONS = Opcodes.IRETURN.toByte()..Opcodes.RETURN.toByte() - - // Check that method contains only load and invokeStatic instructions. Note that if after load goes ldc instruction it could be checkParametersNotNull method invocation - private fun hasOnlyInvokeStatic(m: Method): Boolean { - val bytecodes = m.bytecodes() - var i = 0 - var isALoad0BeforeStaticCall = false - while (i < bytecodes.size) { - val instr = bytecodes[i] - when { - instr == 42.toByte() /* ALOAD_0 */ -> { - i += 1 - isALoad0BeforeStaticCall = true +class KotlinSyntheticTypeComponentProvider : KotlinSyntheticTypeComponentProviderBase() { + override fun isNotSynthetic(typeComponent: TypeComponent?): Boolean { + if (typeComponent is Method && typeComponent.name().endsWith(SUSPEND_IMPL_NAME_SUFFIX)) { + if (typeComponent.location()?.isInKotlinSources() == true) { + val containingClass = typeComponent.declaringType() + if (typeComponent.argumentTypeNames().firstOrNull() == containingClass.name()) { + // Suspend wrapper for open method + return true } - instr in LOAD_INSTRUCTIONS_WITH_INDEX || instr in LOAD_INSTRUCTIONS -> { - i += 1 - if (instr in LOAD_INSTRUCTIONS_WITH_INDEX) i += 1 - val nextInstr = bytecodes[i] - if (nextInstr == Opcodes.LDC.toByte()) { - i += 2 - isALoad0BeforeStaticCall = false - } - } - instr == Opcodes.INVOKESTATIC.toByte() -> { - i += 3 - if (isALoad0BeforeStaticCall && i == (bytecodes.size - 1)) { - val nextInstr = bytecodes[i] - return nextInstr in RETURN_INSTRUCTIONS - } - } - else -> return false } } - return false - } - // TODO: class DefaultImpl can be not loaded - private fun hasInterfaceWithImplementation(method: Method): Boolean { - val declaringType = method.declaringType() as? ClassType ?: return false - val interfaces = declaringType.allInterfaces() - val vm = declaringType.virtualMachine() - val traitImpls = interfaces.flatMap { vm.classesByName(it.name() + JvmAbi.DEFAULT_IMPLS_SUFFIX) } - return traitImpls.any { !it.methodsByName(method.name()).isEmpty() } + return super.isNotSynthetic(typeComponent) } } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt.183 b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt.183 new file mode 100644 index 00000000000..11bf246a472 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProvider.kt.183 @@ -0,0 +1,8 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.filter + +class KotlinSyntheticTypeComponentProvider : KotlinSyntheticTypeComponentProviderBase() \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProviderBase.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProviderBase.kt new file mode 100644 index 00000000000..8093a083701 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/filter/KotlinSyntheticTypeComponentProviderBase.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.debugger.filter + +import com.intellij.debugger.engine.SyntheticTypeComponentProvider +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.name.FqNameUnsafe +import org.jetbrains.org.objectweb.asm.Opcodes +import kotlin.jvm.internal.FunctionReference +import kotlin.jvm.internal.PropertyReference + +abstract class KotlinSyntheticTypeComponentProviderBase: SyntheticTypeComponentProvider { + override fun isSynthetic(typeComponent: TypeComponent?): Boolean { + if (typeComponent !is Method) return false + + val containingType = typeComponent.declaringType() + val typeName = containingType.name() + if (!FqNameUnsafe.isValid(typeName)) return false + + // TODO: this is most likely not necessary since KT-28453 is fixed, but still can be useful when debugging old compiled code + if (containingType.isCallableReferenceSyntheticClass()) { + return true + } + + try { + if (typeComponent.isDelegateToDefaultInterfaceImpl()) return true + + if (typeComponent.location()?.lineNumber() != 1) return false + + if (typeComponent.allLineLocations().any { it.lineNumber() != 1 }) { + return false + } + + return !typeComponent.declaringType().allLineLocations().any { it.lineNumber() != 1 } + } + catch(e: AbsentInformationException) { + return false + } + catch(e: UnsupportedOperationException) { + return false + } + } + + private tailrec fun ReferenceType?.isCallableReferenceSyntheticClass(): Boolean { + if (this !is ClassType) return false + val superClass = this.superclass() ?: return false + val superClassName = superClass.name() + if (superClassName == PropertyReference::class.java.name || superClassName == FunctionReference::class.java.name) { + return true + } + + // The direct supertype may be PropertyReference0 or something + return if (superClassName.startsWith("kotlin.jvm.internal.")) + superClass.isCallableReferenceSyntheticClass() + else + false + } + + private fun Method.isDelegateToDefaultInterfaceImpl(): Boolean { + if (safeAllLineLocations().size != 1) return false + if (!virtualMachine().canGetBytecodes()) return false + + if (!hasOnlyInvokeStatic(this)) return false + + return hasInterfaceWithImplementation(this) + } + + private val LOAD_INSTRUCTIONS_WITH_INDEX = Opcodes.ILOAD.toByte()..Opcodes.ALOAD.toByte() + private val LOAD_INSTRUCTIONS = (Opcodes.ALOAD + 1).toByte()..(Opcodes.IALOAD - 1).toByte() + + private val RETURN_INSTRUCTIONS = Opcodes.IRETURN.toByte()..Opcodes.RETURN.toByte() + + // Check that method contains only load and invokeStatic instructions. Note that if after load goes ldc instruction it could be checkParametersNotNull method invocation + private fun hasOnlyInvokeStatic(m: Method): Boolean { + val bytecodes = m.bytecodes() + var i = 0 + var isALoad0BeforeStaticCall = false + while (i < bytecodes.size) { + val instr = bytecodes[i] + when { + instr == 42.toByte() /* ALOAD_0 */ -> { + i += 1 + isALoad0BeforeStaticCall = true + } + instr in LOAD_INSTRUCTIONS_WITH_INDEX || instr in LOAD_INSTRUCTIONS -> { + i += 1 + if (instr in LOAD_INSTRUCTIONS_WITH_INDEX) i += 1 + val nextInstr = bytecodes[i] + if (nextInstr == Opcodes.LDC.toByte()) { + i += 2 + isALoad0BeforeStaticCall = false + } + } + instr == Opcodes.INVOKESTATIC.toByte() -> { + i += 3 + if (isALoad0BeforeStaticCall && i == (bytecodes.size - 1)) { + val nextInstr = bytecodes[i] + return nextInstr in RETURN_INSTRUCTIONS + } + } + else -> return false + } + } + return false + } + + // TODO: class DefaultImpl can be not loaded + private fun hasInterfaceWithImplementation(method: Method): Boolean { + val declaringType = method.declaringType() as? ClassType ?: return false + val interfaces = declaringType.allInterfaces() + val vm = declaringType.virtualMachine() + val traitImpls = interfaces.flatMap { vm.classesByName(it.name() + JvmAbi.DEFAULT_IMPLS_SUFFIX) } + return traitImpls.any { !it.methodsByName(method.name()).isEmpty() } + } +} \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.kt b/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.kt new file mode 100644 index 00000000000..5f073befad4 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.kt @@ -0,0 +1,16 @@ +package suspendImpl + +suspend fun main() { + Foo().foo() +} + +open class Foo { + open fun foo() { + //Breakpoint! + val a = 5 + val b = 6 + val c = 7 + } +} + +// STEP_OVER: 2 \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.out b/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.out new file mode 100644 index 00000000000..dcb233ec5b0 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.out @@ -0,0 +1,10 @@ +LineBreakpoint created at suspendImpl.kt:10 +Run Java +Connected to the target VM +suspendImpl.kt:10 +suspendImpl.kt:11 +suspendImpl.kt:12 +Disconnected from the target VM + +Process finished with exit code 0 + diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java index de0c556a109..9396d3c1544 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/KotlinSteppingTestGenerated.java @@ -923,6 +923,11 @@ public class KotlinSteppingTestGenerated extends AbstractKotlinSteppingTest { runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/stopInlineCallInLocalFunInSecondaryConstructor.kt"); } + @TestMetadata("suspendImpl.kt") + public void testSuspendImpl() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/suspendImpl.kt"); + } + @TestMetadata("whenWithoutExpression.kt") public void testWhenWithoutExpression() throws Exception { runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/whenWithoutExpression.kt");