Debugger: Support parameters of inlined lambda in evaluate expression

This commit is contained in:
Natalia Ukhorskaya
2015-08-25 13:52:15 +03:00
parent 32bf7d9520
commit 19e8c9edb0
12 changed files with 224 additions and 28 deletions
@@ -17,6 +17,7 @@
package org.jetbrains.kotlin.psi package org.jetbrains.kotlin.psi
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.* import com.intellij.psi.*
import com.intellij.psi.impl.PsiManagerEx import com.intellij.psi.impl.PsiManagerEx
import com.intellij.psi.impl.source.tree.FileElement import com.intellij.psi.impl.source.tree.FileElement
@@ -24,14 +25,9 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.tree.IElementType import com.intellij.psi.tree.IElementType
import com.intellij.testFramework.LightVirtualFile import com.intellij.testFramework.LightVirtualFile
import org.jetbrains.kotlin.idea.JetFileType import org.jetbrains.kotlin.idea.JetFileType
import java.util.HashSet
import com.intellij.openapi.util.Key
import com.intellij.psi.impl.PsiModificationTrackerImpl
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.types.JetType import org.jetbrains.kotlin.types.JetType
import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.kotlin.utils.addToStdlib.check
import java.util.LinkedHashSet import java.util.LinkedHashSet
import java.util.regex.Pattern
public abstract class JetCodeFragment( public abstract class JetCodeFragment(
private val _project: Project, private val _project: Project,
@@ -44,6 +40,9 @@ public abstract class JetCodeFragment(
private var viewProvider = super<JetFile>.getViewProvider() as SingleRootFileViewProvider private var viewProvider = super<JetFile>.getViewProvider() as SingleRootFileViewProvider
private var myImports = LinkedHashSet<String>() private var myImports = LinkedHashSet<String>()
private val additionalContextForLambda: PsiElement? by lazy {
this.getCopyableUserData(ADDITIONAL_CONTEXT_FOR_LAMBDA)?.invoke()
}
init { init {
getViewProvider().forceCachedPsi(this) getViewProvider().forceCachedPsi(this)
@@ -70,7 +69,7 @@ public abstract class JetCodeFragment(
override fun isValid() = true override fun isValid() = true
override fun getContext() = context override fun getContext() = additionalContextForLambda ?: context
override fun getResolveScope() = context?.getResolveScope() ?: super<JetFile>.getResolveScope() override fun getResolveScope() = context?.getResolveScope() ?: super<JetFile>.getResolveScope()
@@ -146,5 +145,6 @@ public abstract class JetCodeFragment(
companion object { companion object {
public val IMPORT_SEPARATOR: String = "," public val IMPORT_SEPARATOR: String = ","
public val RUNTIME_TYPE_EVALUATOR: Key<Function1<JetExpression, JetType?>> = Key.create("RUNTIME_TYPE_EVALUATOR") public val RUNTIME_TYPE_EVALUATOR: Key<Function1<JetExpression, JetType?>> = Key.create("RUNTIME_TYPE_EVALUATOR")
public val ADDITIONAL_CONTEXT_FOR_LAMBDA: Key<Function0<JetElement?>> = Key.create("ADDITIONAL_CONTEXT_FOR_LAMBDA")
} }
} }
@@ -20,6 +20,10 @@ import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory import com.intellij.debugger.engine.evaluation.CodeFragmentFactory
import com.intellij.debugger.engine.evaluation.CodeFragmentKind import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.TextWithImports import com.intellij.debugger.engine.evaluation.TextWithImports
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@@ -32,20 +36,34 @@ import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.XDebugSessionImpl import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.intellij.xdebugger.impl.ui.tree.ValueMarkup import com.intellij.xdebugger.impl.ui.tree.ValueMarkup
import com.sun.jdi.ArrayReference import com.sun.jdi.ArrayReference
import com.sun.jdi.ObjectReference
import com.sun.jdi.PrimitiveValue import com.sun.jdi.PrimitiveValue
import com.sun.jdi.Value import com.sun.jdi.Value
import org.jetbrains.annotations.TestOnly
import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.asJava.KotlinLightClass import org.jetbrains.kotlin.asJava.KotlinLightClass
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.idea.JetFileType import org.jetbrains.kotlin.idea.JetFileType
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.refactoring.j2kText import org.jetbrains.kotlin.idea.core.refactoring.j2kText
import org.jetbrains.kotlin.idea.core.refactoring.quoteIfNeeded
import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.application.runReadAction
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.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.inline.InlineUtil
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.JetType import org.jetbrains.kotlin.types.JetType
import org.jetbrains.kotlin.types.typeUtil.makeNullable
import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.kotlin.utils.addToStdlib.check
import java.util.HashMap import java.util.*
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.jvm.java import kotlin.reflect.jvm.java
@@ -106,6 +124,65 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
} }
}) })
if (contextElement != null) {
val lambdas = getInlinedLambdasInside(contextElement)
if (lambdas.isNotEmpty()) {
codeFragment.putCopyableUserData(JetCodeFragment.ADDITIONAL_CONTEXT_FOR_LAMBDA, lamdba@ {
val debuggerContext = DebuggerManagerEx.getInstanceEx(project).context
val semaphore = Semaphore()
semaphore.down()
var visibleVariables: List<Pair<LocalVariableProxyImpl, Value>>? = null
val worker = object : DebuggerCommandImpl() {
override fun action() {
try {
val frameProxy = if (ApplicationManager.getApplication().isUnitTestMode)
context?.getCopyableUserData(DEBUG_FRAME_FOR_TESTS)
else
debuggerContext.frameProxy
visibleVariables = frameProxy?.let { f -> f.visibleVariables().map { it to f.getValue(it) } } ?: emptyList()
}
finally {
semaphore.up()
}
}
}
debuggerContext.debugProcess?.managerThread?.invoke(worker)
for (i in 0..50) {
if (semaphore.waitFor(20)) break
}
fun isLocalVariableForParameterPresent(p: ValueParameterDescriptor): Boolean {
return visibleVariables?.firstOrNull {
if (it.first.name() != p.name.asString()) return@firstOrNull false
val parameterClassDescriptor = p.type.constructor.declarationDescriptor as? ClassDescriptor ?: return@firstOrNull true
val actualClassDescriptor = it.second.asValue().asmType.getClassDescriptor(debuggerContext.project) ?: return@firstOrNull true
return@firstOrNull runReadAction { DescriptorUtils.isSubclass(actualClassDescriptor, parameterClassDescriptor) }
} != null
}
for (lambda in lambdas) {
val function = lambda.analyze(BodyResolveMode.PARTIAL).get(BindingContext.FUNCTION, lambda)
if (function != null && function.valueParameters.all { isLocalVariableForParameterPresent(it) }) {
val fragmentForVisibleVariables = createCodeFragmentForVisibleVariables(lambda.project, visibleVariables!!)
return@lamdba createWrappingContext(
fragmentForVisibleVariables.first,
fragmentForVisibleVariables.second,
lambda.bodyExpression,
lambda.project)
}
}
return@lamdba null
})
}
}
return codeFragment return codeFragment
} }
@@ -131,6 +208,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
companion object { companion object {
public val LABEL_VARIABLE_VALUE_KEY: Key<Value> = Key.create<Value>("_label_variable_value_key_") public val LABEL_VARIABLE_VALUE_KEY: Key<Value> = Key.create<Value>("_label_variable_value_key_")
public val DEBUG_LABEL_SUFFIX: String = "_DebugLabel" public val DEBUG_LABEL_SUFFIX: String = "_DebugLabel"
@TestOnly val DEBUG_FRAME_FOR_TESTS: Key<StackFrameProxyImpl> = Key.create("DEBUG_FRAME_FOR_TESTS")
fun getContextElement(elementAt: PsiElement?): JetElement? { fun getContextElement(elementAt: PsiElement?): JetElement? {
if (elementAt == null) return null if (elementAt == null) return null
@@ -161,6 +239,16 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
private fun JetElement?.check(): Boolean = this != null && this.check { KotlinEditorTextProvider.isAcceptedAsCodeFragmentContext(it) } != null private fun JetElement?.check(): Boolean = this != null && this.check { KotlinEditorTextProvider.isAcceptedAsCodeFragmentContext(it) } != null
private fun getInlinedLambdasInside(contextElement: JetElement): List<JetFunction> {
val start = contextElement.startOffset
val end = contextElement.endOffset
val bindingContext = contextElement.analyze(BodyResolveMode.PARTIAL)
return CodeInsightUtils.findElementsOfClassInRange(contextElement.getContainingJetFile(), start, end, javaClass<JetFunctionLiteral>(), javaClass<JetNamedFunction>())
.filterIsInstance<JetFunction>()
.filter { JetPsiUtil.getParentCallIfPresent(it as JetExpression) != null && InlineUtil.isInlinedArgument(it, bindingContext, false) }
}
//internal for tests //internal for tests
fun createCodeFragmentForLabeledObjects(project: Project, markupMap: Map<*, ValueMarkup>): Pair<String, Map<String, Value>> { fun createCodeFragmentForLabeledObjects(project: Project, markupMap: Map<*, ValueMarkup>): Pair<String, Map<String, Value>> {
val sb = StringBuilder() val sb = StringBuilder()
@@ -172,7 +260,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
val objectRef = value as? Value ?: continue val objectRef = value as? Value ?: continue
val labelNameWithSuffix = "$labelName$DEBUG_LABEL_SUFFIX" val labelNameWithSuffix = "$labelName$DEBUG_LABEL_SUFFIX"
sb.append("${createKotlinProperty(project, labelNameWithSuffix, objectRef.type().name(), TypeKind.getTypeKind(objectRef))}\n") sb.append("${createKotlinProperty(project, labelNameWithSuffix, objectRef.type().name(), objectRef)}\n")
labeledObjects.put(labelNameWithSuffix, objectRef) labeledObjects.put(labelNameWithSuffix, objectRef)
} }
@@ -180,28 +268,34 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return sb.toString() to labeledObjects return sb.toString() to labeledObjects
} }
private enum class TypeKind { private fun createCodeFragmentForVisibleVariables(project: Project, visibleVariables: List<Pair<LocalVariableProxyImpl, Value>>): Pair<String, Map<String, Value>> {
PRIMITIVE, val sb = StringBuilder()
ARRAY, val labeledObjects = HashMap<String, Value>()
OBJECT; for ((variable, value) in visibleVariables) {
val variableName = variable.name()
if (!Name.isValidIdentifier(variableName)) continue
companion object { val kotlinProperty = createKotlinProperty(project, variableName, variable.typeName(), value) ?: continue
fun getTypeKind(value: Value): TypeKind { sb.append("$kotlinProperty\n")
return when(value) {
is ArrayReference -> ARRAY labeledObjects.put(variableName, value)
is PrimitiveValue -> PRIMITIVE
else -> OBJECT
}
}
} }
sb.append("val _debug_context_val = 1")
return sb.toString() to labeledObjects
} }
private fun createKotlinProperty(project: Project, variableName: String, variableTypeName: String, typeKind: TypeKind): String? { private fun createKotlinProperty(project: Project, variableName: String, variableTypeName: String, value: Value): String? {
fun String.addArraySuffix() = if (typeKind == TypeKind.ARRAY) this + "[]" else this val actualClassDescriptor = value.asValue().asmType.getClassDescriptor(project)
if (actualClassDescriptor != null && actualClassDescriptor.defaultType.arguments.isEmpty()) {
val renderedType = IdeDescriptorRenderers.SOURCE_CODE.renderType(actualClassDescriptor.defaultType.makeNullable())
return "val ${variableName.quoteIfNeeded()}: $renderedType = null"
}
fun String.addArraySuffix() = if (value is ArrayReference) this + "[]" else this
val className = variableTypeName.replace("$", ".").substringBefore("[]") val className = variableTypeName.replace("$", ".").substringBefore("[]")
val classType = PsiType.getTypeByName(className, project, GlobalSearchScope.allScope(project)) val classType = PsiType.getTypeByName(className, project, GlobalSearchScope.allScope(project))
val type = (if (typeKind != TypeKind.PRIMITIVE && classType.resolve() == null) val type = (if (value !is PrimitiveValue && classType.resolve() == null)
CommonClassNames.JAVA_LANG_OBJECT CommonClassNames.JAVA_LANG_OBJECT
else else
className).addArraySuffix() className).addArraySuffix()
@@ -3,6 +3,7 @@ LineBreakpoint created at inlineFunctionalExpression.kt:9
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
inlineFunctionalExpression.kt:9 inlineFunctionalExpression.kt:9
inlineFunctionalExpression.kt:9 inlineFunctionalExpression.kt:9
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0 Process finished with exit code 0
+1
View File
@@ -3,6 +3,7 @@ LineBreakpoint created at inlineLambda.kt:9
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
inlineLambda.kt:9 inlineLambda.kt:9
inlineLambda.kt:9 inlineLambda.kt:9
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0 Process finished with exit code 0
@@ -0,0 +1,14 @@
LineBreakpoint created at parametersOfInlineFun.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 !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! parametersOfInlineFun.ParametersOfInlineFunPackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
parametersOfInlineFun.kt:8
parametersOfInlineFun.kt:8
Compile bytecode for primitive
Compile bytecode for it
Compile bytecode for array
Compile bytecode for str
Compile bytecode for list
Compile bytecode for `$receiver`.prop
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -0,0 +1,11 @@
LineBreakpoint created at parametersOfInlineFunSeveralOnLine.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 !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! parametersOfInlineFunSeveralOnLine.ParametersOfInlineFunSeveralOnLinePackage
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
parametersOfInlineFunSeveralOnLine.kt:8
parametersOfInlineFunSeveralOnLine.kt:8
parametersOfInlineFunSeveralOnLine.kt:8
parametersOfInlineFunSeveralOnLine.kt:8
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
Process finished with exit code 0
@@ -3,8 +3,8 @@ package inlineFunctionalExpression
fun main(args: Array<String>) { fun main(args: Array<String>) {
val a = array(1) val a = array(1)
// EXPRESSION: it // EXPRESSION: it
// RESULT: Unresolved reference: it // RESULT: 1: I
// STEP_INTO: 1 // RESUME: 1
//Breakpoint! //Breakpoint!
a.map(fun (it): Int { return it * 1 }) a.map(fun (it): Int { return it * 1 })
} }
@@ -3,8 +3,8 @@ package inlineLambda
fun main(args: Array<String>) { fun main(args: Array<String>) {
val a = array(1) val a = array(1)
// EXPRESSION: it // EXPRESSION: it
// RESULT: Unresolved reference: it // RESULT: 1: I
// STEP_INTO: 1 // RESUME: 1
//Breakpoint! //Breakpoint!
a.map { it * 1 } a.map { it * 1 }
} }
@@ -0,0 +1,37 @@
// Check that evaluate expression works inside inline function
package parametersOfInlineFun
fun main(args: Array<String>) {
val a = A(1)
// RESUME: 1
//Breakpoint!
a.foo { 1 + 1 }
}
inline fun A.foo(f: (i: Int) -> Unit) {
val primitive = 1
val array = arrayOf(1)
val str = "str"
val list = listOf("str")
f(1)
}
class A(val prop: Int)
// EXPRESSION: primitive
// RESULT: 1: I
// EXPRESSION: it
// RESULT: 1: I
// EXPRESSION: array
// RESULT: instance of java.lang.Integer[1] (id=ID): [Ljava/lang/Integer;
// EXPRESSION: str
// RESULT: "str": Ljava/lang/String;
// EXPRESSION: list
// RESULT: instance of java.util.Collections$SingletonList(id=ID): Ljava/util/Collections$SingletonList;
// EXPRESSION: `$receiver`.prop
// RESULT: 1: I
@@ -0,0 +1,24 @@
// Check that evaluate expression works inside inline function
package parametersOfInlineFunSeveralOnLine
fun main(args: Array<String>) {
val a = A()
// RESUME: 3
//Breakpoint!
a.foo { 1 + 1 }.bar { 1 + 1 }
}
inline fun A.foo(f: (i: Int) -> Unit): A {
f(1)
return this
}
inline fun A.bar(f: (s: String) -> Unit): A {
f("str")
return this
}
class A()
// EXPRESSION: it
// RESULT: "str": Ljava/lang/String;
@@ -311,6 +311,8 @@ public abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestB
val sourcePosition = ContextUtil.getSourcePosition(this) val sourcePosition = ContextUtil.getSourcePosition(this)
val contextElement = createContextElement(this) val contextElement = createContextElement(this)
contextElement.putCopyableUserData(KotlinCodeFragmentFactory.DEBUG_FRAME_FOR_TESTS, evaluationContext?.frameProxy)
try { try {
val evaluator = val evaluator =
@@ -211,6 +211,18 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
doSingleBreakpointTest(fileName); doSingleBreakpointTest(fileName);
} }
@TestMetadata("parametersOfInlineFun.kt")
public void testParametersOfInlineFun() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/parametersOfInlineFun.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("parametersOfInlineFunSeveralOnLine.kt")
public void testParametersOfInlineFunSeveralOnLine() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/parametersOfInlineFunSeveralOnLine.kt");
doSingleBreakpointTest(fileName);
}
@TestMetadata("privateMember.kt") @TestMetadata("privateMember.kt")
public void testPrivateMember() throws Exception { public void testPrivateMember() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/privateMember.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/privateMember.kt");