[ObjCExport] Initial support for inline classes in type mapping
^KT-65167
This commit is contained in:
committed by
Space Team
parent
262bfb18cc
commit
858af02a24
+77
@@ -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
|
||||
}
|
||||
+5
-1
@@ -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)
|
||||
}
|
||||
|
||||
+15
-3
@@ -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
|
||||
}
|
||||
|
||||
+61
-42
@@ -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 {
|
||||
|
||||
+16
@@ -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)
|
||||
|
||||
+87
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user