From 28466cb472fa35ed20d01b7827a9439717515327 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 9 Apr 2025 09:50:12 +0200 Subject: [PATCH] Introduce CompletableDeferred.asDeferred that prevents downcasting Fixes #4408 --- .../api/kotlinx-coroutines-core.api | 1 + .../api/kotlinx-coroutines-core.klib.api | 1 + .../common/src/CompletableDeferred.kt | 31 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index c8f1e550eb..7846da9680 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -124,6 +124,7 @@ public final class kotlinx/coroutines/CompletableDeferredKt { public static final fun CompletableDeferred (Ljava/lang/Object;)Lkotlinx/coroutines/CompletableDeferred; public static final fun CompletableDeferred (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/CompletableDeferred; public static synthetic fun CompletableDeferred$default (Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableDeferred; + public static final fun asDeferred (Lkotlinx/coroutines/CompletableDeferred;)Lkotlinx/coroutines/Deferred; public static final fun completeWith (Lkotlinx/coroutines/CompletableDeferred;Ljava/lang/Object;)Z } diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api index 373a1eee52..ced9ac4610 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api @@ -906,6 +906,7 @@ final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/StateFlow<#A>).kotlinx.coro final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/StateFlow<#A>).kotlinx.coroutines.flow/distinctUntilChanged(): kotlinx.coroutines.flow/Flow<#A> // kotlinx.coroutines.flow/distinctUntilChanged|distinctUntilChanged@kotlinx.coroutines.flow.StateFlow<0:0>(){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.selects/SelectBuilder<#A>).kotlinx.coroutines.selects/onTimeout(kotlin.time/Duration, kotlin.coroutines/SuspendFunction0<#A>) // kotlinx.coroutines.selects/onTimeout|onTimeout@kotlinx.coroutines.selects.SelectBuilder<0:0>(kotlin.time.Duration;kotlin.coroutines.SuspendFunction0<0:0>){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.selects/SelectBuilder<#A>).kotlinx.coroutines.selects/onTimeout(kotlin/Long, kotlin.coroutines/SuspendFunction0<#A>) // kotlinx.coroutines.selects/onTimeout|onTimeout@kotlinx.coroutines.selects.SelectBuilder<0:0>(kotlin.Long;kotlin.coroutines.SuspendFunction0<0:0>){0§}[0] +final fun <#A: kotlin/Any?> (kotlinx.coroutines/CompletableDeferred<#A>).kotlinx.coroutines/asDeferred(): kotlinx.coroutines/Deferred<#A> // kotlinx.coroutines/asDeferred|asDeferred@kotlinx.coroutines.CompletableDeferred<0:0>(){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines/CompletableDeferred<#A>).kotlinx.coroutines/completeWith(kotlin/Result<#A>): kotlin/Boolean // kotlinx.coroutines/completeWith|completeWith@kotlinx.coroutines.CompletableDeferred<0:0>(kotlin.Result<0:0>){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines.channels/broadcast(kotlin.coroutines/CoroutineContext = ..., kotlin/Int = ..., kotlinx.coroutines/CoroutineStart = ..., kotlin/Function1? = ..., kotlin.coroutines/SuspendFunction1, kotlin/Unit>): kotlinx.coroutines.channels/BroadcastChannel<#A> // kotlinx.coroutines.channels/broadcast|broadcast@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlin.Int;kotlinx.coroutines.CoroutineStart;kotlin.Function1?;kotlin.coroutines.SuspendFunction1,kotlin.Unit>){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines.channels/produce(kotlin.coroutines/CoroutineContext = ..., kotlin/Int = ..., kotlin.coroutines/SuspendFunction1, kotlin/Unit>): kotlinx.coroutines.channels/ReceiveChannel<#A> // kotlinx.coroutines.channels/produce|produce@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlin.Int;kotlin.coroutines.SuspendFunction1,kotlin.Unit>){0§}[0] diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index 2788ce8298..17df78ad0a 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -69,6 +69,37 @@ public fun CompletableDeferred(parent: Job? = null): CompletableDeferred @Suppress("FunctionName") public fun CompletableDeferred(value: T): CompletableDeferred = CompletableDeferredImpl(null).apply { complete(value) } +/** + * Creates a view of this [CompletableDeferred] as a [Deferred], which prevents downcasting to a completable version. + * + * ``` + * class MyClass(val scope: CoroutineScope) { + * // can be completed + * private val actualDeferred: CompletableDeferred = CompletableDeferred() + * + * // can not be completed from outside + * public val operationCompleted: Deferred = actualDeferred.asDeferred() + * + * fun startOperation() = scope.launch { + * // do some work + * delay(2.seconds) + * actualDeferred.complete("Done") + * } + * } + * + * // (myClass.operationCompleted as CompletableDeferred<*>) will fail + * ``` + */ +@ExperimentalCoroutinesApi +public fun CompletableDeferred.asDeferred(): Deferred = ReadonlyDeferred(this) + +@OptIn(InternalForInheritanceCoroutinesApi::class) +private class ReadonlyDeferred( + val deferred: CompletableDeferred, +) : Deferred by deferred { + override fun toString(): String = "ReadonlyDeferred($deferred)" +} + /** * Concrete implementation of [CompletableDeferred]. */