Debugger: Fix breakpoint firing in '$suspendImpl' (KT-27645)
This commit is contained in:
+3
-1
@@ -498,8 +498,10 @@ fun InstructionAdapter.invokeInvokeSuspendWithUnit(thisName: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val SUSPEND_IMPL_NAME_SUFFIX = "\$suspendImpl"
|
||||||
|
|
||||||
fun Method.getImplForOpenMethod(ownerInternalName: String) =
|
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) {
|
fun FunctionDescriptor.isSuspendLambdaOrLocalFunction() = this.isSuspend && when (this) {
|
||||||
is AnonymousFunctionDescriptor -> this.isSuspendLambda
|
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
|
package org.jetbrains.kotlin.idea.fir
|
||||||
|
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode
|
|
||||||
import org.jetbrains.kotlin.fir.FirElement
|
import org.jetbrains.kotlin.fir.FirElement
|
||||||
import org.jetbrains.kotlin.fir.FirReference
|
import org.jetbrains.kotlin.fir.FirReference
|
||||||
import org.jetbrains.kotlin.fir.declarations.*
|
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.name.Name
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||||
|
import org.jetbrains.kotlin.util.containingNonLocalDeclaration
|
||||||
|
|
||||||
private val FirResolvePhase.stubMode: Boolean
|
private val FirResolvePhase.stubMode: Boolean
|
||||||
get() = this <= FirResolvePhase.DECLARATIONS
|
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(
|
fun KtElement.getOrBuildFir(
|
||||||
state: FirResolveState,
|
state: FirResolveState,
|
||||||
phase: FirResolvePhase = FirResolvePhase.BODY_RESOLVE
|
phase: FirResolvePhase = FirResolvePhase.BODY_RESOLVE
|
||||||
|
|||||||
+15
-119
@@ -1,131 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2010-2015 JetBrains s.r.o.
|
* 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.
|
||||||
* 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
|
package org.jetbrains.kotlin.idea.debugger.filter
|
||||||
|
|
||||||
import com.intellij.debugger.engine.SyntheticTypeComponentProvider
|
import com.sun.jdi.Method
|
||||||
import com.sun.jdi.*
|
import com.sun.jdi.TypeComponent
|
||||||
import org.jetbrains.kotlin.idea.debugger.safeAllLineLocations
|
import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_IMPL_NAME_SUFFIX
|
||||||
import org.jetbrains.kotlin.load.java.JvmAbi
|
import org.jetbrains.kotlin.idea.debugger.isInKotlinSources
|
||||||
import org.jetbrains.kotlin.name.FqNameUnsafe
|
|
||||||
import org.jetbrains.org.objectweb.asm.Opcodes
|
|
||||||
import kotlin.jvm.internal.FunctionReference
|
|
||||||
import kotlin.jvm.internal.PropertyReference
|
|
||||||
|
|
||||||
class KotlinSyntheticTypeComponentProvider: SyntheticTypeComponentProvider {
|
class KotlinSyntheticTypeComponentProvider : KotlinSyntheticTypeComponentProviderBase() {
|
||||||
override fun isSynthetic(typeComponent: TypeComponent?): Boolean {
|
override fun isNotSynthetic(typeComponent: TypeComponent?): Boolean {
|
||||||
if (typeComponent !is Method) return false
|
if (typeComponent is Method && typeComponent.name().endsWith(SUSPEND_IMPL_NAME_SUFFIX)) {
|
||||||
|
if (typeComponent.location()?.isInKotlinSources() == true) {
|
||||||
val containingType = typeComponent.declaringType()
|
val containingClass = typeComponent.declaringType()
|
||||||
val typeName = containingType.name()
|
if (typeComponent.argumentTypeNames().firstOrNull() == containingClass.name()) {
|
||||||
if (!FqNameUnsafe.isValid(typeName)) return false
|
// Suspend wrapper for open method
|
||||||
|
return true
|
||||||
// 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
|
return super.isNotSynthetic(typeComponent)
|
||||||
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() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
@@ -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()
|
||||||
+131
@@ -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
|
||||||
|
|
||||||
+5
@@ -923,6 +923,11 @@ public class KotlinSteppingTestGenerated extends AbstractKotlinSteppingTest {
|
|||||||
runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/stopInlineCallInLocalFunInSecondaryConstructor.kt");
|
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")
|
@TestMetadata("whenWithoutExpression.kt")
|
||||||
public void testWhenWithoutExpression() throws Exception {
|
public void testWhenWithoutExpression() throws Exception {
|
||||||
runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/whenWithoutExpression.kt");
|
runTest("idea/testData/debugger/tinyApp/src/stepping/stepOver/whenWithoutExpression.kt");
|
||||||
|
|||||||
Reference in New Issue
Block a user