From 97f585dd25b19e273d85e14c528cf845b45991c4 Mon Sep 17 00:00:00 2001 From: "eugene.levenetc" Date: Tue, 13 Feb 2024 14:35:09 +0100 Subject: [PATCH] [ObjCExport] Add companion object translation KT-64953: Required part for enum translation --- .../objcexport/analysisApiUtils/companions.kt | 8 ++ .../analysisApiUtils/isVisibleInObjC.kt | 4 +- .../objcexport/buildCompanionProperty.kt | 31 +++++++ .../objcexport/needsCompanionProperty.kt | 15 ++++ .../kotlin/objcexport/translateToObjCClass.kt | 14 +--- .../objcexport/translateToObjCHeader.kt | 1 + .../objcexport/translateToObjCObject.kt | 19 ++--- .../kotlin/objcexport/translateToObjCType.kt | 16 ++-- .../tests/NeedsCompanionPropertyTest.kt | 58 +++++++++++++ .../tests/ObjCExportHeaderGeneratorTest.kt | 5 ++ .../testData/headers/companion/!companion.h | 81 +++++++++++++++++++ .../testData/headers/companion/Foo.kt | 23 ++++++ 12 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/companions.kt create mode 100644 native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/buildCompanionProperty.kt create mode 100644 native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/needsCompanionProperty.kt create mode 100644 native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/NeedsCompanionPropertyTest.kt create mode 100644 native/objcexport-header-generator/testData/headers/companion/!companion.h create mode 100644 native/objcexport-header-generator/testData/headers/companion/Foo.kt diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/companions.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/companions.kt new file mode 100644 index 00000000000..6dd2975656f --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/companions.kt @@ -0,0 +1,8 @@ +package org.jetbrains.kotlin.objcexport.analysisApiUtils + +import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol + + +internal val KtClassOrObjectSymbol.isCompanion: Boolean + get() = classKind == KtClassKind.COMPANION_OBJECT \ No newline at end of file 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 eb3f645162e..80f8ca89db7 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 @@ -173,6 +173,6 @@ private fun KtClassOrObjectSymbol.isInlined(): Boolean { } private fun KtClassKind.isVisibleInObjC(): Boolean = when (this) { - CLASS, ENUM_CLASS, INTERFACE, OBJECT -> true - COMPANION_OBJECT, ANONYMOUS_OBJECT, ANNOTATION_CLASS -> false + CLASS, ENUM_CLASS, INTERFACE, OBJECT, COMPANION_OBJECT -> true + ANONYMOUS_OBJECT, ANNOTATION_CLASS -> false } diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/buildCompanionProperty.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/buildCompanionProperty.kt new file mode 100644 index 00000000000..37b3861a64e --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/buildCompanionProperty.kt @@ -0,0 +1,31 @@ +package org.jetbrains.kotlin.objcexport + +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClassType +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCProperty +import org.jetbrains.kotlin.backend.konan.objcexport.swiftNameAttribute +import org.jetbrains.kotlin.objcexport.analysisApiUtils.isCompanion + +/** + * If object class has companion object it needs to have property which returns this companion. + * To check whether class has companion object see [needsCompanionProperty] + */ +context(KtAnalysisSession, KtObjCExportSession) +internal fun KtClassOrObjectSymbol.buildCompanionProperty(): ObjCProperty { + + val companion = this.getStaticMemberScope().getClassifierSymbols().toList() + .firstOrNull { (it as? KtClassOrObjectSymbol)?.isCompanion == true } + val typeName = (companion as KtClassOrObjectSymbol).getObjCClassOrProtocolName() + val propertyName = ObjCPropertyNames.companionObjectPropertyName + + return ObjCProperty( + propertyName, + null, + null, + ObjCClassType(typeName.objCName, classId = companion.classIdIfNonLocal), + listOf("class", "readonly"), + getterName = propertyName, + declarationAttributes = listOf(swiftNameAttribute(propertyName)) + ) +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/needsCompanionProperty.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/needsCompanionProperty.kt new file mode 100644 index 00000000000..5584fa6c485 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/needsCompanionProperty.kt @@ -0,0 +1,15 @@ +package org.jetbrains.kotlin.objcexport + +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.objcexport.analysisApiUtils.isCompanion + +/** + * [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslator.needCompanionObjectProperty] + */ +context(KtAnalysisSession) +internal val KtClassOrObjectSymbol.needsCompanionProperty: Boolean + get() { + return this.getStaticMemberScope().getClassifierSymbols() + .any { (it as? KtClassOrObjectSymbol)?.isCompanion == true } + } \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt index abe820f97ae..831f90a257b 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCClass.kt @@ -29,10 +29,12 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? { val superProtocols: List = superProtocols() val constructors = getMemberScope().getConstructors().filter { !it.hasExportForCompilerAnnotation } - val members: List = getMemberScope().getCallableSymbols().plus(constructors) + val members = getMemberScope().getCallableSymbols().plus(constructors) .sortedWith(StableCallableOrder) .flatMap { it.translateToObjCExportStubs() } - .toList() + .toMutableList() + + if (needsCompanionProperty) members.add(buildCompanionProperty()) val categoryName: String? = null @@ -57,15 +59,7 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? { ) } -private fun abbreviate(name: String): String { - val normalizedName = name - .replaceFirstChar(Char::uppercaseChar) - .replace("-|\\.".toRegex(), "_") - val uppers = normalizedName.filterIndexed { index, character -> index == 0 || character.isUpperCase() } - if (uppers.length >= 3) return uppers - return normalizedName -} context(KtAnalysisSession, KtObjCExportSession) internal fun KtNonErrorClassType.getSuperClassName(): ObjCExportClassOrProtocolName? { 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 321f405e1ac..728bdae4fb2 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 @@ -52,6 +52,7 @@ fun translateToObjCHeader(files: List): ObjCHeader { KtClassKind.CLASS -> symbol.translateToObjCClass() KtClassKind.OBJECT -> symbol.translateToObjCObject() KtClassKind.ENUM_CLASS -> symbol.translateToObjCClass() + KtClassKind.COMPANION_OBJECT -> symbol.translateToObjCObject() else -> return result } ?: return result diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCObject.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCObject.kt index ba4869ddd71..232dc3fbcf3 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCObject.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCObject.kt @@ -6,24 +6,24 @@ 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.isCompanion import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC context(KtAnalysisSession, KtObjCExportSession) fun KtClassOrObjectSymbol.translateToObjCObject(): ObjCClass? { - require(classKind == KtClassKind.OBJECT) + require(classKind == KtClassKind.OBJECT || classKind == KtClassKind.COMPANION_OBJECT) if (!isVisibleInObjC()) return null 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 attributes = (if (enumKind || final) listOf(OBJC_SUBCLASSING_RESTRICTED) else emptyList()) + name.toNameAttributes() val comment: ObjCComment? = annotationsList.translateToObjCComment() val origin: ObjCExportStubOrigin = getObjCExportStubOrigin() val superProtocols: List = superProtocols() val categoryName: String? = null val generics: List = emptyList() - val objectMembers = getDefaultMembers() + val objectMembers = getDefaultMembers().toMutableList() val superClass = translateSuperClass() @@ -47,7 +47,7 @@ fun KtClassOrObjectSymbol.translateToObjCObject(): ObjCClass? { } context(KtAnalysisSession, KtObjCExportSession) -private fun KtClassOrObjectSymbol.getDefaultMembers(): MutableList { +private fun KtClassOrObjectSymbol.getDefaultMembers(): List { val result = mutableListOf() val allocWithZoneParameter = ObjCParameter("zone", null, ObjCRawType("struct _NSZone *"), null) @@ -91,8 +91,9 @@ private fun KtClassOrObjectSymbol.getDefaultMembers(): MutableList { diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/NeedsCompanionPropertyTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/NeedsCompanionPropertyTest.kt new file mode 100644 index 00000000000..e319b92d15c --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/NeedsCompanionPropertyTest.kt @@ -0,0 +1,58 @@ +package org.jetbrains.kotlin.objcexport.tests + +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.objcexport.needsCompanionProperty +import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis +import org.jetbrains.kotlin.objcexport.testUtils.getClassOrFail +import org.junit.jupiter.api.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NeedsCompanionPropertyTest( + private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis, +) { + + @Test + fun `test - no companion`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + class NoCompanion { + + } + """.trimIndent() + ) + analyze(file) { + assertFalse(file.getClassOrFail("NoCompanion").needsCompanionProperty) + } + } + + @Test + fun `test - empty companion`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + class EmptyCompanion { + companion object {} + } + """.trimIndent() + ) + analyze(file) { + assertTrue(file.getClassOrFail("EmptyCompanion").needsCompanionProperty) + } + } + + @Test + fun `test - simple companion`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + class SimpleCompanion { + companion object { + const val a = 42 + } + } + """.trimIndent() + ) + analyze(file) { + assertTrue(file.getClassOrFail("SimpleCompanion").needsCompanionProperty) + } + } +} \ No newline at end of file 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 9d98095d67e..c4e04f58fc3 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 @@ -277,6 +277,11 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) { doTest(headersTestDataDir.resolve("constructors")) } + @Test + fun `test - companion`() { + doTest(headersTestDataDir.resolve("companion")) + } + private fun doTest(root: File, configuration: Configuration = Configuration()) { if (!root.isDirectory) fail("Expected ${root.absolutePath} to be directory") val generatedHeaders = generator.generateHeaders(root, configuration).toString() diff --git a/native/objcexport-header-generator/testData/headers/companion/!companion.h b/native/objcexport-header-generator/testData/headers/companion/!companion.h new file mode 100644 index 00000000000..197a89418bf --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/companion/!companion.h @@ -0,0 +1,81 @@ +#import +#import +#import +#import +#import +#import +#import + +@class Foo1Companion, Foo2Companion, Foo3Companion; + +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 Foo1 : Base +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@property (class, readonly, getter=companion) Foo1Companion *companion __attribute__((swift_name("companion"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("Foo1.Companion"))) +@interface Foo1Companion : Base ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion __attribute__((swift_name("init()"))); +@property (class, readonly, getter=shared) Foo1Companion *shared __attribute__((swift_name("shared"))); +- (void)publicFoo __attribute__((swift_name("publicFoo()"))); +@property (readonly) int32_t constant __attribute__((swift_name("constant"))); +@property (readonly) Foo2Companion *refToFoo2 __attribute__((swift_name("refToFoo2"))); +@property (readonly) Foo1Companion *refToItself __attribute__((swift_name("refToItself"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface Foo2 : Base +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@property (class, readonly, getter=companion) Foo2Companion *companion __attribute__((swift_name("companion"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("Foo2.Companion"))) +@interface Foo2Companion : Base ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion __attribute__((swift_name("init()"))); +@property (class, readonly, getter=shared) Foo2Companion *shared __attribute__((swift_name("shared"))); +- (void)publicFoo __attribute__((swift_name("publicFoo()"))); +@property (readonly) int32_t constant __attribute__((swift_name("constant"))); +@property (readonly) Foo1Companion *refToFoo1 __attribute__((swift_name("refToFoo1"))); +@property (readonly) Foo2Companion *refToItself __attribute__((swift_name("refToItself"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface Foo3 : Base +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@property (class, readonly, getter=companion) Foo3Companion *companion __attribute__((swift_name("companion"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("Foo3.Companion"))) +@interface Foo3Companion : Base ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion __attribute__((swift_name("init()"))); +@property (class, readonly, getter=shared) Foo3Companion *shared __attribute__((swift_name("shared"))); +@end + +#pragma pop_macro("_Nullable_result") +#pragma clang diagnostic pop +NS_ASSUME_NONNULL_END diff --git a/native/objcexport-header-generator/testData/headers/companion/Foo.kt b/native/objcexport-header-generator/testData/headers/companion/Foo.kt new file mode 100644 index 00000000000..162ca32be60 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/companion/Foo.kt @@ -0,0 +1,23 @@ +class Foo2 { + companion object { + const val constant = 42 + val refToItself = Foo2 + val refToFoo1 = Foo1 + fun publicFoo() {} + } +} + +class Foo3 { + companion object { + + } +} + +class Foo1 { + companion object { + const val constant = 42 + val refToItself = Foo1 + val refToFoo2 = Foo2 + fun publicFoo() {} + } +} \ No newline at end of file