Skip to content

Commit

Permalink
Compiler plugin tests (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mr3zee authored Aug 13, 2024
1 parent 9c46d86 commit f382784
Show file tree
Hide file tree
Showing 63 changed files with 4,018 additions and 683 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ build
!.idea/codeStyles/*
!.idea/icon.svg
!.idea/detekt.xml
!.idea/kotlinTestDataPluginTestDataPaths.xml

.DS_Store

Expand Down
19 changes: 19 additions & 0 deletions .idea/kotlinTestDataPluginTestDataPaths.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ apiValidation {

ignoredProjects.addAll(
listOf(
"codegen-tests-jvm",
"codegen-tests-mpp",
"compiler-plugin-tests",
"krpc-test",
"utils",
)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ package kotlinx.rpc.codegen.extension
import kotlinx.rpc.codegen.VersionSpecificApi
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment

internal class RPCIrExtension(
private val logger: MessageCollector,
private val versionSpecificApi: VersionSpecificApi,
) : IrGenerationExtension {
class RPCIrExtension(configuration: CompilerConfiguration) : IrGenerationExtension {
private val logger = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)

override fun generate(
moduleFragment: IrModuleFragment,
pluginContext: IrPluginContext,
) {
val context = RPCIrContext(pluginContext, versionSpecificApi)
val context = RPCIrContext(pluginContext, VersionSpecificApi.INSTANCE)

val processor = RPCIrServiceProcessor(logger)
moduleFragment.transform(processor, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.rpc.codegen.extension

import kotlinx.rpc.codegen.VersionSpecificApi
import kotlinx.rpc.codegen.VersionSpecificApiImpl.copyToVS
import kotlinx.rpc.codegen.common.rpcMethodClassName
import kotlinx.rpc.codegen.common.rpcMethodClassNameKsp
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
Expand Down Expand Up @@ -1274,7 +1275,7 @@ internal class RPCStubGenerator(
it.name == OperatorNameConventions.EQUALS
}.symbol

dispatchReceiverParameter = anyClass.thisReceiver
dispatchReceiverParameter = anyClass.copyThisReceiver(this@apply)

addValueParameter {
name = Name.identifier("other")
Expand All @@ -1295,7 +1296,7 @@ internal class RPCStubGenerator(
it.name == OperatorNameConventions.HASH_CODE
}.symbol

dispatchReceiverParameter = anyClass.thisReceiver
dispatchReceiverParameter = anyClass.copyThisReceiver(this@apply)
}

addFunction {
Expand All @@ -1311,10 +1312,13 @@ internal class RPCStubGenerator(
it.name == OperatorNameConventions.TO_STRING
}.symbol

dispatchReceiverParameter = anyClass.thisReceiver
dispatchReceiverParameter = anyClass.copyThisReceiver(this@apply)
}
}

private fun IrClass.copyThisReceiver(function: IrFunction) =
thisReceiver?.copyToVS(function, origin = IrDeclarationOrigin.DEFINED)

private fun stringConst(value: String) = IrConstImpl.string(
startOffset = UNDEFINED_OFFSET,
endOffset = UNDEFINED_OFFSET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package kotlinx.rpc.codegen

import kotlinx.rpc.codegen.extension.RPCIrExtension
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
Expand All @@ -21,16 +22,17 @@ class RPCCommandLineProcessor : CommandLineProcessor {

@OptIn(ExperimentalCompilerApi::class)
class RPCCompilerPlugin : CompilerPluginRegistrar() {
init {
VersionSpecificApi.INSTANCE = VersionSpecificApiImpl
}

override val supportsK2: Boolean = true

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
val irExtension = RPCIrPlugin.provideExtension(configuration)

IrGenerationExtension.registerExtension(irExtension)
FirExtensionRegistrarAdapter.registerExtension(FirRPCExtensionRegistrar(configuration))
registerRpcExtensions(configuration)
}
}

@OptIn(ExperimentalCompilerApi::class)
fun CompilerPluginRegistrar.ExtensionStorage.registerRpcExtensions(configuration: CompilerConfiguration) {
VersionSpecificApi.INSTANCE = VersionSpecificApiImpl

IrGenerationExtension.registerExtension(RPCIrExtension(configuration))
FirExtensionRegistrarAdapter.registerExtension(FirRPCExtensionRegistrar(configuration))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.rpc

import kotlin.reflect.KProperty

/**
* Thrown when an uninitialized field of an RPC interface is accessed.
*
* Use [awaitFieldInitialization] to await for the field initialization
*/
public class UninitializedRPCFieldException(serviceName: String, property: KProperty<*>): Exception() {
override val message: String = "${property.name} field of RPC service \"$serviceName\" in not initialized"
}
88 changes: 88 additions & 0 deletions core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.rpc

import kotlinx.rpc.internal.RPCDeferredField
import kotlinx.rpc.internal.RPCServiceFieldsProvider
import kotlinx.rpc.internal.findRPCStubProvider
import kotlinx.rpc.internal.safeCast
import kotlin.reflect.KClass

/**
* Waits for the initialization of an RPC field in the generated client:
*
* ```kotlin
* interface MyService : RPC {
* val stateFlow: StateFlow<Int>
* }
*
* val service = rpcClient.withService<MyService>()
* val currentValue = service.awaitFieldInitialization { stateFlow }.value
* ```
*
* @param T service type
* @param R field type
* @param getter function that returns the field of the context service to wait for.
* @return service filed after it was initialized.
*/
public suspend fun <T : RPC, R> T.awaitFieldInitialization(getter: T.() -> R): R {
val field = getter()

if (field is RPCDeferredField<*>) {
@Suppress("UNCHECKED_CAST")
return (field as RPCDeferredField<R>).await()
}

error("Please choose required field for a valid RPC client generated by RPCClient.withService method")
}

/**
* Waits for the initialization of all RPC fields in the generated client:
*
* ```kotlin
* interface MyService : RPC {
* val stateFlow1: StateFlow<Int>
* val stateFlow2: StateFlow<Int>
* }
*
* val service = rpcClient.withService<MyService>()
* val currentValue = service.awaitFieldInitialization()
* // fields `stateFlow1` and `stateFlow2` are initialized
* ```
*
* @param T service type
* @return specified service, after all of it's field were initialized.
*/
public suspend inline fun <reified T : RPC> T.awaitFieldInitialization(): T {
return awaitFieldInitialization(T::class)
}

/**
* Waits for the initialization of all RPC fields in the generated client:
*
* ```kotlin
* interface MyService : RPC {
* val stateFlow1: StateFlow<Int>
* val stateFlow2: StateFlow<Int>
* }
*
* val service = rpcClient.withService<MyService>()
* val currentValue = service.awaitFieldInitialization(MyService::class)
* // fields `stateFlow1` and `stateFlow2` are initialized
* ```
*
* @param T service type
* @param kClass [KClass] of the [T] type.
* @return specified service, after all of it's field were initialized.
*/
public suspend fun <T : RPC> T.awaitFieldInitialization(kClass: KClass<T>): T {
findRPCStubProvider<RPCServiceFieldsProvider<T>>(kClass, RPCServiceFieldsProvider::class.safeCast())
.rpcFields(this)
.forEach { field ->
field.await()
}

return this
}
64 changes: 64 additions & 0 deletions core/src/commonMain/kotlin/kotlinx/rpc/withService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.rpc

import kotlinx.atomicfu.atomic
import kotlinx.rpc.internal.RPCStubServiceProvider
import kotlinx.rpc.internal.findRPCStubProvider
import kotlinx.rpc.internal.kClass
import kotlinx.rpc.internal.safeCast
import kotlin.reflect.KClass
import kotlin.reflect.KType

/**
* Creates instance of the generated service [T], that is able to communicate with server using RPCClient.
*
* [awaitFieldInitialization] method can be used on that instance.
*
* @param T the exact type of the service to be created.
* @return instance of the generated service.
*/
public inline fun <reified T : RPC> RPCClient.withService(): T {
return withService(T::class)
}

/**
* Creates instance of the generated service [T], that is able to communicate with server using RPCClient.
*
* [awaitFieldInitialization] method can be used on that instance.
*
* @param T the exact type of the service to be created.
* @param serviceKType [KType] of the service to be created.
* @return instance of the generated service.
*/
public fun <T : RPC> RPCClient.withService(serviceKType: KType): T {
return withService(serviceKType.kClass())
}

/**
* Counter for locally added services.
* Used to differentiate uniques local services, regardless of their type.
*/
private val SERVICE_ID = atomic(0L)

/**
* Creates instance of the generated service [T], that is able to communicate with server using RPCClient.
*
* [awaitFieldInitialization] method can be used on that instance.
*
* @param T the exact type of the service to be created.
* @param serviceKClass [KClass] of the service to be created.
* @return instance of the generated service.
*/
public fun <T : RPC> RPCClient.withService(serviceKClass: KClass<T>): T {
val provider = findRPCStubProvider<RPCStubServiceProvider<T>>(
kClass = serviceKClass,
resultKClass = RPCStubServiceProvider::class.safeCast(),
)

val id = SERVICE_ID.incrementAndGet()

return provider.withClient(id, this)
}
Loading

0 comments on commit f382784

Please sign in to comment.