diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 0a6bcb6509..e2cc2d9164 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -1,6 +1,7 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -424,10 +425,12 @@ internal fun CancellableContinuation.invokeOnCancellation(handler: Cancel * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor * can resume execution of a previously suspended coroutine even if its job was already cancelled. */ +@OptIn(ExperimentalContracts::class) public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit -): T = - suspendCoroutineUninterceptedOrReturn { uCont -> +): T { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE) /* * For non-atomic cancellation we setup parent-child relationship immediately @@ -438,6 +441,7 @@ public suspend inline fun suspendCancellableCoroutine( block(cancellable) cancellable.getResult() } +} /** * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt index a47a889431..83b036421f 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt @@ -134,4 +134,15 @@ class CancellableContinuationTest : TestBase() { }) finish(5) } + + /** Tests that the compiler recognizes that [suspendCancellableCoroutine] invokes its block exactly once. */ + @Test + fun testSuspendCancellableCoroutineContract() = runTest { + val i: Int + suspendCancellableCoroutine { cont -> + i = 1 + cont.resume(Unit) + } + assertEquals(1, i) + } } \ No newline at end of file