Debugger: Fix breakpoint firing in '$suspendImpl' (KT-27645)

This commit is contained in:
Yan Zhulanow
2019-08-01 21:18:16 +09:00
parent 9096d211d3
commit 0a092d6613
9 changed files with 208 additions and 129 deletions
@@ -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
@@ -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
}
@@ -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
@@ -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)
}
}
@@ -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()
@@ -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() }
}
}
@@ -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
@@ -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
@@ -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");