[ObjCExport] Initial support for inline classes in type mapping

^KT-65167
This commit is contained in:
Sebastian Sellmair
2024-01-24 09:55:22 +01:00
committed by Space Team
parent 262bfb18cc
commit 858af02a24
6 changed files with 261 additions and 46 deletions
@@ -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
}
@@ -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)
}
@@ -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
}
@@ -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<ClassId, String>().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<ObjCNonNullReferenceType>()
}
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<ClassId, String>().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<ObjCNonNullReferenceType>()
}
ownTypeArguments.mapNotNull { typeArgument -> typeArgument.type }
.map { typeArgumentType -> typeArgumentType.mapToReferenceTypeIgnoringNullability() }
}
return ObjCClassType(typeName, typeArguments)
}
private fun ObjCNonNullReferenceType.withNullabilityOf(kotlinType: KtType): ObjCReferenceType {
@@ -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)
@@ -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
}
}