[ObjCExport] Add object translation and error handling

- Also add forward declaration tests
- Add hidden types support
- Issues: KT-64857, KT-64952, KT-65080
This commit is contained in:
eugene.levenetc
2024-01-15 13:18:07 +01:00
committed by Space Team
parent c483e54e2d
commit 480b8ec516
34 changed files with 751 additions and 142 deletions
@@ -0,0 +1,14 @@
package org.jetbrains.kotlin.objcexport
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer]
*/
internal object ObjCPropertyNames {
@Suppress("unused")
const val kotlinThrowableAsErrorMethodName: String = "asError"
const val objectPropertyName: String = "shared"
@Suppress("unused")
const val companionObjectPropertyName: String = "companion"
}
@@ -0,0 +1,48 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.types.KtClassErrorType
import org.jetbrains.kotlin.analysis.api.types.KtType
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.objcexport.KtObjCExportSession
/**
* Traverses stubs and returns true if [objCErrorType] is used as a return, parameter or property type
*/
internal fun Iterable<ObjCExportStub>.hasErrorTypes(): Boolean {
return any { stub -> stub.hasErrorTypes() }
}
internal fun ObjCExportStub.hasErrorTypes(): Boolean {
return when (val stub = this) {
is ObjCClass -> stub.members.hasErrorTypes()
is ObjCProperty -> stub.type == objCErrorType
is ObjCMethod -> {
if (stub.returnType == objCErrorType) true
else stub.parameters.any { parameter -> parameter.type == objCErrorType }
}
else -> false
}
}
internal val KtType.isError
get() = this is KtClassErrorType
internal const val errorClassName = "ERROR"
context(KtObjCExportSession)
internal val errorInterface
get() = ObjCInterfaceImpl(
name = errorClassName,
comment = null,
origin = null,
attributes = emptyList(),
superProtocols = emptyList(),
members = emptyList(),
categoryName = null,
generics = emptyList(),
superClass = getDefaultSuperClassOrProtocolName().objCName,
superClassGenerics = emptyList()
)
internal val objCErrorType = ObjCClassType(errorClassName)
internal val errorForwardClass = ObjCClassForwardDeclaration(errorClassName)
@@ -7,6 +7,7 @@ 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.analysis.api.types.KtClassErrorType
import org.jetbrains.kotlin.analysis.api.types.KtClassType
import org.jetbrains.kotlin.analysis.api.types.KtClassTypeQualifier
@@ -29,6 +30,10 @@ context(KtAnalysisSession)
internal fun KtClassOrObjectSymbol.getSuperClassSymbolNotAny(): KtClassOrObjectSymbol? {
return superTypes.firstNotNullOfOrNull find@{ superType ->
if (superType.isAny) return@find null
if (superType.isError && superType is KtClassErrorType) {
//Header should have just a Base type in case unresolved super type
return@find null
}
if (superType is KtClassType) {
val classifier = superType.qualifiers.firstNotNullOfOrNull { qualifier ->
(qualifier as? KtClassTypeQualifier.KtResolvedClassTypeQualifier)?.symbol
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.*
context(KtAnalysisSession)
internal fun KtClassOrObjectSymbol.members(): List<KtSymbol> {
internal fun KtClassOrObjectSymbol.getAllMembers(): List<KtSymbol> {
return getMemberScope()
.getAllSymbols()
.sortedBy { sortMembers(it) }
@@ -13,6 +13,37 @@ internal fun KtClassOrObjectSymbol.members(): List<KtSymbol> {
.toList()
}
/**
* Returns members explicitly defined in the symbol. All super methods are excluded.
*
* Also handles edge case with covariant method overwrite:
*
* ```
* interface A {
* fun hello(): Any
* }
*
* interface B: A {
* override fun hello(): String
* }
*
* B.getBaseMembers().isEmpty() == true
* ```
*
* More context is around [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportMapperKt.isBaseMethod]
*/
context(KtAnalysisSession)
internal fun KtClassOrObjectSymbol.getDeclaredMembers(): List<KtSymbol> {
return getDeclaredMemberScope()
.getAllSymbols()
.sortedBy { sortMembers(it) }
.filterIsInstance<KtCallableSymbol>()
.filter { member ->
member.getAllOverriddenSymbols().isEmpty() && member.isVisibleInObjC()
}
.toList()
}
/**
* Temp workaround of [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorKt.makeMethodsOrderStable]
*/
@@ -6,10 +6,10 @@ import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getAllMembers
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getSuperClassSymbolNotAny
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC
import org.jetbrains.kotlin.objcexport.analysisApiUtils.members
context(KtAnalysisSession, KtObjCExportSession)
fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? {
@@ -27,7 +27,7 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? {
val comment: ObjCComment? = annotationsList.translateToObjCComment()
val origin: ObjCExportStubOrigin = getObjCExportStubOrigin()
val superProtocols: List<String> = superProtocols()
val members: List<ObjCExportStub> = members().flatMap { it.translateToObjCExportStubs() }
val members: List<ObjCExportStub> = getAllMembers().flatMap { it.translateToObjCExportStubs() }
val categoryName: String? = null
val generics: List<ObjCGenericTypeDeclaration> = emptyList()
val superClassGenerics: List<ObjCNonNullReferenceType> = emptyList()
@@ -7,30 +7,65 @@ package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind.CLASS
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind.INTERFACE
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCHeader
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCProtocol
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind.*
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.objcexport.analysisApiUtils.errorForwardClass
import org.jetbrains.kotlin.objcexport.analysisApiUtils.errorInterface
import org.jetbrains.kotlin.objcexport.analysisApiUtils.hasErrorTypes
import org.jetbrains.kotlin.psi.KtFile
context(KtAnalysisSession, KtObjCExportSession)
fun translateToObjCHeader(files: List<KtFile>) : ObjCHeader {
val declarations = files.flatMap { ktFile -> ktFile.translateToObjCExportStubs() }
fun translateToObjCHeader(files: List<KtFile>): ObjCHeader {
val declarations = files.flatMap { ktFile -> ktFile.translateToObjCExportStubs() }.toMutableList()
val classForwardDeclarations = getClassForwardDeclarations(declarations).toMutableSet()
val protocolForwardDeclarations = getProtocolForwardDeclarations(declarations)
if (declarations.hasErrorTypes()) {
declarations.add(errorInterface)
classForwardDeclarations.add(errorForwardClass)
}
return ObjCHeader(
stubs = declarations,
classForwardDeclarations = emptySet(),
protocolForwardDeclarations = declarations
.filterIsInstance<ObjCProtocol>()
.flatMap { it.superProtocols }
.toSet(),
classForwardDeclarations = classForwardDeclarations,
protocolForwardDeclarations = protocolForwardDeclarations,
additionalImports = emptyList(),
exportKDoc = configuration.exportKDoc
)
}
/**
* Class which have static property must have forward declaration
*
* ```
* @class Foo;
*
* @interface Foo
* @property (class) Foo
* @end
* ```
*/
private fun getClassForwardDeclarations(declarations: List<ObjCExportStub>): Set<ObjCClassForwardDeclaration> {
return declarations
.filterIsInstance<ObjCClass>()
.filter { clazz ->
clazz.members
.filterIsInstance<ObjCProperty>()
.any { property ->
val className = (property.type as? ObjCClassType)?.className == clazz.name
val static = property.propertyAttributes.contains("class")
className && static
}
}.map { clazz ->
ObjCClassForwardDeclaration(clazz.name)
}.toSet()
}
private fun getProtocolForwardDeclarations(declarations: List<ObjCExportStub>) = declarations
.filterIsInstance<ObjCClass>()
.flatMap { it.superProtocols }
.toSet()
context(KtAnalysisSession, KtObjCExportSession)
fun KtFile.translateToObjCExportStubs(): List<ObjCExportStub> {
return this.getFileSymbol().translateToObjCExportStubs()
@@ -49,6 +84,7 @@ internal fun KtSymbol.translateToObjCExportStubs(): List<ObjCExportStub> {
this is KtFileSymbol -> translateToObjCExportStubs()
this is KtClassOrObjectSymbol && classKind == INTERFACE -> listOfNotNull(translateToObjCProtocol())
this is KtClassOrObjectSymbol && classKind == CLASS -> listOfNotNull(translateToObjCClass())
this is KtClassOrObjectSymbol && classKind == OBJECT -> listOfNotNull(translateToObjCObject())
this is KtConstructorSymbol -> translateToObjCConstructors()
this is KtPropertySymbol -> listOfNotNull(translateToObjCProperty())
this is KtFunctionSymbol -> listOfNotNull(translateToObjCMethod())
@@ -134,7 +134,7 @@ internal fun KtFunctionLikeSymbol.getSwiftName(methodBridge: MethodBridge): Stri
1 -> "_"
else -> "value"
}
else -> symbol!!.getObjCName().name(true)
else -> symbol!!.name
}
MethodBridgeValueParameter.ErrorOutParameter -> continue@parameters
is MethodBridgeValueParameter.SuspendCompletion -> "completionHandler"
@@ -208,7 +208,9 @@ fun KtFunctionLikeSymbol.getSelector(methodBridge: MethodBridge): String {
1 -> ""
else -> "value"
}
else -> typeParameterSymbol!!.getObjCName().name(false)
else -> {
typeParameterSymbol!!.name.toString()
}
}
MethodBridgeValueParameter.ErrorOutParameter -> "error"
is MethodBridgeValueParameter.SuspendCompletion -> "completionHandler"
@@ -282,32 +284,35 @@ private fun String.mangleIfSpecialFamily(prefix: String): String {
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl.startsWithWords]
*/
private fun String.startsWithWords(words: String) = this.startsWith(words) &&
(this.length == words.length || !this[words.length].isLowerCase())
(this.length == words.length || !this[words.length].isLowerCase())
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.MethodBrideExtensionsKt.valueParametersAssociated]
*/
@InternalKotlinNativeApi
fun MethodBridge.valueParametersAssociated(
function: KtFunctionLikeSymbol,
): List<Pair<MethodBridgeValueParameter, KtTypeParameterSymbol?>> {
): List<Pair<MethodBridgeValueParameter, KtValueParameterSymbol?>> {
val kotlinParameters = function.typeParameters.iterator()
if (!kotlinParameters.hasNext()) return emptyList()
val allParameters = function.valueParameters.iterator()
if (!allParameters.hasNext()) return emptyList()
val skipFirstKotlinParameter = when (this.receiver) {
MethodBridgeReceiver.Static -> false
MethodBridgeReceiver.Factory, MethodBridgeReceiver.Instance -> true
}
if (skipFirstKotlinParameter) {
kotlinParameters.next()
if (skipFirstKotlinParameter && allParameters.hasNext()) {
allParameters.next()
}
return this.valueParameters.map {
when (it) {
is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next()
is MethodBridgeValueParameter.Mapped -> it to allParameters.next()
is MethodBridgeValueParameter.SuspendCompletion,
is MethodBridgeValueParameter.ErrorOutParameter,
-> it to null
}
}.also { assert(!kotlinParameters.hasNext()) }
}
}
@@ -329,7 +334,7 @@ fun KtFunctionLikeSymbol.mapReturnType(returnBridge: MethodBridge.ReturnValue):
if (!returnBridge.successMayBeZero) {
check(
successReturnType is ObjCNonNullReferenceType
|| (successReturnType is ObjCPointerType && !successReturnType.nullable)
|| (successReturnType is ObjCPointerType && !successReturnType.nullable)
) {
"Unexpected return type: $successReturnType in $this"
}
@@ -0,0 +1,119 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getAllMembers
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getSuperClassSymbolNotAny
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC
context(KtAnalysisSession, KtObjCExportSession)
fun KtClassOrObjectSymbol.translateToObjCObject(): ObjCClass? {
require(classKind == KtClassKind.OBJECT)
if (!isVisibleInObjC()) return null
val superClass = getSuperClassSymbolNotAny()
val kotlinAnyName = getDefaultSuperClassOrProtocolName()
val superName = if (superClass == null) kotlinAnyName else throw RuntimeException("Super class translation isn't implemented yet")
val enumKind = this.classKind == KtClassKind.ENUM_CLASS
val final = if (this is KtSymbolWithModality) this.modality == Modality.FINAL else false
val attributes = if (enumKind || final) listOf(OBJC_SUBCLASSING_RESTRICTED) else emptyList()
val name = getObjCClassOrProtocolName()
val comment: ObjCComment? = annotationsList.translateToObjCComment()
val origin: ObjCExportStubOrigin = getObjCExportStubOrigin()
val superProtocols: List<String> = superProtocols()
val categoryName: String? = null
val generics: List<ObjCGenericTypeDeclaration> = emptyList()
val superClassGenerics: List<ObjCNonNullReferenceType> = emptyList()
val objectMembers = getDefaultMembers()
getAllMembers().flatMap { it.translateToObjCExportStubs() }.forEach {
objectMembers.add(it)
}
return ObjCInterfaceImpl(
name.objCName,
comment,
origin,
attributes,
superProtocols,
objectMembers,
categoryName,
generics,
superName.objCName,
superClassGenerics
)
}
context(KtAnalysisSession, KtObjCExportSession)
private fun KtClassOrObjectSymbol.getDefaultMembers(): MutableList<ObjCExportStub> {
val result = mutableListOf<ObjCExportStub>()
val allocWithZoneParameter = ObjCParameter("zone", null, ObjCRawType("struct _NSZone *"), null)
result.add(
ObjCMethod(null, null, false, ObjCInstanceType, listOf("alloc"), emptyList(), listOf("unavailable"))
)
result.add(
ObjCMethod(null, null, false, ObjCInstanceType, listOf("allocWithZone:"), listOf(allocWithZoneParameter), listOf("unavailable"))
)
result.add(
ObjCMethod(
null,
null,
false,
ObjCInstanceType,
listOf(getObjectInstanceSelector(this)),
emptyList(),
listOf(swiftNameAttribute("init()"))
)
)
result.add(
ObjCProperty(
name = ObjCPropertyNames.objectPropertyName,
comment = null,
type = toPropertyType(),
propertyAttributes = listOf("class", "readonly"),
getterName = getObjectPropertySelector(this),
declarationAttributes = listOf(swiftNameAttribute(ObjCPropertyNames.objectPropertyName)),
origin = null
)
)
return result
}
/**
* TODO: Temp implementation
* Use translateToObjCReferenceType() to make type
* See also: [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mapReferenceType]
*/
private fun KtClassOrObjectSymbol.toPropertyType() = ObjCClassType(
this.classIdIfNonLocal!!.shortClassName.asString(),
emptyList()
)
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl.getObjectInstanceSelector]
*/
context(KtAnalysisSession, KtObjCExportSession)
private fun getObjectInstanceSelector(objectSymbol: KtClassOrObjectSymbol): String {
return objectSymbol.getObjCClassOrProtocolName().objCName.lowercase()
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl.getObjectPropertySelector]
*/
context(KtAnalysisSession, KtObjCExportSession)
private fun getObjectPropertySelector(descriptor: KtClassOrObjectSymbol): String {
val collides = ObjCPropertyNames.objectPropertyName == getObjectInstanceSelector(descriptor)
return ObjCPropertyNames.objectPropertyName + (if (collides) "_" else "")
}
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtPropertySetterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtTypeParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtValueParameterSymbol
import org.jetbrains.kotlin.backend.konan.cKeywords
import org.jetbrains.kotlin.backend.konan.objcexport.*
@@ -23,13 +24,13 @@ internal fun KtFunctionLikeSymbol.translateToObjCParameters(baseMethodBridge: Me
val usedNames = mutableSetOf<String>()
valueParametersAssociated.forEach { (bridge: MethodBridgeValueParameter, parameter: KtTypeParameterSymbol?) ->
valueParametersAssociated.forEach { (bridge: MethodBridgeValueParameter, parameter: KtValueParameterSymbol?) ->
val candidateName: String = when (bridge) {
is MethodBridgeValueParameter.Mapped -> {
if (parameter == null) throw IllegalStateException("Parameter shouldn't be null")
when {
this is KtPropertySetterSymbol -> "value"
else -> parameter.getObjCName().name(false)
else -> parameter.name.toString()
}
}
MethodBridgeValueParameter.ErrorOutParameter -> "error"
@@ -40,7 +41,8 @@ internal fun KtFunctionLikeSymbol.translateToObjCParameters(baseMethodBridge: Me
usedNames += uniqueName
val type = when (bridge) {
is MethodBridgeValueParameter.Mapped -> TODO("Fetch KtType from KtTypeParameterSymbol: $parameter")
is MethodBridgeValueParameter.Mapped ->
parameter!!.returnType.translateToObjCReferenceType()
MethodBridgeValueParameter.ErrorOutParameter ->
ObjCPointerType(ObjCNullableReferenceType(ObjCClassType("NSError")), nullable = true)
@@ -48,9 +50,9 @@ internal fun KtFunctionLikeSymbol.translateToObjCParameters(baseMethodBridge: Me
val resultType = if (bridge.useUnitCompletion) {
null
} else {
when (val it = this.returnType.translateToObjCReferenceType()) {
is ObjCNonNullReferenceType -> ObjCNullableReferenceType(it, isNullableResult = false)
is ObjCNullableReferenceType -> ObjCNullableReferenceType(it.nonNullType, isNullableResult = true)
when (val type = this.returnType.translateToObjCReferenceType()) {
is ObjCNonNullReferenceType -> ObjCNullableReferenceType(type, isNullableResult = false)
is ObjCNullableReferenceType -> ObjCNullableReferenceType(type.nonNullType, isNullableResult = true)
}
}
ObjCBlockPointerType(
@@ -14,8 +14,8 @@ import org.jetbrains.kotlin.backend.konan.objcexport.ObjCComment
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCProtocol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCProtocolImpl
import org.jetbrains.kotlin.backend.konan.objcexport.toNameAttributes
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDeclaredMembers
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC
import org.jetbrains.kotlin.objcexport.analysisApiUtils.members
context(KtAnalysisSession, KtObjCExportSession)
fun KtClassOrObjectSymbol.translateToObjCProtocol(): ObjCProtocol? {
@@ -26,7 +26,7 @@ fun KtClassOrObjectSymbol.translateToObjCProtocol(): ObjCProtocol? {
// TODO: Check error type!
val name = getObjCClassOrProtocolName()
val members = members().flatMap { it.translateToObjCExportStubs() }
val members = getDeclaredMembers().flatMap { it.translateToObjCExportStubs() }
val comment: ObjCComment? = annotationsList.translateToObjCComment()
@@ -5,6 +5,9 @@ import org.jetbrains.kotlin.analysis.api.types.KtType
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.objcexport.analysisApiUtils.isError
import org.jetbrains.kotlin.objcexport.analysisApiUtils.objCErrorType
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mapReferenceType]
@@ -20,30 +23,41 @@ internal fun KtType.translateToObjCReferenceType(): ObjCReferenceType {
context(KtAnalysisSession, KtObjCExportSession)
private fun KtType.mapToReferenceTypeIgnoringNullability(): ObjCNonNullReferenceType {
/**
* Simplified version of [org.jetbrains.kotlin.backend.konan.objcexport.CustomTypeMapper]
*/
val typesMap = mutableMapOf<ClassId, String>().apply {
this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray"
this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray"
this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet"
this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName
this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary"
this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName
this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString"
}
val classId = this.expandedClassSymbol?.classIdIfNonLocal
val isInlined = false //TODO: replace when KT-65176 is implemented
val isHidden = classId in hiddenTypes
NSNumberKind.entries.forEach {
val classId = it.mappedKotlinClassId
if (classId != null) {
typesMap[classId] = classId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName
return if (isError) {
objCErrorType
} else if (isAny || isHidden || isInlined) {
ObjCIdType
} else {
/**
* Simplified version of [org.jetbrains.kotlin.backend.konan.objcexport.CustomTypeMapper]
*/
val typesMap = mutableMapOf<ClassId, String>().apply {
this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray"
this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray"
this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet"
this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName
this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary"
this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName
this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString"
}
NSNumberKind.entries.forEach { number ->
val numberClassId = number.mappedKotlinClassId
if (numberClassId != null) {
typesMap[numberClassId] = numberClassId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName
}
}
val typeName = typesMap[classId]
?: classId!!.shortClassName.asString()
.getObjCKotlinStdlibClassOrProtocolName().objCName //throw IllegalStateException("Unsupported mapping type for $this")
ObjCClassType(typeName)
}
val typeName = typesMap[this.expandedClassSymbol?.classIdIfNonLocal]
?: throw IllegalStateException("Unsupported mapping type for $this")
return ObjCClassType(typeName)
}
private fun ObjCNonNullReferenceType.withNullabilityOf(kotlinType: KtType): ObjCReferenceType {
@@ -53,3 +67,21 @@ private fun ObjCNonNullReferenceType.withNullabilityOf(kotlinType: KtType): ObjC
this
}
}
/**
* Types to be "hidden" during mapping, i.e. represented as `id`.
*
* Currently contains super types of classes handled by custom type mappers.
* Note: can be generated programmatically, but requires stdlib in this case.
*/
private val hiddenTypes: Set<ClassId> = listOf(
"kotlin.Any",
"kotlin.CharSequence",
"kotlin.Comparable",
"kotlin.Function",
"kotlin.Number",
"kotlin.collections.Collection",
"kotlin.collections.Iterable",
"kotlin.collections.MutableCollection",
"kotlin.collections.MutableIterable"
).map { ClassId.topLevel(FqName(it)) }.toSet()
@@ -6,7 +6,8 @@
package org.jetbrains.kotlin.objcexport.testUtils
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.backend.konan.tests.ObjCExportHeaderGeneratorTest.HeaderGenerator
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCHeader
import org.jetbrains.kotlin.backend.konan.testUtils.HeaderGenerator
import org.jetbrains.kotlin.objcexport.KtObjCExportConfiguration
import org.jetbrains.kotlin.objcexport.KtObjCExportSession
import org.jetbrains.kotlin.objcexport.translateToObjCHeader
@@ -27,12 +28,12 @@ class AnalysisApiHeaderGeneratorExtension : ParameterResolver {
}
object AnalysisApiHeaderGenerator : HeaderGenerator {
override fun generateHeaders(root: File): String {
override fun generateHeaders(root: File, configuration: HeaderGenerator.Configuration): ObjCHeader {
val session = createStandaloneAnalysisApiSession(root.listFiles().orEmpty().filter { it.extension == "kt" })
val (module, files) = session.modulesWithFiles.entries.single()
return analyze(module) {
KtObjCExportSession(KtObjCExportConfiguration()) {
translateToObjCHeader(files.map { it as KtFile }).toString()
KtObjCExportSession(KtObjCExportConfiguration(frameworkName = configuration.frameworkName)) {
translateToObjCHeader(files.map { it as KtFile })
}
}
}
@@ -0,0 +1,104 @@
package org.jetbrains.kotlin.objcexport.tests
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub
import org.jetbrains.kotlin.backend.konan.testUtils.HeaderGenerator
import org.jetbrains.kotlin.objcexport.analysisApiUtils.hasErrorTypes
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import kotlin.io.path.writeText
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class HasErrorTypesTest(
private val headerGenerator: HeaderGenerator,
) {
@TempDir
private lateinit var tempDir: Path
@Test
fun `test - no errors`() {
val stubs = stubs(
"""
fun foo() = Unit
class Foo {
val myProperty: Int = 42
fun myFunction(): Int = 42
}
""".trimIndent()
)
assertFalse(stubs.hasErrorTypes())
}
@Test
fun `test - property return type error`() {
val stubs = stubs(
"""
val foo: Unresolved get() = error("stub")
""".trimIndent()
)
assertTrue(stubs.hasErrorTypes())
}
@Test
fun `test - function return type error`() {
val stubs = stubs(
"""
fun foo(): Unresolved = error("stub")
""".trimIndent()
)
assertTrue(stubs.hasErrorTypes())
}
@Test
fun `test - nested member function return type error`() {
val stubs = stubs(
"""
class Foo {
fun foo(): Unresolved = error("stub")
}
""".trimIndent()
)
assertTrue(stubs.hasErrorTypes())
}
@Test
fun `test - nested member property return type error`() {
val stubs = stubs(
"""
class Foo {
val foo: Unresolved get() = error("stub")
}
""".trimIndent()
)
assertTrue(stubs.hasErrorTypes())
}
@Test
fun `test - nested error class property`() {
val stubs = stubs(
"""
class A {
class B {
val e = error("error")
}
}
""".trimIndent()
)
assertTrue(stubs.hasErrorTypes())
}
private fun stubs(@Language("kotlin") sourceCode: String): List<ObjCExportStub> {
val sourceFile = tempDir.resolve("sources.kt")
sourceFile.writeText(sourceCode)
return headerGenerator.generateHeaders(tempDir.toFile()).stubs
}
}
@@ -36,13 +36,15 @@ abstract class ObjCExportHeaderGenerator @InternalKotlinNativeApi constructor(
open val shouldExportKDoc = false
fun build(): List<String> = ObjCHeader(
@InternalKotlinNativeApi
fun buildHeader(): ObjCHeader = ObjCHeader(
stubs = stubs,
classForwardDeclarations = classForwardDeclarations,
protocolForwardDeclarations = protocolForwardDeclarations,
additionalImports = getAdditionalImports(),
exportKDoc = shouldExportKDoc
).lines
)
fun build(): List<String> = buildHeader().render(shouldExportKDoc)
@InternalKotlinNativeApi
fun buildInterface(): ObjCExportedInterface {
@@ -9,7 +9,6 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.backend.konan.UnitSuspendFunctionObjCExport
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.backend.konan.tests.ObjCExportHeaderGeneratorTest
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
@@ -28,7 +27,7 @@ class Fe10HeaderGeneratorExtension : ParameterResolver, AfterEachCallback {
}
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
return parameterContext.parameter.type == ObjCExportHeaderGeneratorTest.HeaderGenerator::class.java
return parameterContext.parameter.type == HeaderGenerator::class.java
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
@@ -43,15 +42,16 @@ class Fe10HeaderGeneratorExtension : ParameterResolver, AfterEachCallback {
}
}
private class Fe10HeaderGeneratorImpl(private val disposable: Disposable) :
ObjCExportHeaderGeneratorTest.HeaderGenerator {
override fun generateHeaders(root: File): String {
val headerGenerator = createObjCExportHeaderGenerator(disposable, root)
private class Fe10HeaderGeneratorImpl(private val disposable: Disposable) : HeaderGenerator {
override fun generateHeaders(root: File, configuration: HeaderGenerator.Configuration): ObjCHeader {
val headerGenerator = createObjCExportHeaderGenerator(disposable, root, configuration)
headerGenerator.translateModuleDeclarations()
return headerGenerator.build().joinToString(System.lineSeparator())
return headerGenerator.buildHeader()
}
private fun createObjCExportHeaderGenerator(disposable: Disposable, root: File): ObjCExportHeaderGenerator {
private fun createObjCExportHeaderGenerator(
disposable: Disposable, root: File, configuration: HeaderGenerator.Configuration,
): ObjCExportHeaderGenerator {
val mapper = ObjCExportMapper(
unitSuspendFunctionExport = UnitSuspendFunctionObjCExport.DEFAULT
)
@@ -62,7 +62,7 @@ private class Fe10HeaderGeneratorImpl(private val disposable: Disposable) :
local = false,
problemCollector = ObjCExportProblemCollector.SILENT,
configuration = object : ObjCExportNamer.Configuration {
override val topLevelNamePrefix: String get() = ""
override val topLevelNamePrefix: String get() = configuration.frameworkName
override fun getAdditionalPrefix(module: ModuleDescriptor): String? = null
override val objcGenerics: Boolean = true
}
@@ -5,74 +5,85 @@
package org.jetbrains.kotlin.backend.konan.objcexport
data class ObjCHeader(val lines: List<String>) {
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
data class ObjCHeader(
@property:InternalKotlinNativeApi val stubs: List<ObjCExportStub>,
@property:InternalKotlinNativeApi val classForwardDeclarations: Set<ObjCClassForwardDeclaration>,
@property:InternalKotlinNativeApi val protocolForwardDeclarations: Set<String>,
@property:InternalKotlinNativeApi val additionalImports: List<String>,
) {
fun renderClassForwardDeclarations(): List<String> = buildList {
if (classForwardDeclarations.isNotEmpty()) {
add("@class ${
classForwardDeclarations.joinToString {
buildString {
append(it.className)
formatGenerics(this, it.typeDeclarations)
}
}
};")
add("")
}
}
fun renderProtocolForwardDeclarations(): List<String> = buildList {
if (protocolForwardDeclarations.isNotEmpty()) {
add("@protocol ${protocolForwardDeclarations.joinToString()};")
add("")
}
}
fun render(exportKDoc: Boolean = true): List<String> {
return buildList {
addImports(foundationImports)
addImports(additionalImports)
add("")
addAll(renderClassForwardDeclarations())
addAll(renderProtocolForwardDeclarations())
add("NS_ASSUME_NONNULL_BEGIN")
add("#pragma clang diagnostic push")
listOf(
"-Wunknown-warning-option",
// Protocols don't have generics, classes do. So generated header may contain
// overriding property with "incompatible" type, e.g. `Generic<T>`-typed property
// overriding `Generic<id>`. Suppress these warnings:
"-Wincompatible-property-type",
"-Wnullability"
).forEach {
add("#pragma clang diagnostic ignored \"$it\"")
}
add("")
// If _Nullable_result is not supported, then use _Nullable:
add("#pragma push_macro(\"$objcNullableResultAttribute\")")
add("#if !__has_feature(nullability_nullable_result)")
add("#undef $objcNullableResultAttribute")
add("#define $objcNullableResultAttribute $objcNullableAttribute")
add("#endif")
add("")
stubs.forEach {
addAll(StubRenderer.render(it, exportKDoc))
add("")
}
add("#pragma pop_macro(\"$objcNullableResultAttribute\")")
add("#pragma clang diagnostic pop")
add("NS_ASSUME_NONNULL_END")
}
}
override fun toString(): String {
return lines.joinToString(System.lineSeparator())
return render().joinToString(System.lineSeparator())
}
}
fun ObjCHeader(
stubs: List<ObjCExportStub>,
classForwardDeclarations: Set<ObjCClassForwardDeclaration>,
protocolForwardDeclarations: Set<String>,
additionalImports: List<String> = emptyList(),
exportKDoc: Boolean = false,
): ObjCHeader = ObjCHeader(buildList {
addImports(foundationImports)
addImports(additionalImports)
add("")
if (classForwardDeclarations.isNotEmpty()) {
add("@class ${
classForwardDeclarations.joinToString {
buildString {
append(it.className)
formatGenerics(this, it.typeDeclarations)
}
}
};")
add("")
}
if (protocolForwardDeclarations.isNotEmpty()) {
add("@protocol ${protocolForwardDeclarations.joinToString()};")
add("")
}
add("NS_ASSUME_NONNULL_BEGIN")
add("#pragma clang diagnostic push")
listOf(
"-Wunknown-warning-option",
// Protocols don't have generics, classes do. So generated header may contain
// overriding property with "incompatible" type, e.g. `Generic<T>`-typed property
// overriding `Generic<id>`. Suppress these warnings:
"-Wincompatible-property-type",
"-Wnullability"
).forEach {
add("#pragma clang diagnostic ignored \"$it\"")
}
add("")
// If _Nullable_result is not supported, then use _Nullable:
add("#pragma push_macro(\"$objcNullableResultAttribute\")")
add("#if !__has_feature(nullability_nullable_result)")
add("#undef $objcNullableResultAttribute")
add("#define $objcNullableResultAttribute $objcNullableAttribute")
add("#endif")
add("")
stubs.forEach {
addAll(StubRenderer.render(it, exportKDoc))
add("")
}
add("#pragma pop_macro(\"$objcNullableResultAttribute\")")
add("#pragma clang diagnostic pop")
add("NS_ASSUME_NONNULL_END")
})
private fun MutableList<String>.addImports(imports: Iterable<String>) {
imports.forEach {
add("#import <$it>")
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.konan.testUtils
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCHeader
import java.io.File
interface HeaderGenerator {
data class Configuration(
val frameworkName: String = "",
)
fun generateHeaders(root: File, configuration: Configuration = Configuration()): ObjCHeader
}
@@ -9,4 +9,5 @@ import java.io.File
val testDataDir = File("native/objcexport-header-generator/testData")
val headersTestDataDir = testDataDir.resolve("headers")
val baseDeclarationsDir = testDataDir.resolve("baseDeclarations")
val baseDeclarationsDir = testDataDir.resolve("baseDeclarations")
val forwardDeclarationsDir = testDataDir.resolve("forwardDeclarations")
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.konan.tests
import org.jetbrains.kotlin.backend.konan.testUtils.HeaderGenerator
import org.jetbrains.kotlin.backend.konan.testUtils.forwardDeclarationsDir
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.jupiter.api.Test
import java.io.File
import kotlin.test.fail
class ObjCExportForwardDeclarationsTest(
private val generator: HeaderGenerator,
) {
@Test
fun `test - function returning interface`() {
doTest(forwardDeclarationsDir.resolve("functionReturningInterface"))
}
@Test
fun `test - function returning class`() {
doTest(forwardDeclarationsDir.resolve("functionReturningClass"))
}
@Test
fun `test - property returning interface`() {
doTest(forwardDeclarationsDir.resolve("propertyReturningInterface"))
}
@Test
fun `test - property returning class`() {
doTest(forwardDeclarationsDir.resolve("propertyReturningClass"))
}
private fun doTest(root: File) {
if (!root.isDirectory) fail("Expected ${root.absolutePath} to be directory")
val generatedHeaders = generator.generateHeaders(root)
val renderedForwardDeclarations = buildString {
generatedHeaders.renderClassForwardDeclarations().forEach(this::appendLine)
generatedHeaders.renderProtocolForwardDeclarations().forEach(this::appendLine)
}
KotlinTestUtils.assertEqualsToFile(root.resolve("!${root.nameWithoutExtension}.h"), renderedForwardDeclarations)
}
}
@@ -5,6 +5,8 @@
package org.jetbrains.kotlin.backend.konan.tests
import org.jetbrains.kotlin.backend.konan.testUtils.HeaderGenerator
import org.jetbrains.kotlin.backend.konan.testUtils.HeaderGenerator.Configuration
import org.jetbrains.kotlin.backend.konan.testUtils.headersTestDataDir
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.jupiter.api.Test
@@ -110,6 +112,11 @@ class ObjCExportHeaderGeneratorTest(val generator: HeaderGenerator) {
doTest(headersTestDataDir.resolve("functionWithErrorType"))
}
@Test
fun `test - functionWithErrorTypeAndFrameworkName`() {
doTest(headersTestDataDir.resolve("functionWithErrorTypeAndFrameworkName"), Configuration(frameworkName = "shared"))
}
@Test
fun `test - kdocWithBlockTags`() {
doTest(headersTestDataDir.resolve("kdocWithBlockTags"))
@@ -145,13 +152,19 @@ class ObjCExportHeaderGeneratorTest(val generator: HeaderGenerator) {
doTest(headersTestDataDir.resolve("dispatchAndExtensionReceiverWithMustBeDocumentedAnnotation"))
}
fun interface HeaderGenerator {
fun generateHeaders(root: File): String
@Test
fun `test - classWithUnresolvedSuperType`() {
doTest(headersTestDataDir.resolve("classWithUnresolvedSuperType"))
}
private fun doTest(root: File) {
@Test
fun `test - classWithUnresolvedSuperTypeGenerics`() {
doTest(headersTestDataDir.resolve("classWithUnresolvedSuperTypeGenerics"))
}
private fun doTest(root: File, configuration: Configuration = Configuration()) {
if (!root.isDirectory) fail("Expected ${root.absolutePath} to be directory")
val generatedHeaders = generator.generateHeaders(root)
val generatedHeaders = generator.generateHeaders(root, configuration).toString()
KotlinTestUtils.assertEqualsToFile(root.resolve("!${root.nameWithoutExtension}.h"), generatedHeaders)
}
}
@@ -0,0 +1,3 @@
fun foo(): Foo
class Foo
@@ -0,0 +1,3 @@
fun foo(): Foo
interface Foo
@@ -0,0 +1,3 @@
val foo: Foo get() = TODO()
class Foo
@@ -0,0 +1,3 @@
val foo: Foo get() = TODO()
interface Foo
@@ -0,0 +1,29 @@
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"
#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif
__attribute__((objc_subclassing_restricted))
@interface Foo : Base
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
#pragma pop_macro("_Nullable_result")
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
@@ -0,0 +1 @@
class Foo : Unresolved()
@@ -0,0 +1,35 @@
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
@protocol Foo;
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"
#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif
@protocol Foo
@required
@end
__attribute__((objc_subclassing_restricted))
@interface Bar : Base <Foo>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
#pragma pop_macro("_Nullable_result")
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
@@ -0,0 +1,3 @@
interface Foo<T>
class Bar : Foo<Unresolved>
@@ -0,0 +1,34 @@
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
@class ERROR;
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"
#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("FooKt")))
@interface sharedFooKt : sharedBase
+ (ERROR *)foo __attribute__((swift_name("foo()")));
@end
@interface ERROR : sharedBase
@end
#pragma pop_macro("_Nullable_result")
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
@@ -0,0 +1 @@
fun foo() = Unresolved