[ObjCExport] Fix suspend function and @Throws translations

KT-66115, KT-66239
This commit is contained in:
eugene.levenetc
2024-03-01 10:36:47 +01:00
committed by Space Team
parent b7d1126861
commit 42d8db1c52
11 changed files with 310 additions and 22 deletions
@@ -0,0 +1,34 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.KtArrayAnnotationValue
import org.jetbrains.kotlin.analysis.api.annotations.KtKClassAnnotationValue
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.name.ClassId
/**
* Returns [Throws] classIds defined only for current function
*
* See [effectiveThrows]
* See K1: org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.getDefinedThrows
*/
context(KtAnalysisSession)
internal val KtFunctionLikeSymbol.definedThrows: List<ClassId>
get() {
if (isSuspend) return listOf(ClassId.topLevel(KonanFqNames.cancellationException))
if (!hasThrowsAnnotation) return emptyList()
val throwsAnnotations = annotationsList.annotations
.filter { annotation -> annotation.classId?.asSingleFqName() == KonanFqNames.throws }
.asSequence()
return throwsAnnotations
.flatMap { annotation -> annotation.arguments }
.map { argument -> argument.expression }
.filterIsInstance<KtArrayAnnotationValue>()
.flatMap { arrayAnnotationValue -> arrayAnnotationValue.values }
.filterIsInstance<KtKClassAnnotationValue.KtNonLocalKClassAnnotationValue>()
.map { it.classId }
.toList()
}
@@ -0,0 +1,37 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.name.ClassId
/**
* Returns root [Throws] classIds
*
* ```kotlin
* class A {
* @Throws(ExcA)
* foo()
* }
*
* class B: A {
* @Throws(ExcB)
* override foo()
* }
*
* class C: B {
* @Throws(ExcC)
* override foo()
* }
*
* C.foo.effectiveThrows = [ExcA]
* ```
*
* See [definedThrows]
* See K1: [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.getEffectiveThrows]
*/
context(KtAnalysisSession)
internal val KtFunctionLikeSymbol.effectiveThrows: List<ClassId>
get() {
getAllOverriddenSymbols().firstOrNull()?.let { return (it as KtFunctionLikeSymbol).effectiveThrows }
return definedThrows
}
@@ -20,8 +20,14 @@ internal fun KtFunctionLikeSymbol.getFunctionMethodBridge(): MethodBridge {
valueParameters += bridgeParameter(it.returnType)
}
if (this is KtFunctionSymbol && isSuspend) {
valueParameters += MethodBridgeValueParameter.SuspendCompletion(false)
if (isSuspend) {
valueParameters += MethodBridgeValueParameter.SuspendCompletion(true)
} else if (hasThrowsAnnotation) {
// Add error out parameter before tail block parameters. The convention allows this.
// Placing it after would trigger https://bugs.swift.org/browse/SR-12201
// (see also https://github.com/JetBrains/kotlin-native/issues/3825).
val tailBlocksCount = valueParameters.reversed().takeWhile { it.isBlockPointer() }.count()
valueParameters.add(valueParameters.size - tailBlocksCount, MethodBridgeValueParameter.ErrorOutParameter)
}
return MethodBridge(
@@ -125,13 +131,11 @@ private fun bridgeFunctionType(type: KtType): TypeBridge {
context(KtAnalysisSession, KtObjCExportSession)
private fun KtCallableSymbol.bridgeReturnType(): MethodBridge.ReturnValue {
val convertExceptionsToErrors = false // TODO: Add exception handling and return MethodBridge.ReturnValue.WithError.ZeroForError
if (isArrayConstructor) {
return MethodBridge.ReturnValue.Instance.FactoryResult
} else if (isConstructor) {
val result = MethodBridge.ReturnValue.Instance.InitResult
if (convertExceptionsToErrors) {
if (hasThrowsAnnotation) {
MethodBridge.ReturnValue.WithError.ZeroForError(result, successMayBeZero = false)
} else {
return result
@@ -151,7 +155,7 @@ private fun KtCallableSymbol.bridgeReturnType(): MethodBridge.ReturnValue {
// }
if (returnType.isUnit || returnType.isNothing) {
return if (convertExceptionsToErrors) {
return if (hasThrowsAnnotation) {
MethodBridge.ReturnValue.WithError.Success
} else {
MethodBridge.ReturnValue.Void
@@ -162,7 +166,7 @@ private fun KtCallableSymbol.bridgeReturnType(): MethodBridge.ReturnValue {
val returnTypeBridge = bridgeType(returnType)
val successReturnValueBridge = MethodBridge.ReturnValue.Mapped(returnTypeBridge)
return if (convertExceptionsToErrors) {
return if (hasThrowsAnnotation) {
val canReturnZero = !returnTypeBridge.isReferenceOrPointer() || returnType.canBeNull
MethodBridge.ReturnValue.WithError.ZeroForError(
successReturnValueBridge,
@@ -180,3 +184,12 @@ private fun TypeBridge.isReferenceOrPointer(): Boolean = when (this) {
ReferenceBridge, is BlockPointerBridge -> true
is ValueTypeBridge -> this.objCValueType == ObjCValueType.POINTER
}
private fun MethodBridgeValueParameter.isBlockPointer(): Boolean = when (this) {
is MethodBridgeValueParameter.Mapped -> when (this.bridge) {
ReferenceBridge, is ValueTypeBridge -> false
is BlockPointerBridge -> true
}
MethodBridgeValueParameter.ErrorOutParameter -> false
is MethodBridgeValueParameter.SuspendCompletion -> true
}
@@ -0,0 +1,11 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.name.ClassId
internal val KtCallableSymbol.hasThrowsAnnotation: Boolean
get() {
return annotationsList.hasAnnotation(ClassId.topLevel(KonanFqNames.throws))
}
@@ -0,0 +1,7 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
internal val KtFunctionLikeSymbol.isSuspend: Boolean
get() = if (this is KtFunctionSymbol) isSuspend else false
@@ -0,0 +1,34 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClassType
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCMethod
import org.jetbrains.kotlin.backend.konan.objcexport.swiftNameAttribute
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.name.ClassId
context(KtAnalysisSession)
internal val KtClassOrObjectSymbol.isThrowable: Boolean
get() {
val classId = classIdIfNonLocal ?: return false
return classId.isThrowable
}
context(KtAnalysisSession)
internal val ClassId.isThrowable: Boolean
get() {
return StandardNames.FqNames.throwable == this.asSingleFqName()
}
internal fun buildThrowableAsErrorMethod(): ObjCMethod {
return ObjCMethod(
comment = null,
isInstanceMethod = true,
returnType = ObjCClassType("NSError"),
selectors = listOf("asError"),
parameters = emptyList(),
attributes = listOf(swiftNameAttribute("asError()")),
origin = null
)
}
@@ -6,6 +6,9 @@ import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality
import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.objcexport.analysisApiUtils.buildThrowableAsErrorMethod
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isThrowable
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC
context(KtAnalysisSession, KtObjCExportSession)
@@ -40,6 +43,10 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? {
if (classKind == KtClassKind.ENUM_CLASS) {
this += translateEnumMembers()
}
if (isThrowable) {
this += buildThrowableAsErrorMethod()
}
}
val categoryName: String? = null
@@ -6,9 +6,11 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.*
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationApplicationWithArgumentsInfo
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationsList
import org.jetbrains.kotlin.analysis.api.annotations.KtNamedAnnotationValue
import org.jetbrains.kotlin.analysis.api.annotations.renderAsSourceCode
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithVisibility
@@ -19,7 +21,9 @@ import org.jetbrains.kotlin.backend.konan.objcexport.plus
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.objcexport.analysisApiUtils.effectiveThrows
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getObjCDocumentedAnnotations
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isSuspend
context(KtAnalysisSession)
@@ -36,10 +40,8 @@ internal fun KtAnnotationsList.translateToObjCComment(): ObjCComment? {
*/
context(KtAnalysisSession)
internal fun KtFunctionLikeSymbol.translateToObjCComment(bridge: MethodBridge, parameters: List<ObjCParameter>): ObjCComment? {
val isSuspend: Boolean = if (this is KtFunctionSymbol) this.isSuspend else false
val throwsComments = if (isSuspend || bridge.returnsError) {
val effectiveThrows = getEffectiveThrows(this).toSet()
val effectiveThrows = effectiveThrows.toSet()
when {
effectiveThrows.contains(StandardClassIds.Throwable) -> {
listOf("@note This method converts all Kotlin exceptions to errors.")
@@ -105,14 +107,6 @@ private fun KtNamedAnnotationValue.render(): String {
return "$name=${expression.renderAsSourceCode()}"
}
/**
* Not implemented [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.getEffectiveThrows]
*/
@Suppress("UNUSED_PARAMETER")
private fun getEffectiveThrows(method: KtFunctionLikeSymbol): Sequence<ClassId> {
return emptySequence()
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.visibilityComments]
*/
@@ -20,6 +20,7 @@ fun KtClassOrObjectSymbol.translateToObjCConstructors(): List<ObjCMethod> {
result += getDeclaredMemberScope().getConstructors()
.filter { !it.hasExportForCompilerAnnotation }
.filter { it.isVisibleInObjC() }
.sortedWith(StableCallableOrder)
.map { it.buildObjCMethod() }
/* Create special 'alloc' constructors */
@@ -61,9 +62,13 @@ fun KtClassOrObjectSymbol.translateToObjCConstructors(): List<ObjCMethod> {
}
}
if (result.size == 1 && result.first().name == "init") {
val initIndex = result.indexOfFirst { it.name == "init" }
if (initIndex > -1) {
/**
* If there is "init" we need always add this special constructor
*/
result.add(
ObjCMethod(
initIndex + 1, ObjCMethod(
comment = null,
origin = getObjCExportStubOrigin(),
isInstanceMethod = false,
@@ -28,6 +28,12 @@ internal val StableFunctionOrder: Comparator<KtFunctionSymbol>
*/
)
internal val StableConstructorOrder: Comparator<KtConstructorSymbol>
get() = compareBy(
{ it.valueParameters.size },
// TODO NOW! { KonanManglerDesc.run { it.signatureString(false) } }
)
internal val StableClassifierOrder: Comparator<KtClassifierSymbol> =
compareBy<KtClassifierSymbol> { classifier ->
if (classifier !is KtClassOrObjectSymbol) return@compareBy 0
@@ -45,6 +51,7 @@ internal val StableCallableOrder: Comparator<KtCallableSymbol> = compareBy<KtCal
else -> 3
}
}
.thenComparing(StableConstructorOrder)
.thenComparing(StablePropertyOrder)
.thenComparing(StableFunctionOrder)
@@ -0,0 +1,139 @@
package org.jetbrains.kotlin.objcexport.tests
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.objcexport.analysisApiUtils.definedThrows
import org.jetbrains.kotlin.objcexport.analysisApiUtils.effectiveThrows
import org.jetbrains.kotlin.objcexport.analysisApiUtils.hasThrowsAnnotation
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.jetbrains.kotlin.objcexport.testUtils.getClassOrFail
import org.jetbrains.kotlin.objcexport.testUtils.getFunctionOrFail
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ThrowsAnnotationTest(private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis) {
@Test
fun `test - has throws annotation`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@Throws
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
assertTrue(file.getFunctionOrFail("foo").hasThrowsAnnotation)
}
}
@Test
fun `test - override has no throws annotation`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
open class A {
@Throws
open fun foo() = Unit
}
class B: A() {
override fun foo() = Unit
}
""".trimIndent()
)
analyze(file) {
assertTrue(file.getClassOrFail("A").getFunctionOrFail("foo").hasThrowsAnnotation)
assertFalse(file.getClassOrFail("B").getFunctionOrFail("foo").hasThrowsAnnotation)
}
}
@Test
fun `test - defined function throws`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@Throws(IllegalStateException::class, RuntimeException::class)
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val foo = file.getFunctionOrFail("foo")
assertEquals(listOf("IllegalStateException", "RuntimeException"), foo.definedThrows.mapName())
}
}
@Test
fun `test - effective and defined classes throws`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
abstract class A {
@Throws(IllegalStateException::class)
abstract fun foo()
}
class B : A() {
@Throws(RuntimeException::class)
override fun foo() = Unit
}
class C : B() {
@Throws(IndexOutOfBoundsException::class)
override fun foo() = Unit
}
""".trimIndent()
)
analyze(file) {
val fooA = file.getClassOrFail("A").getMemberScope().getFunctionOrFail("foo")
assertEquals(listOf("IllegalStateException"), fooA.effectiveThrows.mapName())
assertEquals(listOf("IllegalStateException"), fooA.definedThrows.mapName())
val fooB = file.getClassOrFail("B").getMemberScope().getFunctionOrFail("foo")
assertEquals(listOf("IllegalStateException"), fooB.effectiveThrows.mapName())
assertEquals(listOf("RuntimeException"), fooB.definedThrows.mapName())
val fooC = file.getClassOrFail("C").getMemberScope().getFunctionOrFail("foo")
assertEquals(listOf("IllegalStateException"), fooC.effectiveThrows.mapName())
assertEquals(listOf("IndexOutOfBoundsException"), fooC.definedThrows.mapName())
}
}
@Test
fun `test - constructor throws`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
class Foo @Throws(IllegalStateException::class) constructor()
""".trimIndent()
)
analyze(file) {
val foo = file.getClassOrFail("Foo").getMemberScope().getConstructors().first()
assertEquals(listOf("IllegalStateException"), foo.effectiveThrows.mapName())
assertEquals(listOf("IllegalStateException"), foo.definedThrows.mapName())
}
}
@Test
fun `test - non throws annotation`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
annotation class NonThrows(vararg val exceptionClasses: KClass<out Throwable>)
@Throws(RuntimeException::class)
@NonThrows(IllegalStateException::class)
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val foo = file.getFunctionOrFail("foo")
assertEquals(listOf("RuntimeException"), foo.definedThrows.mapName())
}
}
}
private fun List<ClassId>.mapName(): List<String> {
return map { it.shortClassName.asString() }
}