[ObjCExport] Split ObjCExport into K1 and Analysis Api implementation

FL-23390
^KT-64168 Fixed
This commit is contained in:
Sebastian Sellmair
2023-12-06 18:14:00 +01:00
committed by Space Team
parent 3e57265fcb
commit e409c60780
95 changed files with 1042 additions and 383 deletions
@@ -0,0 +1,30 @@
plugins {
kotlin("jvm")
}
kotlin {
compilerOptions {
/* Required to use Analysis Api */
freeCompilerArgs.add("-Xcontext-receivers")
}
}
dependencies {
api(project(":native:objcexport-header-generator"))
api(project(":analysis:analysis-api"))
testImplementation(projectTests(":native:objcexport-header-generator"))
testApi(project(":analysis:analysis-api-standalone"))
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
testsJar()
nativeTest("test", tag = null) {
useJUnitPlatform()
enableJunit5ExtensionsAutodetection()
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2023 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
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtPropertySymbol
import org.jetbrains.kotlin.analysis.api.symbols.nameOrAnonymous
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportClassOrProtocolName
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportPropertyName
interface KtObjCExportNamer {
context(KtAnalysisSession)
fun getClassOrProtocolName(symbol: KtClassLikeSymbol): ObjCExportClassOrProtocolName
context(KtAnalysisSession)
fun getPropertyName(symbol: KtPropertySymbol): ObjCExportPropertyName
}
fun ObjCExportNamer(): KtObjCExportNamer {
return ObjCExportNamerImpl()
}
private class ObjCExportNamerImpl : KtObjCExportNamer {
context(KtAnalysisSession)
override fun getClassOrProtocolName(symbol: KtClassLikeSymbol): ObjCExportClassOrProtocolName {
val resolvedObjCNameAnnotation = symbol.resolveObjCNameAnnotation()
return ObjCExportClassOrProtocolName(
objCName = resolvedObjCNameAnnotation.objCName ?: symbol.nameOrAnonymous.asString(),
swiftName = resolvedObjCNameAnnotation.swiftName ?: symbol.nameOrAnonymous.asString()
)
}
context(KtAnalysisSession)
override fun getPropertyName(symbol: KtPropertySymbol): ObjCExportPropertyName {
val resolveObjCNameAnnotation = symbol.resolveObjCNameAnnotation()
return ObjCExportPropertyName(
objCName = resolveObjCNameAnnotation.objCName ?: symbol.name.asString(),
swiftName = resolveObjCNameAnnotation.swiftName ?: symbol.name.asString()
)
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2010-2023 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
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.KtConstantAnnotationValue
import org.jetbrains.kotlin.analysis.api.base.KtConstantValue
import org.jetbrains.kotlin.analysis.api.base.KtConstantValue.KtStringConstantValue
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtAnnotatedSymbol
import org.jetbrains.kotlin.backend.konan.KonanFqNames
/**
* Represents the values resolved from the [kotlin.native.ObjCName] annotation.
*
* ### Example
*
* **Given a class Foo**
* ```kotlin
* @ObjCName("FooObjC", "FooSwift", true)
* class Foo
* ```
*
* **Given class Foo being analyzed**
* ```kotlin
* val foo = getFooClassOrObjectSymbol()
* // ^
* // Imaginary method to get the symbol 'Foo' from above
*
* val resolvedObjCNameAnnotation = foo.resolveObjCNameAnnotation()
* // ^
* // objCName = "FooObjC"
* // swiftName = "FooSwift"
* // isExaclt = true
* ```
*/
internal class KtResolvedObjCNameAnnotation(
val objCName: String?,
val swiftName: String?,
val isExact: Boolean,
)
context(KtAnalysisSession)
internal fun KtAnnotatedSymbol.resolveObjCNameAnnotation(): KtResolvedObjCNameAnnotation {
var objCName: String? = null
var swiftName: String? = null
var isExact = false
annotationsList.annotations.find { it.classId?.asSingleFqName() == KonanFqNames.objCName }?.let { annotation ->
annotation.arguments.forEach { argument ->
when (argument.name.identifier) {
"name" -> objCName = argument.expression.let { it as? KtConstantAnnotationValue }
?.constantValue?.let { it as KtStringConstantValue }
?.value
"swiftName" -> swiftName = argument.expression.let { it as? KtConstantAnnotationValue }
?.constantValue?.let { it as KtStringConstantValue }
?.value
"exact" -> isExact = argument.expression.let { it as? KtConstantAnnotationValue }
?.constantValue?.let { it as KtConstantValue.KtBooleanConstantValue }
?.value ?: isExact
}
}
}
return KtResolvedObjCNameAnnotation(
objCName = objCName,
swiftName = swiftName,
isExact = isExact
)
}
@@ -0,0 +1,28 @@
/*
* Copyright 2010-2023 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.testUtils
import org.jetbrains.kotlin.backend.konan.tests.ObjCExportHeaderGeneratorTest.HeaderGenerator
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import java.io.File
class AnalysisApiHeaderGeneratorExtension : ParameterResolver {
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
return parameterContext.parameter.type == HeaderGenerator::class.java
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
return AnalysisApiHeaderGenerator
}
}
object AnalysisApiHeaderGenerator : HeaderGenerator {
override fun generateHeaders(root: File): String {
TODO("Analysis Api based header generation in not yet implemented")
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2010-2023 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.testUtils
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.psi.KtFile
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ExtensionContext.Namespace
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import java.io.File
import java.nio.file.Files
/**
* Provides ability to quickly write tests with 'inline source code' aka passing Kotlin source code as String.
*
* This interface can be injected into any test class constructor.
*
* ### Example
* ```
* class MyTest(
* private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis
* ) {
* @Test
* fun `test - something important`() {
* val myFile = inlineSourceCodeAnalysis.createKtFile("class Foo")
* analyze(myFile) {
* // Use analysis session to write advanced tests
* }
* }
* }
* ```
*/
interface InlineSourceCodeAnalysis {
fun createKtFile(@Language("kotlin") sourceCode: String): KtFile
}
/**
* Extension used to inject an instance of [InlineSourceCodeAnalysis] into tests.
*/
class InlineSourceCodeAnalysisExtension : ParameterResolver, AfterEachCallback {
private companion object {
val namespace: Namespace = Namespace.create(Any())
val tempDirKey = Any()
}
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
return parameterContext.parameter.type == InlineSourceCodeAnalysis::class.java
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
val temporaryDirectory = Files.createTempDirectory("inlineSourceCode").toFile()
extensionContext.getStore(namespace.append(extensionContext.requiredTestClass)).put(tempDirKey, temporaryDirectory)
return InlineSourceCodeAnalysisImpl(temporaryDirectory)
}
override fun afterEach(context: ExtensionContext) {
context.getStore(namespace.append(context.requiredTestClass))?.get(tempDirKey, File::class.java)?.deleteRecursively()
}
}
/**
* Simple implementation [InlineSourceCodeAnalysis]
*/
private class InlineSourceCodeAnalysisImpl(private val tempDir: File) : InlineSourceCodeAnalysis {
override fun createKtFile(@Language("kotlin") sourceCode: String): KtFile {
return createStandaloneAnalysisApiSession(tempDir, listOf(sourceCode))
.modulesWithFiles.entries.single()
.value.single() as KtFile
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2010-2023 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.testUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisApiInternals
import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeTokenProvider
import org.jetbrains.kotlin.analysis.api.standalone.KtAlwaysAccessibleLifetimeTokenProvider
import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession
import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession
import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule
import org.jetbrains.kotlin.backend.konan.testUtils.kotlinNativeStdlibPath
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.platform.konan.NativePlatforms
import java.io.File
import kotlin.io.path.Path
/**
* Creates a standalone analysis session from Kotlin source code passed as [kotlinSources]
*/
fun createStandaloneAnalysisApiSession(
tempDir: File,
kotlinSources: List<String>,
): StandaloneAnalysisAPISession {
val testModuleRoot = tempDir.resolve("testModule")
testModuleRoot.mkdirs()
kotlinSources.forEachIndexed { index, kotlinSource ->
testModuleRoot.resolve("TestSources$index.kt").apply {
writeText(kotlinSource)
}
}
return createStandaloneAnalysisApiSession(listOf(testModuleRoot))
}
/**
* Creates a standalone analysis session from [kotlinFiles] on disk.
* The Kotlin/Native stdlib will be provided as dependency
*/
fun createStandaloneAnalysisApiSession(kotlinFiles: List<File>): StandaloneAnalysisAPISession {
val currentArchitectureTarget = HostManager.host
val nativePlatform = NativePlatforms.nativePlatformByTargets(listOf(currentArchitectureTarget))
return buildStandaloneAnalysisAPISession {
@OptIn(KtAnalysisApiInternals::class)
registerProjectService(KtLifetimeTokenProvider::class.java, KtAlwaysAccessibleLifetimeTokenProvider())
buildKtModuleProvider {
platform = nativePlatform
val kLib = addModule(
buildKtLibraryModule {
addBinaryRoot(Path(kotlinNativeStdlibPath))
platform = nativePlatform
libraryName = "klib"
}
)
addModule(
buildKtSourceModule {
addSourceRoots(kotlinFiles.map { it.toPath() })
addRegularDependency(kLib)
platform = nativePlatform
moduleName = "source"
}
)
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2023 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.symbols.KtNamedClassOrObjectSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportClassOrProtocolName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.objcexport.ObjCExportNamer
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class KtObjCExportNamerTest(
private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis,
) {
private val namer = ObjCExportNamer()
@Test
fun `test - simple class`() {
val foo = inlineSourceCodeAnalysis.createKtFile("class Foo")
analyze(foo) {
val fooSymbol = foo.getFileSymbol().getFileScope()
.getClassifierSymbols(Name.identifier("Foo"))
.single() as KtNamedClassOrObjectSymbol
assertEquals(
ObjCExportClassOrProtocolName("Foo", "Foo"),
namer.getClassOrProtocolName(fooSymbol)
)
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2010-2023 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.objcexport.resolveObjCNameAnnotation
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class KtResolvedObjCNameAnnotationTest(
private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis,
) {
@Test
fun `test - class - no ObjCName annotation`() {
val ktFile = inlineSourceCodeAnalysis.createKtFile("class Foo")
analyze(ktFile) {
val fooSymbol = ktFile.getFileSymbol().getFileScope().getClassifierSymbols(Name.identifier("Foo")).single() as KtClassLikeSymbol
val resolvedObjCAnnotation = fooSymbol.resolveObjCNameAnnotation()
assertNull(resolvedObjCAnnotation.swiftName)
assertNull(resolvedObjCAnnotation.objCName)
assertFalse(resolvedObjCAnnotation.isExact)
}
}
@Test
fun `test - class - with ObjCName annotation`() {
val ktFile = inlineSourceCodeAnalysis.createKtFile(
"""
@kotlin.native.ObjCName("FooObjC", "FooSwift", true)
class Foo
""".trimIndent()
)
analyze(ktFile) {
val fooSymbol = ktFile.getFileSymbol().getFileScope().getClassifierSymbols(Name.identifier("Foo")).single() as KtClassLikeSymbol
val resolvedObjCAnnotation = fooSymbol.resolveObjCNameAnnotation()
assertEquals("FooObjC", resolvedObjCAnnotation.objCName)
assertEquals("FooSwift", resolvedObjCAnnotation.swiftName)
assertTrue(resolvedObjCAnnotation.isExact)
}
}
@Test
fun `test - function - with ObjCName annotation`() {
val ktFile = inlineSourceCodeAnalysis.createKtFile(
"""
@kotlin.native.ObjCName("fooObjC", "fooSwift", true)
fun foo() = Unit
""".trimIndent()
)
analyze(ktFile) {
val fooSymbol = ktFile.getFileSymbol().getFileScope().getCallableSymbols(Name.identifier("foo")).single() as KtFunctionSymbol
val resolvedObjCAnnotation = fooSymbol.resolveObjCNameAnnotation()
assertEquals("fooObjC", resolvedObjCAnnotation.objCName)
assertEquals("fooSwift", resolvedObjCAnnotation.swiftName)
assertTrue(resolvedObjCAnnotation.isExact)
}
}
}
@@ -0,0 +1,6 @@
#
# Copyright 2010-2023 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.
#
org.jetbrains.kotlin.objcexport.testUtils.AnalysisApiHeaderGeneratorExtension
org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysisExtension
@@ -0,0 +1,31 @@
plugins {
kotlin("jvm")
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
dependencies {
api(project(":native:objcexport-header-generator"))
implementation(project(":compiler:cli-base"))
implementation(project(":compiler:ir.objcinterop"))
implementation(project(":compiler:ir.serialization.native"))
implementation(project(":core:descriptors"))
testImplementation(projectTests(":native:objcexport-header-generator"))
}
kotlin {
compilerOptions {
optIn.add("org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi")
}
}
testsJar()
nativeTest("test", tag = null) {
useJUnitPlatform()
enableJunit5ExtensionsAutodetection()
}
@@ -0,0 +1,168 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.builtins.functions.FunctionTypeKind
import org.jetbrains.kotlin.builtins.getFunctionTypeKind
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.descriptorUtil.classId
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
@InternalKotlinNativeApi
fun ClassDescriptor.isMappedFunctionClass() =
this.getFunctionTypeKind() == FunctionTypeKind.Function &&
// Type parameters include return type.
declaredTypeParameters.size - 1 < CustomTypeMappers.functionTypeMappersArityLimit
@InternalKotlinNativeApi
interface CustomTypeMapper {
val mappedClassId: ClassId
fun mapType(
mappedSuperType: KotlinType,
translator: ObjCExportTranslatorImpl,
objCExportScope: ObjCExportScope,
): ObjCNonNullReferenceType
}
internal object CustomTypeMappers {
/**
* Custom type mappers.
*
* Don't forget to update [hiddenTypes] after adding new one.
*/
private val predefined: Map<ClassId, CustomTypeMapper> = with(StandardNames.FqNames) {
val result = mutableListOf<CustomTypeMapper>()
result += Collection(list, "NSArray")
result += Collection(mutableList, "NSMutableArray")
result += Collection(set, "NSSet")
result += Collection(mutableSet, { namer.mutableSetName.objCName })
result += Collection(map, "NSDictionary")
result += Collection(mutableMap, { namer.mutableMapName.objCName })
NSNumberKind.values().forEach {
// TODO: NSNumber seem to have different equality semantics.
val classId = it.mappedKotlinClassId
if (classId != null) {
result += Simple(classId, { namer.numberBoxName(classId).objCName })
}
}
result += Simple(ClassId.topLevel(string.toSafe()), "NSString")
result.associateBy { it.mappedClassId }
}
internal val functionTypeMappersArityLimit = 33 // not including, i.e. [0..33)
fun hasMapper(descriptor: ClassDescriptor): Boolean {
// Should be equivalent to `getMapper(descriptor) != null`.
if (descriptor.classId in predefined) return true
if (descriptor.isMappedFunctionClass()) return true
return false
}
fun getMapper(descriptor: ClassDescriptor): CustomTypeMapper? {
val classId = descriptor.classId
predefined[classId]?.let { return it }
if (descriptor.isMappedFunctionClass()) {
// TODO: somewhat hacky, consider using FunctionClassDescriptor.arity later.
val arity = descriptor.declaredTypeParameters.size - 1 // Type parameters include return type.
assert(classId == StandardNames.getFunctionClassId(arity))
return Function(arity)
}
return null
}
/**
* Types to be "hidden" during mapping, i.e. represented as `id`.
*
* Currently contains super types of classes handled by custom type mappers.
* Note: can be generated programmatically, but requires stdlib in this case.
*/
val hiddenTypes: Set<ClassId> = listOf(
"kotlin.Any",
"kotlin.CharSequence",
"kotlin.Comparable",
"kotlin.Function",
"kotlin.Number",
"kotlin.collections.Collection",
"kotlin.collections.Iterable",
"kotlin.collections.MutableCollection",
"kotlin.collections.MutableIterable"
).map { ClassId.topLevel(FqName(it)) }.toSet()
private class Simple(
override val mappedClassId: ClassId,
private val getObjCClassName: ObjCExportTranslatorImpl.() -> String,
) : CustomTypeMapper {
constructor(
mappedClassId: ClassId,
objCClassName: String,
) : this(mappedClassId, { objCClassName })
override fun mapType(
mappedSuperType: KotlinType,
translator: ObjCExportTranslatorImpl,
objCExportScope: ObjCExportScope,
): ObjCNonNullReferenceType =
ObjCClassType(translator.getObjCClassName())
}
private class Collection(
mappedClassFqName: FqName,
private val getObjCClassName: ObjCExportTranslatorImpl.() -> String,
) : CustomTypeMapper {
constructor(
mappedClassFqName: FqName,
objCClassName: String,
) : this(mappedClassFqName, { objCClassName })
override val mappedClassId = ClassId.topLevel(mappedClassFqName)
override fun mapType(
mappedSuperType: KotlinType,
translator: ObjCExportTranslatorImpl,
objCExportScope: ObjCExportScope,
): ObjCNonNullReferenceType {
val typeArguments = mappedSuperType.arguments.map {
val argument = it.type
if (TypeUtils.isNullableType(argument)) {
// Kotlin `null` keys and values are represented as `NSNull` singleton.
ObjCIdType
} else {
translator.mapReferenceTypeIgnoringNullability(argument, objCExportScope)
}
}
return ObjCClassType(translator.getObjCClassName(), typeArguments)
}
}
private class Function(private val parameterCount: Int) : CustomTypeMapper {
override val mappedClassId: ClassId
get() = StandardNames.getFunctionClassId(parameterCount)
override fun mapType(
mappedSuperType: KotlinType,
translator: ObjCExportTranslatorImpl,
objCExportScope: ObjCExportScope,
): ObjCNonNullReferenceType {
return translator.mapFunctionTypeIgnoringNullability(mappedSuperType, objCExportScope, returnsVoid = false)
}
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2022 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.backend.konan.descriptors.getPackageFragments
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.isSubpackageOf
/**
* Tries to infer a main package name that can be used
* for bundle ID of a framework.
*/
@InternalKotlinNativeApi
class MainPackageGuesser {
fun guess(
moduleDescriptor: ModuleDescriptor,
includedLibraryDescriptors: List<ModuleDescriptor>,
exportedDependencies: List<ModuleDescriptor>,
): FqName {
// Consider exported libraries only if we cannot infer the package from sources or included libs.
return guessMainPackage(includedLibraryDescriptors + moduleDescriptor)
?: guessMainPackage(exportedDependencies)
?: FqName.ROOT
}
private fun guessMainPackage(modules: List<ModuleDescriptor>): FqName? {
if (modules.isEmpty()) {
return null
}
val allPackages = modules.flatMap {
it.getPackageFragments() // Includes also all parent packages, e.g. the root one.
}
val nonEmptyPackages = allPackages
.filter { it.getMemberScope().getContributedDescriptors().isNotEmpty() }
.map { it.fqName }.distinct()
return allPackages.map { it.fqName }.distinct()
.filter { candidate -> nonEmptyPackages.all { it.isSubpackageOf(candidate) } }
// Now there are all common ancestors of non-empty packages. Longest of them is the least common accessor:
.maxByOrNull { it.asString().length }
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.common.descriptors.allParameters
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ParameterDescriptor
@InternalKotlinNativeApi
sealed class TypeBridge
@InternalKotlinNativeApi
object ReferenceBridge : TypeBridge()
@InternalKotlinNativeApi
data class BlockPointerBridge(
val numberOfParameters: Int,
val returnsVoid: Boolean,
) : TypeBridge()
@InternalKotlinNativeApi
data class ValueTypeBridge(val objCValueType: ObjCValueType) : TypeBridge()
@InternalKotlinNativeApi
sealed class MethodBridgeParameter
@InternalKotlinNativeApi
sealed class MethodBridgeReceiver : MethodBridgeParameter() {
object Static : MethodBridgeReceiver()
object Factory : MethodBridgeReceiver()
object Instance : MethodBridgeReceiver()
}
@InternalKotlinNativeApi
object MethodBridgeSelector : MethodBridgeParameter()
@InternalKotlinNativeApi
sealed class MethodBridgeValueParameter : MethodBridgeParameter() {
data class Mapped(val bridge: TypeBridge) : MethodBridgeValueParameter()
object ErrorOutParameter : MethodBridgeValueParameter()
data class SuspendCompletion(val useUnitCompletion: Boolean) : MethodBridgeValueParameter()
}
@InternalKotlinNativeApi
data class MethodBridge(
val returnBridge: ReturnValue,
val receiver: MethodBridgeReceiver,
val valueParameters: List<MethodBridgeValueParameter>,
) {
sealed class ReturnValue {
object Void : ReturnValue()
object HashCode : ReturnValue()
data class Mapped(val bridge: TypeBridge) : ReturnValue()
sealed class Instance : ReturnValue() {
object InitResult : Instance()
object FactoryResult : Instance()
}
sealed class WithError : ReturnValue() {
object Success : WithError()
data class ZeroForError(val successBridge: ReturnValue, val successMayBeZero: Boolean) : WithError()
}
object Suspend : ReturnValue()
}
val paramBridges: List<MethodBridgeParameter> =
listOf(receiver) + MethodBridgeSelector + valueParameters
// TODO: it is not exactly true in potential future cases.
val isInstance: Boolean
get() = when (receiver) {
MethodBridgeReceiver.Static,
MethodBridgeReceiver.Factory,
-> false
MethodBridgeReceiver.Instance -> true
}
val returnsError: Boolean
get() = returnBridge is ReturnValue.WithError
}
@InternalKotlinNativeApi
fun MethodBridge.valueParametersAssociated(
descriptor: FunctionDescriptor,
): List<Pair<MethodBridgeValueParameter, ParameterDescriptor?>> {
val kotlinParameters = descriptor.allParameters.iterator()
val skipFirstKotlinParameter = when (this.receiver) {
MethodBridgeReceiver.Static -> false
MethodBridgeReceiver.Factory, MethodBridgeReceiver.Instance -> true
}
if (skipFirstKotlinParameter) {
kotlinParameters.next()
}
return this.valueParameters.map {
when (it) {
is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next()
is MethodBridgeValueParameter.SuspendCompletion,
is MethodBridgeValueParameter.ErrorOutParameter,
-> it to null
}
}.also { assert(!kotlinParameters.hasNext()) }
}
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
internal class ObjCExportHeaderGeneratorImpl(
moduleDescriptors: List<ModuleDescriptor>,
mapper: ObjCExportMapper,
namer: ObjCExportNamer,
problemCollector: ObjCExportProblemCollector,
objcGenerics: Boolean,
override val shouldExportKDoc: Boolean,
private val additionalImports: List<String>,
) : ObjCExportHeaderGenerator(moduleDescriptors, mapper, namer, objcGenerics, problemCollector) {
override fun getAdditionalImports(): List<String> =
additionalImports
}
@@ -0,0 +1,476 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.backend.konan.UnitSuspendFunctionObjCExport
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl
import org.jetbrains.kotlin.descriptors.konan.isNativeStdlib
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier
import org.jetbrains.kotlin.psi.psiUtil.modalityModifier
import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierTypeOrDefault
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.BindingTraceContext
import org.jetbrains.kotlin.resolve.DescriptorResolver
import org.jetbrains.kotlin.resolve.TypeResolver
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
import org.jetbrains.kotlin.resolve.descriptorUtil.module
import org.jetbrains.kotlin.resolve.lazy.FileScopeProvider
import org.jetbrains.kotlin.resolve.lazy.KotlinCodeAnalyzer
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
import org.jetbrains.kotlin.resolve.scopes.LexicalScopeKind
import org.jetbrains.kotlin.resolve.scopes.LexicalWritableScope
import org.jetbrains.kotlin.resolve.scopes.LocalRedeclarationChecker
import org.jetbrains.kotlin.resolve.source.PsiSourceFile
import org.jetbrains.kotlin.types.error.ErrorUtils
interface ObjCExportLazy {
interface Configuration {
val frameworkName: String
fun isIncluded(moduleInfo: ModuleInfo): Boolean
fun getCompilerModuleName(moduleInfo: ModuleInfo): String
val objcGenerics: Boolean
val disableSwiftMemberNameMangling: Boolean
get() = false
val unitSuspendFunctionExport: UnitSuspendFunctionObjCExport
val ignoreInterfaceMethodCollisions: Boolean
get() = false
}
fun generateBase(): List<ObjCTopLevel>
fun translate(file: KtFile): List<ObjCTopLevel>
}
@JvmOverloads
fun createObjCExportLazy(
configuration: ObjCExportLazy.Configuration,
problemCollector: ObjCExportProblemCollector,
codeAnalyzer: KotlinCodeAnalyzer,
typeResolver: TypeResolver,
descriptorResolver: DescriptorResolver,
fileScopeProvider: FileScopeProvider,
builtIns: KotlinBuiltIns,
deprecationResolver: DeprecationResolver? = null,
): ObjCExportLazy = ObjCExportLazyImpl(
configuration,
problemCollector,
codeAnalyzer,
typeResolver,
descriptorResolver,
fileScopeProvider,
builtIns,
deprecationResolver
)
@InternalKotlinNativeApi
class ObjCExportLazyImpl(
private val configuration: ObjCExportLazy.Configuration,
problemCollector: ObjCExportProblemCollector,
private val codeAnalyzer: KotlinCodeAnalyzer,
private val typeResolver: TypeResolver,
private val descriptorResolver: DescriptorResolver,
private val fileScopeProvider: FileScopeProvider,
builtIns: KotlinBuiltIns,
deprecationResolver: DeprecationResolver?,
) : ObjCExportLazy {
private val namerConfiguration = createNamerConfiguration(configuration)
private val nameTranslator: ObjCExportNameTranslator = ObjCExportNameTranslatorImpl(namerConfiguration)
private val mapper = ObjCExportMapper(deprecationResolver, local = true, configuration.unitSuspendFunctionExport)
private val namer = ObjCExportNamerImpl(namerConfiguration, builtIns, mapper, problemCollector, local = true)
private val translator: ObjCExportTranslator = ObjCExportTranslatorImpl(
null,
mapper,
namer,
problemCollector,
configuration.objcGenerics
)
private val isValid: Boolean
get() = codeAnalyzer.moduleDescriptor.isValid
override fun generateBase() = translator.generateBaseDeclarations()
override fun translate(file: KtFile): List<ObjCTopLevel> =
translateClasses(file) + translateTopLevels(file)
private fun translateClasses(container: KtDeclarationContainer): List<ObjCClass> {
val result = mutableListOf<ObjCClass>()
container.declarations.forEach { declaration ->
// Supposed to be true if ObjCExportMapper.shouldBeVisible is true.
if (declaration is KtClassOrObject && declaration.isPublic && declaration !is KtEnumEntry
&& !declaration.hasExpectModifier()
) {
if (!declaration.isAnnotation() && !declaration.hasModifier(KtTokens.INLINE_KEYWORD)) {
result += translateClass(declaration)
}
declaration.body?.let {
result += translateClasses(it)
}
}
}
return result
}
private fun translateClass(ktClassOrObject: KtClassOrObject): ObjCClass {
val name = nameTranslator.getClassOrProtocolName(ktClassOrObject)
// Note: some attributes may be missing (e.g. "unavailable" for unexposed classes).
return if (ktClassOrObject.isInterface) {
LazyObjCProtocolImpl(name, ktClassOrObject, this)
} else {
val isFinal = ktClassOrObject.modalityModifier() == null ||
ktClassOrObject.hasModifier(KtTokens.FINAL_KEYWORD)
val attributes = if (isFinal) {
listOf(OBJC_SUBCLASSING_RESTRICTED)
} else {
emptyList()
}
LazyObjCInterfaceImpl(
name,
attributes,
generics = translateGenerics(ktClassOrObject),
psi = ktClassOrObject,
lazy = this
)
}
}
private fun translateGenerics(ktClassOrObject: KtClassOrObject): List<ObjCGenericTypeDeclaration> = if (configuration.objcGenerics) {
ktClassOrObject.typeParametersWithOuter
.map {
ObjCGenericTypeRawDeclaration(
nameTranslator.getTypeParameterName(it),
ObjCVariance.fromKotlinVariance(it.variance)
)
}
.toList()
} else {
emptyList()
}
private fun translateTopLevels(file: KtFile): List<ObjCInterface> {
val extensions =
mutableMapOf<ClassDescriptor, MutableList<KtCallableDeclaration>>()
val topLevel = mutableListOf<KtCallableDeclaration>()
file.children.filterIsInstance<KtCallableDeclaration>().forEach {
// Supposed to be similar to ObjCExportMapper.shouldBeVisible.
if ((it is KtFunction || it is KtProperty) && it.isPublic && !it.hasExpectModifier()) {
val classDescriptor = getClassIfExtension(it)
if (classDescriptor != null) {
// If a class is hidden from Objective-C API then it is meaningless
// to export its extensions.
if (!classDescriptor.isHiddenFromObjC()) {
extensions.getOrPut(classDescriptor, { mutableListOf() }) += it
}
} else {
topLevel += it
}
}
}
val result = mutableListOf<ObjCInterface>()
extensions.mapTo(result) { (classDescriptor, declarations) ->
translateExtensions(file, classDescriptor, declarations)
}
if (topLevel.isNotEmpty()) result += translateFileClass(file, topLevel)
return result
}
private fun translateFileClass(file: KtFile, declarations: List<KtCallableDeclaration>): ObjCInterface {
val name = nameTranslator.getFileClassName(file)
return LazyObjCFileInterface(name, file, declarations, this)
}
private fun translateExtensions(
file: KtFile,
classDescriptor: ClassDescriptor,
declarations: List<KtCallableDeclaration>,
): ObjCInterface {
// TODO: consider using file-based categories in compiler too.
val name = if (ErrorUtils.isError(classDescriptor)) {
ObjCExportNamer.ClassOrProtocolName("ERROR", "ERROR")
} else {
namer.getClassOrProtocolName(classDescriptor)
}
return LazyObjCExtensionInterface(name, nameTranslator.getCategoryName(file), classDescriptor, declarations, this)
}
private fun resolveDeclaration(ktDeclaration: KtDeclaration): DeclarationDescriptor =
codeAnalyzer.resolveToDescriptor(ktDeclaration)
private fun resolve(ktClassOrObject: KtClassOrObject) =
resolveDeclaration(ktClassOrObject) as ClassDescriptor
private fun resolve(ktCallableDeclaration: KtCallableDeclaration) =
resolveDeclaration(ktCallableDeclaration) as CallableMemberDescriptor
private fun getClassIfExtension(topLevelDeclaration: KtCallableDeclaration): ClassDescriptor? {
val receiverType = topLevelDeclaration.receiverTypeReference ?: return null
val fileScope = fileScopeProvider.getFileResolutionScope(topLevelDeclaration.containingKtFile)
val trace = BindingTraceContext() // TODO: revise.
val kotlinReceiverType = typeResolver.resolveType(
createHeaderScope(topLevelDeclaration, fileScope, trace),
receiverType,
trace,
checkBounds = false
)
return translator.getClassIfExtension(kotlinReceiverType)
}
private fun createHeaderScope(
declaration: KtCallableDeclaration,
parent: LexicalScope,
trace: BindingTrace,
): LexicalScope {
if (declaration.typeParameters.isEmpty()) return parent
val fakeName = Name.special("<fake>")
val sourceElement = SourceElement.NO_SOURCE
val descriptor: CallableMemberDescriptor
val scopeKind: LexicalScopeKind
when (declaration) {
is KtFunction -> {
descriptor = SimpleFunctionDescriptorImpl.create(
parent.ownerDescriptor,
Annotations.EMPTY,
fakeName,
CallableMemberDescriptor.Kind.DECLARATION,
sourceElement
)
scopeKind = LexicalScopeKind.FUNCTION_HEADER
}
is KtProperty -> {
descriptor = PropertyDescriptorImpl.create(
parent.ownerDescriptor,
Annotations.EMPTY,
Modality.FINAL,
DescriptorVisibilities.PUBLIC,
declaration.isVar,
fakeName,
CallableMemberDescriptor.Kind.DECLARATION,
sourceElement,
false,
false,
false,
false,
false,
false
)
scopeKind = LexicalScopeKind.PROPERTY_HEADER
}
else -> TODO("${declaration::class}")
}
val result = LexicalWritableScope(
parent,
descriptor,
false,
LocalRedeclarationChecker.DO_NOTHING,
scopeKind
)
val typeParameters = descriptorResolver.resolveTypeParametersForDescriptor(
descriptor,
result,
result,
declaration.typeParameters,
trace
)
descriptorResolver.resolveGenericBounds(declaration, descriptor, result, typeParameters, trace)
return result
}
private class LazyObjCProtocolImpl(
name: ObjCExportNamer.ClassOrProtocolName,
private val psi: KtClassOrObject,
private val lazy: ObjCExportLazyImpl,
) : LazyObjCProtocol(name) {
private val descriptor: ClassDescriptor by lazy { lazy.resolve(psi) }
override val origin: ObjCExportStubOrigin? by lazy { ObjCExportStubOrigin(descriptor) }
override fun computeRealStub(): ObjCProtocol = lazy.translator.translateInterface(descriptor)
}
private class LazyObjCInterfaceImpl(
name: ObjCExportNamer.ClassOrProtocolName,
attributes: List<String>,
generics: List<ObjCGenericTypeDeclaration>,
private val psi: KtClassOrObject,
private val lazy: ObjCExportLazyImpl,
) : LazyObjCInterface(name = name, generics = generics, categoryName = null, attributes = attributes) {
private val descriptor: ClassDescriptor by lazy { lazy.resolve(psi) }
override val origin: ObjCExportStubOrigin? by lazy { ObjCExportStubOrigin(descriptor) }
override fun computeRealStub(): ObjCInterface = lazy.translator.translateClass(descriptor)
}
private class LazyObjCFileInterface(
name: ObjCExportNamer.ClassOrProtocolName,
private val file: KtFile,
private val declarations: List<KtCallableDeclaration>,
private val lazy: ObjCExportLazyImpl,
) : LazyObjCInterface(name = name, generics = emptyList(), categoryName = null, attributes = listOf(OBJC_SUBCLASSING_RESTRICTED)) {
override val origin: Nothing? = null
override fun computeRealStub(): ObjCInterface = lazy.translator.translateFile(
PsiSourceFile(file),
declarations.mapNotNull { declaration ->
lazy.resolve(declaration).takeIf { descriptor ->
lazy.mapper.shouldBeExposed(descriptor)
}
}
)
}
private class LazyObjCExtensionInterface(
name: ObjCExportNamer.ClassOrProtocolName,
categoryName: String,
private val classDescriptor: ClassDescriptor,
private val declarations: List<KtCallableDeclaration>,
private val lazy: ObjCExportLazyImpl,
) : LazyObjCInterface(name = name.objCName, generics = emptyList(), categoryName = categoryName, attributes = emptyList()) {
override val origin: ObjCExportStubOrigin? = ObjCExportStubOrigin(classDescriptor)
override fun computeRealStub(): ObjCInterface = lazy.translator.translateExtensions(
classDescriptor,
declarations.mapNotNull { declaration ->
lazy.resolve(declaration).takeIf { lazy.mapper.shouldBeExposed(it) }
}
)
}
}
private abstract class LazyObjCInterface(
override val name: String,
override val generics: List<ObjCGenericTypeDeclaration>,
override val categoryName: String?,
override val attributes: List<String>,
) : ObjCInterface() {
constructor(
name: ObjCExportNamer.ClassOrProtocolName,
generics: List<ObjCGenericTypeDeclaration>,
categoryName: String?,
attributes: List<String>,
) : this(name.objCName, generics, categoryName, attributes + name.toNameAttributes())
protected abstract fun computeRealStub(): ObjCInterface
private val realStub by lazy { computeRealStub() }
override val members: List<ObjCExportStub>
get() = realStub.members
override val superProtocols: List<String>
get() = realStub.superProtocols
override val superClass: String?
get() = realStub.superClass
override val superClassGenerics: List<ObjCNonNullReferenceType>
get() = realStub.superClassGenerics
final override val comment: Nothing? = null
}
private abstract class LazyObjCProtocol(
name: ObjCExportNamer.ClassOrProtocolName,
) : ObjCProtocol() {
override val name: String = name.objCName
override val comment: Nothing? = null
override val attributes: List<String> = name.toNameAttributes()
protected abstract fun computeRealStub(): ObjCProtocol
private val realStub by lazy { computeRealStub() }
override val members: List<ObjCExportStub>
get() = realStub.members
override val superProtocols: List<String>
get() = realStub.superProtocols
}
internal fun createNamerConfiguration(configuration: ObjCExportLazy.Configuration): ObjCExportNamer.Configuration {
return object : ObjCExportNamer.Configuration {
override val topLevelNamePrefix = abbreviate(configuration.frameworkName)
override fun getAdditionalPrefix(module: ModuleDescriptor): String? {
if (module.isNativeStdlib() || module.isCommonStdlibCheckSpecificallyForIDE()) return "Kotlin"
// Note: incorrect for compiler since it doesn't store ModuleInfo to ModuleDescriptor.
val moduleInfo = module.getCapability(ModuleInfo.Capability) ?: return null
if (configuration.isIncluded(moduleInfo)) return null
return abbreviate(configuration.getCompilerModuleName(moduleInfo))
}
override val objcGenerics = configuration.objcGenerics
override val disableSwiftMemberNameMangling = configuration.disableSwiftMemberNameMangling
override val ignoreInterfaceMethodCollisions: Boolean = configuration.ignoreInterfaceMethodCollisions
}
}
private val kotlinSequenceClassId = ClassId.topLevel(FqName("kotlin.sequences.Sequence"))
// This is a special workaround needed for resolve in the IDE.
private fun ModuleDescriptor.isCommonStdlibCheckSpecificallyForIDE() =
findClassAcrossModuleDependencies(kotlinSequenceClassId)?.module == this
private val KtModifierListOwner.isPublic: Boolean
get() = this.visibilityModifierTypeOrDefault() == KtTokens.PUBLIC_KEYWORD
internal val KtPureClassOrObject.isInterface: Boolean
get() = this is KtClass && this.isInterface()
internal val KtClassOrObject.typeParametersWithOuter
get() = generateSequence(this, { if (it is KtClass && it.isInner()) it.containingClassOrObject else null })
.flatMap { it.typeParameters.asSequence() }
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.psi.KtFile
@InternalKotlinNativeApi
fun ObjCExportLazy.dumpObjCHeader(files: Collection<KtFile>, outputFile: String, shouldExportKDoc: Boolean) {
val lines = (this.generateBase() + files.flatMap { this.translate(it) })
.flatMap { StubRenderer.render(it, shouldExportKDoc) + listOf("") }
File(outputFile).writeLines(lines)
}
@@ -0,0 +1,479 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.common.descriptors.allParameters
import org.jetbrains.kotlin.backend.konan.*
import org.jetbrains.kotlin.backend.konan.descriptors.allOverriddenDescriptors
import org.jetbrains.kotlin.backend.konan.descriptors.isArray
import org.jetbrains.kotlin.backend.konan.descriptors.isInterface
import org.jetbrains.kotlin.builtins.*
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.ir.objcinterop.isObjCObjectType
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.DataClassResolver
import org.jetbrains.kotlin.resolve.deprecation.DeprecationInfo
import org.jetbrains.kotlin.resolve.deprecation.DeprecationLevelValue
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
import org.jetbrains.kotlin.resolve.descriptorUtil.*
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.typeUtil.isNothing
import org.jetbrains.kotlin.types.typeUtil.isUnit
@InternalKotlinNativeApi
class ObjCExportMapper(
internal val deprecationResolver: DeprecationResolver? = null,
private val local: Boolean = false,
internal val unitSuspendFunctionExport: UnitSuspendFunctionObjCExport,
) {
fun getCustomTypeMapper(descriptor: ClassDescriptor): CustomTypeMapper? = CustomTypeMappers.getMapper(descriptor)
val hiddenTypes: Set<ClassId> get() = CustomTypeMappers.hiddenTypes
fun isSpecialMapped(descriptor: ClassDescriptor): Boolean {
// TODO: this method duplicates some of the [ObjCExportTranslatorImpl.mapReferenceType] logic.
return KotlinBuiltIns.isAny(descriptor) ||
descriptor.getAllSuperClassifiers().any { it is ClassDescriptor && CustomTypeMappers.hasMapper(it) }
}
private val methodBridgeCache = mutableMapOf<FunctionDescriptor, MethodBridge>()
fun bridgeMethod(descriptor: FunctionDescriptor): MethodBridge = if (local) {
bridgeMethodImpl(descriptor)
} else {
methodBridgeCache.getOrPut(descriptor) {
bridgeMethodImpl(descriptor)
}
}
}
internal fun ObjCExportMapper.getClassIfCategory(descriptor: CallableMemberDescriptor): ClassDescriptor? {
if (descriptor.dispatchReceiverParameter != null) return null
val extensionReceiverType = descriptor.extensionReceiverParameter?.type ?: return null
return getClassIfCategory(extensionReceiverType)
}
internal fun ObjCExportMapper.getClassIfCategory(extensionReceiverType: KotlinType): ClassDescriptor? {
// FIXME: this code must rely on type mapping instead of copying its logic.
if (extensionReceiverType.isObjCObjectType()) return null
val erasedClass = extensionReceiverType.getErasedTypeClass()
return if (!erasedClass.isInterface && !erasedClass.isInlined() && !this.isSpecialMapped(erasedClass)) {
erasedClass
} else {
// E.g. receiver is protocol, or some type with custom mapping.
null
}
}
private fun isSealedClassConstructor(descriptor: ConstructorDescriptor) = descriptor.constructedClass.isSealed()
/**
* Check that given [method] is a synthetic .componentN() method of a data class.
*/
private fun isComponentNMethod(method: CallableMemberDescriptor): Boolean {
if ((method as? FunctionDescriptor)?.isOperator != true) return false
val parent = method.containingDeclaration
if (parent is ClassDescriptor && parent.isData && DataClassResolver.isComponentLike(method.name)) {
// componentN method of data class.
return true
}
return false
}
// Note: partially duplicated in ObjCExportLazyImpl.translateTopLevels.
@InternalKotlinNativeApi
fun ObjCExportMapper.shouldBeExposed(descriptor: CallableMemberDescriptor): Boolean = when {
!descriptor.isEffectivelyPublicApi -> false
descriptor.isExpect -> false
isHiddenByDeprecation(descriptor) -> false
descriptor is ConstructorDescriptor && isSealedClassConstructor(descriptor) -> false
// KT-42641. Don't expose componentN methods of data classes
// because they are useless in Objective-C/Swift.
isComponentNMethod(descriptor) && descriptor.overriddenDescriptors.isEmpty() -> false
descriptor.isHiddenFromObjC() -> false
else -> true
}
private fun CallableMemberDescriptor.isHiddenFromObjC(): Boolean = when {
// Note: the front-end checker requires all overridden descriptors to be either refined or not refined.
overriddenDescriptors.isNotEmpty() -> overriddenDescriptors.first().isHiddenFromObjC()
else -> annotations.any { annotation ->
annotation.annotationClass?.annotations?.any { it.fqName == KonanFqNames.hidesFromObjC } == true
}
}
/**
* Check if the given class or its enclosing declaration is marked as @HiddenFromObjC.
*/
internal fun ClassDescriptor.isHiddenFromObjC(): Boolean = when {
(this.containingDeclaration as? ClassDescriptor)?.isHiddenFromObjC() == true -> true
else -> annotations.any { annotation ->
annotation.annotationClass?.annotations?.any { it.fqName == KonanFqNames.hidesFromObjC } == true
}
}
internal fun ObjCExportMapper.shouldBeExposed(descriptor: ClassDescriptor): Boolean =
shouldBeVisible(descriptor) && !isSpecialMapped(descriptor) && !descriptor.defaultType.isObjCObjectType()
private fun ObjCExportMapper.isHiddenByDeprecation(descriptor: CallableMemberDescriptor): Boolean {
// Note: ObjCExport generally expect overrides of exposed methods to be exposed.
// So don't hide a "deprecated hidden" method which overrides non-hidden one:
if (deprecationResolver != null && deprecationResolver.isDeprecatedHidden(descriptor) &&
descriptor.overriddenDescriptors.all { isHiddenByDeprecation(it) }
) {
return true
}
// Note: ObjCExport expects members of unexposed classes to be unexposed too.
// So hide a declaration if it is from a hidden class:
val containingDeclaration = descriptor.containingDeclaration
if (containingDeclaration is ClassDescriptor && isHiddenByDeprecation(containingDeclaration)) {
return true
}
return false
}
internal fun ObjCExportMapper.getDeprecation(descriptor: DeclarationDescriptor): DeprecationInfo? {
deprecationResolver?.getDeprecations(descriptor).orEmpty().maxByOrNull {
when (it.deprecationLevel) {
DeprecationLevelValue.WARNING -> 1
DeprecationLevelValue.ERROR -> 2
DeprecationLevelValue.HIDDEN -> 3
}
}?.let { return it }
(descriptor as? ConstructorDescriptor)?.let {
// Note: a deprecation can't be applied to a class itself when generating header
// since the class can be referred from the header.
// Apply class deprecations to its constructors instead:
return getDeprecation(it.constructedClass)
}
return null
}
private fun ObjCExportMapper.isHiddenByDeprecation(descriptor: ClassDescriptor): Boolean {
if (deprecationResolver == null) return false
if (deprecationResolver.isDeprecatedHidden(descriptor)) return true
// Note: ObjCExport requires super class of exposed class to be exposed.
// So hide a class if its super class is hidden:
val superClass = descriptor.getSuperClassNotAny()
if (superClass != null && isHiddenByDeprecation(superClass)) {
return true
}
// Note: ObjCExport requires enclosing class of exposed class to be exposed.
// Also in Kotlin hidden class members (including other classes) aren't directly accessible.
// So hide a class if its enclosing class is hidden:
val containingDeclaration = descriptor.containingDeclaration
if (containingDeclaration is ClassDescriptor && isHiddenByDeprecation(containingDeclaration)) {
return true
}
return false
}
// Note: the logic is partially duplicated in ObjCExportLazyImpl.translateClasses.
internal fun ObjCExportMapper.shouldBeVisible(descriptor: ClassDescriptor): Boolean =
descriptor.isEffectivelyPublicApi && when (descriptor.kind) {
ClassKind.CLASS, ClassKind.INTERFACE, ClassKind.ENUM_CLASS, ClassKind.OBJECT -> true
ClassKind.ENUM_ENTRY, ClassKind.ANNOTATION_CLASS -> false
} && !descriptor.isExpect && !descriptor.isInlined() && !isHiddenByDeprecation(descriptor) && !descriptor.isHiddenFromObjC()
private fun ObjCExportMapper.isBase(descriptor: CallableMemberDescriptor): Boolean =
descriptor.overriddenDescriptors.all { !shouldBeExposed(it) }
// e.g. it is not `override`, or overrides only unexposed methods.
/**
* Check that given [descriptor] is a so-called "base method", i.e. method
* that doesn't override anything in a generated Objective-C interface.
* Note that it does not mean that it has no "override" keyword.
* Consider example:
* ```kotlin
* private interface I {
* fun f()
* }
*
* class C : I {
* override fun f() {}
* }
* ```
* Interface `I` is not exposed to the generated header, so C#f is considered to be a base method even though it has an "override" keyword.
*/
@InternalKotlinNativeApi
fun ObjCExportMapper.isBaseMethod(descriptor: FunctionDescriptor) =
this.isBase(descriptor)
@InternalKotlinNativeApi
fun ObjCExportMapper.getBaseMethods(descriptor: FunctionDescriptor): List<FunctionDescriptor> =
if (isBaseMethod(descriptor)) {
listOf(descriptor)
} else {
descriptor.overriddenDescriptors.filter { shouldBeExposed(it) }
.flatMap { getBaseMethods(it.original) }
.distinct()
}
@InternalKotlinNativeApi
fun ObjCExportMapper.isBaseProperty(descriptor: PropertyDescriptor) =
isBase(descriptor)
@InternalKotlinNativeApi
fun ObjCExportMapper.getBaseProperties(descriptor: PropertyDescriptor): List<PropertyDescriptor> =
if (isBaseProperty(descriptor)) {
listOf(descriptor)
} else {
descriptor.overriddenDescriptors
.flatMap { getBaseProperties(it.original) }
.distinct()
}
@Suppress("NO_TAIL_CALLS_FOUND", "NON_TAIL_RECURSIVE_CALL") // K2 warning suppression, TODO: KT-62472
internal tailrec fun KotlinType.getErasedTypeClass(): ClassDescriptor =
TypeUtils.getClassDescriptor(this) ?: this.constructor.supertypes.first().getErasedTypeClass()
internal fun ObjCExportMapper.isTopLevel(descriptor: CallableMemberDescriptor): Boolean =
descriptor.containingDeclaration !is ClassDescriptor && this.getClassIfCategory(descriptor) == null
internal fun ObjCExportMapper.isObjCProperty(property: PropertyDescriptor): Boolean =
property.extensionReceiverParameter == null || getClassIfCategory(property) != null
@InternalKotlinNativeApi
fun ClassDescriptor.getEnumValuesFunctionDescriptor(): SimpleFunctionDescriptor? {
require(this.kind == ClassKind.ENUM_CLASS)
return this.staticScope.getContributedFunctions(
StandardNames.ENUM_VALUES,
NoLookupLocation.FROM_BACKEND
).singleOrNull { it.extensionReceiverParameter == null && it.valueParameters.size == 0 }
}
@InternalKotlinNativeApi
fun ClassDescriptor.getEnumEntriesPropertyDescriptor(): PropertyDescriptor? {
require(this.kind == ClassKind.ENUM_CLASS)
return this.staticScope.getContributedVariables(
StandardNames.ENUM_ENTRIES,
NoLookupLocation.FROM_BACKEND
).singleOrNull { it.extensionReceiverParameter == null }
}
internal fun ObjCExportMapper.doesThrow(method: FunctionDescriptor): Boolean = method.allOverriddenDescriptors.any {
it.overriddenDescriptors.isEmpty() && it.annotations.hasAnnotation(KonanFqNames.throws)
}
private fun ObjCExportMapper.bridgeType(
kotlinType: KotlinType,
): TypeBridge = kotlinType.unwrapToPrimitiveOrReference<TypeBridge>(
eachInlinedClass = { inlinedClass, _ ->
when (inlinedClass.classId) {
UnsignedType.UBYTE.classId -> return ValueTypeBridge(ObjCValueType.UNSIGNED_CHAR)
UnsignedType.USHORT.classId -> return ValueTypeBridge(ObjCValueType.UNSIGNED_SHORT)
UnsignedType.UINT.classId -> return ValueTypeBridge(ObjCValueType.UNSIGNED_INT)
UnsignedType.ULONG.classId -> return ValueTypeBridge(ObjCValueType.UNSIGNED_LONG_LONG)
}
},
ifPrimitive = { primitiveType, _ ->
val objCValueType = when (primitiveType) {
KonanPrimitiveType.BOOLEAN -> ObjCValueType.BOOL
KonanPrimitiveType.CHAR -> ObjCValueType.UNICHAR
KonanPrimitiveType.BYTE -> ObjCValueType.CHAR
KonanPrimitiveType.SHORT -> ObjCValueType.SHORT
KonanPrimitiveType.INT -> ObjCValueType.INT
KonanPrimitiveType.LONG -> ObjCValueType.LONG_LONG
KonanPrimitiveType.FLOAT -> ObjCValueType.FLOAT
KonanPrimitiveType.DOUBLE -> ObjCValueType.DOUBLE
KonanPrimitiveType.NON_NULL_NATIVE_PTR -> ObjCValueType.POINTER
KonanPrimitiveType.VECTOR128 -> TODO()
}
ValueTypeBridge(objCValueType)
},
ifReference = {
if (kotlinType.isFunctionType) {
bridgeFunctionType(kotlinType)
} else {
ReferenceBridge
}
}
)
private fun ObjCExportMapper.bridgeFunctionType(kotlinType: KotlinType): TypeBridge {
// kotlinType.arguments include return type: <P1, P2, ..., Pn, R>
val numberOfParameters = kotlinType.arguments.size - 1
val returnType = kotlinType.getReturnTypeFromFunctionType()
val returnsVoid = returnType.isUnit() || returnType.isNothing()
// Note: this is correct because overriding method can't turn this into false
// neither for a parameter nor for a return type.
return BlockPointerBridge(numberOfParameters, returnsVoid)
}
private fun ObjCExportMapper.bridgeParameter(parameter: ParameterDescriptor): MethodBridgeValueParameter =
MethodBridgeValueParameter.Mapped(bridgeType(parameter.type))
private fun ObjCExportMapper.bridgeReturnType(
descriptor: FunctionDescriptor,
convertExceptionsToErrors: Boolean,
): MethodBridge.ReturnValue {
val returnType = descriptor.returnType!!
return when {
descriptor.isSuspend -> MethodBridge.ReturnValue.Suspend
descriptor is ConstructorDescriptor -> if (descriptor.constructedClass.isArray) {
MethodBridge.ReturnValue.Instance.FactoryResult
} else {
MethodBridge.ReturnValue.Instance.InitResult
}.let {
if (convertExceptionsToErrors) {
MethodBridge.ReturnValue.WithError.ZeroForError(it, successMayBeZero = false)
} else {
it
}
}
descriptor.containingDeclaration.let { it is ClassDescriptor && KotlinBuiltIns.isAny(it) } &&
descriptor.name.asString() == "hashCode" -> {
assert(!convertExceptionsToErrors)
MethodBridge.ReturnValue.HashCode
}
descriptor is PropertyGetterDescriptor -> {
assert(!convertExceptionsToErrors)
MethodBridge.ReturnValue.Mapped(bridgePropertyType(descriptor.correspondingProperty))
}
returnType.isUnit() || returnType.isNothing() -> if (convertExceptionsToErrors) {
MethodBridge.ReturnValue.WithError.Success
} else {
MethodBridge.ReturnValue.Void
}
else -> {
val returnTypeBridge = bridgeType(returnType)
val successReturnValueBridge = MethodBridge.ReturnValue.Mapped(returnTypeBridge)
if (convertExceptionsToErrors) {
val canReturnZero = !returnTypeBridge.isReferenceOrPointer() || TypeUtils.isNullableType(returnType)
MethodBridge.ReturnValue.WithError.ZeroForError(
successReturnValueBridge,
successMayBeZero = canReturnZero
)
} else {
successReturnValueBridge
}
}
}
}
private fun TypeBridge.isReferenceOrPointer(): Boolean = when (this) {
ReferenceBridge, is BlockPointerBridge -> true
is ValueTypeBridge -> this.objCValueType == ObjCValueType.POINTER
}
private fun ObjCExportMapper.bridgeMethodImpl(descriptor: FunctionDescriptor): MethodBridge {
assert(isBaseMethod(descriptor))
val convertExceptionsToErrors = this.doesThrow(descriptor)
val kotlinParameters = descriptor.allParameters.iterator()
val isTopLevel = isTopLevel(descriptor)
val receiver = if (descriptor is ConstructorDescriptor && descriptor.constructedClass.isArray) {
kotlinParameters.next()
MethodBridgeReceiver.Factory
} else if (isTopLevel) {
MethodBridgeReceiver.Static
} else {
kotlinParameters.next()
MethodBridgeReceiver.Instance
}
val valueParameters = mutableListOf<MethodBridgeValueParameter>()
kotlinParameters.forEach {
valueParameters += bridgeParameter(it)
}
val returnBridge = bridgeReturnType(descriptor, convertExceptionsToErrors)
if (descriptor.isSuspend) {
val useUnitCompletion = (unitSuspendFunctionExport == UnitSuspendFunctionObjCExport.PROPER) && (descriptor.returnType!!.isUnit())
valueParameters += MethodBridgeValueParameter.SuspendCompletion(useUnitCompletion)
} else if (convertExceptionsToErrors) {
// Add error out parameter before tail block parameters. The convention allows this.
// Placing it after would trigger https://bugs.swift.org/browse/SR-12201
// (see also https://github.com/JetBrains/kotlin-native/issues/3825).
val tailBlocksCount = valueParameters.reversed().takeWhile { it.isBlockPointer() }.count()
valueParameters.add(valueParameters.size - tailBlocksCount, MethodBridgeValueParameter.ErrorOutParameter)
}
return MethodBridge(returnBridge, receiver, valueParameters)
}
private fun MethodBridgeValueParameter.isBlockPointer(): Boolean = when (this) {
is MethodBridgeValueParameter.Mapped -> when (this.bridge) {
ReferenceBridge, is ValueTypeBridge -> false
is BlockPointerBridge -> true
}
MethodBridgeValueParameter.ErrorOutParameter -> false
is MethodBridgeValueParameter.SuspendCompletion -> true
}
internal fun ObjCExportMapper.bridgePropertyType(descriptor: PropertyDescriptor): TypeBridge {
assert(isBaseProperty(descriptor))
return bridgeType(descriptor.type)
}
@InternalKotlinNativeApi
enum class NSNumberKind(val mappedKotlinClassId: ClassId?, val objCType: ObjCType) {
CHAR(PrimitiveType.BYTE, ObjCPrimitiveType.char),
UNSIGNED_CHAR(UnsignedType.UBYTE, ObjCPrimitiveType.unsigned_char),
SHORT(PrimitiveType.SHORT, ObjCPrimitiveType.short),
UNSIGNED_SHORT(UnsignedType.USHORT, ObjCPrimitiveType.unsigned_short),
INT(PrimitiveType.INT, ObjCPrimitiveType.int),
UNSIGNED_INT(UnsignedType.UINT, ObjCPrimitiveType.unsigned_int),
LONG(ObjCPrimitiveType.long),
UNSIGNED_LONG(ObjCPrimitiveType.unsigned_long),
LONG_LONG(PrimitiveType.LONG, ObjCPrimitiveType.long_long),
UNSIGNED_LONG_LONG(UnsignedType.ULONG, ObjCPrimitiveType.unsigned_long_long),
FLOAT(PrimitiveType.FLOAT, ObjCPrimitiveType.float),
DOUBLE(PrimitiveType.DOUBLE, ObjCPrimitiveType.double),
BOOL(PrimitiveType.BOOLEAN, ObjCPrimitiveType.BOOL),
INTEGER(ObjCPrimitiveType.NSInteger),
UNSIGNED_INTEGER(ObjCPrimitiveType.NSUInteger)
;
// UNSIGNED_SHORT -> unsignedShort
private val kindName = this.name.split('_')
.joinToString("") { it.lowercase().replaceFirstChar(Char::uppercaseChar) }.replaceFirstChar(Char::lowercaseChar)
val valueSelector = kindName // unsignedShort
val initSelector = "initWith${kindName.replaceFirstChar(Char::uppercaseChar)}:" // initWithUnsignedShort:
val factorySelector = "numberWith${kindName.replaceFirstChar(Char::uppercaseChar)}:" // numberWithUnsignedShort:
constructor(
primitiveType: PrimitiveType,
objCPrimitiveType: ObjCPrimitiveType,
) : this(ClassId.topLevel(primitiveType.typeFqName), objCPrimitiveType)
constructor(
unsignedType: UnsignedType,
objCPrimitiveType: ObjCPrimitiveType,
) : this(unsignedType.classId, objCPrimitiveType)
constructor(objCPrimitiveType: ObjCPrimitiveType) : this(null, objCPrimitiveType)
}
@@ -0,0 +1,82 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.backend.konan.descriptors.isInterface
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor
import org.jetbrains.kotlin.types.KotlinType
interface ObjCExportScope {
class RecursionBreachException : Exception {
constructor(type: KotlinType) : super("$type was already encountered during type mapping process.")
}
val parent: ObjCExportScope?
get() = null
fun deriveForType(kotlinType: KotlinType): ObjCTypeExportScope = ObjCTypeExportScopeImpl(kotlinType, this)
fun deriveForClass(container: DeclarationDescriptor, namer: ObjCExportNamer): ObjCClassExportScope =
ObjCClassExportScopeImpl(container, namer, this)
}
internal inline fun <reified T : ObjCExportScope> ObjCExportScope.nearestScopeOfType(): T? {
var parent: ObjCExportScope? = this
while (parent != null) {
if (parent is T) {
return parent
}
parent = parent.parent
}
return null
}
@InternalKotlinNativeApi
object ObjCRootExportScope : ObjCExportScope
interface ObjCClassExportScope : ObjCExportScope {
fun getGenericTypeUsage(typeParameterDescriptor: TypeParameterDescriptor?): ObjCGenericTypeUsage?
}
private class ObjCClassExportScopeImpl constructor(
container: DeclarationDescriptor,
val namer: ObjCExportNamer,
override val parent: ObjCExportScope?,
) : ObjCClassExportScope {
private val typeParameterNames: List<TypeParameterDescriptor> =
if (container is ClassDescriptor && !container.isInterface) {
container.typeConstructor.parameters
} else {
emptyList<TypeParameterDescriptor>()
}
override fun getGenericTypeUsage(typeParameterDescriptor: TypeParameterDescriptor?): ObjCGenericTypeUsage? {
return typeParameterDescriptor?.let { descriptor ->
typeParameterNames.firstOrNull {
it == descriptor || it.isCapturedFromOuterDeclaration && it.original == descriptor
}?.let {
ObjCGenericTypeParameterUsage(it, namer)
}
}
}
}
interface ObjCTypeExportScope : ObjCExportScope {
val kotlinType: KotlinType
}
private class ObjCTypeExportScopeImpl(override val kotlinType: KotlinType, override val parent: ObjCExportScope?) : ObjCTypeExportScope {
init {
var parent = this.parent
while (parent != null && parent is ObjCTypeExportScope) {
if (parent.kotlinType == kotlinType)
throw ObjCExportScope.RecursionBreachException(kotlinType)
parent = parent.parent
}
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.common.serialization.extractSerializedKdocString
import org.jetbrains.kotlin.backend.common.serialization.metadata.findKDocString
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.resolve.source.PsiSourceElement
fun ObjCExportStubOrigin(descriptor: DeclarationDescriptor?): ObjCExportStubOrigin? {
if (descriptor == null) return null
if (descriptor is DeclarationDescriptorWithSource) {
return ObjCExportStubOrigin.Source(descriptor.name, descriptor.findKDocString(), (descriptor.source as? PsiSourceElement)?.psi)
}
assert(descriptor is DeserializedDescriptor) { "Expected '$descriptor' to implement ${DeserializedDescriptor::class.simpleName}" }
return ObjCExportStubOrigin.Binary(descriptor.name, descriptor.extractSerializedKdocString())
}
fun ObjCProtocolImpl(
name: String,
descriptor: ClassDescriptor,
superProtocols: List<String>,
members: List<ObjCExportStub>,
attributes: List<String> = emptyList(),
comment: ObjCComment? = null,
) = ObjCProtocolImpl(
name = name,
comment = comment,
origin = ObjCExportStubOrigin(descriptor),
attributes = attributes,
superProtocols = superProtocols,
members = members
)
fun ObjCInterfaceImpl(
name: String,
generics: List<ObjCGenericTypeDeclaration> = emptyList(),
descriptor: ClassDescriptor? = null,
superClass: String? = null,
superClassGenerics: List<ObjCNonNullReferenceType> = emptyList(),
superProtocols: List<String> = emptyList(),
categoryName: String? = null,
members: List<ObjCExportStub> = emptyList(),
attributes: List<String> = emptyList(),
comment: ObjCComment? = null,
) = ObjCInterfaceImpl(
name = name,
comment = comment,
origin = ObjCExportStubOrigin(descriptor),
attributes = attributes,
superProtocols = superProtocols,
members = members,
categoryName = categoryName,
generics = generics,
superClass = superClass,
superClassGenerics = superClassGenerics
)
fun ObjCMethod(
descriptor: DeclarationDescriptor?,
isInstanceMethod: Boolean,
returnType: ObjCType,
selectors: List<String>,
parameters: List<ObjCParameter>,
attributes: List<String>,
comment: ObjCComment? = null,
) = ObjCMethod(
comment = comment,
origin = ObjCExportStubOrigin(descriptor),
isInstanceMethod = isInstanceMethod,
returnType = returnType,
selectors = selectors,
parameters = parameters,
attributes = attributes
)
fun ObjCParameter(
name: String,
descriptor: ParameterDescriptor?,
type: ObjCType,
) = ObjCParameter(
name = name,
origin = ObjCExportStubOrigin(descriptor),
type = type,
todo = null
)
fun ObjCProperty(
name: String,
descriptor: DeclarationDescriptorWithSource?,
type: ObjCType,
propertyAttributes: List<String>,
setterName: String? = null,
getterName: String? = null,
declarationAttributes: List<String> = emptyList(),
comment: ObjCComment? = null,
) = ObjCProperty(
name = name,
comment = comment,
origin = ObjCExportStubOrigin(descriptor),
type = type,
propertyAttributes = propertyAttributes,
setterName = setterName,
getterName = getterName,
declarationAttributes = declarationAttributes
)
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
class ObjCExportTranslatorMobile internal constructor(private val delegate: ObjCExportTranslatorImpl) : ObjCExportTranslator by delegate {
companion object {
fun create(namer: ObjCExportNamer, configuration: ObjCExportLazy.Configuration): ObjCExportTranslatorMobile {
val mapper = ObjCExportMapper(local = true, unitSuspendFunctionExport = configuration.unitSuspendFunctionExport)
return ObjCExportTranslatorMobile(
ObjCExportTranslatorImpl(
null,
mapper,
namer,
ObjCExportProblemCollector.SILENT,
configuration.objcGenerics
)
)
}
}
fun translateBaseFunction(descriptor: FunctionDescriptor): ObjCMethod {
val classDescriptor = descriptor.containingDeclaration as? ClassDescriptor
val scope = classDescriptor?.let { delegate.createGenericExportScope(it) } ?: ObjCRootExportScope
return delegate.buildMethod(descriptor, descriptor, scope)
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor
fun ObjCGenericTypeParameterUsage(
typeParameterDescriptor: TypeParameterDescriptor,
namer: ObjCExportNamer,
) = ObjCGenericTypeParameterUsage(
typeName = namer.getTypeParameterName(typeParameterDescriptor)
)
fun ObjCGenericTypeParameterDeclaration(
typeParameterDescriptor: TypeParameterDescriptor,
namer: ObjCExportNamer,
) = ObjCGenericTypeParameterDeclaration(
typeName = namer.getTypeParameterName(typeParameterDescriptor),
variance = ObjCVariance.fromKotlinVariance(typeParameterDescriptor.variance)
)
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.SourceFile
@InternalKotlinNativeApi
class ObjCExportedInterface internal constructor(
val generatedClasses: Set<ClassDescriptor>,
val categoryMembers: Map<ClassDescriptor, List<CallableMemberDescriptor>>,
val topLevel: Map<SourceFile, List<CallableMemberDescriptor>>,
val headerLines: List<String>,
val namer: ObjCExportNamer,
val mapper: ObjCExportMapper,
)
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2022 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.backend.konan.objcexport
import org.jetbrains.kotlin.konan.file.File
// For now, the object is pretty dumb.
// Later it will accept an object with ObjC declarations instead of lines.
class ObjCHeaderWriter {
fun write(
headerName: String,
headerLines: List<String>,
headersDirectory: File,
) {
headersDirectory.child(headerName).writeLines(headerLines)
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
import org.jetbrains.kotlin.resolve.descriptorUtil.module
class ObjcExportHeaderGeneratorMobile internal constructor(
moduleDescriptors: List<ModuleDescriptor>,
mapper: ObjCExportMapper,
namer: ObjCExportNamer,
problemCollector: ObjCExportProblemCollector,
objcGenerics: Boolean,
private val restrictToLocalModules: Boolean,
) : ObjCExportHeaderGenerator(moduleDescriptors, mapper, namer, objcGenerics, problemCollector) {
companion object {
fun createInstance(
configuration: ObjCExportLazy.Configuration,
problemCollector: ObjCExportProblemCollector,
builtIns: KotlinBuiltIns,
moduleDescriptors: List<ModuleDescriptor>,
deprecationResolver: DeprecationResolver? = null,
local: Boolean = false,
restrictToLocalModules: Boolean = false,
): ObjCExportHeaderGenerator {
val mapper = ObjCExportMapper(deprecationResolver, local, configuration.unitSuspendFunctionExport)
val namerConfiguration = createNamerConfiguration(configuration)
val namer = ObjCExportNamerImpl(namerConfiguration, builtIns, mapper, problemCollector, local)
return ObjcExportHeaderGeneratorMobile(
moduleDescriptors,
mapper,
namer,
problemCollector,
configuration.objcGenerics,
restrictToLocalModules
)
}
}
override fun shouldTranslateExtraClass(descriptor: ClassDescriptor): Boolean =
!restrictToLocalModules || descriptor.module in moduleDescriptors
}
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2023 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.backend.konan.objcexport
internal class StubBuilder<S : ObjCExportStub>(private val problemCollector: ObjCExportProblemCollector) {
private val children = mutableListOf<S>()
inline fun add(provider: () -> S) {
try {
children.add(provider())
} catch (t: Throwable) {
problemCollector.reportException(t)
}
}
operator fun plusAssign(set: Collection<S>) {
children += set
}
fun build(): List<S> = children
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2023 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.backend.konan.testUtils
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.backend.konan.UnitSuspendFunctionObjCExport
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.backend.konan.tests.ObjCExportHeaderGeneratorTest
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import java.io.File
class Fe10HeaderGeneratorExtension : ParameterResolver, AfterEachCallback {
companion object {
val namespace = ExtensionContext.Namespace.create(Fe10HeaderGeneratorExtension::class)
val disposableKey = "disposable"
}
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
return parameterContext.parameter.type == ObjCExportHeaderGeneratorTest.HeaderGenerator::class.java
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
val disposable = Disposer.newDisposable()
extensionContext.getStore(namespace).put(disposableKey, disposable)
return Fe10HeaderGeneratorImpl(disposable)
}
override fun afterEach(context: ExtensionContext) {
val disposable = context.getStore(namespace).get(disposableKey, Disposable::class.java) ?: return
Disposer.dispose(disposable)
}
}
private class Fe10HeaderGeneratorImpl(private val disposable: Disposable) :
ObjCExportHeaderGeneratorTest.HeaderGenerator {
override fun generateHeaders(root: File): String {
val headerGenerator = createObjCExportHeaderGenerator(disposable, root)
headerGenerator.translateModuleDeclarations()
return headerGenerator.build().joinToString(System.lineSeparator())
}
private fun createObjCExportHeaderGenerator(disposable: Disposable, root: File): ObjCExportHeaderGenerator {
val mapper = ObjCExportMapper(
unitSuspendFunctionExport = UnitSuspendFunctionObjCExport.DEFAULT
)
val namer = ObjCExportNamerImpl(
mapper = mapper,
builtIns = DefaultBuiltIns.Instance,
local = false,
problemCollector = ObjCExportProblemCollector.SILENT,
configuration = object : ObjCExportNamer.Configuration {
override val topLevelNamePrefix: String get() = ""
override fun getAdditionalPrefix(module: ModuleDescriptor): String? = null
override val objcGenerics: Boolean = true
}
)
val environment: KotlinCoreEnvironment = createKotlinCoreEnvironment(disposable)
val kotlinFiles = root.walkTopDown().filter { it.isFile }.filter { it.extension == "kt" }.toList()
return ObjCExportHeaderGeneratorImpl(
moduleDescriptors = listOf(createModuleDescriptor(environment, kotlinFiles)),
mapper = mapper,
namer = namer,
problemCollector = ObjCExportProblemCollector.SILENT,
objcGenerics = true,
shouldExportKDoc = true,
additionalImports = emptyList()
)
}
}
@@ -0,0 +1,135 @@
/*
* Copyright 2010-2023 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.backend.konan.testUtils
import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.analyzer.common.CommonDependenciesContainer
import org.jetbrains.kotlin.analyzer.common.CommonResolverForModuleFactory
import org.jetbrains.kotlin.backend.common.serialization.metadata.DynamicTypeDeserializer
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.builtins.konan.KonanBuiltIns
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.arguments.K2NativeCompilerArguments
import org.jetbrains.kotlin.cli.common.createFlexiblePhaseConfig
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.*
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PackageFragmentProvider
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.library.metadata.KlibMetadataFactories
import org.jetbrains.kotlin.library.resolveSingleFileKlib
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.platform.CommonPlatforms
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.konan.NativePlatforms
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.CompilerEnvironment
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.resolve.konan.platform.NativePlatformAnalyzerServices
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.util.KtTestUtil
import java.io.File
fun createModuleDescriptor(
environment: KotlinCoreEnvironment,
tempDir: File,
kotlinSources: List<String>,
): ModuleDescriptor {
val testModuleRoot = tempDir.resolve("testModule")
testModuleRoot.mkdirs()
val testSourcesFiles = kotlinSources.mapIndexed { index, kotlinSource ->
testModuleRoot.resolve("TestSources$index.kt").apply {
writeText(kotlinSource)
}
}
return createModuleDescriptor(environment, testSourcesFiles)
}
fun createModuleDescriptor(
environment: KotlinCoreEnvironment,
kotlinFiles: List<File>,
): ModuleDescriptor {
val psiFactory = KtPsiFactory(environment.project)
val kotlinPsiFiles = kotlinFiles.map { file -> psiFactory.createFile(file.name, KtTestUtil.doLoadFile(file)) }
val analysisResult = CommonResolverForModuleFactory.analyzeFiles(
files = kotlinPsiFiles,
moduleName = Name.special("<test_module>"),
dependOnBuiltIns = true,
languageVersionSettings = environment.configuration.languageVersionSettings,
targetPlatform = CommonPlatforms.defaultCommonPlatform,
targetEnvironment = CompilerEnvironment,
dependenciesContainer = DependenciesContainerImpl,
) { content ->
environment.createPackagePartProvider(content.moduleContentScope)
}
return analysisResult.moduleDescriptor
}
fun createKotlinCoreEnvironment(
disposable: Disposable, compilerConfiguration: CompilerConfiguration = createCompilerConfiguration(),
): KotlinCoreEnvironment {
return KotlinCoreEnvironment.createForTests(
parentDisposable = disposable,
initialConfiguration = compilerConfiguration,
extensionConfigs = EnvironmentConfigFiles.METADATA_CONFIG_FILES
)
}
private fun createCompilerConfiguration(): CompilerConfiguration {
val configuration = KotlinTestUtils.newConfiguration()
configuration.put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, createLanguageVersionSettings())
configuration.put(CLIConfigurationKeys.FLEXIBLE_PHASE_CONFIG, createFlexiblePhaseConfig(K2NativeCompilerArguments()))
return configuration
}
private fun createLanguageVersionSettings() = LanguageVersionSettingsImpl(
languageVersion = LanguageVersion.LATEST_STABLE,
apiVersion = ApiVersion.LATEST_STABLE
)
private object DependenciesContainerImpl : CommonDependenciesContainer {
override val moduleInfos: List<ModuleInfo> get() = listOf(DefaultBuiltInsModuleInfo, KotlinNativeStdlibModuleInfo)
override val friendModuleInfos: List<ModuleInfo> get() = emptyList()
override val refinesModuleInfos: List<ModuleInfo> get() = emptyList()
override fun registerDependencyForAllModules(moduleInfo: ModuleInfo, descriptorForModule: ModuleDescriptorImpl) = Unit
override fun packageFragmentProviderForModuleInfo(moduleInfo: ModuleInfo): PackageFragmentProvider? = null
private val klibFactory = KlibMetadataFactories(::KonanBuiltIns, DynamicTypeDeserializer)
private val stdlibModuleDescriptor = klibFactory.DefaultDeserializedDescriptorFactory.createDescriptor(
library = resolveSingleFileKlib(org.jetbrains.kotlin.konan.file.File(kotlinNativeStdlibPath)),
languageVersionSettings = createLanguageVersionSettings(),
builtIns = DefaultBuiltIns.Instance,
storageManager = LockBasedStorageManager.NO_LOCKS,
packageAccessHandler = null
).also { it.setDependencies(it) }
override fun moduleDescriptorForModuleInfo(moduleInfo: ModuleInfo): ModuleDescriptor {
if (moduleInfo == DefaultBuiltInsModuleInfo) return DefaultBuiltIns.Instance.builtInsModule
if (moduleInfo == KotlinNativeStdlibModuleInfo) return stdlibModuleDescriptor
error("Unknown module info $moduleInfo")
}
}
private object DefaultBuiltInsModuleInfo : ModuleInfo {
override val name get() = DefaultBuiltIns.Instance.builtInsModule.name
override fun dependencies() = listOf(this)
override fun dependencyOnBuiltIns() = ModuleInfo.DependencyOnBuiltIns.LAST
override val platform get() = NativePlatforms.unspecifiedNativePlatform
override val analyzerServices get() = NativePlatformAnalyzerServices
}
private object KotlinNativeStdlibModuleInfo : ModuleInfo {
override val analyzerServices: PlatformDependentAnalyzerServices = NativePlatformAnalyzerServices
override val name: Name = Name.special("<stdlib>")
override val platform: TargetPlatform = NativePlatforms.unspecifiedNativePlatform
override fun dependencies(): List<ModuleInfo> = listOf(this)
}
@@ -0,0 +1,71 @@
/*
* Copyright 2010-2023 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.backend.konan.testUtils
import com.intellij.openapi.Disposable
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import java.io.File
/**
* Test Environment that enables quick 'inline' creation of [ModuleDescriptor] instances.
*
* # Examples
* ## Test operating on a single Kotlin Source file:
*
* ```kotlin
* class MyTest : InlineSourceTestEnvironment {
* @Test
* fun `test - my unit`() {
* val moduleDescriptor = createModuleDescriptor("""
* package com.example
* class MyClassUnderTest
* """.trimIndent()
* )
*
* // my test!
* }
* }
* ```
*
* ## Test that requires several source files (e.g. multiple packages are involved)
* ```kotlin
* @Test
* fun `test - my unit`() {
* val moduleDescriptor = createModuleDescriptor {
* source("class Foo")
* source("""
* package com.example
* class Foo
* """)
* }
* }
* ```
*/
interface InlineSourceTestEnvironment {
val kotlinCoreEnvironment: KotlinCoreEnvironment
val testDisposable: Disposable
val testTempDir: File
}
interface InlineSourceCodeCollector {
fun source(@Language("kotlin") sourceCode: String)
}
fun InlineSourceTestEnvironment.createModuleDescriptor(@Language("kotlin") sourceCode: String): ModuleDescriptor =
createModuleDescriptor(kotlinCoreEnvironment, testTempDir, listOf(sourceCode))
fun InlineSourceTestEnvironment.createModuleDescriptor(build: InlineSourceCodeCollector.() -> Unit): ModuleDescriptor {
val sources = mutableListOf<String>()
object : InlineSourceCodeCollector {
override fun source(sourceCode: String) {
sources.add(sourceCode)
}
}.build()
return createModuleDescriptor(kotlinCoreEnvironment, testTempDir, sources.toList())
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2023 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.backend.konan.testUtils
import org.jetbrains.kotlin.backend.konan.UnitSuspendFunctionObjCExport
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportMapper
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamerImpl
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportProblemCollector
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
internal fun createObjCExportNamerConfiguration(
topLevelNamePrefix: String = "",
additionalPrefix: (moduleName: Name) -> String? = { null },
): ObjCExportNamer.Configuration {
return object : ObjCExportNamer.Configuration {
override val topLevelNamePrefix: String get() = topLevelNamePrefix
override fun getAdditionalPrefix(module: ModuleDescriptor): String? = additionalPrefix(module.name)
override val objcGenerics: Boolean = true
}
}
internal fun createObjCExportMapper(
deprecationResolver: DeprecationResolver? = null,
local: Boolean = false,
unitSuspendFunctionObjCExport: UnitSuspendFunctionObjCExport = UnitSuspendFunctionObjCExport.DEFAULT,
): ObjCExportMapper {
return ObjCExportMapper(deprecationResolver, local, unitSuspendFunctionObjCExport)
}
internal fun createObjCExportNamer(
configuration: ObjCExportNamer.Configuration = createObjCExportNamerConfiguration(),
builtIns: KotlinBuiltIns = DefaultBuiltIns.Instance,
local: Boolean = false,
problemCollector: ObjCExportProblemCollector = ObjCExportProblemCollector.SILENT,
mapper: ObjCExportMapper = createObjCExportMapper(local = local),
): ObjCExportNamer {
return ObjCExportNamerImpl(
builtIns = builtIns,
mapper = mapper,
local = local,
problemCollector = problemCollector,
configuration = configuration
)
}
@@ -0,0 +1,100 @@
/*
* Copyright 2010-2023 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.backend.konan.tests
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.backend.konan.testUtils.*
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.types.KotlinTypeFactory
import org.jetbrains.kotlin.types.TypeAttributes
import org.jetbrains.kotlin.types.TypeProjectionImpl
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
/**
* ## Test Scope
* This test shall cover test the [ObjCExportMapper];
* The test will invoke the type mapping a known type (e.g. List<Int>) and check
* the corresponding conversion to [ObjCNonNullReferenceType].
*
* The test also acts as quick entry point for debugging the [ObjCExportMapper]
*/
class ObjCExportMapperTest : InlineSourceTestEnvironment {
override val testDisposable = Disposer.newDisposable("${ObjCExportMapperTest::class.simpleName}.testDisposable")
override val kotlinCoreEnvironment: KotlinCoreEnvironment = createKotlinCoreEnvironment(testDisposable)
@TempDir
override lateinit var testTempDir: File
@AfterEach
fun dispose() {
Disposer.dispose(testDisposable)
}
/**
* No 'type mapper' expected for a simple Kotlin class like 'Foo'.
* Only well known standard types (List, ...) will get mapped to their corresponding
* ObjC counterpart (NSArray, ...)
*/
@Test
fun `test - simple class`() {
val module = createModuleDescriptor("class Foo")
val foo = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!!
assertNull(createObjCExportMapper().getCustomTypeMapper(foo))
}
/**
* Will test ObjC type mapping from List<Int> to NSArray<Int *> *
*/
@Test
fun `test - List of int`() {
/* We are only using built in / stdlib parts, so we can provide empty source code */
val module = createModuleDescriptor("")
val objcExportMapper = createObjCExportMapper()
val objcExportNamer = createObjCExportNamer(mapper = objcExportMapper)
val objcExportTranslator = ObjCExportTranslatorImpl(
generator = ObjCExportHeaderGeneratorImpl(
moduleDescriptors = listOf(module),
mapper = objcExportMapper,
namer = objcExportNamer,
problemCollector = ObjCExportProblemCollector.SILENT,
objcGenerics = true,
shouldExportKDoc = false,
additionalImports = emptyList()
),
mapper = objcExportMapper,
namer = objcExportNamer,
problemCollector = ObjCExportProblemCollector.SILENT,
objcGenerics = true
)
val listClassDescriptor = module.findClassAcrossModuleDependencies(ClassId.fromString("kotlin/collections/List"))!!
val intClassDescriptor = module.findClassAcrossModuleDependencies(ClassId.fromString("kotlin/Int"))!!
/* Represents List<Int> */
val listOfIntType = KotlinTypeFactory.simpleNotNullType(
TypeAttributes.Empty, listClassDescriptor, listOf(
TypeProjectionImpl(KotlinTypeFactory.simpleNotNullType(TypeAttributes.Empty, intClassDescriptor, emptyList()))
)
)
val typeMapper = assertNotNull(objcExportMapper.getCustomTypeMapper(listClassDescriptor))
assertEquals(ClassId.fromString("kotlin/collections/List"), typeMapper.mappedClassId)
val listOfIntMapped = typeMapper.mapType(listOfIntType, objcExportTranslator, objCExportScope = ObjCRootExportScope)
assertEquals(ObjCClassType("NSArray", typeArguments = listOf(ObjCClassType("Int"))), listOfIntMapped)
assertEquals("NSArray<Int *> *", listOfIntMapped.toString())
}
}
@@ -0,0 +1,204 @@
/*
* Copyright 2010-2023 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.backend.konan.tests
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.backend.konan.descriptors.enumEntries
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer.ClassOrProtocolName
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer.PropertyName
import org.jetbrains.kotlin.backend.konan.testUtils.*
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi2ir.findSingleFunction
import org.jetbrains.kotlin.resolve.scopes.findFirstVariable
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import kotlin.test.assertEquals
/**
* ## Test Scope
* This test will cover basic cases of the [ObjCExportNamer]. <br>
*
* The __input__ to this test will be descriptors defined as 'inline source code'.
* (e.g. a String like "class Foo").<br>
*
* The __output__ of this test will be result of the request to the [ObjCExportNamer].
* (e.g. the [ClassOrProtocolName])
*/
class ObjCExportNamerTest : InlineSourceTestEnvironment {
override val testDisposable = Disposer.newDisposable("${ObjCExportNamerTest::class.simpleName}.testDisposable")
override val kotlinCoreEnvironment = createKotlinCoreEnvironment(testDisposable)
@TempDir
override lateinit var testTempDir: File
@AfterEach
fun dispose() {
Disposer.dispose(testDisposable)
}
@Test
fun `test - simple class`() {
val module = createModuleDescriptor("class Foo")
val clazz = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!!
assertEquals(
ClassOrProtocolName("Foo", "Foo"),
createObjCExportNamer().getClassOrProtocolName(clazz)
)
}
@Test
fun `test - simple class - with prefix`() {
val module = createModuleDescriptor("class Foo")
val clazz = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!!
assertEquals(
ClassOrProtocolName("Foo", "XFoo"),
createObjCExportNamer(createObjCExportNamerConfiguration(topLevelNamePrefix = "X")).getClassOrProtocolName(clazz)
)
}
@Test
fun `test - simple function`() {
val module = createModuleDescriptor("fun foo() = 42")
val foo = module.getPackage(FqName.ROOT).memberScope.findSingleFunction(Name.identifier("foo"))
assertEquals("foo()", createObjCExportNamer().getSwiftName(foo))
assertEquals("foo", createObjCExportNamer().getSelector(foo))
}
@Test
fun `test - function with parameters`() {
val module = createModuleDescriptor("fun foo(a: Int, b: Int) = a + b")
val foo = module.getPackage(FqName.ROOT).memberScope.findSingleFunction(Name.identifier("foo"))
assertEquals("foo(a:b:)", createObjCExportNamer().getSwiftName(foo))
assertEquals("fooA:b:", createObjCExportNamer().getSelector(foo))
}
@Test
fun `test - simple property`() {
val module = createModuleDescriptor("val foo = 42")
val foo = module.getPackage(FqName.ROOT).memberScope.findFirstVariable("foo") { true }!!
assertEquals(PropertyName("foo", "foo"), createObjCExportNamer().getPropertyName(foo))
}
@Test
fun `test - simple property - with prefix`() {
val module = createModuleDescriptor("val foo = 42")
val foo = module.getPackage(FqName.ROOT).memberScope.findFirstVariable("foo") { true }!!
assertEquals(
PropertyName("foo", "foo"),
createObjCExportNamer(createObjCExportNamerConfiguration(topLevelNamePrefix = "X")).getPropertyName(foo)
)
}
@Test
fun `test - function inside class`() {
val module = createModuleDescriptor(
"""
package bar
class Foo {
fun someFunction(a: Int, b: Int) = a + b
}
""".trimIndent()
)
val fooClass = module.findClassAcrossModuleDependencies(ClassId.fromString("bar/Foo"))!!
val someFunction = fooClass.unsubstitutedMemberScope.findSingleFunction(Name.identifier("someFunction"))
assertEquals("someFunction(a:b:)", createObjCExportNamer().getSwiftName(someFunction))
assertEquals("someFunctionA:b:", createObjCExportNamer().getSelector(someFunction))
}
@Test
fun `test - class with ObjCName annotation`() {
val module = createModuleDescriptor(
"""
@kotlin.native.ObjCName("ObjCFoo", "SwiftFoo")
class Foo
""".trimIndent()
)
val fooClass = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!!
assertEquals(
ClassOrProtocolName(swiftName = "SwiftFoo", objCName = "ObjCFoo"),
createObjCExportNamer().getClassOrProtocolName(fooClass)
)
}
@Test
fun `test - simple parameter`() {
val module = createModuleDescriptor("fun foo(a: Int)")
val foo = module.getPackage(FqName.ROOT).memberScope.findSingleFunction(Name.identifier("foo"))
val parameterA = foo.valueParameters.find { it.name == Name.identifier("a") }!!
assertEquals("a", createObjCExportNamer().getParameterName(parameterA))
}
@Test
fun `test - parameter with ObjCName annotation`() {
val module = createModuleDescriptor(
"""
import kotlin.native.ObjCName
fun foo(@ObjCName("aObjC", "aSwift") a: Int)
""".trimIndent()
)
val foo = module.getPackage(FqName.ROOT).memberScope.findSingleFunction(Name.identifier("foo"))
val parameterA = foo.valueParameters.find { it.name == Name.identifier("a") }!!
assertEquals("aObjC", createObjCExportNamer().getParameterName(parameterA))
}
@Test
fun `test - class mangling`() {
val module = createModuleDescriptor {
source(
"""
package a
class Foo
""".trimIndent()
)
source(
"""
package b
class Foo
""".trimIndent()
)
}
val aFoo = module.findClassAcrossModuleDependencies(ClassId.fromString("a/Foo"))!!
val bFoo = module.findClassAcrossModuleDependencies(ClassId.fromString("b/Foo"))!!
val namer = createObjCExportNamer()
assertEquals(ClassOrProtocolName("Foo", "Foo"), namer.getClassOrProtocolName(aFoo))
assertEquals(ClassOrProtocolName("Foo_", "Foo_"), namer.getClassOrProtocolName(bFoo))
/* Check caching in namer: Should return same name again */
assertEquals(ClassOrProtocolName("Foo", "Foo"), namer.getClassOrProtocolName(aFoo))
}
@Test
fun `test - simple enum`() {
val module = createModuleDescriptor(
"""
enum class Foo {
A, B, C
}
""".trimIndent()
)
val foo = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!!
val fooA = foo.enumEntries.find { it.name == Name.identifier("A") }!!
val namer = createObjCExportNamer()
assertEquals(ClassOrProtocolName("Foo", "Foo"), namer.getClassOrProtocolName(foo))
assertEquals("a", namer.getEnumEntrySelector(fooA))
assertEquals("a", namer.getEnumEntrySwiftName(fooA))
}
}
@@ -0,0 +1,5 @@
#
# Copyright 2010-2023 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.
#
org.jetbrains.kotlin.backend.konan.testUtils.Fe10HeaderGeneratorExtension