Debugger: Support parameters of inlined lambda in evaluate expression
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+112
-18
@@ -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
|
||||
|
||||
@@ -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
|
||||
Vendored
+2
-2
@@ -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 })
|
||||
}
|
||||
|
||||
+2
-2
@@ -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 }
|
||||
}
|
||||
|
||||
+37
@@ -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
|
||||
Vendored
+24
@@ -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;
|
||||
+2
@@ -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 =
|
||||
|
||||
+12
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user