Support labels evaluation in debugger

This commit is contained in:
Natalia Ukhorskaya
2015-06-17 12:19:26 +03:00
parent 0f108eee2f
commit 8bb33f0df5
12 changed files with 283 additions and 58 deletions
@@ -16,34 +16,51 @@
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.psi.PsiElement
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaCodeFragment
import org.jetbrains.kotlin.idea.JetFileType
import org.jetbrains.kotlin.psi.JetExpressionCodeFragment
import com.intellij.psi.PsiCodeBlock
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.JetExpression
import org.jetbrains.kotlin.psi.JetBlockCodeFragment
import org.jetbrains.kotlin.asJava.KotlinLightClass
import com.intellij.debugger.DebuggerManagerEx
import org.jetbrains.kotlin.psi.JetCodeFragment
import org.jetbrains.kotlin.types.JetType
import com.intellij.util.concurrency.Semaphore
import java.util.concurrent.atomic.AtomicReference
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.JavaCodeFragment
import com.intellij.psi.PsiCodeBlock
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.concurrency.Semaphore
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
import com.sun.jdi.ObjectReference
import com.sun.jdi.Value
import org.jetbrains.kotlin.asJava.KotlinLightClass
import org.jetbrains.kotlin.idea.JetFileType
import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.types.JetType
import java.util.HashMap
import java.util.concurrent.atomic.AtomicReference
class KotlinCodeFragmentFactory: CodeFragmentFactory() {
override fun createCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
val codeFragment = if (item.getKind() == CodeFragmentKind.EXPRESSION) {
JetExpressionCodeFragment(project, "fragment.kt", item.getText(), item.getImports(), getContextElement(context))
JetExpressionCodeFragment(
project,
"fragment.kt",
item.getText(),
item.getImports(),
getWrappedContextElement(project, context)
)
}
else {
JetBlockCodeFragment(project, "fragment.kt", item.getText(), item.getImports(), getContextElement(context))
JetBlockCodeFragment(
project,
"fragment.kt",
item.getText(),
item.getImports(),
getWrappedContextElement(project, context)
)
}
codeFragment.putCopyableUserData(JetCodeFragment.RUNTIME_TYPE_EVALUATOR, {
@@ -79,6 +96,9 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return codeFragment
}
private fun getWrappedContextElement(project: Project, context: PsiElement?)
= wrapContextIfNeeded(project, getContextElement(context))
override fun createPresentationCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment {
return createCodeFragment(item, context, project)
}
@@ -96,6 +116,9 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
override fun getEvaluatorBuilder() = KotlinEvaluationBuilder
companion object {
public val LABEL_VARIABLE_VALUE_KEY: Key<Value> = Key.create<Value>("_label_variable_value_key_")
public val DEBUG_LABEL_SUFFIX: String = "_DebugLabel"
fun getContextElement(elementAt: PsiElement?): PsiElement? {
if (elementAt == null) return null
@@ -107,16 +130,65 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return getContextElement(elementAt.getOrigin())
}
// If label for some variable is set
if (elementAt.getParent() is JetCodeFragment) {
return elementAt.getParent().getContext()
}
val expressionAtOffset = PsiTreeUtil.findElementOfClassAtOffset(elementAt.getContainingFile()!!, elementAt.getTextOffset(), javaClass<JetExpression>(), false)
if (expressionAtOffset != null) {
return expressionAtOffset
}
return KotlinEditorTextProvider.findExpressionInner(elementAt, true)
}
//internal for tests
fun createCodeFragmentForLabeledObjects(markupMap: Map<*, ValueMarkup>): Pair<String, Map<String, ObjectReference>> {
val sb = StringBuilder()
val labeledObjects = HashMap<String, ObjectReference>()
for ((value, markup) in markupMap.entrySet()) {
val labelName = markup.getText()
if (!Name.isValidIdentifier(labelName)) continue
val objectRef = value as ObjectReference
val typeName = value.type().name()
val labelNameWithSuffix = labelName + DEBUG_LABEL_SUFFIX
sb.append("val ").append(labelNameWithSuffix).append(": ").append(typeName).append("? = null\n")
labeledObjects.put(labelNameWithSuffix, objectRef)
}
sb.append("val _debug_context_val = 1")
return sb.toString() to labeledObjects
}
}
}
private fun wrapContextIfNeeded(project: Project, originalContext: PsiElement?): PsiElement? {
val session = XDebuggerManager.getInstance(project).getCurrentSession() as? XDebugSessionImpl
?: return originalContext
val markupMap = session.getValueMarkers()?.getAllMarkers()
if (markupMap == null || markupMap.isEmpty()) return originalContext
val (text, labels) = createCodeFragmentForLabeledObjects(markupMap)
if (text.isEmpty()) return originalContext
return createWrappingContext(text, labels, originalContext, project)
}
// internal for test
fun createWrappingContext(
newFragmentText: String,
labels: Map<String, ObjectReference>,
originalContext: PsiElement?,
project: Project
): PsiElement? {
val codeFragment = JetPsiFactory(project).createBlockCodeFragment(newFragmentText, originalContext)
codeFragment.accept(object : JetTreeVisitorVoid() {
override fun visitProperty(property: JetProperty) {
val reference = labels.get(property.getName())
if (reference != null) {
property.putUserData(LABEL_VARIABLE_VALUE_KEY, reference)
}
}
})
return getContextElement(codeFragment.findElementAt(codeFragment.getText().length() - 1))
}
}
@@ -44,6 +44,7 @@ import com.sun.jdi.request.EventRequest
import org.jetbrains.eval4j.*
import org.jetbrains.eval4j.jdi.JDIEval
import org.jetbrains.eval4j.jdi.asJdiValue
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.eval4j.jdi.makeInitialFrame
import org.jetbrains.kotlin.codegen.*
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
@@ -80,7 +81,7 @@ import org.jetbrains.kotlin.types.Flexibility
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.Opcodes.ASM5
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import java.util.Collections
import java.util.*
private val RECEIVER_NAME = "\$receiver"
private val THIS_NAME = "this"
@@ -176,7 +177,7 @@ class KotlinEvaluator(val codeFragment: JetCodeFragment,
if (extractionResult == null) {
throw IllegalStateException("Code fragment cannot be extracted to function")
}
val parametersDescriptor = extractionResult.getParametersForDebugger()
val parametersDescriptor = extractionResult.getParametersForDebugger(codeFragment)
val extractedFunction = extractionResult.declaration as JetNamedFunction
val classFileFactory = createClassFileFactory(codeFragment, extractedFunction, context, parametersDescriptor)
@@ -287,8 +288,22 @@ class KotlinEvaluator(val codeFragment: JetCodeFragment,
return jdiValue.asJdiValue(vm, jdiValue.asmType)
}
private fun ExtractionResult.getParametersForDebugger(): ParametersDescriptor {
private fun ExtractionResult.getParametersForDebugger(fragment: JetCodeFragment): ParametersDescriptor {
return runReadAction {
val valuesForLabels = HashMap<String, Value>()
val contextElementFile = fragment.getContext()?.getContainingFile()
if (contextElementFile is JetCodeFragment) {
contextElementFile.accept(object: JetTreeVisitorVoid() {
override fun visitProperty(property: JetProperty) {
val value = property.getUserData(KotlinCodeFragmentFactory.LABEL_VARIABLE_VALUE_KEY)
if (value != null) {
valuesForLabels.put(property.getName(), value.asValue())
}
}
})
}
val parameters = ParametersDescriptor()
val receiver = config.descriptor.receiverParameter
if (receiver != null) {
@@ -301,7 +316,7 @@ class KotlinEvaluator(val codeFragment: JetCodeFragment,
param.argumentText.startsWith("::") -> param.argumentText.substring(2)
else -> param.argumentText
}
parameters.add(paramName, param.getParameterType(true))
parameters.add(paramName, param.getParameterType(true), valuesForLabels[paramName])
}
parameters
}
@@ -310,7 +325,12 @@ class KotlinEvaluator(val codeFragment: JetCodeFragment,
private fun EvaluationContextImpl.getArgumentsForEval4j(parameters: ParametersDescriptor, parameterTypes: Array<Type>): List<Value> {
val frameVisitor = FrameVisitor(this)
return parameters.zip(parameterTypes).map {
frameVisitor.findValue(it.first.callText, it.second, checkType = false, failIfNotFound = true)!!
if (it.first.value != null) {
it.first.value!!
}
else {
frameVisitor.findValue(it.first.callText, it.second, checkType = false, failIfNotFound = true)!!
}
}
}
@@ -153,6 +153,11 @@ private fun getExpressionToAddDebugExpressionBefore(tmpFile: JetFile, contextEle
return CodeInsightUtils.getTopmostElementAtOffset(elementAtOffset, lineStart) ?: elementAtOffset
}
val containingFile = contextElement.getContainingFile()
if (containingFile is JetCodeFragment) {
return getExpressionToAddDebugExpressionBefore(tmpFile, containingFile.getContext(), line)
}
fun shouldStop(el: PsiElement?, p: PsiElement?) = p is JetBlockExpression || el is JetDeclaration
var elementAt = tmpFile.getElementInCopy(contextElement)
@@ -239,28 +244,25 @@ private fun addDebugExpressionBeforeContextElement(codeFragment: JetCodeFragment
parent.addBefore(psiFactory.createNewLine(), elementBefore)
val debugExpression = codeFragment.getContentElement() ?: return emptyList()
val expressions = ArrayList<JetExpression>()
fun insertExpression(expr: JetExpression) {
val newDebugExpression = parent.addBefore(expr, elementBefore) ?: return
expressions.add(newDebugExpression as JetExpression)
parent.addBefore(psiFactory.createNewLine(), elementBefore)
}
when (debugExpression) {
is JetBlockExpression -> {
for (statement in debugExpression.getStatements()) {
insertExpression(statement)
fun insertExpression(expr: JetElement?): List<JetExpression> {
when (expr) {
is JetBlockExpression -> return expr.getStatements().flatMap { insertExpression(it) }
is JetExpression -> {
val newDebugExpression = parent.addBefore(expr, elementBefore) ?: return emptyList()
parent.addBefore(psiFactory.createNewLine(), elementBefore)
return listOf(newDebugExpression as JetExpression)
}
}
is JetExpression -> insertExpression(debugExpression)
return emptyList()
}
return expressions
val containingFile = codeFragment.getContext()?.getContainingFile()
if (containingFile is JetCodeFragment) {
insertExpression(containingFile.getContentElement() as? JetExpression)
}
val debugExpression = codeFragment.getContentElement() ?: return emptyList()
return insertExpression(debugExpression)
}
private fun replaceByRunFunction(expression: JetExpression): JetCallExpression {
@@ -0,0 +1,8 @@
LineBreakpoint created at lCallOnLabeledObj.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! lCallOnLabeledObj.LCallOnLabeledObjPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
lCallOnLabeledObj.kt:6
Compile bytecode for myClass_DebugLabel.foo()
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+10
View File
@@ -0,0 +1,10 @@
LineBreakpoint created at lSeveralLabels.kt:8
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! lSeveralLabels.LSeveralLabelsPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
lSeveralLabels.kt:8
Compile bytecode for a_DebugLabel
Compile bytecode for b_DebugLabel
Compile bytecode for c_DebugLabel
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
+9
View File
@@ -0,0 +1,9 @@
LineBreakpoint created at lSimple.kt:6
!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !APP_PATH!\classes;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! lSimple.LSimplePackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
lSimple.kt:6
Compile bytecode for a
Compile bytecode for aLabel_DebugLabel
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,16 @@
package lCallOnLabeledObj
fun main(args: Array<String>) {
val myClass = MyClass()
//Breakpoint!
val a = 1
}
class MyClass {
fun foo() = 1
}
// DEBUG_LABEL: myClass = myClass
// EXPRESSION: myClass_DebugLabel.foo()
// RESULT: 1: I
@@ -0,0 +1,22 @@
package lSeveralLabels
fun main(args: Array<String>) {
val a = "str1"
val b = "str2"
val c = "str3"
//Breakpoint!
val d = 1
}
// DEBUG_LABEL: a = a
// DEBUG_LABEL: b = b
// DEBUG_LABEL: c = c
// EXPRESSION: a_DebugLabel
// RESULT: "str1": Ljava/lang/String;
// EXPRESSION: b_DebugLabel
// RESULT: "str2": Ljava/lang/String;
// EXPRESSION: c_DebugLabel
// RESULT: "str3": Ljava/lang/String;
@@ -0,0 +1,15 @@
package lSimple
fun main(args: Array<String>) {
val a = "str"
//Breakpoint!
val b = 1
}
// DEBUG_LABEL: a = aLabel
// EXPRESSION: a
// RESULT: "str": Ljava/lang/String;
// EXPRESSION: aLabel_DebugLabel
// RESULT: "str": Ljava/lang/String;
@@ -16,8 +16,6 @@
package org.jetbrains.kotlin.idea.debugger.evaluate
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.ContextUtil
import com.intellij.debugger.engine.SourcePositionProvider
@@ -35,13 +33,11 @@ import com.intellij.debugger.ui.tree.LocalVariableDescriptor
import com.intellij.debugger.ui.tree.StackFrameDescriptor
import com.intellij.debugger.ui.tree.StaticDescriptor
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.PsiElement
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
import com.sun.jdi.ObjectReference
import org.apache.log4j.AppenderSkeleton
import org.apache.log4j.Level
@@ -58,6 +54,7 @@ import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.junit.Assert
import java.io.File
import java.util.Collections
import kotlin.test.fail
public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
private val logger = Logger.getLogger(javaClass<KotlinEvaluateExpressionCache>())!!
@@ -278,12 +275,39 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestB
return mainFile.getParentFile()?.listFiles()?.filter { it.name.startsWith(mainFileName) && it.name != mainFileName } ?: Collections.emptyList()
}
private fun createContextElement(context: SuspendContextImpl): PsiElement {
val contextElement = ContextUtil.getContextElement(debuggerContext)
Assert.assertTrue("KotlinCodeFragmentFactory should be accepted for context element otherwise default evaluator will be called. ContextElement = ${contextElement.getText()}",
KotlinCodeFragmentFactory().isContextAccepted(contextElement))
if (contextElement != null) {
val labelsAsText = InTextDirectivesUtils.findLinesWithPrefixesRemoved(contextElement.getContainingFile().getText(), "// DEBUG_LABEL: ")
if (labelsAsText.isEmpty()) return contextElement
val markupMap = hashMapOf<ObjectReference, ValueMarkup>()
for (labelAsText in labelsAsText) {
val labelParts = labelAsText.splitBy("=")
assert(labelParts.size() == 2) { "Wrong format for DEBUG_LABEL directive: // DEBUG_LABEL: {localVariableName} = {labelText}"}
val localVariableName = labelParts[0].trim()
val labelName = labelParts[1].trim()
val localVariable = context.getFrameProxy().visibleVariableByName(localVariableName)
assert(localVariable != null) { "Couldn't find localVariable for label: name = $localVariableName" }
val localVariableValue = context.getFrameProxy().getValue(localVariable) as? ObjectReference
assert(localVariableValue != null) { "Local variable $localVariableName should be an ObjectReference" }
markupMap.put(localVariableValue, ValueMarkup(labelName, null, labelName))
}
val (text, labels) = KotlinCodeFragmentFactory.createCodeFragmentForLabeledObjects(markupMap)
return KotlinCodeFragmentFactory().createWrappingContext(text, labels, KotlinCodeFragmentFactory.getContextElement(contextElement), getProject())!!
}
return contextElement!!
}
private fun SuspendContextImpl.evaluate(text: String, codeFragmentKind: CodeFragmentKind, expectedResult: String) {
runReadAction {
val sourcePosition = ContextUtil.getSourcePosition(this)
val contextElement = ContextUtil.getContextElement(sourcePosition)!!
Assert.assertTrue("KotlinCodeFragmentFactory should be accepted for context element otherwise default evaluator will be called. ContextElement = ${contextElement.getText()}",
KotlinCodeFragmentFactory().isContextAccepted(contextElement))
val contextElement = createContextElement(this)
try {
@@ -526,6 +526,33 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
}
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Labels extends AbstractKotlinEvaluateExpressionTest {
public void testAllFilesPresentInLabels() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("lCallOnLabeledObj.kt")
public void testLCallOnLabeledObj() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels/lCallOnLabeledObj.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("lSeveralLabels.kt")
public void testLSeveralLabels() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels/lSeveralLabels.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("lSimple.kt")
public void testLSimple() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/labels/lSimple.kt");
doSingleBreakpointTest(fileName);
}
}
@TestMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/lambdas")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)