Skip to main content
Version: v2.4.0-0.14.0

Custom Transformers

This guide covers how to create custom transformers when the default transformers don't meet your specific needs.

Overview​

You can create fully customized transformers if the default ones don't meet your requirements. For example, if you want to implement blocking logic without using kotlinx.coroutines.runBlocking, or if you need platform-specific behavior.

info

A fully customized implementation of JVM Blocking/Async Transformers can be found at: simbot's BlockingRunner

Known Issue

Custom transformer functions cannot be placed in the same module as the one using the compiler plugin. They need to be created in a separate module.

For more information: #100

Basic Setup​

When using custom transformers, you typically won't need the default annotation and runtime dependencies:

suspendTransformPlugin {
// If customized, then you may not use the annotation and runtime we provide.
includeAnnotation = false
includeRuntime = false

transformers {
// Your custom transformer configuration
}
}

Creating a Custom Transformer​

Let's create a custom transformer step by step. We'll create a @JBlock annotation that uses a custom inBlock function.

Step 1: Define Your Custom Components​

First, define your custom annotation and transform function:

// Your custom annotation
annotation class JBlock(
val baseName: String = "",
val suffix: String = "Blocking",
val asProperty: Boolean = false
)

// Your custom transform function
fun <T> inBlock(block: suspend () -> T): T {
// Your custom implementation
TODO("Your impl")
}
Agreement

According to the agreement: the first parameter of a transform function should be a lambda of type suspend () -> T.

Step 2: Configure the Annotation​

Configure the annotation properties in your transformer:

suspendTransformPlugin {
includeAnnotation = false
includeRuntime = false
transformers {
addJvm {
markAnnotation {
// Your annotation class information
classInfo {
packageName = "com.example"
className = "JBlock"
}

// Property names in your annotation
baseNameProperty = "baseName" // Default is `baseName`
suffixProperty = "suffix" // Default is `suffix`
asPropertyProperty = "asProperty" // Default is `asProperty`

// Default values (compiler plugin can't get annotation defaults)
defaultSuffix = "Blocking"
defaultAsProperty = false
}
}
}
}

Step 3: Configure the Transform Function​

Configure your custom transform function:

import love.forte.plugin.suspendtrans.configuration.TransformReturnTypeGenericMode

suspendTransformPlugin {
transformers {
addJvm {
markAnnotation {
// ... annotation configuration
}

// Transform function information
transformFunctionInfo {
packageName = "com.example"
functionName = "inBlock"
}

// Return type configuration
transformReturnType = null // null means same type as original function
transformReturnTypeGeneric = false // true if return type has generics
transformReturnTypeGenericMode = TransformReturnTypeGenericMode.NORMAL
}
}
}

Advanced Configuration​

Custom Property Names​

You can use different property names in your annotation:

annotation class JBlock(
val myBaseName: String = "",
val mySuffix: String = "Blocking",
val myAsProperty: Boolean = false
)

Configure the mapping:

suspendTransformPlugin {
transformers {
addJvm {
markAnnotation {
classInfo {
packageName = "com.example"
className = "JBlock"
}

// Map to your custom property names
baseNameProperty = "myBaseName"
suffixProperty = "mySuffix"
asPropertyProperty = "myAsProperty"

defaultSuffix = "Blocking"
defaultAsProperty = false
}
}
}
}

Return Type Configuration​

Same Return Type​

For functions that return the same type as the original:

transformReturnType = null
transformReturnTypeGeneric = false

Generic Return Type​

For functions that return a generic type containing the original type (e.g., CompletableFuture<T>):

transformReturnType = "java.util.concurrent.CompletableFuture"
transformReturnTypeGeneric = true

Generic Argument Nullability​

Version 0.14.0

When transformReturnTypeGeneric is true, transformReturnTypeGenericMode controls the nullability of the original return type when it is copied into the transformed return type's generic argument.

The primary reason for this option is NON_NULL. Some wrappers describe an emitted element rather than a nullable value slot. Reactive Streams-style APIs, for example Reactor Mono<T>, can complete with no element, but they must not emit null. For such wrappers, a transformed return type should usually expose a non-null generic argument even when the source suspend function returns a type parameter whose bound may be nullable.

NULLABLE exists for the opposite case, where the wrapper intentionally carries a nullable value.

Import the enum in your Gradle script when setting this option:

import love.forte.plugin.suspendtrans.configuration.TransformReturnTypeGenericMode

Available modes:

  • TransformReturnTypeGenericMode.NORMAL: keep the original return type unchanged. This is the default.
  • TransformReturnTypeGenericMode.NULLABLE: make the copied generic argument nullable.
  • TransformReturnTypeGenericMode.NON_NULL: make the copied generic argument non-nullable.

Example:

transformReturnType = "java.util.concurrent.CompletableFuture"
transformReturnTypeGeneric = true
transformReturnTypeGenericMode = TransformReturnTypeGenericMode.NON_NULL

You can also use the convenience DSL method:

transformReturnTypeGenericModeNonNull()

For a source function like:

suspend fun <T : Foo?> load(): T

NON_NULL keeps the original type parameter declaration unchanged and expresses the non-null requirement at the use site, producing a type like CompletableFuture<out T & Any>.

For a Reactor-style transformer returning Mono<T>, the same source function would be represented as a use-site type like Mono<T & Any>. The setting only changes the generated type signature; your transform function must still enforce the wrapper's runtime contract, for example by converting a null result to an empty completion instead of emitting null.

Specific Return Type​

For functions that return a specific type without generics (e.g., Job):

transformReturnType = "kotlinx.coroutines.Job"
transformReturnTypeGeneric = false

Annotation Management​

Adding Annotations to Original Function​

Add annotations to the original suspend function:

suspendTransformPlugin {
transformers {
addJvm {
// ... other configuration

// Add @JvmSynthetic to original function
addOriginFunctionIncludeAnnotation {
classInfo {
packageName = "kotlin.jvm"
className = "JvmSynthetic"
}
repeatable = false // Default is false
}
}
}
}

Adding Annotations to Generated Function​

Add annotations to the generated synthetic function:

suspendTransformPlugin {
transformers {
addJvm {
// ... other configuration

// Add custom @JApi annotation to generated function
addSyntheticFunctionIncludeAnnotation {
classInfo {
packageName = "com.example"
className = "JApi"
}
includeProperty = true // Can be added to properties
}
}
}
}

Copying Annotations​

Enable copying annotations from original to synthetic function:

suspendTransformPlugin {
transformers {
addJvm {
// ... other configuration

// Enable annotation copying
copyAnnotationsToSyntheticFunction = true
copyAnnotationsToSyntheticProperty = true // For properties

// Exclude specific annotations from copying
addCopyAnnotationExclude {
classInfo {
packageName = "kotlin.jvm"
className = "JvmSynthetic"
}
}
}
}
}

Complete Example​

Here's a complete example of a custom transformer:

Custom Components​

// Custom annotation
annotation class JBlock(
val myBaseName: String = "",
val mySuffix: String = "Blocking",
val myAsProperty: Boolean = false
)

// Custom warning annotation
@RequiresOptIn(message = "Api for Java", level = RequiresOptIn.Level.WARNING)
@Retention(AnnotationRetention.BINARY)
annotation class JApi

// Custom transform function
fun <T> inBlock(block: suspend () -> T): T {
// Your custom blocking implementation
TODO("Your impl")
}

Configuration​

suspendTransformPlugin {
includeAnnotation = false
includeRuntime = false
transformers {
addJvm {
// Annotation configuration
markAnnotation {
classInfo {
packageName = "com.example"
className = "JBlock"
}

baseNameProperty = "myBaseName"
suffixProperty = "mySuffix"
asPropertyProperty = "myAsProperty"

defaultSuffix = "Blocking"
defaultAsProperty = false
}

// Transform function configuration
transformFunctionInfo {
packageName = "com.example"
functionName = "inBlock"
}

// Annotation management
copyAnnotationsToSyntheticFunction = true
copyAnnotationsToSyntheticProperty = true

// Add @JvmSynthetic to original function
addOriginFunctionIncludeAnnotation {
classInfo {
from(SuspendTransformConfigurations.jvmSyntheticClassInfo)
}
repeatable = false
}

// Add @JApi to generated function
addSyntheticFunctionIncludeAnnotation {
classInfo {
packageName = "com.example"
className = "JApi"
}
includeProperty = true
}

// Exclude @JvmSynthetic from copying
addCopyAnnotationExclude {
from(SuspendTransformConfigurations.jvmSyntheticClassInfo)
}
}
}
}

Multi-Purpose Annotations​

You can create annotations that work with multiple transformers by using different property names:

Annotation Definition​

annotation class JTrans(
val blockingBaseName: String = "",
val blockingSuffix: String = "Blocking",
val blockingAsProperty: Boolean = false,

val asyncBaseName: String = "",
val asyncSuffix: String = "Async",
val asyncAsProperty: Boolean = false
)

Configuration​

suspendTransformPlugin {
includeAnnotation = false
includeRuntime = false
transformers {
// Blocking transformer
addJvm {
markAnnotation {
classInfo {
packageName = "com.example"
className = "JTrans"
}
baseNameProperty = "blockingBaseName"
suffixProperty = "blockingSuffix"
asPropertyProperty = "blockingAsProperty"
defaultSuffix = "Blocking"
defaultAsProperty = false
}

transformFunctionInfo {
packageName = "com.example"
functionName = "inBlock"
}
// ... other config
}

// Async transformer
addJvm {
markAnnotation {
classInfo {
packageName = "com.example"
className = "JTrans"
}
baseNameProperty = "asyncBaseName"
suffixProperty = "asyncSuffix"
asPropertyProperty = "asyncAsProperty"
defaultSuffix = "Async"
defaultAsProperty = false
}

transformFunctionInfo {
packageName = "com.example"
functionName = "inAsync"
}
// ... other config
}
}
}

Built-in Configurations​

The plugin provides some common configurations in love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations:

// Common annotation class infos
SuspendTransformConfigurations.jvmSyntheticClassInfo
SuspendTransformConfigurations.kotlinJsExportIgnoreClassInfo

// Use them in your configuration
addCopyAnnotationExclude {
from(SuspendTransformConfigurations.jvmSyntheticClassInfo)
}

Others​

MarkName​

Version 0.13.0

For configuration of markName in custom annotations, refer to Mark Name.