From 858af02a24bf5de0d303af122e33d8c367195cd4 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Wed, 24 Jan 2024 09:55:22 +0100 Subject: [PATCH] [ObjCExport] Initial support for inline classes in type mapping ^KT-65167 --- .../getInlineTargetTypeOrNull.kt | 77 +++++++++++++ .../analysisApiUtils/getMethodBridge.kt | 6 +- .../objcexport/translateToObjCMethod.kt | 18 ++- .../translateToObjCReferenceType.kt | 103 +++++++++++------- .../testUtils/AnalysisApiAssertions.kt | 16 +++ .../tests/GetInlineTargetTypeOrNullTest.kt | 87 +++++++++++++++ 6 files changed, 261 insertions(+), 46 deletions(-) create mode 100644 native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getInlineTargetTypeOrNull.kt create mode 100644 native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/GetInlineTargetTypeOrNullTest.kt diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getInlineTargetTypeOrNull.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getInlineTargetTypeOrNull.kt new file mode 100644 index 00000000000..16de1053ce5 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getInlineTargetTypeOrNull.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.objcexport.analysisApiUtils + +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.kotlin.analysis.api.types.KtTypeNullability +import org.jetbrains.kotlin.backend.konan.InteropFqNames +import org.jetbrains.kotlin.backend.konan.KonanFqNames + +context(KtAnalysisSession) +internal fun KtType.getInlineTargetTypeOrNull(): KtType? { + if (this !is KtNonErrorClassType) return null + val classSymbol = classSymbol as? KtNamedClassOrObjectSymbol ?: return null + return classSymbol.getInlineTargetTypeOrNull()?.markNullableIf(isMarkedNullable) +} + +context(KtAnalysisSession) +internal fun KtNamedClassOrObjectSymbol.getInlineTargetTypeOrNull(): KtType? { + if (!isInlineIncludingKotlinNativeSpecialClasses()) return null + + val constructor = getMemberScope().getConstructors() + .find { constructor -> constructor.isPrimary && constructor.valueParameters.size == 1 } + ?: return null + + val inlinedType = constructor.valueParameters.single().returnType + + /* What if this type is also inline type? */ + val inlineInlineType = inlinedType.getInlineTargetTypeOrNull() + if (inlineInlineType != null) { + return inlineInlineType + } + + return inlinedType +} + +/** + * Kotlin/Native specific implementation for testing if a certain class can be considered 'inline'. + * Classes that are marked as 'inline' (e.g. ```value class X(val value: Int)```) will be considered inline (of course). + * However, there seemingly exist classes like 'kotlin.native.internal.NativePtr' which shall also be considered 'inline' + * despite no modifier being present. This is considered a 'special Kotlin Native' class in the context of this function. + */ +context(KtAnalysisSession) +private fun KtNamedClassOrObjectSymbol.isInlineIncludingKotlinNativeSpecialClasses(): Boolean { + if (this.isInline) return true + val classId = classIdIfNonLocal ?: return false + + /* Top Level symbols can be special K/N types */ + if (getContainingSymbol() is KtClassOrObjectSymbol) return false + + if (classId.packageFqName == KonanFqNames.internalPackageName && classId.shortClassName == KonanFqNames.nativePtr.shortName()) { + return true + } + + if (classId.packageFqName == InteropFqNames.packageName && classId.shortClassName == InteropFqNames.cPointer.shortName()) { + return true + } + + return false +} + +context(KtAnalysisSession) +private fun KtType.markNullable(): KtType { + if (this.nullability == KtTypeNullability.NULLABLE) return this + return this.withNullability(KtTypeNullability.NULLABLE) +} + +context(KtAnalysisSession) +private fun KtType.markNullableIf(shouldMarkNullable: Boolean): KtType { + return if (shouldMarkNullable) markNullable() else this +} \ 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 d9574284778..a50a6052570 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 @@ -67,7 +67,6 @@ context(KtAnalysisSession) private fun bridgeType( type: KtType, ): TypeBridge { - val primitiveObjCValueType = when { type.isBoolean -> ObjCValueType.BOOL type.isChar -> ObjCValueType.UNICHAR @@ -89,6 +88,11 @@ private fun bridgeType( return ValueTypeBridge(primitiveObjCValueType) } + /* If type is inlined, then build the bridge for the inlined target type */ + type.getInlineTargetTypeOrNull()?.let { inlinedTargetType -> + return bridgeType(inlinedTargetType) + } + if (type.isFunctionType) { return bridgeFunctionType(type) } 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 69881868595..6e5223c9c61 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 @@ -14,6 +14,7 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.NameUtils import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSelectors import org.jetbrains.kotlin.objcexport.analysisApiUtils.getFunctionMethodBridge +import org.jetbrains.kotlin.objcexport.analysisApiUtils.getInlineTargetTypeOrNull import org.jetbrains.kotlin.objcexport.analysisApiUtils.isVisibleInObjC import org.jetbrains.kotlin.psi.KtFile @@ -284,7 +285,7 @@ private fun String.mangleIfSpecialFamily(prefix: String): String { * [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl.startsWithWords] */ 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.MethodBrideExtensionsKt.valueParametersAssociated] @@ -334,7 +335,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" } @@ -374,7 +375,18 @@ private fun KtType.mapType(typeBridge: TypeBridge): ObjCType { ObjCValueType.UNSIGNED_LONG_LONG -> ObjCPrimitiveType.uint64_t ObjCValueType.FLOAT -> ObjCPrimitiveType.float ObjCValueType.DOUBLE -> ObjCPrimitiveType.double - ObjCValueType.POINTER -> ObjCPointerType(ObjCVoidType, this.isMarkedNullable) + ObjCValueType.POINTER -> ObjCPointerType(ObjCVoidType, isBinaryRepresentationNullable()) } } } + +context(KtAnalysisSession) +private fun KtType.isBinaryRepresentationNullable(): Boolean { + if (fullyExpandedType.isMarkedNullable) return true + + getInlineTargetTypeOrNull()?.let { inlineTargetType -> + if (inlineTargetType.isMarkedNullable) return true + } + + return false +} diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCReferenceType.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCReferenceType.kt index 61ecb867a4e..262999e2dce 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCReferenceType.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCReferenceType.kt @@ -1,12 +1,14 @@ package org.jetbrains.kotlin.objcexport import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType import org.jetbrains.kotlin.analysis.api.types.KtType import org.jetbrains.kotlin.backend.konan.objcexport.* import org.jetbrains.kotlin.builtins.StandardNames import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.objcexport.analysisApiUtils.getInlineTargetTypeOrNull import org.jetbrains.kotlin.objcexport.analysisApiUtils.isError import org.jetbrains.kotlin.objcexport.analysisApiUtils.objCErrorType @@ -23,50 +25,67 @@ internal fun KtType.translateToObjCReferenceType(): ObjCReferenceType { */ context(KtAnalysisSession, KtObjCExportSession) private fun KtType.mapToReferenceTypeIgnoringNullability(): ObjCNonNullReferenceType { + val fullyExpandedType = fullyExpandedType + val classId = (fullyExpandedType as? KtNonErrorClassType)?.classId - val classId = this.expandedClassSymbol?.classIdIfNonLocal - val isInlined = false //TODO: replace when KT-65176 is implemented - val isHidden = classId in hiddenTypes - - return if (isError) { - objCErrorType - } else if (isAny || isHidden || isInlined) { - ObjCIdType - } else { - /** - * Simplified version of [org.jetbrains.kotlin.backend.konan.objcexport.CustomTypeMapper] - */ - val typesMap = mutableMapOf().apply { - this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray" - this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray" - this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet" - this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName - this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary" - this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName - this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString" - } - - NSNumberKind.entries.forEach { number -> - val numberClassId = number.mappedKotlinClassId - if (numberClassId != null) { - typesMap[numberClassId] = numberClassId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName - } - } - - val typeName = typesMap[classId] - ?: classId!!.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName - - val typeArguments = run { - if (this !is KtNonErrorClassType) { - return@run listOf() - } - - ownTypeArguments.mapNotNull { typeArgument -> typeArgument.type } - .map { typeArgumentType -> typeArgumentType.mapToReferenceTypeIgnoringNullability() } - } - - ObjCClassType(typeName, typeArguments) + if (isError) { + return objCErrorType } + + if (isAny) { + return ObjCIdType + } + + if (classId in hiddenTypes) { + return ObjCIdType + } + + /* Check if inline type represents 'regular' inline class */ + run check@{ + if (classId == null) return@check + val classSymbol = getClassOrObjectSymbolByClassId(classId) ?: return@check + if (classSymbol !is KtNamedClassOrObjectSymbol) return@check + if (classSymbol.isInline) return ObjCIdType + } + + /* 'Irregular' inline class: Not marked as inline, but special K/N type that still gets inlined */ + fullyExpandedType.getInlineTargetTypeOrNull()?.let { inlineTargetType -> + return inlineTargetType.mapToReferenceTypeIgnoringNullability() + } + + /** + * Simplified version of [org.jetbrains.kotlin.backend.konan.objcexport.CustomTypeMapper] + */ + val typesMap = mutableMapOf().apply { + this[ClassId.topLevel(StandardNames.FqNames.list)] = "NSArray" + this[ClassId.topLevel(StandardNames.FqNames.mutableList)] = "NSMutableArray" + this[ClassId.topLevel(StandardNames.FqNames.set)] = "NSSet" + this[ClassId.topLevel(StandardNames.FqNames.mutableSet)] = "MutableSet".getObjCKotlinStdlibClassOrProtocolName().objCName + this[ClassId.topLevel(StandardNames.FqNames.map)] = "NSDictionary" + this[ClassId.topLevel(StandardNames.FqNames.mutableMap)] = "MutableDictionary".getObjCKotlinStdlibClassOrProtocolName().objCName + this[ClassId.topLevel(StandardNames.FqNames.string.toSafe())] = "NSString" + } + + NSNumberKind.entries.forEach { number -> + val numberClassId = number.mappedKotlinClassId + if (numberClassId != null) { + typesMap[numberClassId] = numberClassId.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName + } + } + + val typeName = typesMap[classId] + ?: classId!!.shortClassName.asString().getObjCKotlinStdlibClassOrProtocolName().objCName + + val typeArguments = run { + if (this !is KtNonErrorClassType) { + return@run listOf() + } + + ownTypeArguments.mapNotNull { typeArgument -> typeArgument.type } + .map { typeArgumentType -> typeArgumentType.mapToReferenceTypeIgnoringNullability() } + } + + return ObjCClassType(typeName, typeArguments) } private fun ObjCNonNullReferenceType.withNullabilityOf(kotlinType: KtType): ObjCReferenceType { diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/testUtils/AnalysisApiAssertions.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/testUtils/AnalysisApiAssertions.kt index c4a55b4c441..d0ddedeedf3 100644 --- a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/testUtils/AnalysisApiAssertions.kt +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/testUtils/AnalysisApiAssertions.kt @@ -10,6 +10,7 @@ import org.jetbrains.kotlin.analysis.api.scopes.KtScope import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtPropertySymbol import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtFile import kotlin.test.fail @@ -34,6 +35,11 @@ fun KtFile.getFunctionOrFail(name: String): KtFunctionSymbol { return getFileSymbol().getFileScope().getFunctionOrFail(name) } +context(KtAnalysisSession) +fun KtFile.getPropertyOrFail(name: String): KtPropertySymbol { + return getFileSymbol().getFileScope().getPropertyOrFail(name) +} + context(KtAnalysisSession) fun KtScope.getFunctionOrFail(name: String): KtFunctionSymbol { val allSymbols = getCallableSymbols(Name.identifier(name)).toList() @@ -44,6 +50,16 @@ fun KtScope.getFunctionOrFail(name: String): KtFunctionSymbol { return symbol } +context(KtAnalysisSession) +fun KtScope.getPropertyOrFail(name: String): KtPropertySymbol { + val allSymbols = getCallableSymbols(Name.identifier(name)).toList() + if (allSymbols.isEmpty()) fail("Missing property '$name'") + if (allSymbols.size > 1) fail("Found multiple callables with name '$name'") + val symbol = allSymbols.single() + if (symbol !is KtPropertySymbol) fail("$symbol is not a property") + return symbol +} + context(KtAnalysisSession) fun KtClassOrObjectSymbol.getFunctionOrFail(name: String) : KtFunctionSymbol { return this.getMemberScope().getFunctionOrFail(name) diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/GetInlineTargetTypeOrNullTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/GetInlineTargetTypeOrNullTest.kt new file mode 100644 index 00000000000..6ed41932f00 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/GetInlineTargetTypeOrNullTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.objcexport.tests + +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.components.DefaultTypeClassIds +import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.objcexport.analysisApiUtils.getInlineTargetTypeOrNull +import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis +import org.jetbrains.kotlin.objcexport.testUtils.getClassOrFail +import org.jetbrains.kotlin.objcexport.testUtils.getPropertyOrFail +import org.junit.jupiter.api.Test +import kotlin.test.* + +class GetInlineTargetTypeOrNullTest( + private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis, +) { + @Test + fun `test - no inlined class`() { + val file = inlineSourceCodeAnalysis.createKtFile("class Foo") + analyze(file) { + val foo = file.getClassOrFail("Foo") + assertNull(foo.getInlineTargetTypeOrNull()) + } + } + + @Test + fun `test - simple inline class`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + value class Foo(val value: Int) + """.trimIndent() + ) + + analyze(file) { + val foo = file.getClassOrFail("Foo") + val inlineTargetType = assertNotNull(foo.getInlineTargetTypeOrNull()) + assertEquals(DefaultTypeClassIds.INT, inlineTargetType.classIdOrFail()) + } + } + + @Test + fun `test - transitive inline class`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + value class A(val value: Int) + value class B(val value: A) + + val foo: B get() = error("stub") + """.trimIndent() + ) + + analyze(file) { + val foo = file.getPropertyOrFail("foo") + assertEquals(DefaultTypeClassIds.INT, foo.returnType.getInlineTargetTypeOrNull().classIdOrFail()) + } + } + + @Test + fun `test - transitive inline class - with nullability`() { + val file = inlineSourceCodeAnalysis.createKtFile( + """ + value class A(val value: Int) + value class B(val value: A?) + + val foo: B get() = error("stub") + """.trimIndent() + ) + + analyze(file) { + val foo = file.getPropertyOrFail("foo") + assertEquals(DefaultTypeClassIds.INT, foo.returnType.getInlineTargetTypeOrNull().classIdOrFail()) + assertTrue(foo.returnType.getInlineTargetTypeOrNull()?.isMarkedNullable ?: false) + } + } + + private fun KtType?.classIdOrFail(): ClassId { + if (this == null) error("Type was null") + if (this !is KtNonErrorClassType) fail("Unexpected error type: '$this'") + return classId + } +} \ No newline at end of file