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.
A fully customized implementation of JVM Blocking/Async Transformers can be found at: simbot's BlockingRunner
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")
}
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.0When 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.0For configuration of markName in custom annotations,
refer to Mark Name.