diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getFileName.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getFileName.kt new file mode 100644 index 00000000000..45b2c5294ad --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getFileName.kt @@ -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)) +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getMethodBridge.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getMethodBridge.kt index 727f75a1dad..6379401bf5a 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getMethodBridge.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getMethodBridge.kt @@ -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 diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt index 80f8ca89db7..6f3b63affd3 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt @@ -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 diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getExtensionsFacade.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getExtensionsFacade.kt new file mode 100644 index 00000000000..bdd7892a48e --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getExtensionsFacade.kt @@ -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() + ) +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCFileClassOrProtocolName.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCFileClassOrProtocolName.kt new file mode 100644 index 00000000000..7b22e42a985 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCFileClassOrProtocolName.kt @@ -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() +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCTopLevelInterfaceFileFacade.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getTopLevelFacade.kt similarity index 68% rename from native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCTopLevelInterfaceFileFacade.kt rename to native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getTopLevelFacade.kt index da1bfa11a25..bd887722f81 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCTopLevelInterfaceFileFacade.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getTopLevelFacade.kt @@ -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() ) -} +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt index 1a9ecb0f658..758edcc5539 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt @@ -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() @@ -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() .map { stub -> ObjCClassForwardDeclaration(stub.name, stub.generics) } - .plus(listOfNotNull(errorForwardClass.takeIf { hasErrorTypes })).toSet() + .plus(listOfNotNull(errorForwardClass.takeIf { hasErrorTypes })) + .plus(objCStubs.filterIsInstance().filter { it.isExtensionsFacade }.map { ObjCClassForwardDeclaration(it.name) }) + .toSet() val stubs = (if (configuration.generateBaseDeclarationStubs) objCBaseDeclarations() else emptyList()).plus(objCStubs) .plus(listOfNotNull(errorInterface.takeIf { hasErrorTypes })) diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCMethod.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCMethod.kt index 25cde6fd8f0..c53981c3bfd 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCMethod.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCMethod.kt @@ -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" } diff --git a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt index 86e502864bd..fb040a4bde8 100644 --- a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt +++ b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt @@ -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")) diff --git a/native/objcexport-header-generator/testData/headers/extensionFunctions/!extensionFunctions.h b/native/objcexport-header-generator/testData/headers/extensionFunctions/!extensionFunctions.h index e7524ce886b..0af39349a61 100644 --- a/native/objcexport-header-generator/testData/headers/extensionFunctions/!extensionFunctions.h +++ b/native/objcexport-header-generator/testData/headers/extensionFunctions/!extensionFunctions.h @@ -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") diff --git a/native/objcexport-header-generator/testData/headers/extensionFunctions/Foo.kt b/native/objcexport-header-generator/testData/headers/extensionFunctions/Foo.kt index bc3042b3a6d..f20146dd380 100644 --- a/native/objcexport-header-generator/testData/headers/extensionFunctions/Foo.kt +++ b/native/objcexport-header-generator/testData/headers/extensionFunctions/Foo.kt @@ -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() {} +} diff --git a/native/objcexport-header-generator/testData/headers/extensionProperties/!extensionProperties.h b/native/objcexport-header-generator/testData/headers/extensionProperties/!extensionProperties.h new file mode 100644 index 00000000000..ba8a01761a4 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/extensionProperties/!extensionProperties.h @@ -0,0 +1,45 @@ +#import +#import +#import +#import +#import +#import +#import + +@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 diff --git a/native/objcexport-header-generator/testData/headers/extensionProperties/Foo.kt b/native/objcexport-header-generator/testData/headers/extensionProperties/Foo.kt new file mode 100644 index 00000000000..8d8434f6a65 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/extensionProperties/Foo.kt @@ -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() {} +} \ No newline at end of file