Simplify calling primitive and nullable @Throws from Swift
Prefer Swift code simplicity over following Obj-C conventions
This commit is contained in:
committed by
SvyatoslavScherbina
parent
cd9812b504
commit
537188fce8
+21
-41
@@ -755,8 +755,6 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
|
||||
}
|
||||
|
||||
var errorOutPtr: LLVMValueRef? = null
|
||||
var kotlinResultOutPtr: LLVMValueRef? = null
|
||||
lateinit var kotlinResultOutBridge: TypeBridge
|
||||
|
||||
val kotlinArgs = methodBridge.paramBridges.mapIndexedNotNull { index, paramBridge ->
|
||||
val parameter = param(index)
|
||||
@@ -774,13 +772,6 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
|
||||
errorOutPtr = parameter
|
||||
null
|
||||
}
|
||||
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> {
|
||||
assert(kotlinResultOutPtr == null)
|
||||
kotlinResultOutPtr = parameter
|
||||
kotlinResultOutBridge = paramBridge.bridge
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,12 +794,12 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
|
||||
|
||||
MethodBridge.ReturnValue.WithError.Success -> Int8(0).llvm // false
|
||||
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> {
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> {
|
||||
if (returnType.successBridge == MethodBridge.ReturnValue.Instance.InitResult) {
|
||||
// Release init receiver, as required by convention.
|
||||
callFromBridge(objcRelease, listOf(param(0)))
|
||||
}
|
||||
kNullInt8Ptr
|
||||
Zero(returnType.objCType(context)).llvm
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,13 +809,6 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
|
||||
|
||||
val targetResult = callKotlin(kotlinArgs, Lifetime.ARGUMENT, exceptionHandler)
|
||||
|
||||
kotlinResultOutPtr?.let {
|
||||
ifThen(icmpNe(it, LLVMConstNull(it.type)!!)) {
|
||||
val objCResult = kotlinToObjC(targetResult!!, kotlinResultOutBridge)
|
||||
store(objCResult, it)
|
||||
}
|
||||
}
|
||||
|
||||
tailrec fun genReturnValueOnSuccess(returnBridge: MethodBridge.ReturnValue): LLVMValueRef? = when (returnBridge) {
|
||||
MethodBridge.ReturnValue.Void -> null
|
||||
MethodBridge.ReturnValue.HashCode -> {
|
||||
@@ -833,7 +817,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
|
||||
}
|
||||
is MethodBridge.ReturnValue.Mapped -> kotlinToObjC(targetResult!!, returnBridge.bridge)
|
||||
MethodBridge.ReturnValue.WithError.Success -> Int8(1).llvm // true
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> genReturnValueOnSuccess(returnBridge.successBridge)
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> genReturnValueOnSuccess(returnBridge.successBridge)
|
||||
MethodBridge.ReturnValue.Instance.InitResult -> param(0)
|
||||
MethodBridge.ReturnValue.Instance.FactoryResult -> kotlinReferenceToObjC(targetResult!!) // provided by [callKotlin]
|
||||
}
|
||||
@@ -904,8 +888,6 @@ private fun ObjCExportCodeGenerator.generateKotlinToObjCBridge(
|
||||
|
||||
val result = generateFunction(codegen, functionType, "kotlin2objc") {
|
||||
var errorOutPtr: LLVMValueRef? = null
|
||||
var kotlinResultOutPtr: LLVMValueRef? = null
|
||||
lateinit var kotlinResultOutBridge: TypeBridge
|
||||
|
||||
val parameters = irFunction.allParameters.mapIndexed { index, parameterDescriptor ->
|
||||
parameterDescriptor to param(index)
|
||||
@@ -937,12 +919,9 @@ private fun ObjCExportCodeGenerator.generateKotlinToObjCBridge(
|
||||
error("Method is not instance and thus can't have bridge for overriding: $baseMethod")
|
||||
|
||||
MethodBridgeValueParameter.ErrorOutParameter ->
|
||||
alloca(int8TypePtr).also { errorOutPtr = it }
|
||||
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter ->
|
||||
alloca(bridge.bridge.objCType).also {
|
||||
kotlinResultOutPtr = it
|
||||
kotlinResultOutBridge = bridge.bridge
|
||||
alloca(int8TypePtr).also {
|
||||
store(kNullInt8Ptr, it)
|
||||
errorOutPtr = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,17 +960,20 @@ private fun ObjCExportCodeGenerator.generateKotlinToObjCBridge(
|
||||
ifThen(icmpEq(targetResult, Int8(0).llvm)) {
|
||||
rethrow()
|
||||
}
|
||||
|
||||
kotlinResultOutPtr?.let {
|
||||
objCToKotlin(load(it), kotlinResultOutBridge, lifetime)
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> {
|
||||
ifThen(icmpEq(targetResult, kNullInt8Ptr)) {
|
||||
rethrow()
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> {
|
||||
if (returnBridge.successMayBeZero) {
|
||||
val error = load(errorOutPtr!!)
|
||||
ifThen(icmpNe(error, kNullInt8Ptr)) {
|
||||
rethrow()
|
||||
}
|
||||
} else {
|
||||
ifThen(icmpEq(targetResult, kNullInt8Ptr)) {
|
||||
rethrow()
|
||||
}
|
||||
}
|
||||
assert(kotlinResultOutPtr == null)
|
||||
genKotlinBaseMethodResult(lifetime, returnBridge.successBridge)
|
||||
}
|
||||
|
||||
@@ -1446,7 +1428,6 @@ private val MethodBridgeParameter.objCType: LLVMTypeRef get() = when (this) {
|
||||
is MethodBridgeReceiver -> ReferenceBridge.objCType
|
||||
MethodBridgeSelector -> int8TypePtr
|
||||
MethodBridgeValueParameter.ErrorOutParameter -> pointerType(ReferenceBridge.objCType)
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> pointerType(this.bridge.objCType)
|
||||
}
|
||||
|
||||
private fun MethodBridge.ReturnValue.objCType(context: Context): LLVMTypeRef {
|
||||
@@ -1457,8 +1438,8 @@ private fun MethodBridge.ReturnValue.objCType(context: Context): LLVMTypeRef {
|
||||
MethodBridge.ReturnValue.WithError.Success -> ObjCValueType.BOOL.llvmType
|
||||
|
||||
MethodBridge.ReturnValue.Instance.InitResult,
|
||||
MethodBridge.ReturnValue.Instance.FactoryResult,
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> ReferenceBridge.objCType
|
||||
MethodBridge.ReturnValue.Instance.FactoryResult -> ReferenceBridge.objCType
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> this.successBridge.objCType(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1503,8 +1484,8 @@ private fun MethodBridge.ReturnValue.getObjCEncoding(targetFamily: Family): Stri
|
||||
MethodBridge.ReturnValue.WithError.Success -> ObjCValueType.BOOL.encoding
|
||||
|
||||
MethodBridge.ReturnValue.Instance.InitResult,
|
||||
MethodBridge.ReturnValue.Instance.FactoryResult,
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> ReferenceBridge.objCEncoding
|
||||
MethodBridge.ReturnValue.Instance.FactoryResult -> ReferenceBridge.objCEncoding
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> this.successBridge.getObjCEncoding(targetFamily)
|
||||
}
|
||||
|
||||
private val MethodBridgeParameter.objCEncoding: String get() = when (this) {
|
||||
@@ -1512,7 +1493,6 @@ private val MethodBridgeParameter.objCEncoding: String get() = when (this) {
|
||||
is MethodBridgeReceiver -> ReferenceBridge.objCEncoding
|
||||
MethodBridgeSelector -> ":"
|
||||
MethodBridgeValueParameter.ErrorOutParameter -> "^${ReferenceBridge.objCEncoding}"
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> "^${this.bridge.objCEncoding}"
|
||||
}
|
||||
|
||||
private val TypeBridge.objCEncoding: String get() = when (this) {
|
||||
|
||||
+3
-6
@@ -35,7 +35,6 @@ internal object MethodBridgeSelector : MethodBridgeParameter()
|
||||
internal sealed class MethodBridgeValueParameter : MethodBridgeParameter() {
|
||||
data class Mapped(val bridge: TypeBridge) : MethodBridgeValueParameter()
|
||||
object ErrorOutParameter : MethodBridgeValueParameter()
|
||||
data class KotlinResultOutParameter(val bridge: TypeBridge) : MethodBridgeValueParameter()
|
||||
}
|
||||
|
||||
internal data class MethodBridge(
|
||||
@@ -55,7 +54,7 @@ internal data class MethodBridge(
|
||||
|
||||
sealed class WithError : ReturnValue() {
|
||||
object Success : WithError()
|
||||
data class RefOrNull(val successBridge: ReturnValue) : WithError()
|
||||
data class ZeroForError(val successBridge: ReturnValue, val successMayBeZero: Boolean) : WithError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +86,7 @@ internal fun MethodBridge.valueParametersAssociated(
|
||||
when (it) {
|
||||
is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next()
|
||||
|
||||
is MethodBridgeValueParameter.ErrorOutParameter,
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> it to null
|
||||
is MethodBridgeValueParameter.ErrorOutParameter -> it to null
|
||||
}
|
||||
}.also { assert(!kotlinParameters.hasNext()) }
|
||||
}
|
||||
@@ -103,8 +101,7 @@ internal fun MethodBridge.parametersAssociated(
|
||||
is MethodBridgeValueParameter.Mapped, MethodBridgeReceiver.Instance ->
|
||||
it to kotlinParameters.next()
|
||||
|
||||
MethodBridgeReceiver.Static, MethodBridgeSelector, MethodBridgeValueParameter.ErrorOutParameter,
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter ->
|
||||
MethodBridgeReceiver.Static, MethodBridgeSelector, MethodBridgeValueParameter.ErrorOutParameter ->
|
||||
it to null
|
||||
|
||||
MethodBridgeReceiver.Factory -> {
|
||||
|
||||
+18
-15
@@ -15,7 +15,6 @@ import org.jetbrains.kotlin.builtins.getReturnTypeFromFunctionType
|
||||
import org.jetbrains.kotlin.builtins.getValueParameterTypesFromFunctionType
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.name.ClassId
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.resolve.constants.ArrayValue
|
||||
import org.jetbrains.kotlin.resolve.constants.KClassValue
|
||||
import org.jetbrains.kotlin.resolve.deprecation.Deprecation
|
||||
@@ -582,7 +581,6 @@ internal class ObjCExportTranslatorImpl(
|
||||
}
|
||||
}
|
||||
MethodBridgeValueParameter.ErrorOutParameter -> "error"
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> "result"
|
||||
}
|
||||
|
||||
val uniqueName = unifyName(candidateName, usedNames)
|
||||
@@ -592,15 +590,6 @@ internal class ObjCExportTranslatorImpl(
|
||||
is MethodBridgeValueParameter.Mapped -> mapType(p!!.type, bridge.bridge, objCExportScope)
|
||||
MethodBridgeValueParameter.ErrorOutParameter ->
|
||||
ObjCPointerType(ObjCNullableReferenceType(ObjCClassType("NSError")), nullable = true)
|
||||
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> {
|
||||
val resultType = mapType(method.returnType!!, bridge.bridge, objCExportScope)
|
||||
// Note: non-nullable reference or pointer type is unusable here
|
||||
// when passing reference to local variable from Swift because it
|
||||
// would require a non-null initializer then.
|
||||
val pointeeType = resultType.makeNullableIfReferenceOrPointer()
|
||||
ObjCPointerType(pointeeType, nullable = true)
|
||||
}
|
||||
}
|
||||
|
||||
parameters += ObjCParameter(uniqueName, p, type)
|
||||
@@ -621,6 +610,14 @@ internal class ObjCExportTranslatorImpl(
|
||||
val attributes = mutableListOf<String>()
|
||||
|
||||
attributes += swiftNameAttribute(swiftName)
|
||||
if (baseMethodBridge.returnBridge is MethodBridge.ReturnValue.WithError.ZeroForError
|
||||
&& baseMethodBridge.returnBridge.successMayBeZero) {
|
||||
|
||||
// Method may return zero on success, but
|
||||
// standard Objective-C convention doesn't suppose this happening.
|
||||
// Add non-standard convention hint for Swift:
|
||||
attributes += "swift_error(nonnull_error)" // Means "failure <=> (error != nil)".
|
||||
}
|
||||
|
||||
if (method is ConstructorDescriptor && !method.constructedClass.isArray) { // TODO: check methodBridge instead.
|
||||
attributes += "objc_designated_initializer"
|
||||
@@ -659,11 +656,17 @@ internal class ObjCExportTranslatorImpl(
|
||||
MethodBridge.ReturnValue.HashCode -> ObjCPrimitiveType.NSUInteger
|
||||
is MethodBridge.ReturnValue.Mapped -> mapType(method.returnType!!, returnBridge.bridge, objCExportScope)
|
||||
MethodBridge.ReturnValue.WithError.Success -> ObjCPrimitiveType.BOOL
|
||||
is MethodBridge.ReturnValue.WithError.RefOrNull -> {
|
||||
val successReturnType = mapReturnType(returnBridge.successBridge, method, objCExportScope) as? ObjCNonNullReferenceType
|
||||
?: error("Function is expected to have non-null return type: $method")
|
||||
is MethodBridge.ReturnValue.WithError.ZeroForError -> {
|
||||
val successReturnType = mapReturnType(returnBridge.successBridge, method, objCExportScope)
|
||||
|
||||
ObjCNullableReferenceType(successReturnType)
|
||||
if (!returnBridge.successMayBeZero) {
|
||||
check(successReturnType is ObjCNonNullReferenceType
|
||||
|| (successReturnType is ObjCPointerType && !successReturnType.nullable)) {
|
||||
"Unexpected return type: $successReturnType in $method"
|
||||
}
|
||||
}
|
||||
|
||||
successReturnType.makeNullableIfReferenceOrPointer()
|
||||
}
|
||||
|
||||
MethodBridge.ReturnValue.Instance.InitResult,
|
||||
|
||||
+18
-10
@@ -237,7 +237,6 @@ private fun ObjCExportMapper.bridgeParameter(parameter: ParameterDescriptor): Me
|
||||
|
||||
private fun ObjCExportMapper.bridgeReturnType(
|
||||
descriptor: FunctionDescriptor,
|
||||
valueParameters: MutableList<MethodBridgeValueParameter>,
|
||||
convertExceptionsToErrors: Boolean
|
||||
): MethodBridge.ReturnValue {
|
||||
val returnType = descriptor.returnType!!
|
||||
@@ -247,7 +246,11 @@ private fun ObjCExportMapper.bridgeReturnType(
|
||||
} else {
|
||||
MethodBridge.ReturnValue.Instance.InitResult
|
||||
}.let {
|
||||
if (convertExceptionsToErrors) MethodBridge.ReturnValue.WithError.RefOrNull(it) else it
|
||||
if (convertExceptionsToErrors) {
|
||||
MethodBridge.ReturnValue.WithError.ZeroForError(it, successMayBeZero = false)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
descriptor.containingDeclaration == descriptor.builtIns.any && descriptor.name.asString() == "hashCode" -> {
|
||||
@@ -268,20 +271,25 @@ private fun ObjCExportMapper.bridgeReturnType(
|
||||
|
||||
else -> {
|
||||
val returnTypeBridge = bridgeType(returnType)
|
||||
val successReturnValueBridge = MethodBridge.ReturnValue.Mapped(returnTypeBridge)
|
||||
if (convertExceptionsToErrors) {
|
||||
if (returnTypeBridge is ReferenceBridge && !TypeUtils.isNullableType(returnType)) {
|
||||
MethodBridge.ReturnValue.WithError.RefOrNull(MethodBridge.ReturnValue.Mapped(returnTypeBridge))
|
||||
} else {
|
||||
valueParameters += MethodBridgeValueParameter.KotlinResultOutParameter(returnTypeBridge)
|
||||
MethodBridge.ReturnValue.WithError.Success
|
||||
}
|
||||
val canReturnZero = !returnTypeBridge.isReferenceOrPointer() || TypeUtils.isNullableType(returnType)
|
||||
MethodBridge.ReturnValue.WithError.ZeroForError(
|
||||
successReturnValueBridge,
|
||||
successMayBeZero = canReturnZero
|
||||
)
|
||||
} else {
|
||||
MethodBridge.ReturnValue.Mapped(returnTypeBridge)
|
||||
successReturnValueBridge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypeBridge.isReferenceOrPointer(): Boolean = when (this) {
|
||||
ReferenceBridge, is BlockPointerBridge -> true
|
||||
is ValueTypeBridge -> this.objCValueType == ObjCValueType.POINTER
|
||||
}
|
||||
|
||||
private fun ObjCExportMapper.bridgeMethodImpl(descriptor: FunctionDescriptor): MethodBridge {
|
||||
assert(isBaseMethod(descriptor))
|
||||
|
||||
@@ -306,7 +314,7 @@ private fun ObjCExportMapper.bridgeMethodImpl(descriptor: FunctionDescriptor): M
|
||||
valueParameters += bridgeParameter(it)
|
||||
}
|
||||
|
||||
val returnBridge = bridgeReturnType(descriptor, valueParameters, convertExceptionsToErrors)
|
||||
val returnBridge = bridgeReturnType(descriptor, convertExceptionsToErrors)
|
||||
if (convertExceptionsToErrors) {
|
||||
valueParameters += MethodBridgeValueParameter.ErrorOutParameter
|
||||
}
|
||||
|
||||
+1
-4
@@ -453,13 +453,11 @@ internal class ObjCExportNamerImpl(
|
||||
else -> it!!.name.asString().toIdentifier()
|
||||
}
|
||||
MethodBridgeValueParameter.ErrorOutParameter -> "error"
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> "result"
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
append(when {
|
||||
bridge is MethodBridgeValueParameter.ErrorOutParameter ||
|
||||
bridge is MethodBridgeValueParameter.KotlinResultOutParameter -> "AndReturn"
|
||||
bridge is MethodBridgeValueParameter.ErrorOutParameter -> "AndReturn"
|
||||
|
||||
method is ConstructorDescriptor -> "With"
|
||||
else -> ""
|
||||
@@ -504,7 +502,6 @@ internal class ObjCExportNamerImpl(
|
||||
else -> it!!.name.asString().toIdentifier()
|
||||
}
|
||||
MethodBridgeValueParameter.ErrorOutParameter -> continue@parameters
|
||||
is MethodBridgeValueParameter.KotlinResultOutParameter -> "result"
|
||||
}
|
||||
|
||||
append(label)
|
||||
|
||||
@@ -270,9 +270,9 @@ __attribute__((swift_name("BridgeBase")))
|
||||
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
||||
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
|
||||
- (id _Nullable)foo1AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo1()")));
|
||||
- (BOOL)foo2AndReturnResult:(int32_t * _Nullable)result error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo2(result:)")));
|
||||
- (int32_t)foo2AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo2()"))) __attribute__((swift_error(nonnull_error)));
|
||||
- (BOOL)foo3AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo3()")));
|
||||
- (BOOL)foo4AndReturnResult:(ValuesKotlinNothing * _Nullable * _Nullable)result error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo4(result:)")));
|
||||
- (ValuesKotlinNothing * _Nullable)foo4AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo4()"))) __attribute__((swift_error(nonnull_error)));
|
||||
@end;
|
||||
|
||||
__attribute__((objc_subclassing_restricted))
|
||||
@@ -281,9 +281,9 @@ __attribute__((swift_name("Bridge")))
|
||||
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
|
||||
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
|
||||
- (ValuesKotlinNothing * _Nullable)foo1AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo1()")));
|
||||
- (BOOL)foo2AndReturnResult:(int32_t * _Nullable)result error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo2(result:)")));
|
||||
- (int32_t)foo2AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo2()"))) __attribute__((swift_error(nonnull_error)));
|
||||
- (BOOL)foo3AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo3()")));
|
||||
- (BOOL)foo4AndReturnResult:(ValuesKotlinNothing * _Nullable * _Nullable)result error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo4(result:)")));
|
||||
- (ValuesKotlinNothing *)foo4AndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("foo4()"))) __attribute__((swift_error(nonnull_error)));
|
||||
@end;
|
||||
|
||||
__attribute__((objc_subclassing_restricted))
|
||||
|
||||
@@ -291,8 +291,7 @@ func testExceptions() throws {
|
||||
try assertTrue(error.kotlinException is MyException)
|
||||
}
|
||||
do {
|
||||
var result: Int32 = 0
|
||||
try bridge.foo2(result: &result)
|
||||
try bridge.foo2()
|
||||
} catch let error as NSError {
|
||||
try assertTrue(error.kotlinException is MyException)
|
||||
}
|
||||
@@ -302,8 +301,7 @@ func testExceptions() throws {
|
||||
try assertTrue(error.kotlinException is MyException)
|
||||
}
|
||||
do {
|
||||
var result: KotlinNothing? = nil
|
||||
try bridge.foo4(result: &result)
|
||||
try bridge.foo4()
|
||||
} catch let error as NSError {
|
||||
try assertTrue(error.kotlinException is MyException)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user