Skip to content

Commit c27022c

Browse files
authored
feat: implement nullmark mode for generic return type transformation (#123)
feat: implement nullmark mode for generic return type transformation and add test coverage
1 parent d886df1 commit c27022c

26 files changed

Lines changed: 2005 additions & 49 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ build/
55
!**/src/main/**/build/
66
!**/src/test/**/build/
77

8+
_docs
9+
810
### IntelliJ IDEA ###
911
.idea/**
1012
!.idea/copyright

compiler/suspend-transform-plugin-cli/src/test/kotlin/love/forte/plugin/suspendtrans/cli/ConfigurationSerializeTests.kt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package love.forte.plugin.suspendtrans.cli
22

3-
import love.forte.plugin.suspendtrans.configuration.InternalSuspendTransformConfigurationApi
4-
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfiguration
3+
import love.forte.plugin.suspendtrans.configuration.*
54
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jsPromiseTransformer
65
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmAsyncTransformer
76
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmBlockingTransformer
8-
import love.forte.plugin.suspendtrans.configuration.TargetPlatform
97
import kotlin.test.Test
108
import kotlin.test.assertEquals
119

@@ -50,6 +48,53 @@ class ConfigurationSerializeTests {
5048
}
5149
}
5250

51+
@OptIn(InternalSuspendTransformConfigurationApi::class)
52+
@Test
53+
fun testDecodeTransformReturnTypeGenericMode() {
54+
val transformer = Transformer(
55+
markAnnotation = jvmAsyncTransformer.markAnnotation,
56+
transformFunctionInfo = jvmAsyncTransformer.transformFunctionInfo,
57+
transformReturnType = jvmAsyncTransformer.transformReturnType,
58+
transformReturnTypeGeneric = jvmAsyncTransformer.transformReturnTypeGeneric,
59+
originFunctionIncludeAnnotations = jvmAsyncTransformer.originFunctionIncludeAnnotations,
60+
syntheticFunctionIncludeAnnotations = jvmAsyncTransformer.syntheticFunctionIncludeAnnotations,
61+
copyAnnotationsToSyntheticFunction = jvmAsyncTransformer.copyAnnotationsToSyntheticFunction,
62+
copyAnnotationExcludes = jvmAsyncTransformer.copyAnnotationExcludes,
63+
copyAnnotationsToSyntheticProperty = jvmAsyncTransformer.copyAnnotationsToSyntheticProperty,
64+
transformReturnTypeGenericMode = TransformReturnTypeGenericMode.NON_NULL,
65+
)
66+
val config = SuspendTransformConfiguration(
67+
transformers = mapOf(TargetPlatform.JVM to listOf(transformer))
68+
)
69+
70+
val decoded = decodeSuspendTransformConfigurationFromHex(config.encodeToHex())
71+
assertEquals(config, decoded)
72+
assertEquals(
73+
TransformReturnTypeGenericMode.NON_NULL,
74+
decoded.transformers.getValue(TargetPlatform.JVM).single().transformReturnTypeGenericMode
75+
)
76+
}
77+
78+
@OptIn(InternalSuspendTransformConfigurationApi::class)
79+
@Test
80+
fun testDecodeLegacyWithoutTransformReturnTypeGenericMode() {
81+
val config = SuspendTransformConfiguration(
82+
transformers = mapOf(
83+
TargetPlatform.JVM to listOf(jvmBlockingTransformer, jvmAsyncTransformer),
84+
TargetPlatform.JS to listOf(jsPromiseTransformer),
85+
)
86+
)
87+
val legacyHex =
88+
"0aba0808011286040a95010a3c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e67180020001208626173654e616d651a06737566666978220a617350726f70657274792a08426c6f636b696e6730003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512390a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120f2472756e496e426c6f636b696e672420002a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d6518002000480012ac040a8f010a390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e63180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a2d0a146a6176612e7574696c2e636f6e63757272656e741211436f6d706c657461626c654675747572651800200020012a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d651800200048000a970308021292030a8e010a3a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d697365180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a290a086d61726b4e616d6512170a096b6f746c696e2e6a7312064a734e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a180a096b6f746c696e2e6a73120750726f6d697365180020002001323d0a370a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1206417069344a7318002000100018013801423a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d6973651800200042130a066b6f746c696e12054f7074496e1800200042170a096b6f746c696e2e6a7312064a734e616d65180020004800"
89+
90+
val decoded = decodeSuspendTransformConfigurationFromHex(legacyHex)
91+
92+
assertEquals(config, decoded)
93+
decoded.transformers.values.flatten().forEach { transformer ->
94+
assertEquals(TransformReturnTypeGenericMode.NORMAL, transformer.transformReturnTypeGenericMode)
95+
}
96+
}
97+
5398
@OptIn(InternalSuspendTransformConfigurationApi::class)
5499
@Test
55100
fun testDecode() {
@@ -68,7 +113,7 @@ class ConfigurationSerializeTests {
68113
val hex = config.encodeToHex()
69114

70115
assertEquals(
71-
"0aba0808011286040a95010a3c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e67180020001208626173654e616d651a06737566666978220a617350726f70657274792a08426c6f636b696e6730003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512390a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120f2472756e496e426c6f636b696e672420002a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d6518002000480012ac040a8f010a390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e63180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a2d0a146a6176612e7574696c2e636f6e63757272656e741211436f6d706c657461626c654675747572651800200020012a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d651800200048000a970308021292030a8e010a3a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d697365180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a290a086d61726b4e616d6512170a096b6f746c696e2e6a7312064a734e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a180a096b6f746c696e2e6a73120750726f6d697365180020002001323d0a370a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1206417069344a7318002000100018013801423a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d6973651800200042130a066b6f746c696e12054f7074496e1800200042170a096b6f746c696e2e6a7312064a734e616d65180020004800",
116+
"0abe0808011288040a95010a3c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e67180020001208626173654e616d651a06737566666978220a617350726f70657274792a08426c6f636b696e6730003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512390a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120f2472756e496e426c6f636b696e672420002a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020004800500012ae040a8f010a390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e63180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a2d0a146a6176612e7574696c2e636f6e63757272656e741211436f6d706c657461626c654675747572651800200020012a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d6518002000480050000a990308021294030a8e010a3a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d697365180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a290a086d61726b4e616d6512170a096b6f746c696e2e6a7312064a734e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a180a096b6f746c696e2e6a73120750726f6d697365180020002001323d0a370a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1206417069344a7318002000100018013801423a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d6973651800200042130a066b6f746c696e12054f7074496e1800200042170a096b6f746c696e2e6a7312064a734e616d651800200048005000",
72117
hex
73118
)
74119

compiler/suspend-transform-plugin-configuration/src/main/kotlin/love/forte/plugin/suspendtrans/configuration/SuspendTransformConfiguration.kt

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,53 @@ class IncludeAnnotation @InternalSuspendTransformConfigurationApi constructor(
246246
}
247247
}
248248

249+
/**
250+
* Controls the nullability applied to the generic argument copied from the
251+
* original suspend function return type into [Transformer.transformReturnType].
252+
*
253+
* This setting is only meaningful when [Transformer.transformReturnType] is not
254+
* `null` and [Transformer.transformReturnTypeGeneric] is `true`.
255+
*
256+
* The main use case is [NON_NULL]: some wrapper APIs, such as Reactive
257+
* Streams-style containers, do not allow `null` elements and represent absence
258+
* as an empty result instead. In that case the generated generic argument should
259+
* describe the emitted element as non-null.
260+
*
261+
* @since 0.14.0
262+
*/
263+
@Serializable
264+
enum class TransformReturnTypeGenericMode {
265+
/**
266+
* Keep the original return type unchanged.
267+
*
268+
* Example: `T` remains `T`, and `Foo?` remains `Foo?`.
269+
*/
270+
NORMAL,
271+
272+
/**
273+
* Make the copied generic argument nullable.
274+
*
275+
* Example: `Foo` becomes `Foo?`.
276+
*
277+
* This is provided for explicit nullable wrappers. The primary motivation of
278+
* this enum is [NON_NULL].
279+
*/
280+
NULLABLE,
281+
282+
/**
283+
* Make the copied generic argument non-nullable.
284+
*
285+
* For type parameters with nullable bounds, this uses a definitely-not-null
286+
* type at the use site, e.g. `T` becomes `T & Any`. It does not rewrite the
287+
* original type parameter declaration or its upper bound.
288+
*
289+
* This is useful for wrappers whose element type cannot be `null`, for
290+
* example Reactive Streams-style APIs where `null` must be modeled as an
291+
* empty result rather than as an emitted element.
292+
*/
293+
NON_NULL
294+
}
295+
249296
@Serializable
250297
class Transformer @InternalSuspendTransformConfigurationApi constructor(
251298
/**
@@ -331,7 +378,33 @@ class Transformer @InternalSuspendTransformConfigurationApi constructor(
331378
*
332379
* @since 0.9.0
333380
*/
334-
val copyAnnotationsToSyntheticProperty: Boolean = false
381+
val copyAnnotationsToSyntheticProperty: Boolean = false,
382+
383+
/**
384+
* Nullability mode for the generic argument copied from the original return type.
385+
*
386+
* This is used only when [transformReturnType] is not `null` and
387+
* [transformReturnTypeGeneric] is `true`. It changes the generic argument
388+
* placed inside the transformed return type, not the original function's type
389+
* parameters or their bounds.
390+
*
391+
* For example, with a transformed return type like `Result<T>` and an
392+
* original return type `T : Foo?`, [TransformReturnTypeGenericMode.NON_NULL]
393+
* produces a use-site type like `Result<T & Any>`.
394+
*
395+
* [TransformReturnTypeGenericMode.NON_NULL] is intended for wrappers whose
396+
* element type does not accept `null`, such as Reactive Streams-style APIs.
397+
* This setting only changes the generated type; the configured transform
398+
* function must still implement the matching runtime semantics, such as
399+
* converting a `null` result to an empty completion when the wrapper requires it.
400+
*
401+
* Keep this property at the end of the constructor parameters because the
402+
* configuration is serialized with ProtoBuf and field order is part of the
403+
* compatibility surface.
404+
*
405+
* @since 0.14.0
406+
*/
407+
val transformReturnTypeGenericMode: TransformReturnTypeGenericMode = TransformReturnTypeGenericMode.NORMAL,
335408
) {
336409
override fun equals(other: Any?): Boolean {
337410
if (this === other) return true
@@ -346,6 +419,7 @@ class Transformer @InternalSuspendTransformConfigurationApi constructor(
346419
if (originFunctionIncludeAnnotations != other.originFunctionIncludeAnnotations) return false
347420
if (syntheticFunctionIncludeAnnotations != other.syntheticFunctionIncludeAnnotations) return false
348421
if (copyAnnotationExcludes != other.copyAnnotationExcludes) return false
422+
if (transformReturnTypeGenericMode != other.transformReturnTypeGenericMode) return false
349423

350424
return true
351425
}
@@ -360,11 +434,12 @@ class Transformer @InternalSuspendTransformConfigurationApi constructor(
360434
result = 31 * result + originFunctionIncludeAnnotations.hashCode()
361435
result = 31 * result + syntheticFunctionIncludeAnnotations.hashCode()
362436
result = 31 * result + copyAnnotationExcludes.hashCode()
437+
result = 31 * result + transformReturnTypeGenericMode.hashCode()
363438
return result
364439
}
365440

366441
override fun toString(): String {
367-
return "Transformer(copyAnnotationExcludes=$copyAnnotationExcludes, markAnnotation=$markAnnotation, transformFunctionInfo=$transformFunctionInfo, transformReturnType=$transformReturnType, transformReturnTypeGeneric=$transformReturnTypeGeneric, originFunctionIncludeAnnotations=$originFunctionIncludeAnnotations, syntheticFunctionIncludeAnnotations=$syntheticFunctionIncludeAnnotations, copyAnnotationsToSyntheticFunction=$copyAnnotationsToSyntheticFunction, copyAnnotationsToSyntheticProperty=$copyAnnotationsToSyntheticProperty)"
442+
return "Transformer(copyAnnotationExcludes=$copyAnnotationExcludes, markAnnotation=$markAnnotation, transformFunctionInfo=$transformFunctionInfo, transformReturnType=$transformReturnType, transformReturnTypeGeneric=$transformReturnTypeGeneric, originFunctionIncludeAnnotations=$originFunctionIncludeAnnotations, syntheticFunctionIncludeAnnotations=$syntheticFunctionIncludeAnnotations, copyAnnotationsToSyntheticFunction=$copyAnnotationsToSyntheticFunction, copyAnnotationsToSyntheticProperty=$copyAnnotationsToSyntheticProperty, transformReturnTypeGenericMode=$transformReturnTypeGenericMode)"
368443
}
369444
}
370445

compiler/suspend-transform-plugin/src/main/kotlin/love/forte/plugin/suspendtrans/fir/SuspendTransformFirTransformerAnnotations.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package love.forte.plugin.suspendtrans.fir
2424

25+
import love.forte.plugin.suspendtrans.configuration.TransformReturnTypeGenericMode
2526
import love.forte.plugin.suspendtrans.configuration.Transformer
2627
import love.forte.plugin.suspendtrans.utils.toClassId
2728
import love.forte.plugin.suspendtrans.utils.toInfo
@@ -72,7 +73,11 @@ internal fun SuspendTransformFirTransformer.resolveReturnConeType(
7273
var typeArguments: Array<ConeTypeProjection> = emptyArray()
7374

7475
if (transformer.transformReturnTypeGeneric) {
75-
typeArguments = arrayOf(ConeKotlinTypeProjectionOut(returnTypeRef.coneType))
76+
typeArguments = arrayOf(
77+
ConeKotlinTypeProjectionOut(
78+
resolveReturnTypeGenericArgument(transformer, returnTypeRef.coneType)
79+
)
80+
)
7681
}
7782

7883
returnType.toClassId().createConeType(
@@ -83,6 +88,25 @@ internal fun SuspendTransformFirTransformer.resolveReturnConeType(
8388
}
8489
?: returnTypeRef.coneType
8590

91+
/**
92+
* Applies the configured nullability mode to the return type copied into the
93+
* transformed return type's generic argument.
94+
*
95+
* `NULLABLE` asks the FIR type context to explicitly mark the use-site type as
96+
* nullable. `NON_NULL` uses Kotlin's definitely-not-null helper so type
97+
* parameters with nullable upper bounds can become `T & Any` instead of only
98+
* clearing the nullable marker.
99+
*/
100+
private fun SuspendTransformFirTransformer.resolveReturnTypeGenericArgument(
101+
transformer: Transformer,
102+
returnType: ConeKotlinType
103+
): ConeKotlinType = when (transformer.transformReturnTypeGenericMode) {
104+
TransformReturnTypeGenericMode.NORMAL -> returnType
105+
TransformReturnTypeGenericMode.NULLABLE -> returnType.withNullability(true, firSession.typeContext)
106+
TransformReturnTypeGenericMode.NON_NULL ->
107+
returnType.makeConeTypeDefinitelyNotNullOrNotNull(firSession.typeContext)
108+
}
109+
86110
/**
87111
* Copies and augments annotations for generated functions, generated properties,
88112
* and the original source function.

0 commit comments

Comments
 (0)