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
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.*
import com.intellij.psi.impl.PsiManagerEx
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.testFramework.LightVirtualFile
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.utils.addToStdlib.check
import java.util.LinkedHashSet
import java.util.regex.Pattern
public abstract class JetCodeFragment(
private val _project: Project,
@@ -44,6 +40,9 @@ public abstract class JetCodeFragment(
private var viewProvider = super<JetFile>.getViewProvider() as SingleRootFileViewProvider
private var myImports = LinkedHashSet<String>()
private val additionalContextForLambda: PsiElement? by lazy {
this.getCopyableUserData(ADDITIONAL_CONTEXT_FOR_LAMBDA)?.invoke()
}
init {
getViewProvider().forceCachedPsi(this)
@@ -70,7 +69,7 @@ public abstract class JetCodeFragment(
override fun isValid() = true
override fun getContext() = context
override fun getContext() = additionalContextForLambda ?: context
override fun getResolveScope() = context?.getResolveScope() ?: super<JetFile>.getResolveScope()
@@ -146,5 +145,6 @@ public abstract class JetCodeFragment(
companion object {
public val IMPORT_SEPARATOR: String = ","
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.CodeFragmentKind
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.progress.ProgressManager
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.ui.tree.ValueMarkup
import com.sun.jdi.ArrayReference
import com.sun.jdi.ObjectReference
import com.sun.jdi.PrimitiveValue
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.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
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.core.refactoring.j2kText
import org.jetbrains.kotlin.idea.core.refactoring.quoteIfNeeded
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.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
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.typeUtil.makeNullable
import org.jetbrains.kotlin.utils.addToStdlib.check
import java.util.HashMap
import java.util.*
import java.util.concurrent.atomic.AtomicReference
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
}
@@ -131,6 +208,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
companion object {
public val LABEL_VARIABLE_VALUE_KEY: Key<Value> = Key.create<Value>("_label_variable_value_key_")
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? {
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 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
fun createCodeFragmentForLabeledObjects(project: Project, markupMap: Map<*, ValueMarkup>): Pair<String, Map<String, Value>> {
val sb = StringBuilder()
@@ -172,7 +260,7 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
val objectRef = value as? Value ?: continue
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)
}
@@ -180,28 +268,34 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() {
return sb.toString() to labeledObjects
}
private enum class TypeKind {
PRIMITIVE,
ARRAY,
OBJECT;
private fun createCodeFragmentForVisibleVariables(project: Project, visibleVariables: List<Pair<LocalVariableProxyImpl, Value>>): Pair<String, Map<String, Value>> {
val sb = StringBuilder()
val labeledObjects = HashMap<String, Value>()
for ((variable, value) in visibleVariables) {
val variableName = variable.name()
if (!Name.isValidIdentifier(variableName)) continue
companion object {
fun getTypeKind(value: Value): TypeKind {
return when(value) {
is ArrayReference -> ARRAY
is PrimitiveValue -> PRIMITIVE
else -> OBJECT
}
}
val kotlinProperty = createKotlinProperty(project, variableName, variable.typeName(), value) ?: continue
sb.append("$kotlinProperty\n")
labeledObjects.put(variableName, value)
}
sb.append("val _debug_context_val = 1")
return sb.toString() to labeledObjects
}
private fun createKotlinProperty(project: Project, variableName: String, variableTypeName: String, typeKind: TypeKind): String? {
fun String.addArraySuffix() = if (typeKind == TypeKind.ARRAY) this + "[]" else this
private fun createKotlinProperty(project: Project, variableName: String, variableTypeName: String, value: Value): String? {
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 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
else
className).addArraySuffix()
@@ -3,6 +3,7 @@ LineBreakpoint created at inlineFunctionalExpression.kt:9
Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
inlineFunctionalExpression.kt:9
inlineFunctionalExpression.kt:9
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
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'
inlineLambda.kt:9
inlineLambda.kt:9
Compile bytecode for it
Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket'
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>) {
val a = array(1)
// EXPRESSION: it
// RESULT: Unresolved reference: it
// STEP_INTO: 1
// RESULT: 1: I
// RESUME: 1
//Breakpoint!
a.map(fun (it): Int { return it * 1 })
}
@@ -3,8 +3,8 @@ package inlineLambda
fun main(args: Array<String>) {
val a = array(1)
// EXPRESSION: it
// RESULT: Unresolved reference: it
// STEP_INTO: 1
// RESULT: 1: I
// RESUME: 1
//Breakpoint!
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 contextElement = createContextElement(this)
contextElement.putCopyableUserData(KotlinCodeFragmentFactory.DEBUG_FRAME_FOR_TESTS, evaluationContext?.frameProxy)
try {
val evaluator =
@@ -211,6 +211,18 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
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")
public void testPrivateMember() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/privateMember.kt");