kotlin-suspend-transform-compiler-plugin icon indicating copy to clipboard operation
kotlin-suspend-transform-compiler-plugin copied to clipboard

Support override return type generic

Open ForteScarlet opened this issue 4 months ago • 2 comments

  /**
    * Indicates whether there is a generic type used to override the function return type
    * on the current mark annotation.
    *
    * If `true`, when determining the return type of the generated function,
    * the content of this generic type will be directly regarded as the return type of the origin function,
    * rather than the actual type of the origin function.
    *
    * For example, Normally, the return type of the generated function
    * depends on the return type of the origin function:
    *
    * ```Kotlin
    * @JvmBlocking
    * suspend fun run(): Result<String>
    *
    * // Generated
    *
    * @Api4J
    * fun runBlocking(): Result<String>
    * ```
    *
    * However, if the annotation has a generic type and `hasReturnTypeOverrideGeneric` is true:
    *
    * ```Kotlin
    * @JvmBlockingWithType<String?>
    * suspend fun run(): Result<String>
    *
    * // Generated
    *
    * @Api4J
    * fun runBlocking(): String?
    * ```
    *
    * As can be seen, the generated function ignores the actual return type of the origin function
    * and instead treats the generic type specified in the annotation as the return type
    * of the origin function.
    *
    * Note: If you want to determine the return type through type overloading,
    * you must ensure that the transformer function you use correctly matches the input and output parameter types.
    * Otherwise, it may result in compilation errors or runtime exceptions.
    *
    * @since 0.14.0
    */
   @ExperimentalReturnTypeOverrideGenericApi
   val hasReturnTypeOverrideGeneric: Property<Boolean>

See also: #99

ForteScarlet avatar Jul 08 '25 08:07 ForteScarlet

I encountered an issue. Previously, I found that when overriding override fun FirDeclarationPredicateRegistrar.registerPredicates() in SuspendTransformFirTransformer to provide a Predicate based on transformer.markAnnotation, if the annotation involves a generic type T, like this:

// The annotation:
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Retention(AnnotationRetention.BINARY)
expect annotation class JvmResultBlock<T>(
    val baseName: String = "",
    val suffix: String = "Blocking",
    val asProperty: Boolean = false
)

// The code:
interface Foo {
    @JvmResultBlock<T>
    suspend fun <T> foo(value: T): Result<T>
}

The compilation fails with this error:

e: file:///suspend-transform-kotlin-compile-plugin/tests/test-js/src/commonMain/kotlin/returntypeoverride/ReturnTypeOverrides.kt:21:22 Unresolved reference 'T'.
e: file:///suspend-transform-kotlin-compile-plugin/tests/test-js/src/commonMain/kotlin/returntypeoverride/ReturnTypeOverrides.kt:25:22 Unresolved reference 'T'.
Finished executing kotlin compiler using DAEMON strategy

Specifically: Unresolved reference 'T'.

This issue only occurs with real generics. If an explicit type is used instead, there’s no problem:

// The code:
interface Foo {
    @JvmResultBlock<String>
    suspend fun <T> foo(value: T): Result<T>
}

However, if I remove override fun FirDeclarationPredicateRegistrar.registerPredicates() or make it register predicates that exclude generic annotations, compilation succeeds and achieves the desired behavior in this project’s test module.

The problem lies in "this project’s test module". When I test with an external project, the code generation fails. For example, given this source:

@ST
public interface SendSupport {
    public suspend fun send(text: String): MessageReceipt
    public suspend fun send(message: Message): MessageReceipt
    public suspend fun send(messageContent: MessageContent): MessageReceipt
}

Viewing the generated SendSupport.class directly in the IDE shows:

@love.forte.simbot.suspendrunner.SuspendTrans public interface SendSupport {
    public abstract suspend fun send(text: kotlin.String): love.forte.simbot.message.MessageReceipt

    public abstract suspend fun send(message: love.forte.simbot.message.Message): love.forte.simbot.message.MessageReceipt

    public abstract suspend fun send(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.message.MessageReceipt

    public open fun sendAsync(message: love.forte.simbot.message.Message): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendAsync(messageContent: love.forte.simbot.message.MessageContent): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendAsync(text: kotlin.String): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendBlocking(message: love.forte.simbot.message.Message): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendBlocking(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendBlocking(text: kotlin.String): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendReserve(message: love.forte.simbot.message.Message): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendReserve(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendReserve(text: kotlin.String): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }
}

Everything appears normal. But decompiling it via IDEA reveals the problem:

@Metadata(
   mv = {2, 2, 0},
   k = 1,
   xi = 48,
   d1 = {"\u00006\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\bg\u0018\u00002\u00020\u0001J\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H¦@¢\u0006\u0002\u0010\u0006J\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0007\u001a\u00020\bH¦@¢\u0006\u0002\u0010\tJ\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\n\u001a\u00020\u000bH¦@¢\u0006\u0002\u0010\cJ\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\u0004\u001a\u00020\u0005H\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\u0004\u001a\u00020\u0005H\u0017ø\u0001\u0000\u0082\u0002\u0006\n\u0004\b!0\u0001¨\u0006\u0012À\u0006\u0001"},
   d2 = {"Llove/forte/simbot/ability/SendSupport;", "", "send", "Llove/forte/simbot/message/MessageReceipt;", "text", "", "(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "message", "Llove/forte/simbot/message/Message;", "(Llove/forte/simbot/message/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "messageContent", "Llove/forte/simbot/message/MessageContent;", "(Llove/forte/simbot/message/MessageContent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "sendAsync", "Ljava/util/concurrent/CompletableFuture;", "sendBlocking", "sendReserve", "Llove/forte/simbot/suspendrunner/reserve/SuspendReserve;", "simbot-api"}
)
@SuspendTrans
public interface SendSupport {
   // $FF: synthetic method
   Object send(String var1, Continuation var2);

   // $FF: synthetic method
   Object send(Message var1, Continuation var2);

   // $FF: synthetic method
   Object send(MessageContent var1, Continuation var2);
}

As seen, no expected synthetic functions are actually generated in the class—they only appear in @Metadata (visible via IDE). Restoring override fun FirDeclarationPredicateRegistrar.registerPredicates() fixes this external project issue, but reintroduces the Unresolved reference 'T' error.

ForteScarlet avatar Jul 15 '25 12:07 ForteScarlet

see: KT-79267

ForteScarlet avatar Jul 16 '25 06:07 ForteScarlet