diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCClassOrProtocolName.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCClassOrProtocolName.kt index c2d00a15f97..6dc012621bf 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCClassOrProtocolName.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/getObjCClassOrProtocolName.kt @@ -6,16 +6,109 @@ 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.KtClassLikeSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol import org.jetbrains.kotlin.analysis.api.symbols.nameOrAnonymous import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportClassOrProtocolName +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly context(KtAnalysisSession, KtObjCExportSession) fun KtClassLikeSymbol.getObjCClassOrProtocolName(): ObjCExportClassOrProtocolName { val resolvedObjCNameAnnotation = resolveObjCNameAnnotation() return ObjCExportClassOrProtocolName( - objCName = resolvedObjCNameAnnotation?.objCName ?: nameOrAnonymous.asString(), - swiftName = resolvedObjCNameAnnotation?.swiftName ?: nameOrAnonymous.asString() + objCName = getObjCName(resolvedObjCNameAnnotation), + swiftName = getSwiftName(resolvedObjCNameAnnotation) ) +} + +context(KtAnalysisSession, KtObjCExportSession) +private fun KtClassLikeSymbol.getObjCName( + resolvedObjCNameAnnotation: KtResolvedObjCNameAnnotation? = resolveObjCNameAnnotation(), +): String { + val objCName = (resolvedObjCNameAnnotation?.objCName ?: nameOrAnonymous.asString()).toValidObjCSwiftIdentifier() + + if (resolvedObjCNameAnnotation != null && resolvedObjCNameAnnotation.isExact) { + return objCName + } + + getContainingSymbol()?.let { it as? KtClassLikeSymbol }?.let { containingClass -> + return containingClass.getObjCName() + objCName.capitalizeAsciiOnly() + } + + // KT-65670: Append module specific prefixes? + return configuration.frameworkName.orEmpty() + objCName +} + +context(KtAnalysisSession, KtObjCExportSession) +private fun KtClassLikeSymbol.getSwiftName( + resolvedObjCNameAnnotation: KtResolvedObjCNameAnnotation? = resolveObjCNameAnnotation(), +): String { + val swiftName = (resolvedObjCNameAnnotation?.swiftName ?: nameOrAnonymous.asString()).toValidObjCSwiftIdentifier() + if (resolvedObjCNameAnnotation != null && resolvedObjCNameAnnotation.isExact) { + return swiftName + } + + getContainingSymbol()?.let { it as? KtClassLikeSymbol }?.let { containingClass -> + val containingClassSwiftName = containingClass.getSwiftName() + return buildString { + if (canBeInnerSwift()) { + append(containingClassSwiftName) + if ("." !in this && containingClass.canBeOuterSwift()) { + // AB -> AB.C + append(".") + append(mangleSwiftNestedClassName(swiftName)) + } else { + // AB -> ABC + // A.B -> A.BC + append(swiftName.capitalizeAsciiOnly()) + } + } else { + append(containingClassSwiftName.replaceFirst(".", "")) + append(swiftName.capitalizeAsciiOnly()) + } + } + } + + // KT-65670: Append module specific prefixes? + return swiftName + +} + +context(KtAnalysisSession, KtObjCExportSession) +private fun KtClassLikeSymbol.canBeInnerSwift(): Boolean { + if (configuration.objcGenerics && this.typeParameters.isNotEmpty()) { + // Swift compiler doesn't seem to handle this case properly. + // See https://bugs.swift.org/browse/SR-14607. + // This behaviour of Kotlin is reported as https://youtrack.jetbrains.com/issue/KT-46518. + return false + } + + if (this is KtClassOrObjectSymbol && this.classKind == KtClassKind.INTERFACE) { + // Swift doesn't support nested protocols. + return false + } + + return true +} + +context(KtAnalysisSession, KtObjCExportSession) +private fun KtClassLikeSymbol.canBeOuterSwift(): Boolean { + if (configuration.objcGenerics && this.typeParameters.isNotEmpty()) { + // Swift nested classes are static but capture outer's generics. + return false + } + + if (this is KtClassOrObjectSymbol && this.classKind == KtClassKind.INTERFACE) { + // Swift doesn't support outer protocols. + return false + } + + return true +} + +private fun mangleSwiftNestedClassName(name: String): String = when (name) { + "Type" -> "${name}_" // See https://github.com/JetBrains/kotlin-native/issues/3167 + else -> name } \ 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 64e2836ed8d..893a8834a59 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 @@ -17,9 +17,10 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? { 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() 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 2c910e1a520..2e817a68a6d 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 @@ -70,15 +70,26 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) { doTest(headersTestDataDir.resolve("sameClassNameInDifferentPackage")) } - /** - * Naming of nested classes: KT-65633 - */ @Test - @TodoAnalysisApi fun `test - nestedClass`() { doTest(headersTestDataDir.resolve("nestedClass")) } + @Test + fun `test - nestedClassWithFrameworkName`() { + doTest(headersTestDataDir.resolve("nestedClassWithFrameworkName"), Configuration(frameworkName = "Shared")) + } + + @Test + fun `test - nestedGenericClass`() { + doTest(headersTestDataDir.resolve("nestedGenericClass")) + } + + @Test + fun `test - nestedInterface`() { + doTest(headersTestDataDir.resolve("nestedInterface")) + } + @Test @TodoAnalysisApi fun `test - samePropertyAndFunctionName`() { @@ -97,7 +108,6 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) { } @Test - @TodoAnalysisApi fun `test - classWithObjCNameAnnotation`() { doTest(headersTestDataDir.resolve("classWithObjCNameAnnotation")) } diff --git a/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/!nestedClassWithFrameworkName.h b/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/!nestedClassWithFrameworkName.h new file mode 100644 index 00000000000..1026fa440f6 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/!nestedClassWithFrameworkName.h @@ -0,0 +1,58 @@ +#import +#import +#import +#import +#import +#import +#import + +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("A"))) +@interface SharedA : SharedBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("A.A1"))) +@interface SharedAA1 : SharedBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("A.A1B1"))) +@interface SharedAA1B1 : SharedBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("A.A2"))) +@interface SharedAA2 : SharedBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("A.A2B2"))) +@interface SharedAA2B2 : SharedBase +- (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 diff --git a/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/Foo.kt b/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/Foo.kt new file mode 100644 index 00000000000..032a4f2d262 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedClassWithFrameworkName/Foo.kt @@ -0,0 +1,8 @@ +class A { + class A1 { + class B1 + } + class A2 { + class B2 + } +} \ No newline at end of file diff --git a/native/objcexport-header-generator/testData/headers/nestedGenericClass/!nestedGenericClass.h b/native/objcexport-header-generator/testData/headers/nestedGenericClass/!nestedGenericClass.h new file mode 100644 index 00000000000..9bf215b19e7 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedGenericClass/!nestedGenericClass.h @@ -0,0 +1,41 @@ +#import +#import +#import +#import +#import +#import +#import + +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 A : Base +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface AB : Base +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end + +__attribute__((objc_subclassing_restricted)) +@interface ABC : 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 diff --git a/native/objcexport-header-generator/testData/headers/nestedGenericClass/Foo.kt b/native/objcexport-header-generator/testData/headers/nestedGenericClass/Foo.kt new file mode 100644 index 00000000000..c4e40e5faf4 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedGenericClass/Foo.kt @@ -0,0 +1,5 @@ +class A { + class B { + class C + } +} \ No newline at end of file diff --git a/native/objcexport-header-generator/testData/headers/nestedInterface/!nestedInterface.h b/native/objcexport-header-generator/testData/headers/nestedInterface/!nestedInterface.h new file mode 100644 index 00000000000..a3b449dc0e3 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedInterface/!nestedInterface.h @@ -0,0 +1,35 @@ +#import +#import +#import +#import +#import +#import +#import + +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 A +@required +@end + +@protocol AB +@required +@end + +@protocol ABC +@required +@end + +#pragma pop_macro("_Nullable_result") +#pragma clang diagnostic pop +NS_ASSUME_NONNULL_END diff --git a/native/objcexport-header-generator/testData/headers/nestedInterface/Foo.kt b/native/objcexport-header-generator/testData/headers/nestedInterface/Foo.kt new file mode 100644 index 00000000000..ed8d8f8b7f4 --- /dev/null +++ b/native/objcexport-header-generator/testData/headers/nestedInterface/Foo.kt @@ -0,0 +1,5 @@ +interface A { + interface B { + interface C + } +} \ No newline at end of file