[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
get() = if (isArrayConstructor) {
MethodBridgeReceiver.Factory
} else if (!isConstructor && isTopLevel) {
} else if (!isConstructor && isTopLevel && !isExtension) {
MethodBridgeReceiver.Static
} else {
MethodBridgeReceiver.Instance
@@ -21,7 +21,7 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
context(KtAnalysisSession)
internal fun KtSymbol.isVisibleInObjC(): Boolean = when(this) {
internal fun KtSymbol.isVisibleInObjC(): Boolean = when (this) {
is KtCallableSymbol -> this.isVisibleInObjC()
is KtClassOrObjectSymbol -> this.isVisibleInObjC()
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.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
/**
* Translates top level functions/properties inside the given [this] file as a single [ObjCInterface].
* ## 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.
*
* See related [getExtensionsFacade]
*/
context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.translateToObjCTopLevelInterfaceFileFacade(): ObjCInterface? {
val topLevelCallableStubs = getFileScope().getCallableSymbols()
.sortedWith(StableCallableOrder)
.mapNotNull { callableSymbol -> callableSymbol.translateToObjCExportStub() }
fun KtFileSymbol.getTopLevelFacade(): ObjCInterface? {
val extensions = getFileScope().getCallableSymbols()
.filter { !it.isExtension }
.toList()
/* If there are no top level functions or properties, we do not need to export a file facade */
.sortedWith(StableCallableOrder)
.ifEmpty { return null }
val fileName = getObjCFileClassOrProtocolName()
?: throw IllegalStateException("Top level file '$this' cannot be translated without file name")
val name = fileName.objCName
val attributes = listOf(OBJC_SUBCLASSING_RESTRICTED)
val superClass = getDefaultSuperClassOrProtocolName()
?: throw IllegalStateException("File '$this' cannot be translated without file name")
return ObjCInterfaceImpl(
name = name,
name = fileName.objCName,
comment = null,
origin = null,
attributes = attributes,
attributes = listOf(OBJC_SUBCLASSING_RESTRICTED),
superProtocols = emptyList(),
members = topLevelCallableStubs,
members = extensions.mapNotNull { it.translateToObjCExportStub() },
categoryName = null,
generics = emptyList(),
superClass = superClass.objCName,
superClass = getDefaultSuperClassOrProtocolName().objCName,
superClassGenerics = emptyList()
)
}
}
@@ -37,7 +37,7 @@ private class KtObjCExportHeaderGenerator {
* 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):
* Note: Top level functions and properties will be translated as part of the file.
* See [translateToObjCTopLevelInterfaceFileFacade]
* See [translateToObjCTopLevelInterfaceFileFacades]
*/
private val symbolDeque = ArrayDeque<QueueElement>()
@@ -90,17 +90,17 @@ private class KtObjCExportHeaderGenerator {
context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileElement(element: QueueElement.File) {
val fileSymbol = element.psi.getFileSymbol()
translateFileSymbol(fileSymbol)
fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol ->
translateClassOrObjectSymbol(classOrObjectSymbol)
}
translateFileSymbol(fileSymbol)
}
context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileSymbol(symbol: KtFileSymbol) {
val objCInterface = symbol.translateToObjCTopLevelInterfaceFileFacade() ?: return
objCStubs += objCInterface
enqueueDependencyClasses(objCInterface)
val objCFacades = listOfNotNull(symbol.getExtensionsFacade(), symbol.getTopLevelFacade()).ifEmpty { return }
objCStubs += objCFacades
objCFacades.forEach { enqueueDependencyClasses(it) }
}
context(KtAnalysisSession, KtObjCExportSession)
@@ -178,7 +178,9 @@ private class KtObjCExportHeaderGenerator {
val classForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCInterface>()
.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)
.plus(listOfNotNull(errorInterface.takeIf { hasErrorTypes }))
@@ -2,7 +2,6 @@
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.annotations.annotationInfos
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.objcexport.*
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.anyMethodSwiftNames
import org.jetbrains.kotlin.objcexport.analysisApiUtils.*
import org.jetbrains.kotlin.psi.KtFile
internal val KtCallableSymbol.isConstructor: Boolean
get() = this is KtConstructorSymbol
@@ -28,13 +25,6 @@ fun KtFunctionSymbol.translateToObjCMethod(): ObjCMethod? {
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]
*/
@@ -272,7 +262,7 @@ fun MethodBridge.valueParametersAssociated(
}
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]
@@ -292,7 +282,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"
}
@@ -117,7 +117,6 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
}
@Test
@TodoAnalysisApi
fun `test - functionWithObjCNameAnnotation`() {
doTest(headersTestDataDir.resolve("functionWithObjCNameAnnotation"))
}
@@ -243,15 +242,17 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
doTest(headersTestDataDir.resolve("interfaceImplementingInterfaceOrder"))
}
/**
* Extension functions aren't supported KT-65630
*/
@Test
@TodoAnalysisApi
fun `test - extensionFunctions`() {
doTest(headersTestDataDir.resolve("extensionFunctions"))
}
@Test
fun `test - extensionProperties`() {
doTest(headersTestDataDir.resolve("extensionProperties"))
}
@Test
fun `test - classWithGenerics`() {
doTest(headersTestDataDir.resolve("classWithGenerics"))
@@ -24,11 +24,18 @@ __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)funA __attribute__((swift_name("funA()")));
- (void)memberFun __attribute__((swift_name("memberFun()")));
@end
@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
#pragma pop_macro("_Nullable_result")
@@ -1,5 +1,9 @@
class Foo {
fun funA() {}
}
fun topLevelFunA() {}
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() {}
}