diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt index 25e5b34ce45..9f3da64f5eb 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt @@ -604,7 +604,7 @@ internal class ObjCExportTranslatorImpl( private fun buildMethod(method: FunctionDescriptor, baseMethod: FunctionDescriptor, objCExportScope: ObjCExportScope): ObjCMethod { fun collectParameters(baseMethodBridge: MethodBridge, method: FunctionDescriptor): List { fun unifyName(initialName: String, usedNames: Set): String { - var unique = initialName + var unique = initialName.toValidObjCSwiftIdentifier() while (unique in usedNames || unique in cKeywords) { unique += "_" } @@ -986,8 +986,7 @@ abstract class ObjCExportHeaderGenerator internal constructor( add("#pragma clang diagnostic push") listOf( "-Wunknown-warning-option", - "-Wnullability", - "-Wswift-name-attribute" + "-Wnullability" ).forEach { add("#pragma clang diagnostic ignored \"$it\"") } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt index beefc5edd28..db51be80919 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt @@ -96,6 +96,7 @@ internal open class ObjCExportNameTranslatorImpl( private fun getClassOrProtocolSwiftName( ktClassOrObject: KtClassOrObject ): String = buildString { + val ownName = ktClassOrObject.name!!.toIdentifier() val outerClass = ktClassOrObject.getStrictParentOfType() if (outerClass != null) { append(getClassOrProtocolSwiftName(outerClass)) @@ -119,12 +120,12 @@ internal open class ObjCExportNameTranslatorImpl( } if (importAsMember) { - append(".").append(ktClassOrObject.name!!) + append(".").append(ownName) } else { - append(ktClassOrObject.name!!.capitalize()) + append(ownName.capitalize()) } } else { - append(ktClassOrObject.name) + append(ownName) } } } @@ -133,7 +134,8 @@ private class ObjCExportNamingHelper( private val topLevelNamePrefix: String ) { - fun translateFileName(fileName: String): String = PackagePartClassUtils.getFilePartShortName(fileName) + fun translateFileName(fileName: String): String = + PackagePartClassUtils.getFilePartShortName(fileName).toIdentifier() fun translateFileName(file: KtFile): String = translateFileName(file.name) @@ -328,10 +330,11 @@ internal class ObjCExportNamerImpl( else -> true } + val ownName = descriptor.name.asString().toIdentifier() if (importAsMember) { - append(".").append(descriptor.name.asString()) + append(".").append(ownName) } else { - append(descriptor.name.asString().capitalize()) + append(ownName.capitalize()) } } else if (containingDeclaration is PackageFragmentDescriptor) { appendTopLevelClassBaseName(descriptor) @@ -364,7 +367,7 @@ internal class ObjCExportNamerImpl( val containingDeclaration = descriptor.containingDeclaration if (containingDeclaration is ClassDescriptor) { append(getClassOrProtocolObjCName(containingDeclaration)) - .append(descriptor.name.asString().capitalize()) + .append(descriptor.name.asString().toIdentifier().capitalize()) } else if (containingDeclaration is PackageFragmentDescriptor) { append(topLevelNamePrefix).appendTopLevelClassBaseName(descriptor) @@ -379,7 +382,7 @@ internal class ObjCExportNamerImpl( configuration.getAdditionalPrefix(descriptor.module)?.let { append(it) } - append(descriptor.name.asString()) + append(descriptor.name.asString().toIdentifier()) } override fun getSelector(method: FunctionDescriptor): String = methodSelectors.getOrPut(method) { @@ -400,7 +403,7 @@ internal class ObjCExportNamerImpl( 1 -> "" else -> "value" } - else -> it!!.name.asString() + else -> it!!.name.asString().toIdentifier() } MethodBridgeValueParameter.ErrorOutParameter -> "error" is MethodBridgeValueParameter.KotlinResultOutParameter -> "result" @@ -451,7 +454,7 @@ internal class ObjCExportNamerImpl( 1 -> "_" else -> "value" } - else -> it!!.name.asString() + else -> it!!.name.asString().toIdentifier() } MethodBridgeValueParameter.ErrorOutParameter -> continue@parameters is MethodBridgeValueParameter.KotlinResultOutParameter -> "result" @@ -482,7 +485,7 @@ internal class ObjCExportNamerImpl( assert(mapper.isObjCProperty(property)) StringBuilder().apply { - append(property.name.asString()) + append(property.name.asString().toIdentifier()) }.mangledSequence { append('_') } @@ -492,7 +495,7 @@ internal class ObjCExportNamerImpl( assert(descriptor.kind == ClassKind.OBJECT) return objectInstanceSelectors.getOrPut(descriptor) { - val name = descriptor.name.asString().decapitalize().mangleIfSpecialFamily("get") + val name = descriptor.name.asString().decapitalize().toIdentifier().mangleIfSpecialFamily("get") StringBuilder(name).mangledBySuffixUnderscores() } @@ -506,7 +509,7 @@ internal class ObjCExportNamerImpl( val name = descriptor.name.asString().split('_').mapIndexed { index, s -> val lower = s.toLowerCase() if (index == 0) lower else lower.capitalize() - }.joinToString("").mangleIfSpecialFamily("the") + }.joinToString("").toIdentifier().mangleIfSpecialFamily("the") StringBuilder(name).mangledBySuffixUnderscores() } @@ -515,7 +518,7 @@ internal class ObjCExportNamerImpl( override fun getTypeParameterName(typeParameterDescriptor: TypeParameterDescriptor): String { return genericTypeParameterNameMapping.getOrPut(typeParameterDescriptor) { StringBuilder().apply { - append(typeParameterDescriptor.name.asString()) + append(typeParameterDescriptor.name.asString().toIdentifier()) }.mangledSequence { append('_') } @@ -580,7 +583,7 @@ internal class ObjCExportNamerImpl( is PropertyGetterDescriptor -> this.correspondingProperty.name.asString() is PropertySetterDescriptor -> "set${this.correspondingProperty.name.asString().capitalize()}" else -> this.name.asString() - } + }.toIdentifier() return candidate.mangleIfSpecialFamily("do") } @@ -825,3 +828,17 @@ internal fun abbreviate(name: String): String { return normalizedName } + +// Note: most usages of this method rely on the fact that concatenation of valid identifiers is valid identifier. +// This may sometimes be a bit conservative (since it requires mangling non-first character as if it was first); +// ignore this for simplicity as having Kotlin identifiers starting from digits is supposed to be rare case. +internal fun String.toValidObjCSwiftIdentifier(): String { + if (this.isEmpty()) return "__" + + return this.replace('$', '_') // TODO: handle more special characters. + .let { if (it.first().isDigit()) "_$it" else it } + .let { if (it == "_") "__" else it } +} + +// Private shortcut. +private fun String.toIdentifier(): String = this.toValidObjCSwiftIdentifier() diff --git a/backend.native/tests/framework/values/expectedLazy.h b/backend.native/tests/framework/values/expectedLazy.h index ae3d539c632..259413433ea 100644 --- a/backend.native/tests/framework/values/expectedLazy.h +++ b/backend.native/tests/framework/values/expectedLazy.h @@ -593,6 +593,54 @@ __attribute__((swift_name("TestClashesImpl"))) @property (readonly) ValuesInt *clashingProperty_ __attribute__((swift_name("clashingProperty_"))); @end; +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("TestInvalidIdentifiers"))) +@interface ValuesTestInvalidIdentifiers : KotlinBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (int32_t)a_d_d_1:(int32_t)_1 _2:(int32_t)_2 _3:(int32_t)_3 __attribute__((swift_name("a_d_d(_1:_2:_3:)"))); +@property NSString *_status __attribute__((swift_name("_status"))); +@property (readonly) unichar __ __attribute__((swift_name("__"))); +@property (readonly) unichar __ __attribute__((swift_name("__"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("TestInvalidIdentifiers._Foo"))) +@interface ValuesTestInvalidIdentifiers_Foo : KotlinBase +- (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("TestInvalidIdentifiers.Bar_"))) +@interface ValuesTestInvalidIdentifiersBar_ : KotlinBase +- (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("TestInvalidIdentifiers.E"))) +@interface ValuesTestInvalidIdentifiersE : ValuesKotlinEnum ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); +@property (class, readonly) ValuesTestInvalidIdentifiersE *_4_ __attribute__((swift_name("_4_"))); +@property (class, readonly) ValuesTestInvalidIdentifiersE *_5_ __attribute__((swift_name("_5_"))); +@property (class, readonly) ValuesTestInvalidIdentifiersE *__ __attribute__((swift_name("__"))); +@property (class, readonly) ValuesTestInvalidIdentifiersE *__ __attribute__((swift_name("__"))); +- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +- (int32_t)compareToOther:(ValuesTestInvalidIdentifiersE *)other __attribute__((swift_name("compareTo(other:)"))); +@property (readonly) int32_t value __attribute__((swift_name("value"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("TestInvalidIdentifiers.Companion_"))) +@interface ValuesTestInvalidIdentifiersCompanion_ : KotlinBase ++ (instancetype)alloc __attribute__((unavailable)); ++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); ++ (instancetype)companion_ __attribute__((swift_name("init()"))); +@property (readonly) int32_t _42 __attribute__((swift_name("_42"))); +@end; + @interface ValuesEnumeration (ValuesKt) - (ValuesEnumeration *)getAnswer __attribute__((swift_name("getAnswer()"))); @end; diff --git a/backend.native/tests/framework/values/values.kt b/backend.native/tests/framework/values/values.kt index 27634a5aa0d..0b14743a2ed 100644 --- a/backend.native/tests/framework/values/values.kt +++ b/backend.native/tests/framework/values/values.kt @@ -468,3 +468,26 @@ class TestClashesImpl : TestClashes1, TestClashes2 { override val clashingProperty_: Int get() = 2 } + +class TestInvalidIdentifiers { + class `$Foo` + class `Bar$` + + fun `a$d$d`(`$1`: Int, `2`: Int, `3`: Int): Int = `$1` + `2` + `3` + + var `$status`: String = "" + + enum class E(val value: Int) { + `4$`(4), + `5$`(5), + `_`(6), + `__`(7) + } + + companion object `Companion$` { + val `42` = 42 + } + + val `$` = '$' + val `_` = '_' +} diff --git a/backend.native/tests/framework/values/values.swift b/backend.native/tests/framework/values/values.swift index a945a1939cd..10b7d4db5c8 100644 --- a/backend.native/tests/framework/values/values.swift +++ b/backend.native/tests/framework/values/values.swift @@ -623,6 +623,27 @@ func testClashes() throws { try assertEquals(actual: 2, expected: test2.clashingProperty__ as! Int32) } +func testInvalidIdentifiers() throws { + let test = TestInvalidIdentifiers() + + try assertTrue(TestInvalidIdentifiers._Foo() is TestInvalidIdentifiers._Foo) + try assertFalse(TestInvalidIdentifiers.Bar_() is TestInvalidIdentifiers._Foo) + + try assertEquals(actual: 42, expected: test.a_d_d(_1: 13, _2: 14, _3: 15)) + + test._status = "OK" + try assertEquals(actual: "OK", expected: test._status) + + try assertEquals(actual: TestInvalidIdentifiers.E._4_.value, expected: 4) + try assertEquals(actual: TestInvalidIdentifiers.E._5_.value, expected: 5) + try assertEquals(actual: TestInvalidIdentifiers.E.__.value, expected: 6) + try assertEquals(actual: TestInvalidIdentifiers.E.___.value, expected: 7) + + try assertEquals(actual: TestInvalidIdentifiers.Companion_()._42, expected: 42) + + try assertEquals(actual: Set([test.__, test.___]), expected: Set(["$".utf16.first, "_".utf16.first])) +} + // See https://github.com/JetBrains/kotlin-native/issues/2931 func testGH2931() throws { for i in 0..<50000 { @@ -689,6 +710,7 @@ class ValuesTests : TestProvider { TestCase(name: "TestKClass", method: withAutorelease(testKClass)), TestCase(name: "TestSR10177Workaround", method: withAutorelease(testSR10177Workaround)), TestCase(name: "TestClashes", method: withAutorelease(testClashes)), + TestCase(name: "TestInvalidIdentifiers", method: withAutorelease(testInvalidIdentifiers)), TestCase(name: "TestGH2931", method: withAutorelease(testGH2931)), ] }