From 5777592024fcbca3fc0b547bc146e7c3f959b796 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Tue, 30 Apr 2019 23:59:23 +0300 Subject: [PATCH] Debugger: Support captured values (KT-30740) --- .../org/jetbrains/kotlin/codegen/AsmUtil.java | 8 +- .../idea/debugger/KotlinPositionManager.kt | 1 + .../debugger/stackFrame/CapturedValueData.kt | 43 +++ .../ExistingInstanceThisRemapper.kt | 100 ++++++ .../{ => stackFrame}/KotlinStackFrame.kt | 318 +++++++----------- .../debugger/stackFrame/LabeledThisData.kt | 44 +++ .../kotlin/idea/debugger/safeUtil.kt | 8 + .../singleBreakpoint/frame/capturedValues1.kt | 36 ++ .../frame/capturedValues1.out | 20 ++ .../singleBreakpoint/frame/capturedValues2.kt | 27 ++ .../frame/capturedValues2.out | 13 + ...KotlinEvaluateExpressionTestGenerated.java | 10 + 12 files changed, 437 insertions(+), 191 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/CapturedValueData.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/ExistingInstanceThisRemapper.kt rename idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/{ => stackFrame}/KotlinStackFrame.kt (52%) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/LabeledThisData.kt create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.kt create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.out create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.kt create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.out diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java index 4068ce8741f..344aa4a4793 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java @@ -99,15 +99,19 @@ public class AsmUtil { .put(JavaVisibilities.PACKAGE_VISIBILITY, NO_FLAG_PACKAGE_PRIVATE) .build(); + public static final String CAPTURED_PREFIX = "$"; + public static final String THIS = "this"; public static final String THIS_IN_DEFAULT_IMPLS = "$this"; public static final String LABELED_THIS_FIELD = THIS + "_"; + public static final String CAPTURED_LABELED_THIS_FIELD = CAPTURED_PREFIX + LABELED_THIS_FIELD; + public static final String INLINE_DECLARATION_SITE_THIS = "this_"; - public static final String LABELED_THIS_PARAMETER = "$" + THIS + "$"; + public static final String LABELED_THIS_PARAMETER = CAPTURED_PREFIX + THIS + "$"; public static final String CAPTURED_THIS_FIELD = "this$0"; @@ -145,7 +149,7 @@ public class AsmUtil { @NotNull public static String getCapturedFieldName(@NotNull String originalName) { - return "$" + originalName; + return CAPTURED_PREFIX + originalName; } @NotNull diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt index 33e5b1081f5..40dc33ecf23 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinPositionManager.kt @@ -50,6 +50,7 @@ import org.jetbrains.kotlin.idea.core.util.getLineCount import org.jetbrains.kotlin.idea.core.util.getLineStartOffset import org.jetbrains.kotlin.idea.debugger.breakpoints.getLambdasAtLineIfAny import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches +import org.jetbrains.kotlin.idea.debugger.stackFrame.KotlinStackFrame import org.jetbrains.kotlin.idea.decompiler.classFile.KtClsFile import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope import org.jetbrains.kotlin.idea.util.ProjectRootsUtil diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/CapturedValueData.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/CapturedValueData.kt new file mode 100644 index 00000000000..d4bbec95ce9 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/CapturedValueData.kt @@ -0,0 +1,43 @@ +/* + * 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.stackFrame + +import com.intellij.debugger.DebuggerContext +import com.intellij.debugger.engine.JavaValue +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.ui.impl.watch.* +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiExpression +import com.intellij.xdebugger.frame.XValueModifier +import com.sun.jdi.* +import org.jetbrains.org.objectweb.asm.Type as AsmType + +data class CapturedValueData( + val valueName: String, + val obj: ObjectReference, + val field: Field +) : DescriptorData() { + override fun createDescriptorImpl(project: Project): ValueDescriptorImpl { + val fieldDescriptor = FieldDescriptorImpl(project, obj, field) + return CustomFieldDescriptor(valueName, fieldDescriptor) + } + + private class CustomFieldDescriptor( + val valueName: String, + val delegate: FieldDescriptorImpl + ) : ValueDescriptorImpl(delegate.project) { + override fun getName() = valueName + + override fun calcValue(evaluationContext: EvaluationContextImpl?): Value = delegate.calcValue(evaluationContext) + override fun getDescriptorEvaluation(context: DebuggerContext?): PsiExpression = delegate.getDescriptorEvaluation(context) + override fun getModifier(value: JavaValue?): XValueModifier = delegate.getModifier(value) + } + + override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(field) +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/ExistingInstanceThisRemapper.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/ExistingInstanceThisRemapper.kt new file mode 100644 index 00000000000..d36fef3acca --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/ExistingInstanceThisRemapper.kt @@ -0,0 +1,100 @@ +/* + * 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.stackFrame + +import com.intellij.debugger.engine.JavaValue +import com.intellij.debugger.ui.impl.watch.* +import com.intellij.openapi.diagnostic.Logger +import com.intellij.xdebugger.frame.XValue +import com.intellij.xdebugger.frame.XValueChildrenList +import org.jetbrains.org.objectweb.asm.Type as AsmType +import org.jetbrains.kotlin.utils.getSafe +import java.lang.reflect.Modifier +import java.util.* + +// Very Dirty Work-around. +// We should stop delegating to the Java stack frame and generate our trace elements from scratch. +class ExistingInstanceThisRemapper( + private val children: XValueChildrenList, + private val index: Int, + val value: XValue, + private val size: Int +) { + companion object { + private val LOG = Logger.getInstance(this::class.java) + + private const val THIS_NAME = "this" + + fun find(children: XValueChildrenList): ExistingInstanceThisRemapper? { + val size = children.size() + for (i in 0 until size) { + if (children.getName(i) == THIS_NAME) { + val valueDescriptor = (children.getValue(i) as? JavaValue)?.descriptor + @Suppress("FoldInitializerAndIfToElvis") + if (valueDescriptor !is ThisDescriptorImpl) { + return null + } + + return ExistingInstanceThisRemapper( + children, + i, + children.getValue(i), + size + ) + } + } + + return null + } + } + + fun remapName(newName: String) { + val (names, _) = getLists() ?: return + names[index] = newName + } + + fun remove() { + val (names, values) = getLists() ?: return + names.removeAt(index) + values.removeAt(index) + } + + private fun getLists(): Lists? { + if (children.size() != size) { + throw IllegalStateException("Children list was modified") + } + + var namesList: MutableList? = null + var valuesList: MutableList? = null + + for (field in XValueChildrenList::class.java.declaredFields) { + val mods = field.modifiers + if (Modifier.isPrivate(mods) && Modifier.isFinal(mods) && !Modifier.isStatic(mods) && field.type == List::class.java) { + @Suppress("UNCHECKED_CAST") + val list = (field.getSafe(children) as? MutableList)?.takeIf { it.size == size } ?: continue + + if (list[index] == THIS_NAME) { + namesList = list + } else if (list[index] === value) { + valuesList = list + } + } + + if (namesList != null && valuesList != null) { + return Lists(namesList, valuesList) + } + } + + LOG.error( + "Can't find name/value lists, existing fields: " + + Arrays.toString(XValueChildrenList::class.java.declaredFields) + ) + + return null + } + + private data class Lists(val names: MutableList, val values: MutableList) +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt similarity index 52% rename from idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt rename to idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt index a4b81cd891e..e7c2e531d44 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinStackFrame.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt @@ -1,47 +1,20 @@ /* - * 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 +package org.jetbrains.kotlin.idea.debugger.stackFrame import com.intellij.debugger.engine.DebugProcessImpl -import com.intellij.debugger.DebuggerContext import com.intellij.debugger.engine.JavaStackFrame import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.engine.evaluation.EvaluateException 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.StackFrameProxyImpl -import com.intellij.debugger.ui.impl.watch.MethodsTracker -import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl -import com.intellij.debugger.ui.impl.watch.ThisDescriptorImpl -import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl -import com.intellij.openapi.diagnostic.Logger -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.debugger.ui.impl.watch.* import com.intellij.xdebugger.frame.XValueChildrenList -import com.sun.jdi.ObjectReference -import com.sun.jdi.ReferenceType -import com.sun.jdi.Type -import com.sun.jdi.Value +import com.sun.jdi.* import org.jetbrains.kotlin.codegen.AsmUtil import org.jetbrains.kotlin.codegen.AsmUtil.THIS import org.jetbrains.kotlin.codegen.DESTRUCTURED_LAMBDA_ARGUMENT_VARIABLE_PREFIX @@ -49,17 +22,21 @@ import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_FUNCTION_CONTINUATION_PARAMETER import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline -import org.jetbrains.org.objectweb.asm.Type as AsmType -import org.jetbrains.kotlin.utils.getSafe +import org.jetbrains.kotlin.idea.debugger.* import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerEvaluator -import java.lang.reflect.Modifier import java.util.* -class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDescriptorImpl(frame, MethodsTracker()), true) { - private companion object { - private val LOG = Logger.getInstance(this::class.java) - } +private sealed class ExistingVariable { + interface This + object UnlabeledThis : ExistingVariable(), This + data class LabeledThis(val label: String) : ExistingVariable(), This + data class Ordinary(val name: String) : ExistingVariable() +} + +private typealias ExistingVariables = MutableSet + +class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDescriptorImpl(frame, MethodsTracker()), true) { private val kotlinVariableViewService = ToggleKotlinVariablesState.getService() private val kotlinEvaluator by lazy { @@ -85,41 +62,58 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false)) } - val (thisReferences, otherVariables) = visibleVariables + val (thisVariables, otherVariables) = visibleVariables .partition { it.name() == THIS || it is ThisLocalVariable } - if (!removeSyntheticThisObject(evaluationContext, children, thisReferences) && thisReferences.isNotEmpty()) { - val thisLabels = thisReferences.asSequence() - .filterIsInstance() - .mapNotNullTo(hashSetOf()) { it.label } + val existingVariables = getExistingVariables(thisVariables, otherVariables) - remapThisObjectForOuterThis(evaluationContext, children, thisLabels) + if (!removeSyntheticThisObject(evaluationContext, children, existingVariables) && thisVariables.isNotEmpty()) { + remapThisObjectForOuterThis(evaluationContext, children, existingVariables) } - thisReferences.forEach(::addItem) + thisVariables.forEach(::addItem) otherVariables.forEach(::addItem) } + private fun getExistingVariables( + thisVariables: List, + ordinaryVariables: List + ): ExistingVariables { + val set = HashSet() + thisVariables.forEach { + val thisVariable = it as? ThisLocalVariable ?: return@forEach + val label = thisVariable.label + val newExistingVariable: ExistingVariable = when { + label != null -> ExistingVariable.LabeledThis(label) + else -> ExistingVariable.UnlabeledThis + } + set.add(newExistingVariable) + } + ordinaryVariables.forEach { set += ExistingVariable.Ordinary(it.name()) } + return set + } + private fun removeSyntheticThisObject( evaluationContext: EvaluationContextImpl, children: XValueChildrenList, - thisReferences: List + existingVariables: ExistingVariables ): Boolean { val thisObject = evaluationContext.frameProxy?.thisObject() ?: return false if (thisObject.type().isSubtype(CONTINUATION_TYPE)) { - ExistingInstanceThis.find(children)?.remove() + ExistingInstanceThisRemapper.find(children)?.remove() return true } val thisObjectType = thisObject.type() if (thisObjectType.isSubtype(Function::class.java.name) && '$' in thisObjectType.signature()) { - val existingThis = ExistingInstanceThis.find(children) + val existingThis = ExistingInstanceThisRemapper.find(children) if (existingThis != null) { existingThis.remove() - val javaValue = existingThis.value as? JavaValue - if (javaValue != null) { - attachCapturedThisFromLambda(evaluationContext, children, javaValue, thisReferences) + val containerJavaValue = existingThis.value as? JavaValue + val containerValue = containerJavaValue?.descriptor?.calcValue(evaluationContext) as? ObjectReference + if (containerValue != null) { + attachCapturedValues(evaluationContext, children, containerValue, existingVariables) } } return true @@ -139,44 +133,89 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe } if (inlineDepth > 0 && declarationSiteThis != null) { - ExistingInstanceThis.find(children)?.remove() + ExistingInstanceThisRemapper.find(children)?.remove() return true } return false } - private fun attachCapturedThisFromLambda( + private fun attachCapturedValues( evaluationContext: EvaluationContextImpl, children: XValueChildrenList, - javaValue: JavaValue, - thisReferences: List + containerValue: ObjectReference, + existingVariables: ExistingVariables + ) { + val nodeManager = evaluationContext.debugProcess.xdebugProcess?.nodeManager ?: return + val capturedFields = containerValue.referenceType().safeFields() + .filter { field -> + val name = field.name() + name.startsWith(AsmUtil.CAPTURED_PREFIX) || name == AsmUtil.CAPTURED_THIS_FIELD + } + + for (field in capturedFields) { + val name = field.name() + + if (name == AsmUtil.CAPTURED_THIS_FIELD) { + val value = containerValue.getValue(field) as? ObjectReference ?: continue + val label = getThisValueLabel(value) + if (label != null) { + attachCapturedThis(evaluationContext, nodeManager, children, value, label, existingVariables) + } else { + attachCapturedValues(evaluationContext, children, value, existingVariables) + } + continue + } else if (name.startsWith(AsmUtil.CAPTURED_LABELED_THIS_FIELD)) { + val value = containerValue.getValue(field) + val label = name.drop(AsmUtil.CAPTURED_LABELED_THIS_FIELD.length) + if (label.isNotEmpty()) { + attachCapturedThis(evaluationContext, nodeManager, children, value, label, existingVariables) + } + continue + } + + val capturedValueName = name.drop(1).takeIf { it.isNotEmpty() } ?: continue + + if (!existingVariables.add(ExistingVariable.Ordinary(capturedValueName))) { + continue + } + + val valueData = CapturedValueData(capturedValueName, containerValue, field) + val valueDescriptor = nodeManager.getDescriptor(this.descriptor, valueData) + children.add(JavaValue.create(null, valueDescriptor, evaluationContext, nodeManager, false)) + } + } + + private fun getThisValueLabel(thisValue: ObjectReference): String? { + val thisType = thisValue.referenceType() + val unsafeLabel = generateThisLabelUnsafe(thisType) ?: return null + return checkLabel(unsafeLabel) + } + + private fun attachCapturedThis( + evaluationContext: EvaluationContextImpl, + nodeManager: NodeManagerImpl, + children: XValueChildrenList, + thisValue: Value?, + label: String, + existingVariables: ExistingVariables ) { 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) + val hasThisVariables = existingVariables.any { it is ExistingVariable.This } - if (label != null) { - val thisName = getThisName(label) - - if (thisReferences.any { it.name() == thisName }) { - // Avoid label duplication + val thisName = if (hasThisVariables) { + if (!existingVariables.add(ExistingVariable.LabeledThis(label))) { + // Avoid item duplication return } + + getThisName(label) + } else { + existingVariables.add(ExistingVariable.LabeledThis(label)) + THIS } - 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)) + val thisDescriptor = nodeManager.getDescriptor(this.descriptor, LabeledThisData(label, thisName, thisValue)) children.add(JavaValue.create(null, thisDescriptor, evaluationContext, nodeManager, false)) } catch (e: EvaluateException) { // do nothing @@ -186,13 +225,15 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe private fun remapThisObjectForOuterThis( evaluationContext: EvaluationContextImpl, children: XValueChildrenList, - existingThisLabels: Set + existingVariables: ExistingVariables ) { val thisObject = evaluationContext.frameProxy?.thisObject() ?: return - val variable = ExistingInstanceThis.find(children) ?: return + val variable = ExistingInstanceThisRemapper.find(children) ?: return - val thisLabel = generateThisLabel(thisObject.referenceType())?.takeIf { it !in existingThisLabels } - if (thisLabel == null) { + val thisLabel = generateThisLabel(thisObject.referenceType()) + val thisName = thisLabel?.let(::getThisName) + + if (thisName == null || !existingVariables.add(ExistingVariable.LabeledThis(thisLabel))) { variable.remove() return } @@ -201,83 +242,6 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe variable.remapName(getThisName(thisLabel)) } - // Very Dirty Work-around. - // Hopefully, there will be an API for that in 2019.1. - private class ExistingInstanceThis( - private val children: XValueChildrenList, - private val index: Int, - val value: XValue, - private val size: Int - ) { - companion object { - private const val THIS_NAME = "this" - - fun find(children: XValueChildrenList): ExistingInstanceThis? { - val size = children.size() - for (i in 0 until size) { - if (children.getName(i) == THIS_NAME) { - val valueDescriptor = (children.getValue(i) as? JavaValue)?.descriptor - @Suppress("FoldInitializerAndIfToElvis") - if (valueDescriptor !is ThisDescriptorImpl) { - return null - } - - return ExistingInstanceThis(children, i, children.getValue(i), size) - } - } - - return null - } - } - - fun remapName(newName: String) { - val (names, _) = getLists() ?: return - names[index] = newName - } - - fun remove() { - val (names, values) = getLists() ?: return - names.removeAt(index) - values.removeAt(index) - } - - private fun getLists(): Lists? { - if (children.size() != size) { - throw IllegalStateException("Children list was modified") - } - - var namesList: MutableList? = null - var valuesList: MutableList? = null - - for (field in XValueChildrenList::class.java.declaredFields) { - val mods = field.modifiers - if (Modifier.isPrivate(mods) && Modifier.isFinal(mods) && !Modifier.isStatic(mods) && field.type == List::class.java) { - @Suppress("UNCHECKED_CAST") - val list = (field.getSafe(children) as? MutableList)?.takeIf { it.size == size } ?: continue - - if (list[index] == THIS_NAME) { - namesList = list - } else if (list[index] === value) { - valuesList = list - } - } - - if (namesList != null && valuesList != null) { - return Lists(namesList, valuesList) - } - } - - LOG.error( - "Can't find name/value lists, existing fields: " - + Arrays.toString(XValueChildrenList::class.java.declaredFields) - ) - - return null - } - - private data class Lists(val names: MutableList, val values: MutableList) - } - override fun getVisibleVariables(): List { val allVisibleVariables = super.getStackFrameProxy().safeVisibleVariables() @@ -302,8 +266,8 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe .sortedByDescending { it.variable } .let { it.firstOrNull() to it.drop(1) } - val remappedMainThis = mainThis?.clone(THIS, null) - val remappedOther = (otherThis + otherVariables).map { it.remapVariableNameIfNeeded() } + val remappedMainThis = mainThis?.remapThisVariableIfNeeded(THIS) + val remappedOther = (otherThis + otherVariables).map { it.remapThisVariableIfNeeded() } return (listOfNotNull(remappedMainThis) + remappedOther).sortedBy { it.variable } } @@ -317,23 +281,23 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe || name == SUSPEND_FUNCTION_CONTINUATION_PARAMETER } - private fun LocalVariableProxyImpl.remapVariableNameIfNeeded(): LocalVariableProxyImpl { + private fun LocalVariableProxyImpl.remapThisVariableIfNeeded(customName: String? = null): LocalVariableProxyImpl { val name = this.name().dropInlineSuffix() @Suppress("ConvertToStringTemplate") return when { isLabeledThisReference() -> { val label = name.drop(AsmUtil.LABELED_THIS_PARAMETER.length) - clone(getThisName(label), label) + clone(customName ?: getThisName(label), label) } - name == AsmUtil.THIS_IN_DEFAULT_IMPLS -> clone(THIS + " (outer)", null) - name == AsmUtil.RECEIVER_PARAMETER_NAME -> clone(THIS + " (receiver)", null) + name == AsmUtil.THIS_IN_DEFAULT_IMPLS -> clone(customName ?: THIS + " (outer)", null) + name == AsmUtil.RECEIVER_PARAMETER_NAME -> clone(customName ?: THIS + " (receiver)", null) INLINED_THIS_REGEX.matches(name) -> { val label = generateThisLabel(frame.getValue(this)?.type()) if (label != null) { - clone(getThisName(label), label) + clone(customName ?: getThisName(label), label) } else { - this@remapVariableNameIfNeeded + this@remapThisVariableIfNeeded } } name != this.name() -> { @@ -341,7 +305,7 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe override fun name() = name } } - else -> this@remapVariableNameIfNeeded + else -> this@remapThisVariableIfNeeded } } @@ -399,28 +363,4 @@ private fun LocalVariableProxyImpl.wrapSyntheticInlineVariable(): LocalVariableP private fun getThisName(label: String): String { return "$THIS (@$label)" -} - -private class LabeledThisData(val name: String, val value: ObjectReference) : DescriptorData() { - 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 = SimpleDisplayKey(this) - override fun equals(other: Any?) = other is LabeledThisData && other.name == name - override fun hashCode() = name.hashCode() } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/LabeledThisData.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/LabeledThisData.kt new file mode 100644 index 00000000000..d147eed1a2a --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/LabeledThisData.kt @@ -0,0 +1,44 @@ +/* + * 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.stackFrame + +import com.intellij.debugger.DebuggerContext +import com.intellij.debugger.engine.evaluation.EvaluateException +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.ui.impl.watch.* +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiExpression +import com.intellij.util.IncorrectOperationException +import com.sun.jdi.* +import org.jetbrains.org.objectweb.asm.Type as AsmType + +class LabeledThisData(val label: String, val name: String, val value: Value?) : DescriptorData() { + 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 = SimpleDisplayKey(this) + override fun equals(other: Any?) = other is LabeledThisData && other.name == name + override fun hashCode() = name.hashCode() +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/safeUtil.kt b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/safeUtil.kt index 42c5e51451e..a781eb4fa3c 100644 --- a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/safeUtil.kt +++ b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/safeUtil.kt @@ -33,6 +33,14 @@ fun ReferenceType.safeSourceName(): String? { return wrapAbsentInformationException { sourceName() } } +fun ReferenceType.safeFields(): List { + return try { + fields() + } catch (e: ClassNotPreparedException) { + emptyList() + } +} + fun Method.safeLocationsOfLine(line: Int): List { return wrapAbsentInformationException { locationsOfLine(line) } ?: emptyList() } diff --git a/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.kt b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.kt new file mode 100644 index 00000000000..0e1efb3854a --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.kt @@ -0,0 +1,36 @@ +package capturedValues1 + +fun main(args: Array) { + 1000.foo(args) +} + +fun Int.foo(args: Array) { + val a = 1 + block { + val b = 2 + val b2 = 2 + block("x") place1@ { + val c = 3 + val c2 = 3 + block("y") place2@ { + //Breakpoint! + this@place1 + this@foo + b + c2 + args + } + } + } +} + +fun block(block: () -> Unit) { + block() +} + +fun block(obj: T, block: T.() -> Unit) { + obj.block() +} + +// SHOW_KOTLIN_VARIABLES +// PRINT_FRAME \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.out b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.out new file mode 100644 index 00000000000..7eccaa3f1b4 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.out @@ -0,0 +1,20 @@ +LineBreakpoint created at capturedValues1.kt:17 +Run Java +Connected to the target VM +capturedValues1.kt:17 + frame = invoke:17, CapturedValues1Kt$foo$1$1$1 {capturedValues1} + unknown = this (@foo) = 1000 + unknown = args = {java.lang.String[0]@uniqueID} + unknown = b = 2 + unknown = this (@place1) = x + field = value: char[] = {char[1]@uniqueID} (sp = String.!EXT!) + element = 0 = 'x' 120 + field = hash: int = 0 (sp = String.!EXT!) + unknown = c2 = 3 + local = this: java.lang.String = y (sp = null) + field = value: char[] = {char[1]@uniqueID} (sp = String.!EXT!) + element = 0 = 'y' 121 + field = hash: int = 0 (sp = String.!EXT!) +Disconnected from the target VM + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.kt b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.kt new file mode 100644 index 00000000000..4f8e64c8edb --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.kt @@ -0,0 +1,27 @@ +package capturedValues2 + +fun main() { + 1000.foo() +} + +fun Int.foo() { + block { + val b = 1 + val b2 = 2 + block("x") foo@ { + //Breakpoint! + this@foo + b + } + } +} + +fun block(block: () -> Unit) { + block() +} + +fun block(obj: T, block: T.() -> Unit) { + obj.block() +} + +// SHOW_KOTLIN_VARIABLES +// PRINT_FRAME \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.out b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.out new file mode 100644 index 00000000000..3c15f1f5154 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.out @@ -0,0 +1,13 @@ +LineBreakpoint created at capturedValues2.kt:13 +Run Java +Connected to the target VM +capturedValues2.kt:13 + frame = invoke:13, CapturedValues2Kt$foo$1$1 {capturedValues2} + unknown = b = 1 + local = this: java.lang.String = x (sp = null) + field = value: char[] = {char[1]@uniqueID} (sp = String.!EXT!) + element = 0 = 'x' 120 + field = hash: int = 0 (sp = String.!EXT!) +Disconnected from the target VM + +Process finished with exit code 0 diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java index 93c9bfa7718..80f19a6dac3 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java @@ -685,6 +685,16 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); } + @TestMetadata("capturedValues1.kt") + public void testCapturedValues1() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues1.kt"); + } + + @TestMetadata("capturedValues2.kt") + public void testCapturedValues2() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/capturedValues2.kt"); + } + @TestMetadata("catchVariable.kt") public void testCatchVariable() throws Exception { runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/catchVariable.kt");