[ObjCExport] Add translation of functions and properties extensions

KT-65630
This commit is contained in:
eugene.levenetc
2024-02-20 14:59:19 +01:00
committed by Space Team
parent a34b87c63a
commit 30b63e4843
13 changed files with 212 additions and 47 deletions
@@ -0,0 +1,14 @@
package org.jetbrains.kotlin.objcexport.analysisApiUtils
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.objcexport.KtObjCExportSession
import org.jetbrains.kotlin.psi.KtFile
context(KtAnalysisSession, KtObjCExportSession)
internal fun KtFileSymbol.getFileName(): String? {
val ktFile = this.psi as? KtFile ?: return null
return NameUtils.getPackagePartClassNamePrefix(FileUtil.getNameWithoutExtension(ktFile.name))
}
@@ -44,7 +44,7 @@ context(KtAnalysisSession)
private val KtCallableSymbol.receiverType: MethodBridgeReceiver private val KtCallableSymbol.receiverType: MethodBridgeReceiver
get() = if (isArrayConstructor) { get() = if (isArrayConstructor) {
MethodBridgeReceiver.Factory MethodBridgeReceiver.Factory
} else if (!isConstructor && isTopLevel) { } else if (!isConstructor && isTopLevel && !isExtension) {
MethodBridgeReceiver.Static MethodBridgeReceiver.Static
} else { } else {
MethodBridgeReceiver.Instance MethodBridgeReceiver.Instance
@@ -21,7 +21,7 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
context(KtAnalysisSession) context(KtAnalysisSession)
internal fun KtSymbol.isVisibleInObjC(): Boolean = when(this) { internal fun KtSymbol.isVisibleInObjC(): Boolean = when (this) {
is KtCallableSymbol -> this.isVisibleInObjC() is KtCallableSymbol -> this.isVisibleInObjC()
is KtClassOrObjectSymbol -> this.isVisibleInObjC() is KtClassOrObjectSymbol -> this.isVisibleInObjC()
else -> false else -> false
@@ -0,0 +1,74 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getFileName
private const val extensionsCategoryName = "Extensions"
internal val ObjCInterface.isExtensionsFacade: Boolean
get() = this.categoryName == extensionsCategoryName
/**
* Translates extension functions/properties inside the given [this] file as a single [ObjCInterface]
* with category [extensionsCategoryName]
*
* Later interface should be forwarded using [isExtensionsFacade]
*
* ## example:
* given a file "Foo.kt"
*
* ```kotlin
*
* fun Foo.func() = 42
*
* val Foo.prop get() = 42
*
* class Foo {
*
* }
*
* ```
*
* This will be exporting two Interfaces with forwarded class:
*
* ```
* @class Foo
*
* @interface Foo: Base
*
* @interface Foo (Extensions)
* - func
* - prop
* ```
*
* Where `Foo` would be the "top level interface file extensions facade" returned by this function.
*
* See related [getTopLevelFacade]
*/
context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.getExtensionsFacade(): ObjCInterface? {
val extensions = getFileScope()
.getCallableSymbols().filter { it.isExtension }
.toList().sortedWith(StableCallableOrder)
.ifEmpty { return null }
val fileName = getFileName()
?: throw IllegalStateException("File '$this' cannot be translated without file name")
return ObjCInterfaceImpl(
name = fileName,
comment = null,
origin = null,
attributes = emptyList(),
superProtocols = emptyList(),
members = extensions.mapNotNull { it.translateToObjCExportStub() },
categoryName = extensionsCategoryName,
generics = emptyList(),
superClass = null,
superClassGenerics = emptyList()
)
}
@@ -0,0 +1,13 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportFileName
import org.jetbrains.kotlin.backend.konan.objcexport.toIdentifier
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getFileName
context(KtAnalysisSession, KtObjCExportSession)
internal fun KtFileSymbol.getObjCFileClassOrProtocolName(): ObjCExportFileName? {
val fileName = getFileName() ?: return null
return (fileName + "Kt").toIdentifier().getObjCFileName()
}
@@ -11,6 +11,7 @@ import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
/** /**
* Translates top level functions/properties inside the given [this] file as a single [ObjCInterface]. * Translates top level functions/properties inside the given [this] file as a single [ObjCInterface].
* ## example: * ## example:
@@ -40,34 +41,30 @@ import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrPr
* ``` * ```
* *
* Where `FooKt` would be the "top level interface file facade" returned by this function. * Where `FooKt` would be the "top level interface file facade" returned by this function.
*
* See related [getExtensionsFacade]
*/ */
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.translateToObjCTopLevelInterfaceFileFacade(): ObjCInterface? { fun KtFileSymbol.getTopLevelFacade(): ObjCInterface? {
val topLevelCallableStubs = getFileScope().getCallableSymbols() val extensions = getFileScope().getCallableSymbols()
.sortedWith(StableCallableOrder) .filter { !it.isExtension }
.mapNotNull { callableSymbol -> callableSymbol.translateToObjCExportStub() }
.toList() .toList()
/* If there are no top level functions or properties, we do not need to export a file facade */ .sortedWith(StableCallableOrder)
.ifEmpty { return null } .ifEmpty { return null }
val fileName = getObjCFileClassOrProtocolName() val fileName = getObjCFileClassOrProtocolName()
?: throw IllegalStateException("Top level file '$this' cannot be translated without file name") ?: throw IllegalStateException("File '$this' cannot be translated without file name")
val name = fileName.objCName
val attributes = listOf(OBJC_SUBCLASSING_RESTRICTED)
val superClass = getDefaultSuperClassOrProtocolName()
return ObjCInterfaceImpl( return ObjCInterfaceImpl(
name = name, name = fileName.objCName,
comment = null, comment = null,
origin = null, origin = null,
attributes = attributes, attributes = listOf(OBJC_SUBCLASSING_RESTRICTED),
superProtocols = emptyList(), superProtocols = emptyList(),
members = topLevelCallableStubs, members = extensions.mapNotNull { it.translateToObjCExportStub() },
categoryName = null, categoryName = null,
generics = emptyList(), generics = emptyList(),
superClass = superClass.objCName, superClass = getDefaultSuperClassOrProtocolName().objCName,
superClassGenerics = emptyList() superClassGenerics = emptyList()
) )
} }
@@ -37,7 +37,7 @@ private class KtObjCExportHeaderGenerator {
* Represents all elements that still have to be processed and translated. * Represents all elements that still have to be processed and translated.
* So far this only includes references to top level entities (such as files or classes): * So far this only includes references to top level entities (such as files or classes):
* Note: Top level functions and properties will be translated as part of the file. * Note: Top level functions and properties will be translated as part of the file.
* See [translateToObjCTopLevelInterfaceFileFacade] * See [translateToObjCTopLevelInterfaceFileFacades]
*/ */
private val symbolDeque = ArrayDeque<QueueElement>() private val symbolDeque = ArrayDeque<QueueElement>()
@@ -90,17 +90,17 @@ private class KtObjCExportHeaderGenerator {
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileElement(element: QueueElement.File) { private fun translateFileElement(element: QueueElement.File) {
val fileSymbol = element.psi.getFileSymbol() val fileSymbol = element.psi.getFileSymbol()
translateFileSymbol(fileSymbol)
fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol -> fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol ->
translateClassOrObjectSymbol(classOrObjectSymbol) translateClassOrObjectSymbol(classOrObjectSymbol)
} }
translateFileSymbol(fileSymbol)
} }
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileSymbol(symbol: KtFileSymbol) { private fun translateFileSymbol(symbol: KtFileSymbol) {
val objCInterface = symbol.translateToObjCTopLevelInterfaceFileFacade() ?: return val objCFacades = listOfNotNull(symbol.getExtensionsFacade(), symbol.getTopLevelFacade()).ifEmpty { return }
objCStubs += objCInterface objCStubs += objCFacades
enqueueDependencyClasses(objCInterface) objCFacades.forEach { enqueueDependencyClasses(it) }
} }
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
@@ -178,7 +178,9 @@ private class KtObjCExportHeaderGenerator {
val classForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCInterface>() val classForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCInterface>()
.map { stub -> ObjCClassForwardDeclaration(stub.name, stub.generics) } .map { stub -> ObjCClassForwardDeclaration(stub.name, stub.generics) }
.plus(listOfNotNull(errorForwardClass.takeIf { hasErrorTypes })).toSet() .plus(listOfNotNull(errorForwardClass.takeIf { hasErrorTypes }))
.plus(objCStubs.filterIsInstance<ObjCInterface>().filter { it.isExtensionsFacade }.map { ObjCClassForwardDeclaration(it.name) })
.toSet()
val stubs = (if (configuration.generateBaseDeclarationStubs) objCBaseDeclarations() else emptyList()).plus(objCStubs) val stubs = (if (configuration.generateBaseDeclarationStubs) objCBaseDeclarations() else emptyList()).plus(objCStubs)
.plus(listOfNotNull(errorInterface.takeIf { hasErrorTypes })) .plus(listOfNotNull(errorInterface.takeIf { hasErrorTypes }))
@@ -2,7 +2,6 @@
package org.jetbrains.kotlin.objcexport package org.jetbrains.kotlin.objcexport
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.annotationInfos import org.jetbrains.kotlin.analysis.api.annotations.annotationInfos
import org.jetbrains.kotlin.analysis.api.symbols.* import org.jetbrains.kotlin.analysis.api.symbols.*
@@ -11,11 +10,9 @@ import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.backend.konan.KonanFqNames import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.backend.konan.objcexport.* import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSelectors import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSelectors
import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSwiftNames import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSwiftNames
import org.jetbrains.kotlin.objcexport.analysisApiUtils.* import org.jetbrains.kotlin.objcexport.analysisApiUtils.*
import org.jetbrains.kotlin.psi.KtFile
internal val KtCallableSymbol.isConstructor: Boolean internal val KtCallableSymbol.isConstructor: Boolean
get() = this is KtConstructorSymbol get() = this is KtConstructorSymbol
@@ -28,13 +25,6 @@ fun KtFunctionSymbol.translateToObjCMethod(): ObjCMethod? {
return buildObjCMethod() return buildObjCMethod()
} }
context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.getObjCFileClassOrProtocolName(): ObjCExportFileName? {
val ktFile = this.psi as? KtFile ?: return null
val name = NameUtils.getPackagePartClassNamePrefix(FileUtil.getNameWithoutExtension(ktFile.name)) + "Kt"
return name.toIdentifier().getObjCFileName()
}
/** /**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.buildMethod] * [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.buildMethod]
*/ */
@@ -272,7 +262,7 @@ fun MethodBridge.valueParametersAssociated(
} }
private fun String.startsWithWords(words: String) = this.startsWith(words) && 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.ObjCExportTranslatorImpl.mapReturnType] * [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mapReturnType]
@@ -292,7 +282,7 @@ fun KtFunctionLikeSymbol.mapReturnType(returnBridge: MethodBridge.ReturnValue):
if (!returnBridge.successMayBeZero) { if (!returnBridge.successMayBeZero) {
check( check(
successReturnType is ObjCNonNullReferenceType successReturnType is ObjCNonNullReferenceType
|| (successReturnType is ObjCPointerType && !successReturnType.nullable) || (successReturnType is ObjCPointerType && !successReturnType.nullable)
) { ) {
"Unexpected return type: $successReturnType in $this" "Unexpected return type: $successReturnType in $this"
} }
@@ -117,7 +117,6 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
} }
@Test @Test
@TodoAnalysisApi
fun `test - functionWithObjCNameAnnotation`() { fun `test - functionWithObjCNameAnnotation`() {
doTest(headersTestDataDir.resolve("functionWithObjCNameAnnotation")) doTest(headersTestDataDir.resolve("functionWithObjCNameAnnotation"))
} }
@@ -243,15 +242,17 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
doTest(headersTestDataDir.resolve("interfaceImplementingInterfaceOrder")) doTest(headersTestDataDir.resolve("interfaceImplementingInterfaceOrder"))
} }
/**
* Extension functions aren't supported KT-65630
*/
@Test @Test
@TodoAnalysisApi
fun `test - extensionFunctions`() { fun `test - extensionFunctions`() {
doTest(headersTestDataDir.resolve("extensionFunctions")) doTest(headersTestDataDir.resolve("extensionFunctions"))
} }
@Test
fun `test - extensionProperties`() {
doTest(headersTestDataDir.resolve("extensionProperties"))
}
@Test @Test
fun `test - classWithGenerics`() { fun `test - classWithGenerics`() {
doTest(headersTestDataDir.resolve("classWithGenerics")) doTest(headersTestDataDir.resolve("classWithGenerics"))
@@ -24,11 +24,18 @@ __attribute__((objc_subclassing_restricted))
@interface Foo : Base @interface Foo : Base
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (void)funA __attribute__((swift_name("funA()"))); - (void)memberFun __attribute__((swift_name("memberFun()")));
@end @end
@interface Foo (Extensions) @interface Foo (Extensions)
- (void)funB __attribute__((swift_name("funB()"))); - (void)extensionFunA __attribute__((swift_name("extensionFunA()")));
- (void)extensionFunB __attribute__((swift_name("extensionFunB()")));
@end
__attribute__((objc_subclassing_restricted))
@interface FooKt : Base
+ (void)topLevelFunA __attribute__((swift_name("topLevelFunA()")));
+ (void)topLevelFunB __attribute__((swift_name("topLevelFunB()")));
@end @end
#pragma pop_macro("_Nullable_result") #pragma pop_macro("_Nullable_result")
@@ -1,5 +1,9 @@
class Foo { fun topLevelFunA() {}
fun funA() {} fun topLevelFunB() {}
}
fun Foo.funB() {} fun Foo.extensionFunA() {}
fun Foo.extensionFunB() {}
class Foo {
fun memberFun() {}
}
@@ -0,0 +1,45 @@
#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 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
__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")));
- (void)memberFun __attribute__((swift_name("memberFun()")));
@end
@interface Foo (Extensions)
@property (readonly) int32_t extensionValA __attribute__((swift_name("extensionValA")));
@property (readonly) int32_t extensionValB __attribute__((swift_name("extensionValB")));
@property int32_t extensionVarA __attribute__((swift_name("extensionVarA")));
@property int32_t extensionVarB __attribute__((swift_name("extensionVarB")));
@end
__attribute__((objc_subclassing_restricted))
@interface FooKt : Base
@property (class, readonly) int32_t topLevelPropA __attribute__((swift_name("topLevelPropA")));
@property (class, readonly) int32_t topLevelPropB __attribute__((swift_name("topLevelPropB")));
@end
#pragma pop_macro("_Nullable_result")
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
@@ -0,0 +1,18 @@
val topLevelPropA = 0
val topLevelPropB = 1
val Foo.extensionValA
get() = 0
val Foo.extensionValB
get() = 1
var Foo.extensionVarA
get() = 0
set(value) {}
var Foo.extensionVarB
get() = 1
set(value) {}
class Foo {
fun memberFun() {}
}