Debugger: Show captured outer this in lambdas (Variables view) (KT-30220)

This commit is contained in:
Yan Zhulanow
2019-03-05 01:06:59 +03:00
parent 723f8df23e
commit ba0989801a
10 changed files with 270 additions and 13 deletions
@@ -16,16 +16,27 @@
package org.jetbrains.kotlin.idea.debugger package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.JavaStackFrame import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.descriptors.data.DescriptorData
import com.intellij.debugger.impl.descriptors.data.DisplayKey
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
import com.intellij.debugger.jdi.LocalVariableProxyImpl import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ThisDescriptorImpl import com.intellij.debugger.ui.impl.watch.ThisDescriptorImpl
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiExpression
import com.intellij.util.IncorrectOperationException
import com.intellij.xdebugger.frame.XValue import com.intellij.xdebugger.frame.XValue
import com.intellij.xdebugger.frame.XValueChildrenList import com.intellij.xdebugger.frame.XValueChildrenList
import com.sun.jdi.ObjectReference
import com.sun.jdi.ReferenceType import com.sun.jdi.ReferenceType
import com.sun.jdi.Type import com.sun.jdi.Type
import com.sun.jdi.Value import com.sun.jdi.Value
@@ -49,9 +60,13 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
return super.superBuildVariables(evaluationContext, children) return super.superBuildVariables(evaluationContext, children)
} }
val nodeManager = evaluationContext.debugProcess.xdebugProcess!!.nodeManager val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager
fun addItem(variable: LocalVariableProxyImpl) { fun addItem(variable: LocalVariableProxyImpl) {
if (nodeManager == null) {
return
}
val variableDescriptor = nodeManager.getLocalVariableDescriptor(null, variable) val variableDescriptor = nodeManager.getLocalVariableDescriptor(null, variable)
children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false)) children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false))
} }
@@ -59,7 +74,7 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
val (thisReferences, otherVariables) = visibleVariables val (thisReferences, otherVariables) = visibleVariables
.partition { it.name() == THIS || it is ThisLocalVariable } .partition { it.name() == THIS || it is ThisLocalVariable }
if (!removeSyntheticThisObject(evaluationContext, children) && thisReferences.isNotEmpty()) { if (!removeSyntheticThisObject(evaluationContext, children, thisReferences) && thisReferences.isNotEmpty()) {
val thisLabels = thisReferences.asSequence() val thisLabels = thisReferences.asSequence()
.filterIsInstance<ThisLocalVariable>() .filterIsInstance<ThisLocalVariable>()
.mapNotNullTo(hashSetOf()) { it.label } .mapNotNullTo(hashSetOf()) { it.label }
@@ -71,10 +86,13 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
otherVariables.forEach(::addItem) otherVariables.forEach(::addItem)
} }
private fun removeSyntheticThisObject(evaluationContext: EvaluationContextImpl, children: XValueChildrenList): Boolean { private fun removeSyntheticThisObject(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
thisReferences: List<LocalVariableProxyImpl>
): Boolean {
val thisObject = evaluationContext.frameProxy?.thisObject() ?: return false val thisObject = evaluationContext.frameProxy?.thisObject() ?: return false
if (thisObject.type().isSubtype(VariableFinder.CONTINUATION_TYPE)) { if (thisObject.type().isSubtype(VariableFinder.CONTINUATION_TYPE)) {
ExistingInstanceThis.find(children)?.remove() ExistingInstanceThis.find(children)?.remove()
return true return true
@@ -82,13 +100,57 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
val thisObjectType = thisObject.type() val thisObjectType = thisObject.type()
if (thisObjectType.isSubtype(Function::class.java.name) && '$' in thisObjectType.signature()) { if (thisObjectType.isSubtype(Function::class.java.name) && '$' in thisObjectType.signature()) {
ExistingInstanceThis.find(children)?.remove() val existingThis = ExistingInstanceThis.find(children)
if (existingThis != null) {
existingThis.remove()
val javaValue = existingThis.value as? JavaValue
if (javaValue != null) {
attachCapturedThisFromLambda(evaluationContext, children, javaValue, thisReferences)
}
}
return true return true
} }
return false return false
} }
private fun attachCapturedThisFromLambda(
evaluationContext: EvaluationContextImpl,
children: XValueChildrenList,
javaValue: JavaValue,
thisReferences: List<LocalVariableProxyImpl>
) {
try {
val value = javaValue.descriptor.calcValue(evaluationContext) as? ObjectReference ?: return
val thisField = value.referenceType().fieldByName(AsmUtil.CAPTURED_THIS_FIELD) ?: return
val thisValue = value.getValue(thisField) as? ObjectReference ?: return
val thisType = thisValue.referenceType()
val unsafeLabel = generateThisLabelUnsafe(thisType) ?: return
val label = checkLabel(unsafeLabel)
if (label != null) {
val thisName = getThisName(label)
if (thisReferences.any { it.name() == thisName }) {
// Avoid label duplication
return
}
}
val thisName = when {
thisReferences.isEmpty() -> THIS
label != null -> getThisName(label)
else -> "$THIS (anonymous fun)"
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager ?: return
val thisDescriptor = nodeManager.getDescriptor(this.descriptor, LabeledThisData(thisName, thisValue))
children.add(JavaValue.create(null, thisDescriptor, evaluationContext, nodeManager, false))
} catch (e: EvaluateException) {
// do nothing
}
}
private fun remapThisObjectForOuterThis( private fun remapThisObjectForOuterThis(
evaluationContext: EvaluationContextImpl, evaluationContext: EvaluationContextImpl,
children: XValueChildrenList, children: XValueChildrenList,
@@ -112,7 +174,7 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
private class ExistingInstanceThis( private class ExistingInstanceThis(
private val children: XValueChildrenList, private val children: XValueChildrenList,
private val index: Int, private val index: Int,
private val value: XValue, val value: XValue,
private val size: Int private val size: Int
) { ) {
companion object { companion object {
@@ -251,10 +313,16 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
} }
private fun generateThisLabel(type: Type?): String? { private fun generateThisLabel(type: Type?): String? {
val referenceType = type as? ReferenceType ?: return null return checkLabel(generateThisLabelUnsafe(type) ?: return null)
val label = referenceType.name().substringAfterLast('.').substringAfterLast('$') }
if (label.isEmpty() || label.any { it.isDigit() }) { private fun generateThisLabelUnsafe(type: Type?): String? {
val referenceType = type as? ReferenceType ?: return null
return referenceType.name().substringAfterLast('.').substringAfterLast('$')
}
private fun checkLabel(label: String): String? {
if (label.isEmpty() || label.all { it.isDigit() }) {
return null return null
} }
@@ -270,10 +338,6 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
return dropLast(depth * INLINE_FUN_VAR_SUFFIX.length) return dropLast(depth * INLINE_FUN_VAR_SUFFIX.length)
} }
private fun getThisName(label: String): String {
return "$THIS (@$label)"
}
private fun LocalVariableProxyImpl.clone(name: String, label: String?): LocalVariableProxyImpl { private fun LocalVariableProxyImpl.clone(name: String, label: String?): LocalVariableProxyImpl {
return object : LocalVariableProxyImpl(frame, variable), ThisLocalVariable { return object : LocalVariableProxyImpl(frame, variable), ThisLocalVariable {
override fun name() = name override fun name() = name
@@ -298,4 +362,32 @@ private fun LocalVariableProxyImpl.wrapSyntheticInlineVariable(): LocalVariableP
} }
} }
return LocalVariableProxyImpl(proxyWrapper, variable) return LocalVariableProxyImpl(proxyWrapper, variable)
}
private fun getThisName(label: String): String {
return "$THIS (@$label)"
}
private class LabeledThisData(val name: String, val value: ObjectReference) : DescriptorData<ValueDescriptorImpl>() {
override fun createDescriptorImpl(project: Project): ValueDescriptorImpl {
return object : ValueDescriptorImpl(project, value) {
override fun getName() = this@LabeledThisData.name
override fun calcValue(evaluationContext: EvaluationContextImpl?) = value
override fun canSetValue() = false
override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression {
// TODO change to labeled this
val elementFactory = JavaPsiFacade.getElementFactory(myProject)
try {
return elementFactory.createExpressionFromText("this", null)
} catch (e: IncorrectOperationException) {
throw EvaluateException(e.message, e)
}
}
}
}
override fun getDisplayKey(): DisplayKey<ValueDescriptorImpl> = SimpleDisplayKey(this)
override fun equals(other: Any?) = other is LabeledThisData && other.name == name
override fun hashCode() = name.hashCode()
} }
@@ -0,0 +1,22 @@
package lambdaFun1
fun foo() {
block {
//Breakpoint!
val a = 5
}
}
fun <T> block(block: () -> T): T {
return block()
}
fun main() {
foo()
}
// PRINT_FRAME
// SHOW_KOTLIN_VARIABLES
// EXPRESSION: this
// RESULT: 'this' is not defined in this context
@@ -0,0 +1,8 @@
LineBreakpoint created at lambdaFun1.kt:6
Run Java
Connected to the target VM
lambdaFun1.kt:6
frame = invoke:6, LambdaFun1Kt$foo$1 {lambdaFun1}
Disconnected from the target VM
Process finished with exit code 0
@@ -0,0 +1,24 @@
package lambdaFun2
class Foo {
fun foo() {
block {
//Breakpoint!
val a = this@Foo
}
}
}
fun <T> block(block: () -> T): T {
return block()
}
fun main() {
Foo().foo()
}
// PRINT_FRAME
// SHOW_KOTLIN_VARIABLES
// EXPRESSION: this
// RESULT: instance of lambdaFun2.Foo(id=ID): LlambdaFun2/Foo;
@@ -0,0 +1,11 @@
LineBreakpoint created at lambdaFun2.kt:7
Run Java
Connected to the target VM
lambdaFun2.kt:7
Compile bytecode for this
frame = invoke:7, Foo$foo$1 {lambdaFun2}
unknown = this = {lambdaFun2.Foo@uniqueID}
- Class has no fields
Disconnected from the target VM
Process finished with exit code 0
@@ -0,0 +1,24 @@
package lambdaFun3
object Foo {
fun foo() {
block {
//Breakpoint!
val a = this@Foo
}
}
}
fun <T> block(block: () -> T): T {
return block()
}
fun main() {
Foo.foo()
}
// PRINT_FRAME
// SHOW_KOTLIN_VARIABLES
// EXPRESSION: this
// RESULT: instance of lambdaFun3.Foo(id=ID): LlambdaFun3/Foo;
@@ -0,0 +1,9 @@
LineBreakpoint created at lambdaFun3.kt:7
Run Java
Connected to the target VM
lambdaFun3.kt:7
Compile bytecode for this
frame = invoke:7, Foo$foo$1 {lambdaFun3}
Disconnected from the target VM
Process finished with exit code 0
@@ -0,0 +1,29 @@
package lambdaFun4
interface Foo {
fun foo() {
block {
with ("abc") {
//Breakpoint!
val a = this@Foo
}
}
}
}
fun <T> block(block: () -> T): T {
return block()
}
fun main() {
(object : Foo {}).foo()
}
// PRINT_FRAME
// SHOW_KOTLIN_VARIABLES
// EXPRESSION: this
// RESULT: "abc": Ljava/lang/String;
// EXPRESSION: this@Foo
// RESULT: instance of lambdaFun4.LambdaFun4Kt$main$1(id=ID): LlambdaFun4/LambdaFun4Kt$main$1;
@@ -0,0 +1,18 @@
LineBreakpoint created at lambdaFun4.kt:8
Run Java
Connected to the target VM
lambdaFun4.kt:8
Compile bytecode for this
Compile bytecode for this@Foo
frame = invoke:8, Foo$foo$1 {lambdaFun4}
unknown = this (anonymous fun) = {lambdaFun4.LambdaFun4Kt$main$1@uniqueID}
- Class has no fields
local = this: java.lang.String = abc (sp = null)
field = value: char[] = {char[3]@uniqueID} (sp = String.!EXT!)
element = 0 = 'a' 97
element = 1 = 'b' 98
element = 2 = 'c' 99
field = hash: int = 0 (sp = String.!EXT!)
Disconnected from the target VM
Process finished with exit code 0
@@ -795,6 +795,26 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/hideSyntheticThis.kt"); runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/hideSyntheticThis.kt");
} }
@TestMetadata("lambdaFun1.kt")
public void testLambdaFun1() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaFun1.kt");
}
@TestMetadata("lambdaFun2.kt")
public void testLambdaFun2() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaFun2.kt");
}
@TestMetadata("lambdaFun3.kt")
public void testLambdaFun3() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaFun3.kt");
}
@TestMetadata("lambdaFun4.kt")
public void testLambdaFun4() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaFun4.kt");
}
@TestMetadata("lambdaParameterMangling.kt") @TestMetadata("lambdaParameterMangling.kt")
public void testLambdaParameterMangling() throws Exception { public void testLambdaParameterMangling() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaParameterMangling.kt"); runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/lambdaParameterMangling.kt");