diff --git a/README.md b/README.md index 51201c31..63601b87 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,60 @@ class RandomLettersGenerator { The plugin is currently unable to generate native versions for global properties and functions. In such cases you have to manually create the native versions in your Kotlin native code. +#### Exception propagation + +> Note: for more information about ObjC interop and exceptions please take a look at the +> [Kotlin docs](https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions). + +KMP-NativeCoroutines uses the Kotlin Native runtime to propagate `Exception`s as `NSError`s to Swift/ObjC. +This means that by default only `CancellationException`s are propagated. + +To propagate other exceptions you should mark your functions with the `Throws` annotation. +E.g. use the following to propagate all types of `Throwable`s: +```kotlin +@Throws(Throwable::class) +suspend fun throwingSuspendFunction() { } +``` + +> **Note:** to ignore the `Throws` annotation set the `useThrowsAnnotation` config option to `false`. + +You can also use the `NativeCoroutineThrows` annotation: +```kotlin +@NativeCoroutineThrows(Throwable::class) +suspend fun throwingSuspendFunction() { } + +@get:NativeCoroutineThrows(Throwable::class) +val throwingFlow: Flow = flow { } +``` + +> **Note:** if both `Throws` and `NativeCoroutineThrows` are specified +> only the `NativeCoroutineThrows` one will be used by KMP-NativeCoroutines. + +To reduce code duplication you can also use the `NativeCoroutineThrows` annotation on a class. +```kotlin +@NativeCoroutineThrows(MyException::class) +class ExceptionThrower { + suspend fun throwMyException() { + // This exception will be propagated to Swift/ObjC + throw MyException() + } + + @Throws(MyOtherException::class) + suspend fun throwMyException() { + // Note that annotations on the function or property have a higher precedence. + // This means that the following exception will terminate the program. + throw MyException() + } +} +``` + +or you could specify the exceptions in your `build.gradle.kts`: +```kotlin +nativeCoroutines { + propagatedExceptions = arrayOf("my.company.MyException") +} +``` + #### Custom suffix If you don't like the naming of these generated properties/functions, you can easily change the suffix. diff --git a/build.gradle.kts b/build.gradle.kts index 74a03fb0..98e00e6b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ buildscript { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } dependencies { classpath(Dependencies.Kotlin.gradlePlugin) @@ -14,6 +15,7 @@ allprojects { repositories { mavenCentral() + mavenLocal() } } diff --git a/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt new file mode 100644 index 00000000..ce7c9ddf --- /dev/null +++ b/kmp-nativecoroutines-annotations/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeCoroutineThrows.kt @@ -0,0 +1,17 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.reflect.KClass + +/** + * Defines what exceptions should be propagated as `NSError`s. + * + * Exceptions which are instances of one of the [exceptionClasses] or their subclasses, + * are propagated as a `NSError`s. + * Other Kotlin exceptions are considered unhandled and cause program termination. + * + * @see Throws + */ +@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class NativeCoroutineThrows(vararg val exceptionClasses: KClass) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt index 26c028fa..4036e449 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/CompilerConfigurationKeys.kt @@ -1,6 +1,13 @@ package com.rickclephas.kmp.nativecoroutines.compiler import org.jetbrains.kotlin.config.CompilerConfigurationKey +import org.jetbrains.kotlin.name.FqName internal const val SUFFIX_OPTION_NAME = "suffix" internal val SUFFIX_KEY = CompilerConfigurationKey(SUFFIX_OPTION_NAME) + +internal const val PROPAGATED_EXCEPTIONS_OPTION_NAME = "propagatedExceptions" +internal val PROPAGATED_EXCEPTIONS_KEY = CompilerConfigurationKey>(PROPAGATED_EXCEPTIONS_OPTION_NAME) + +internal const val USE_THROWS_ANNOTATION_OPTION_NAME = "useThrowsAnnotation" +internal val USE_THROWS_ANNOTATION_KEY = CompilerConfigurationKey(USE_THROWS_ANNOTATION_OPTION_NAME) diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt index 0b6cf993..54d70664 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesCommandLineProcessor.kt @@ -4,13 +4,25 @@ import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.name.FqName class KmpNativeCoroutinesCommandLineProcessor: CommandLineProcessor { override val pluginId: String = "com.rickclephas.kmp.nativecoroutines" override val pluginOptions: Collection = listOf( - CliOption(SUFFIX_OPTION_NAME, "string", "suffix used for the generated functions", true) + CliOption(SUFFIX_OPTION_NAME, + valueDescription = "string", + description = "suffix used to generate the native function and property names"), + CliOption(PROPAGATED_EXCEPTIONS_OPTION_NAME, + valueDescription = "fqname", + description = "default Throwable classes that will be propagated as NSError", + required = false, + allowMultipleOccurrences = true), + CliOption(USE_THROWS_ANNOTATION_OPTION_NAME, + valueDescription = "boolean", + description = "indicates if the Throws annotation is used to generate the propagatedExceptions list", + required = false), ) override fun processOption( @@ -19,6 +31,8 @@ class KmpNativeCoroutinesCommandLineProcessor: CommandLineProcessor { configuration: CompilerConfiguration ) = when (option.optionName) { SUFFIX_OPTION_NAME -> configuration.put(SUFFIX_KEY, value) + PROPAGATED_EXCEPTIONS_OPTION_NAME -> configuration.add(PROPAGATED_EXCEPTIONS_KEY, FqName(value)) + USE_THROWS_ANNOTATION_OPTION_NAME -> configuration.put(USE_THROWS_ANNOTATION_KEY, value.toBooleanStrict()) else -> error("Unexpected config option ${option.optionName}") } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt index b4b15044..e23b6a4a 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesComponentRegistrar.kt @@ -12,8 +12,14 @@ class KmpNativeCoroutinesComponentRegistrar: ComponentRegistrar { override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { val suffix = configuration.get(SUFFIX_KEY) ?: return val nameGenerator = NameGenerator(suffix) - SyntheticResolveExtension.registerExtension(project, KmpNativeCoroutinesSyntheticResolveExtension(nameGenerator)) - SyntheticResolveExtension.registerExtension(project, KmpNativeCoroutinesSyntheticResolveExtension.RecursiveCallSyntheticResolveExtension()) - IrGenerationExtension.registerExtension(project, KmpNativeCoroutinesIrGenerationExtension(nameGenerator)) + KmpNativeCoroutinesSyntheticResolveExtension(nameGenerator) + .let { SyntheticResolveExtension.registerExtension(project, it) } + KmpNativeCoroutinesSyntheticResolveExtension.RecursiveCallSyntheticResolveExtension() + .let { SyntheticResolveExtension.registerExtension(project, it) } + KmpNativeCoroutinesIrGenerationExtension( + nameGenerator, + configuration.getList(PROPAGATED_EXCEPTIONS_KEY), + configuration.get(USE_THROWS_ANNOTATION_KEY, true) + ).let { IrGenerationExtension.registerExtension(project, it) } } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt index 6c922575..1647e178 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrGenerationExtension.kt @@ -4,13 +4,17 @@ import com.rickclephas.kmp.nativecoroutines.compiler.utils.NameGenerator import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.name.FqName internal class KmpNativeCoroutinesIrGenerationExtension( - private val nameGenerator: NameGenerator + private val nameGenerator: NameGenerator, + private val propagatedExceptions: List, + private val useThrowsAnnotation: Boolean ): IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { - moduleFragment.accept(KmpNativeCoroutinesIrTransformer(pluginContext, nameGenerator), null) + KmpNativeCoroutinesIrTransformer(pluginContext, nameGenerator, propagatedExceptions, useThrowsAnnotation) + .let { moduleFragment.accept(it, null) } } } diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt index dfb655b3..201b0f26 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesIrTransformer.kt @@ -9,27 +9,27 @@ import org.jetbrains.kotlin.backend.common.ir.passTypeArgumentsFrom import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction -import org.jetbrains.kotlin.ir.expressions.IrBlockBody -import org.jetbrains.kotlin.ir.expressions.IrExpression -import org.jetbrains.kotlin.ir.types.classifierOrFail +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl +import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl -import org.jetbrains.kotlin.ir.types.typeOrNull +import org.jetbrains.kotlin.ir.types.impl.makeTypeProjection import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.utils.addToStdlib.ifTrue internal class KmpNativeCoroutinesIrTransformer( private val context: IrPluginContext, - private val nameGenerator: NameGenerator + private val nameGenerator: NameGenerator, + propagatedExceptions: List, + private val useThrowsAnnotation: Boolean ): IrElementTransformerVoidWithContext() { - private val nativeSuspendFunction = context.referenceNativeSuspendFunction() - private val nativeFlowFunction = context.referenceNativeFlowFunction() - private val stateFlowValueProperty = context.referenceStateFlowValueProperty() - private val sharedFlowReplayCacheProperty = context.referenceSharedFlowReplayCacheProperty() - private val listClass = context.referenceListClass() - override fun visitPropertyNew(declaration: IrProperty): IrStatement { if (declaration.isFakeOverride || declaration.getter?.body != null || declaration.setter != null) return super.visitPropertyNew(declaration) @@ -57,6 +57,8 @@ internal class KmpNativeCoroutinesIrTransformer( return super.visitPropertyNew(declaration) } + private val stateFlowValueProperty = context.referenceStateFlowValueProperty() + private fun createNativeValueBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") @@ -72,6 +74,8 @@ internal class KmpNativeCoroutinesIrTransformer( } } + private val sharedFlowReplayCacheProperty = context.referenceSharedFlowReplayCacheProperty() + private fun createNativeReplayCacheBody(getter: IrFunction, originalGetter: IrSimpleFunction): IrBlockBody { val originalReturnType = originalGetter.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalGetter.returnType}") @@ -79,7 +83,7 @@ internal class KmpNativeCoroutinesIrTransformer( originalGetter.startOffset, originalGetter.endOffset).irBlockBody { val valueType = originalReturnType.getSharedFlowValueTypeOrNull()?.typeOrNull as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported StateFlow value type $originalReturnType") - val returnType = IrSimpleTypeImpl(listClass, false, listOf(valueType), emptyList()) + val returnType = context.irBuiltIns.listClass.typeWith(listOf(valueType)) val valueGetter = sharedFlowReplayCacheProperty.owner.getter?.symbol ?: throw IllegalStateException("Couldn't find StateFlow value getter") +irReturn(irCall(valueGetter, returnType).apply { @@ -102,6 +106,9 @@ internal class KmpNativeCoroutinesIrTransformer( return super.visitFunctionNew(declaration) } + private val nativeSuspendFunction = context.referenceNativeSuspendFunction() + private val nativeFlowFunction = context.referenceNativeFlowFunction() + private fun createNativeBody(declaration: IrFunction, originalFunction: IrSimpleFunction): IrBlockBody { val originalReturnType = originalFunction.returnType as? IrSimpleTypeImpl ?: throw IllegalStateException("Unsupported return type ${originalFunction.returnType}") @@ -110,8 +117,9 @@ internal class KmpNativeCoroutinesIrTransformer( // Call original function var returnType = originalReturnType var call: IrExpression = callOriginalFunction(declaration, originalFunction) - // Call nativeCoroutineScope + // Call nativeCoroutineScope and create propagatedExceptions array val nativeCoroutineScope = callNativeCoroutineScope(declaration) + val propagatedExceptions = createPropagatedExceptionsArray(originalFunction) // Convert Flow types to NativeFlow val flowValueType = returnType.getFlowValueTypeOrNull() if (flowValueType != null) { @@ -126,6 +134,7 @@ internal class KmpNativeCoroutinesIrTransformer( call = irCall(nativeFlowFunction, returnType).apply { putTypeArgument(0, valueType) putValueArgument(0, nativeCoroutineScope) + putValueArgument(1, propagatedExceptions) extensionReceiver = call } } @@ -149,7 +158,8 @@ internal class KmpNativeCoroutinesIrTransformer( call = irCall(nativeSuspendFunction, returnType).apply { putTypeArgument(0, lambda.function.returnType) putValueArgument(0, nativeCoroutineScope) - putValueArgument(1, lambda) + putValueArgument(1, propagatedExceptions) + putValueArgument(2, lambda) } } +irReturn(call) @@ -178,4 +188,31 @@ internal class KmpNativeCoroutinesIrTransformer( dispatchReceiver = function.dispatchReceiverParameter?.let { irGet(it) } } } + + private val propagatedExceptionClasses = propagatedExceptions.map { + context.referenceClass(it) ?: throw NoSuchElementException("Couldn't find $it symbol") + } + private val propagatedExceptionsArrayElementType = context.irBuiltIns.kClassClass.typeWithArguments(listOf( + makeTypeProjection(context.irBuiltIns.throwableType, Variance.OUT_VARIANCE) + )) + + private fun IrBuilderWithScope.createPropagatedExceptionsArray(originalDeclaration: IrDeclaration): IrExpression { + // Find the annotation on the declaration (or a parent class) + fun IrDeclaration.getAnnotation(): IrConstructorCall? = + annotations.findNativeCoroutineThrowsAnnotation() ?: + useThrowsAnnotation.ifTrue { originalDeclaration.annotations.findThrowsAnnotation() } ?: + parentClassOrNull?.getAnnotation() + val annotation = originalDeclaration.getAnnotation() + // Combine the propagatedExceptionClasses list with the classes from the annotation + val propagatedExceptions = buildList { + propagatedExceptionClasses.forEach { + add(IrClassReferenceImpl(startOffset, endOffset, propagatedExceptionsArrayElementType, it, it.defaultType)) + } + annotation?.getValueArgument(0)?.let { vararg -> + if (vararg !is IrVararg) throw IllegalArgumentException("Unexpected vararg: $vararg") + addAll(vararg.elements) + } + } + return irArrayOf(propagatedExceptionsArrayElementType, propagatedExceptions) + } } \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt index bc9a4a6b..fd7496f6 100644 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/KmpNativeCoroutinesSyntheticResolveExtension.kt @@ -16,6 +16,8 @@ import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassMemberScope import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.MemberScope import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.KotlinTypeFactory +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection import java.util.ArrayList internal class KmpNativeCoroutinesSyntheticResolveExtension( @@ -133,7 +135,8 @@ internal class KmpNativeCoroutinesSyntheticResolveExtension( ): PropertyDescriptor { val valueType = coroutinesPropertyDescriptor.getSharedFlowValueTypeOrNull()?.type ?: throw IllegalStateException("Coroutines property doesn't have a value type") - val type = thisDescriptor.module.createListType(valueType) + val type = KotlinTypeFactory.simpleType(thisDescriptor.builtIns.list.defaultType, + arguments = listOf(valueType.asTypeProjection())) return createPropertyDescriptor(thisDescriptor, coroutinesPropertyDescriptor.visibility, name, type, coroutinesPropertyDescriptor.dispatchReceiverParameter, coroutinesPropertyDescriptor.extensionReceiverParameter diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt new file mode 100644 index 00000000..e3e8639c --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/IrArrayOf.kt @@ -0,0 +1,20 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrVarargElement +import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.typeWith + +internal fun IrBuilderWithScope.irArrayOf( + elementType: IrType, + elements: List +): IrExpression { + val arrayType = context.irBuiltIns.arrayClass.typeWith(elementType) + val vararg = IrVarargImpl(startOffset, endOffset, arrayType, elementType, elements) + return irCall(context.irBuiltIns.arrayOf, arrayType, listOf(elementType)).apply { + putValueArgument(0, vararg) + } +} \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt deleted file mode 100644 index 80ff21ab..00000000 --- a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/List.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.rickclephas.kmp.nativecoroutines.compiler.utils - -import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.symbols.IrClassSymbol -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection - -private val listFqName = FqName("kotlin.collections.List") -private val listClassId = ClassId.topLevel(listFqName) - -internal fun ModuleDescriptor.findListClassifier(): ClassifierDescriptor = - findClassifierAcrossModuleDependencies(listClassId) - ?: throw NoSuchElementException("Couldn't find List classifier") - -internal fun ModuleDescriptor.createListType(valueType: KotlinType): KotlinType = - KotlinTypeFactory.simpleType(findListClassifier().defaultType, arguments = listOf(valueType.asTypeProjection())) - -internal fun IrPluginContext.referenceListClass(): IrClassSymbol = - referenceClass(listFqName) ?: throw NoSuchElementException("Couldn't find List symbol") diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt new file mode 100644 index 00000000..662dd10e --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/NativeCoroutineThrows.kt @@ -0,0 +1,10 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.util.findAnnotation +import org.jetbrains.kotlin.name.FqName + +private val nativeCoroutineThrowsFqName = FqName("com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows") + +internal fun List.findNativeCoroutineThrowsAnnotation() = + findAnnotation(nativeCoroutineThrowsFqName) \ No newline at end of file diff --git a/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt new file mode 100644 index 00000000..93a2263c --- /dev/null +++ b/kmp-nativecoroutines-compiler/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/compiler/utils/Throws.kt @@ -0,0 +1,9 @@ +package com.rickclephas.kmp.nativecoroutines.compiler.utils + +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.util.findAnnotation +import org.jetbrains.kotlin.name.FqName + +private val throwsFqName = FqName("kotlin.Throws") + +internal fun List.findThrowsAnnotation() = findAnnotation(throwsFqName) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/build.gradle.kts b/kmp-nativecoroutines-core/build.gradle.kts index 5e922162..3602a3c8 100644 --- a/kmp-nativecoroutines-core/build.gradle.kts +++ b/kmp-nativecoroutines-core/build.gradle.kts @@ -47,6 +47,9 @@ kotlin { } val appleMain by creating { dependsOn(nativeCoroutinesMain) + dependencies { + api("com.rickclephas.kmp:nserror-kt:0.1.0-SNAPSHOT") + } } val appleTest by creating { dependsOn(nativeCoroutinesTest) diff --git a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt index ea6443cb..bfde5b9f 100644 --- a/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt +++ b/kmp-nativecoroutines-core/src/appleMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorApple.kt @@ -1,28 +1,24 @@ package com.rickclephas.kmp.nativecoroutines -import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.convert import platform.Foundation.NSError -import platform.Foundation.NSLocalizedDescriptionKey +import kotlin.coroutines.cancellation.CancellationException import kotlin.native.concurrent.freeze +import kotlin.reflect.KClass +import com.rickclephas.kmp.nserrorkt.throwAsNSError actual typealias NativeError = NSError /** - * Converts a [Throwable] to a [NSError]. + * Uses Kotlin Native runtime functions to convert a [Throwable] to a [NSError]. * - * The returned [NSError] has `KotlinException` as the [NSError.domain], `0` as the [NSError.code] and - * the [NSError.localizedDescription] is set to the [Throwable.message]. + * Warning: [Throwable]s that aren't of a [propagatedExceptions] type will terminate the program. + * Note: [CancellationException]s are always propagated. * - * The Kotlin throwable can be retrieved from the [NSError.userInfo] with the key `KotlinException`. + * @param propagatedExceptions an array of [Throwable] types that should be propagated as [NSError]s. */ -@OptIn(UnsafeNumber::class) -internal actual fun Throwable.asNativeError(): NativeError { - val userInfo = mutableMapOf() - userInfo["KotlinException"] = this.freeze() - val message = message - if (message != null) { - userInfo[NSLocalizedDescriptionKey] = message - } - return NSError.errorWithDomain("KotlinException", 0.convert(), userInfo) -} +internal actual fun Throwable.asNativeError( + propagatedExceptions: Array> +): NSError = freeze().throwAsNSError(*propagatedExceptions.run { + if (contains(CancellationException::class)) this + else plus(CancellationException::class) +}) diff --git a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt index 8989f349..0911341a 100644 --- a/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt +++ b/kmp-nativecoroutines-core/src/appleTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeErrorAppleTests.kt @@ -2,7 +2,11 @@ package com.rickclephas.kmp.nativecoroutines import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.convert +import platform.Foundation.NSError +import kotlin.coroutines.cancellation.CancellationException import kotlin.native.concurrent.isFrozen +import kotlin.native.internal.ObjCErrorException +import kotlin.random.Random import kotlin.test.* class NativeErrorAppleTests { @@ -11,7 +15,7 @@ class NativeErrorAppleTests { fun ensureFrozen() { val exception = RandomException() assertFalse(exception.isFrozen, "Exception shouldn't be frozen yet") - val nsError = exception.asNativeError() + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertTrue(nsError.isFrozen, "NSError should be frozen") assertTrue(exception.isFrozen, "Exception should be frozen") } @@ -20,7 +24,7 @@ class NativeErrorAppleTests { @OptIn(UnsafeNumber::class) fun ensureNSErrorDomainAndCodeAreCorrect() { val exception = RandomException() - val nsError = exception.asNativeError() + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertEquals("KotlinException", nsError.domain, "Incorrect NSError domain") assertEquals(0.convert(), nsError.code, "Incorrect NSError code") } @@ -28,7 +32,7 @@ class NativeErrorAppleTests { @Test fun ensureLocalizedDescriptionIsSetToMessage() { val exception = RandomException() - val nsError = exception.asNativeError() + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertEquals(exception.message, nsError.localizedDescription, "Localized description isn't set to message") } @@ -36,8 +40,24 @@ class NativeErrorAppleTests { @Test fun ensureExceptionIsPartOfUserInfo() { val exception = RandomException() - val nsError = exception.asNativeError() + val nsError = exception.asNativeError(arrayOf(RandomException::class)) assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") assertSame(exception, nsError.kotlinCause, "Incorrect kotlinCause") } -} \ No newline at end of file + + @Test + fun `ensure CancellationException is always propagated`() { + val exception = CancellationException() + val nsError = exception.asNativeError(arrayOf()) + assertSame(exception, nsError.userInfo["KotlinException"], "Exception isn't part of the user info") + } + + @Test + @OptIn(UnsafeNumber::class) + fun `ensure ObjCErrorException is always propagated`() { + val error = NSError.errorWithDomain(Random.nextString(), Random.nextInt().convert(), null) + val exception = ObjCErrorException(Random.nextString(), error) + val nsError = exception.asNativeError(arrayOf()) + assertEquals(error, nsError, "NSError isn't equal") + } +} diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt index 9d62a0ae..714f92af 100644 --- a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomException.kt @@ -5,6 +5,4 @@ import kotlin.random.Random /** * An exception with a message consisting of 20 random capital letter. */ -internal class RandomException: Exception( - (1..20).map { Random.nextInt(65, 91).toChar() }.joinToString() -) \ No newline at end of file +internal class RandomException: Exception(Random.nextString(20)) \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt new file mode 100644 index 00000000..46a3c835 --- /dev/null +++ b/kmp-nativecoroutines-core/src/commonTest/kotlin/com/rickclephas/kmp/nativecoroutines/RandomString.kt @@ -0,0 +1,10 @@ +package com.rickclephas.kmp.nativecoroutines + +import kotlin.random.Random + +/** + * Generates a random string with the specified [length]. + */ +internal fun Random.nextString(length: Int = 10) = (1..length).map { + nextInt(65, 91).toChar() +}.joinToString("") \ No newline at end of file diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt index 0c44b12b..daab1d92 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeError.kt @@ -1,5 +1,7 @@ package com.rickclephas.kmp.nativecoroutines +import kotlin.reflect.KClass + /** * Represents an error in a way that the specific platform is able to handle */ @@ -7,5 +9,9 @@ expect class NativeError /** * Converts a [Throwable] to a [NativeError]. + * + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. */ -internal expect fun Throwable.asNativeError(): NativeError +internal expect fun Throwable.asNativeError( + propagatedExceptions: Array> +): NativeError diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt index 02734a32..f606dafd 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlow.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import kotlin.reflect.KClass /** * A function that collects a [Flow] via callbacks. @@ -18,10 +19,14 @@ typealias NativeFlow = (onItem: NativeCallback, onComplete: NativeCallback * Creates a [NativeFlow] for this [Flow]. * * @param scope the [CoroutineScope] to use for the collection, or `null` to use the [defaultCoroutineScope]. + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. * @receiver the [Flow] to collect. * @see Flow.collect */ -fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { +fun Flow.asNativeFlow( + scope: CoroutineScope? = null, + propagatedExceptions: Array> = arrayOf() +): NativeFlow { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onItem: NativeCallback, onComplete: NativeCallback -> val job = coroutineScope.launch { @@ -33,14 +38,14 @@ fun Flow.asNativeFlow(scope: CoroutineScope? = null): NativeFlow { // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onComplete(e.asNativeError()) + onComplete(e.asNativeError(propagatedExceptions)) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onComplete(cause.asNativeError()) + onComplete(cause.asNativeError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() -} \ No newline at end of file +} diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt index f7253963..648b1e78 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesMain/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspend.kt @@ -3,6 +3,7 @@ package com.rickclephas.kmp.nativecoroutines import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlin.reflect.KClass /** * A function that awaits a suspend function via callbacks. @@ -16,9 +17,14 @@ typealias NativeSuspend = (onResult: NativeCallback, onError: NativeCallba * Creates a [NativeSuspend] for the provided suspend [block]. * * @param scope the [CoroutineScope] to run the [block] in, or `null` to use the [defaultCoroutineScope]. + * @param propagatedExceptions an array of [Throwable] types that should be propagated to ObjC/Swift. * @param block the suspend block to await. */ -fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): NativeSuspend { +fun nativeSuspend( + scope: CoroutineScope? = null, + propagatedExceptions: Array> = arrayOf(), + block: suspend () -> T +): NativeSuspend { val coroutineScope = scope ?: defaultCoroutineScope return (collect@{ onResult: NativeCallback, onError: NativeCallback -> val job = coroutineScope.launch { @@ -29,14 +35,14 @@ fun nativeSuspend(scope: CoroutineScope? = null, block: suspend () -> T): Na // this is required since the job could be cancelled before it is started throw e } catch (e: Throwable) { - onError(e.asNativeError()) + onError(e.asNativeError(propagatedExceptions)) } } job.invokeOnCompletion { cause -> // Only handle CancellationExceptions, all other exceptions should be handled inside the job if (cause !is CancellationException) return@invokeOnCompletion - onError(cause.asNativeError()) + onError(cause.asNativeError(propagatedExceptions)) } return@collect job.asNativeCancellable() }).freeze() -} \ No newline at end of file +} diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt index 3e5c63a2..a9769b0d 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeFlowTests.kt @@ -24,7 +24,7 @@ class NativeFlowTests { fun ensureExceptionsAreReceivedAsErrors() = runTest { val exception = RandomException() val flow = flow { throw exception } - val nativeFlow = flow.asNativeFlow(this) + val nativeFlow = flow.asNativeFlow(this, arrayOf(RandomException::class)) val completionCount = atomic(0) nativeFlow({ _, _ -> }, { error, _ -> assertNotNull(error, "Flow should complete with an error") diff --git a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt index 229d1fc1..30073af1 100644 --- a/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt +++ b/kmp-nativecoroutines-core/src/nativeCoroutinesTest/kotlin/com/rickclephas/kmp/nativecoroutines/NativeSuspendTests.kt @@ -36,7 +36,9 @@ class NativeSuspendTests { @Test fun ensureExceptionsAreReceivedAsErrors() = runTest { val exception = RandomException() - val nativeSuspend = nativeSuspend(this) { delayAndThrow(100, exception) } + val nativeSuspend = nativeSuspend(this, arrayOf(RandomException::class)) { + delayAndThrow(100, exception) + } val receivedResultCount = atomic(0) val receivedErrorCount = atomic(0) nativeSuspend({ _, _ -> diff --git a/kmp-nativecoroutines-gradle-plugin/build.gradle.kts b/kmp-nativecoroutines-gradle-plugin/build.gradle.kts index 41b89353..ab46207e 100644 --- a/kmp-nativecoroutines-gradle-plugin/build.gradle.kts +++ b/kmp-nativecoroutines-gradle-plugin/build.gradle.kts @@ -1,7 +1,6 @@ plugins { `java-gradle-plugin` kotlin("jvm") - kotlin("kapt") `kmp-nativecoroutines-publish` id("com.gradle.plugin-publish") version "0.15.0" } @@ -14,6 +13,10 @@ val copyVersionTemplate by tasks.registering(Copy::class) { filteringCharset = "UTF-8" } +kotlin.sourceSets.all { + languageSettings.optIn("kotlin.RequiresOptIn") +} + tasks.compileKotlin { dependsOn(copyVersionTemplate) } diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt index f2431b34..4772f243 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesExtension.kt @@ -1,5 +1,19 @@ package com.rickclephas.kmp.nativecoroutines.gradle open class KmpNativeCoroutinesExtension { + + /** + * The suffix used to generate the native function and property names. + */ var suffix: String = "Native" + + /** + * The default array of [Throwable] types that should be propagated as `NSError`s. + */ + var propagatedExceptions: Array = arrayOf() + + /** + * Indicates if the [Throws] annotation is used to generate the [propagatedExceptions] list. + */ + var useThrowsAnnotation: Boolean = true } \ No newline at end of file diff --git a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt index d2444414..0e25e293 100644 --- a/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt +++ b/kmp-nativecoroutines-gradle-plugin/src/main/kotlin/com/rickclephas/kmp/nativecoroutines/gradle/KmpNativeCoroutinesPlugin.kt @@ -24,12 +24,19 @@ class KmpNativeCoroutinesPlugin: KotlinCompilerPluginSupportPlugin { override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = kotlinCompilation.target.let { it is KotlinNativeTarget && it.konanTarget.family.isAppleFamily } + @OptIn(ExperimentalStdlibApi::class) override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { val project = kotlinCompilation.target.project val extension = project.extensions.findByType(KmpNativeCoroutinesExtension::class.java) ?: KmpNativeCoroutinesExtension() return project.provider { - listOf(SubpluginOption("suffix", extension.suffix)) + buildList { + add(SubpluginOption("suffix", extension.suffix)) + extension.propagatedExceptions.forEach { + add(SubpluginOption("propagatedExceptions", it)) + } + add(SubpluginOption("useThrowsAnnotation", extension.useThrowsAnnotation.toString())) + } } } diff --git a/sample/IntegrationTests/CompilerIntegrationTests.swift b/sample/IntegrationTests/CompilerIntegrationTests.swift index 2e2d3caf..3cb1b737 100644 --- a/sample/IntegrationTests/CompilerIntegrationTests.swift +++ b/sample/IntegrationTests/CompilerIntegrationTests.swift @@ -12,6 +12,86 @@ import NativeCoroutinesSampleShared class CompilerIntegrationTests: XCTestCase { private typealias IntegrationTests = NativeCoroutinesSampleShared.CompilerIntegrationTests + private let testExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.TestException" + private let classTestExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException" + private let moduleTestExceptionMessage = "com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException" + + func testThrowWithThrows() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithThrowsNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testThrowWithNativeCoroutineThrows() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithNativeCoroutineThrowsNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testThrowWithNativeCoroutineThrowsOnClass() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithNativeCoroutineThrowsOnClassNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.classTestExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testThrowWithPropagatedExceptionInModule() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.throwWithPropagatedExceptionInModuleNative()({ _, unit in unit}, { error, unit in + let error = error as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.moduleTestExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testFlowThrow() { + let integrationTests = IntegrationTests() + let errorExpectation = expectation(description: "Waiting for error") + _ = integrationTests.flowThrowNative({ _, unit in unit}, { error, unit in + let error = error! as NSError + let exception = error.userInfo["KotlinException"] as! KotlinThrowable + XCTAssertEqual(exception.message, self.testExceptionMessage, "Received incorrect exception") + errorExpectation.fulfill() + return unit + }) + wait(for: [errorExpectation], timeout: 2) + } + + func testStateFlowValue() { + let integrationTests = IntegrationTests() + let value = integrationTests.stateFlowNativeValue + XCTAssertEqual(value, 1, "Received inccorect value") + } + + func testSharedFlowReplayCache() { + let integrationTests = IntegrationTests() + let replayCache = integrationTests.sharedFlowNativeReplayCache + XCTAssertEqual(replayCache, [1, 2], "Received inccorect value") + } func testReturnGenericClassValue() { let integrationTests = IntegrationTests() diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index a2d9a51c..652099f4 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -2,6 +2,7 @@ buildscript { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") @@ -13,5 +14,6 @@ buildscript { allprojects { repositories { mavenCentral() + mavenLocal() } } diff --git a/sample/settings.gradle.kts b/sample/settings.gradle.kts index 3bc1adbb..09d98413 100644 --- a/sample/settings.gradle.kts +++ b/sample/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } } diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 77b28776..05216a79 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -72,4 +72,8 @@ afterEvaluate { tasks.withType(org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask::class.java).forEach { it.baseName = "NativeCoroutinesSampleShared" } +} + +nativeCoroutines { + propagatedExceptions = arrayOf("com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException") } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt index 2c1165a0..eab512fb 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/RandomLettersGenerator.kt @@ -1,16 +1,18 @@ package com.rickclephas.kmp.nativecoroutines.sample import kotlinx.coroutines.delay +import kotlin.coroutines.cancellation.CancellationException import kotlin.random.Random import kotlin.time.Duration.Companion.seconds object RandomLettersGenerator { + private class BestExceptionEver: RuntimeException("the best exception ever") + + @Throws(BestExceptionEver::class, CancellationException::class) suspend fun getRandomLetters(throwException: Boolean): String { delay(2.seconds) - if (throwException) { - throw RuntimeException("the best exception ever") - } + if (throwException) throw BestExceptionEver() val chars = mutableListOf() repeat(5) { chars.add(Random.nextInt(65, 91).toChar()) diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt index fbf6ad7b..cfe036c2 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/CompilerIntegrationTests.kt @@ -1,11 +1,47 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import com.rickclephas.kmp.nativecoroutines.NativeCoroutineThrows +import com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException +import com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException +import com.rickclephas.kmp.nativecoroutines.sample.utils.TestException +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze +import kotlinx.coroutines.flow.* +import kotlin.coroutines.cancellation.CancellationException +@NativeCoroutineThrows(ClassTestException::class) class CompilerIntegrationTests: IntegrationTests() { + @Throws(TestException::class, CancellationException::class) + suspend fun throwWithThrows() { + throw TestException() + } + + @NativeCoroutineThrows(TestException::class) + suspend fun throwWithNativeCoroutineThrows() { + throw TestException() + } + + suspend fun throwWithNativeCoroutineThrowsOnClass() { + throw ClassTestException() + } + + suspend fun throwWithPropagatedExceptionInModule() { + throw ModuleTestException() + } + + @get:NativeCoroutineThrows(TestException::class) + val flowThrow: Flow = flow { + throw TestException() + } + + val stateFlow: StateFlow = MutableStateFlow(1) + + val sharedFlow: SharedFlow = MutableSharedFlow(2).apply { + tryEmit(1) + tryEmit(2) + } + suspend fun returnGenericClassValue(value: V): V { return value } @@ -44,4 +80,8 @@ class CompilerIntegrationTests: IntegrationTests() { suspend fun returnIgnoredValue(value: Int): Int { return value } -} \ No newline at end of file + + init { + freeze() + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt index 86a475c9..464a77e7 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/FlowIntegrationTests.kt @@ -1,25 +1,28 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class FlowIntegrationTests: IntegrationTests() { - fun getFlow(count: Int, delay: Long) = flow { + fun getFlow(count: Int, delay: Long): Flow = flow { repeat(count) { delay(delay) emit(it) } } - fun getFlowWithNull(count: Int, nullIndex: Int, delay: Long) = flow { + fun getFlowWithNull(count: Int, nullIndex: Int, delay: Long): Flow = flow { repeat(count) { delay(delay) emit(if (it == nullIndex) null else it) } } - fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long) = flow { + @Throws(Exception::class) + fun getFlowWithException(count: Int, exceptionIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) if (it == exceptionIndex) throw Exception(message) @@ -27,7 +30,8 @@ class FlowIntegrationTests: IntegrationTests() { } } - fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long) = flow { + @Throws(Error::class) + fun getFlowWithError(count: Int, errorIndex: Int, message: String, delay: Long): Flow = flow { repeat(count) { delay(delay) if (it == errorIndex) throw Error(message) @@ -35,11 +39,15 @@ class FlowIntegrationTests: IntegrationTests() { } } - fun getFlowWithCallback(count: Int, callbackIndex: Int, delay: Long, callback: () -> Unit) = flow { + fun getFlowWithCallback(count: Int, callbackIndex: Int, delay: Long, callback: () -> Unit): Flow = flow { repeat(count) { delay(delay) if (it == callbackIndex) callback() emit(it) } } + + init { + freeze() + } } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt index b71fb341..48c40695 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/IntegrationTests.kt @@ -1,7 +1,6 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope -import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -17,8 +16,4 @@ abstract class IntegrationTests { val uncompletedJobCount: Int get() = job.children.count { !it.isCompleted } - - init { - freeze() - } } \ No newline at end of file diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt index 6cb25cb0..6deffbf9 100644 --- a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/tests/SuspendIntegrationTests.kt @@ -1,8 +1,10 @@ package com.rickclephas.kmp.nativecoroutines.sample.tests +import com.rickclephas.kmp.nativecoroutines.sample.utils.freeze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlin.coroutines.cancellation.CancellationException class SuspendIntegrationTests: IntegrationTests() { @@ -16,11 +18,13 @@ class SuspendIntegrationTests: IntegrationTests() { return null } + @Throws(Exception::class) suspend fun throwException(message: String, delay: Long): Int { delay(delay) throw Exception(message) } + @Throws(Error::class, CancellationException::class) suspend fun throwError(message: String, delay: Long): Int { delay(delay) throw Error(message) @@ -40,4 +44,8 @@ class SuspendIntegrationTests: IntegrationTests() { } } } + + init { + freeze() + } } diff --git a/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt new file mode 100644 index 00000000..6746d5ab --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/rickclephas/kmp/nativecoroutines/sample/utils/Exceptions.kt @@ -0,0 +1,7 @@ +package com.rickclephas.kmp.nativecoroutines.sample.utils + +class TestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.TestException") + +class ClassTestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.ClassTestException") + +class ModuleTestException: Exception("com.rickclephas.kmp.nativecoroutines.sample.utils.ModuleTestException") diff --git a/settings.gradle.kts b/settings.gradle.kts index bd7149a8..99e9ba9a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() + mavenLocal() } }