Provide access to 'coroutineContext' inside suspend functions/lambdas (KT-24829)

This commit is contained in:
Yan Zhulanow
2018-12-13 19:37:08 +09:00
parent cf925fc2ad
commit 227a7e5609
8 changed files with 145 additions and 1 deletions
@@ -12,8 +12,10 @@ import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.diagnostics.DiagnosticSink
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtThisExpression
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.calls.callUtil.isCallableReference
@@ -74,7 +76,20 @@ object CoroutineSuspendCallChecker : CallChecker {
val callElement = resolvedCall.call.callElement as KtExpression
if (!InlineUtil.checkNonLocalReturnUsage(enclosingSuspendFunction, callElement, context.resolutionContext)) {
context.trace.report(Errors.NON_LOCAL_SUSPENSION_POINT.on(reportOn))
var shouldReport = true
// Do not report for KtCodeFragment in a suspend function context
val containingFile = callElement.containingFile
if (containingFile is KtCodeFragment) {
val c = containingFile.context?.getParentOfType<KtExpression>(false)
if (c != null && InlineUtil.checkNonLocalReturnUsage(enclosingSuspendFunction, c, context.resolutionContext)) {
shouldReport = false
}
}
if (shouldReport) {
context.trace.report(Errors.NON_LOCAL_SUSPENSION_POINT.on(reportOn))
}
} else if (context.scope.parentsWithSelf.any { it.isScopeForDefaultParameterValuesOf(enclosingSuspendFunction) }) {
context.trace.report(Errors.UNSUPPORTED.on(reportOn, "suspend function calls in a context of default parameter value"))
}
@@ -21,16 +21,26 @@ import org.jetbrains.kotlin.codegen.topLevelClassAsmType
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.attachment.mergeAttachments
import org.jetbrains.kotlin.resolve.calls.checkers.COROUTINE_CONTEXT_1_3_FQ_NAME
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_ARGUMENT
import org.jetbrains.kotlin.load.java.JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType
import kotlin.coroutines.Continuation
import org.jetbrains.org.objectweb.asm.Type as AsmType
import com.sun.jdi.Type as JdiType
class VariableFinder private constructor(private val context: EvaluationContextImpl, private val frameProxy: StackFrameProxyImpl) {
companion object {
private val COROUTINE_CONTEXT_SIMPLE_NAME = COROUTINE_CONTEXT_1_3_FQ_NAME.shortName().asString()
private val CONTINUATION_TYPE = AsmType.getType(Continuation::class.java)
val SUSPEND_LAMBDA_CLASSES = listOf(
"kotlin.coroutines.jvm.internal.SuspendLambda",
"kotlin.coroutines.jvm.internal.RestrictedSuspendLambda"
)
fun instance(context: EvaluationContextImpl): VariableFinder? {
val frameProxy = context.frameProxy ?: return null
return VariableFinder(context, frameProxy)
@@ -248,9 +258,60 @@ class VariableFinder private constructor(private val context: EvaluationContextI
.firstOrNull()
?.let { return Result(it.value()) }
if (name == COROUTINE_CONTEXT_SIMPLE_NAME) {
val coroutineContext = findCoroutineContext()?.takeIf { kind.typeMatches(it.type()) }
if (coroutineContext != null) {
return Result(coroutineContext)
}
}
return null
}
private fun findCoroutineContext(): ObjectReference? {
val method = frameProxy.location().safeMethod() ?: return null
return findCoroutineContextForLambda(method) ?: findCoroutineContextForMethod(method)
}
private fun findCoroutineContextForLambda(method: Method): ObjectReference? {
if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") {
return null
}
val thisObject = frameProxy.thisObject() ?: return null
val thisType = thisObject.referenceType()
if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) {
return null
}
return findCoroutineContextForContinuation(thisObject)
}
private fun findCoroutineContextForMethod(method: Method): ObjectReference? {
if (CONTINUATION_TYPE.descriptor + ")" !in method.signature()) {
return null
}
val continuationVariable = frameProxy.visibleVariableByName("\$continuation") ?: return null
val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null
return findCoroutineContextForContinuation(continuation)
}
private fun findCoroutineContextForContinuation(continuation: ObjectReference): ObjectReference? {
val continuationType = (continuation.referenceType() as? ClassType)
?.allInterfaces()?.firstOrNull { it.name() == Continuation::class.java.name }
?: return null
val getContextMethod = continuationType
.methodsByName("getContext", "()Lkotlin/coroutines/CoroutineContext;").firstOrNull()
?: return null
val threadReference = frameProxy.threadProxy().threadReference.takeIf { it.isSuspended } ?: return null
val invokePolicy = context.suspendContext.getInvokePolicy()
return continuation.invokeMethod(threadReference, getContextMethod, emptyList(), invokePolicy) as? ObjectReference
}
private fun findCapturedVariableInReceiver(variables: List<LocalVariableProxyImpl>, kind: VariableKind): Result? {
fun isReceiverOrPassedThis(name: String) =
name.startsWith(AsmUtil.LABELED_THIS_PARAMETER)
@@ -30,6 +30,14 @@ fun ReferenceType.safeAllLineLocations(): List<Location> {
return DebuggerUtilsEx.allLineLocations(this) ?: emptyList()
}
fun ReferenceType.safeSourceName(): String? {
return try {
sourceName()
} catch (e: AbsentInformationException) {
null
}
}
fun Method.safeLocationsOfLine(line: Int): List<Location> {
return try {
locationsOfLine(line)
@@ -0,0 +1,15 @@
package coroutineContextFun
import kotlin.coroutines.coroutineContext
suspend fun main() {
val a = 5
foo()
//Breakpoint!
foo()
}
private suspend fun foo() {}
// EXPRESSION: coroutineContext
// RESULT: instance of kotlin.coroutines.EmptyCoroutineContext(id=ID): Lkotlin/coroutines/EmptyCoroutineContext;
@@ -0,0 +1,8 @@
LineBreakpoint created at coroutineContextFun.kt:9
Run Java
Connected to the target VM
coroutineContextFun.kt:9
Compile bytecode for coroutineContext
Disconnected from the target VM
Process finished with exit code 0
@@ -0,0 +1,19 @@
package coroutineContextLambda
import kotlin.coroutines.coroutineContext
suspend fun main() {
foo()
}
private var foo: suspend () -> Unit = {
bar()
}
private var bar: suspend () -> Unit = {
//Breakpoint!
val a = 5
}
// EXPRESSION: coroutineContext
// RESULT: instance of kotlin.coroutines.EmptyCoroutineContext(id=ID): Lkotlin/coroutines/EmptyCoroutineContext;
@@ -0,0 +1,8 @@
LineBreakpoint created at coroutineContextLambda.kt:15
Run Java
Connected to the target VM
coroutineContextLambda.kt:15
Compile bytecode for coroutineContext
Disconnected from the target VM
Process finished with exit code 0
@@ -600,6 +600,16 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/catchVariable.kt");
}
@TestMetadata("coroutineContextFun.kt")
public void testCoroutineContextFun() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/coroutineContextFun.kt");
}
@TestMetadata("coroutineContextLambda.kt")
public void testCoroutineContextLambda() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/coroutineContextLambda.kt");
}
@TestMetadata("delegatedPropertyInClass.kt")
public void testDelegatedPropertyInClass() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/delegatedPropertyInClass.kt");