[ObjCExport] Support exported library modules

This new module gives the ability to resolve symbols
from provided klib KtLibraryModules withing a Analysis Session.

^KT-65327 Fixed
This commit is contained in:
Sebastian Sellmair
2024-03-01 11:34:56 +01:00
committed by Space Team
parent b5b7e5f262
commit 10cf97f05b
12 changed files with 234 additions and 98 deletions
@@ -14,8 +14,10 @@ dependencies {
api(project(":analysis:analysis-api"))
api(project(":compiler:psi"))
api(project(":native:objcexport-header-generator"))
implementation(project(":core:compiler.common.native"))
implementation(project(":kotlin-util-klib"))
implementation(project(":native:analysis-api-klib-reader"))
testImplementation(projectTests(":native:objcexport-header-generator"))
testApi(project(":analysis:analysis-api-standalone"))
@@ -0,0 +1,132 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.objcexport
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.klib.reader.*
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getAllClassOrObjectSymbols
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.tooling.core.withClosure
internal interface KtObjCExportFile {
val fileName: String
val packageFqName: FqName
context(KtAnalysisSession)
fun resolve(): KtResolvedObjCExportFile
}
/**
* The 'resolved' version of the [KtObjCExportFile].
* 'Resolved' means that all symbols associated with the file are loaded and available
* under [classifierSymbols] and [callableSymbols].
*/
data class KtResolvedObjCExportFile(
val fileName: String,
val packageFqName: FqName,
val classifierSymbols: List<KtClassOrObjectSymbol>,
val callableSymbols: List<KtCallableSymbol>,
)
/* Factory functions */
internal fun KtObjCExportFile(file: KtFile): KtObjCExportFile {
return KtPsiObjCExportFile(file)
}
internal fun createKtObjCExportFiles(addresses: Iterable<KlibDeclarationAddress>): List<KtObjCExportFile> {
val addressesByPackageName = addresses.groupBy { it.packageFqName }
return addressesByPackageName.flatMap { (packageName, addresses) ->
val addressesByFile = addresses.groupBy { it.sourceFileName }
addressesByFile.mapNotNull { (fileName, addresses) ->
fileName ?: return@mapNotNull null
KtKlibObjCExportFile(FileUtil.getNameWithoutExtension(fileName), packageName, addresses.toSet())
}
}
}
/* Private Implementations */
private class KtPsiObjCExportFile(
private val file: KtFile,
) : KtObjCExportFile {
override val fileName: String
get() = FileUtil.getNameWithoutExtension(file.name)
override val packageFqName: FqName
get() = file.packageFqName
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is KtPsiObjCExportFile) return false
if (other.file != this.file) return false
return true
}
override fun hashCode(): Int {
return file.hashCode()
}
/**
* See [KtResolvedObjCExportFile]
*/
context(KtAnalysisSession)
override fun resolve(): KtResolvedObjCExportFile {
val symbol = file.getFileSymbol()
return KtResolvedObjCExportFile(
fileName = fileName,
packageFqName = packageFqName,
classifierSymbols = symbol.getAllClassOrObjectSymbols(),
callableSymbols = symbol.getFileScope().getCallableSymbols().toList()
)
}
}
private class KtKlibObjCExportFile(
override val fileName: String,
override val packageFqName: FqName,
private val addresses: Set<KlibDeclarationAddress>,
) : KtObjCExportFile {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is KtKlibObjCExportFile) return false
if (other.fileName != this.fileName) return false
if (other.packageFqName != this.packageFqName) return false
if (other.addresses != this.addresses) return false
return true
}
override fun hashCode(): Int {
var result = fileName.hashCode()
result = 31 * result + packageFqName.hashCode()
result = 31 * result + addresses.hashCode()
return result
}
context(KtAnalysisSession)
override fun resolve(): KtResolvedObjCExportFile {
val classifierAddresses = addresses.filterIsInstance<KlibClassAddress>()
val callableAddresses = addresses.filterIsInstance<KlibCallableAddress>()
return KtResolvedObjCExportFile(
fileName = fileName,
packageFqName = packageFqName,
classifierSymbols = classifierAddresses
.mapNotNull { classAddress -> classAddress.getClassOrObjectSymbol() }
.withClosure<KtClassOrObjectSymbol> { symbol ->
symbol.getMemberScope().getClassifierSymbols().filterIsInstance<KtClassOrObjectSymbol>().asIterable()
}.toList(),
callableSymbols = callableAddresses.flatMap { address ->
address.getCallableSymbols()
}
)
}
}
@@ -1,13 +1,10 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportFileName
import org.jetbrains.kotlin.backend.konan.objcexport.toIdentifier
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getFileName
context(KtAnalysisSession, KtObjCExportSession)
internal fun KtFileSymbol.getObjCFileClassOrProtocolName(): ObjCExportFileName? {
val fileName = getFileName() ?: return null
internal fun KtResolvedObjCExportFile.getObjCFileClassOrProtocolName(): ObjCExportFileName {
return (fileName + "Kt").toIdentifier().getObjCFileName()
}
@@ -1,7 +1,6 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl
@@ -45,27 +44,22 @@ internal val ObjCInterface.isExtensionsFacade: Boolean
*
* Where `Foo` would be the "top level interface file extensions facade" returned by this function.
*
* See related [getTopLevelFacade]
* See related [translateToObjCTopLevelFacade]
*/
context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.getExtensionFacades(): List<ObjCInterface> {
val extensions = getFileScope()
.getCallableSymbols().filter { it.isExtension }
.toList()
fun KtResolvedObjCExportFile.translateToObjCExtensionFacades(): List<ObjCInterface> {
val extensions = callableSymbols
.filter { it.isExtension }
.sortedWith(StableCallableOrder)
.ifEmpty { return emptyList() }
.groupBy {
val classSymbol = it.receiverParameter?.type?.expandedClassSymbol
classSymbol?.getObjCClassOrProtocolName()?.objCName
}
.mapNotNull { (key, value) ->
if (key == null) return@mapNotNull null else key to value
}
return extensions.map { (objCName, extensionSymbols) ->
return extensions.mapNotNull { (objCName, extensionSymbols) ->
ObjCInterfaceImpl(
name = objCName,
name = objCName ?: return@mapNotNull null,
comment = null,
origin = null,
attributes = emptyList(),
@@ -6,22 +6,33 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.klib.reader.readKlibDeclarationAddresses
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.allDirectDependencies
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.objcexport.KtObjCExportHeaderGenerator.QueueElement
import org.jetbrains.kotlin.objcexport.analysisApiUtils.*
import org.jetbrains.kotlin.objcexport.extras.originClassId
import org.jetbrains.kotlin.objcexport.extras.requiresForwardDeclaration
import org.jetbrains.kotlin.objcexport.extras.throwsAnnotationClassIds
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.tooling.core.closure
context(KtAnalysisSession, KtObjCExportSession)
fun translateToObjCHeader(files: List<KtFile>): ObjCHeader {
val generator = KtObjCExportHeaderGenerator()
generator.translateAll(files.sortedWith(StableFileOrder).map { QueueElement.File(it) })
val klibDeclarationAddresses = useSiteModule.closure { module -> module.allDirectDependencies().asIterable() }
.filterIsInstance<KtLibraryModule>()
.filter { it.getObjCKotlinModuleName() in configuration.exportedModuleNames }
.flatMap { it.readKlibDeclarationAddresses().orEmpty() }
val unresolvedFiles = files.map { ktFile -> KtObjCExportFile(ktFile) } +
createKtObjCExportFiles(klibDeclarationAddresses)
generator.translateAll(unresolvedFiles.sortedWith(StableFileOrder))
return generator.buildObjCHeader()
}
@@ -32,16 +43,15 @@ fun translateToObjCHeader(files: List<KtFile>): ObjCHeader {
*
* can be made.
*
* Functions inside this class will have side effects such as mutating the [symbolDeque] or adding results to the [objCStubs]
* Functions inside this class will have side effects such as mutating the [classDeque] or adding results to the [objCStubs]
*/
private class KtObjCExportHeaderGenerator {
/**
* Represents all elements that still have to be processed and translated.
* So far this only includes references to top level entities (such as files or classes):
* Note: Top level functions and properties will be translated as part of the file.
* See [translateToObjCTopLevelInterfaceFileFacades]
* Represents a queue containing pointers to all classes that are 'to be translated later'.
* This happens, e.g., when a class is referenced inside a callable signature. Such 'dependency types' are to be
* translated
*/
private val symbolDeque = ArrayDeque<QueueElement>()
private val classDeque = ArrayDeque<ClassId>()
/**
* The mutable aggregate of the already translated elements
@@ -68,57 +78,57 @@ private class KtObjCExportHeaderGenerator {
*/
private val objCClassForwardDeclarations = mutableSetOf<String>()
/**
* See [symbolDeque]:
* All top level 'to do' elements will be represented as [QueueElement] and later handled by the [translateAll] function.
*/
sealed class QueueElement {
class File(val psi: KtFile) : QueueElement()
class Class(val classId: ClassId) : QueueElement()
}
context(KtAnalysisSession, KtObjCExportSession)
fun translateAll(symbolProviders: List<QueueElement>) {
symbolDeque.addAll(symbolProviders)
fun translateAll(files: List<KtObjCExportFile>) {
/**
* Step 1: Translate classifiers (class, interface, object, ...)
*/
files.forEach { file ->
translateFileClassifiers(file)
}
/**
* Step 2: Translate file facades (see [translateToTopLevelFileFacade], [translateToExtensionFacade])
* This step has to be done after all classifiers were translated to match the translation order of K1
*/
files.forEach { file ->
translateFileFacades(file)
}
/**
* Step 3: Translate dependency classes referenced by Step 1 and Step 2
* Note: Transitive dependencies will still add to this queue and will be processed until we're finished
*/
while (true) {
val symbolProvider = symbolDeque.removeFirstOrNull() ?: break
translateElement(symbolProvider)
translateClass(classDeque.removeFirstOrNull() ?: break)
}
}
context(KtAnalysisSession, KtObjCExportSession)
private fun translateElement(element: QueueElement) = when (element) {
is QueueElement.Class -> translateClassElement(element)
is QueueElement.File -> translateFileElement(element)
}
context(KtAnalysisSession, KtObjCExportSession)
private fun translateClassElement(element: QueueElement.Class) {
val classOrObjectSymbol = getClassOrObjectSymbolByClassId(element.classId) ?: return
private fun translateClass(classId: ClassId) {
val classOrObjectSymbol = getClassOrObjectSymbolByClassId(classId) ?: return
translateClassOrObjectSymbol(classOrObjectSymbol)
}
context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileElement(element: QueueElement.File) {
val fileSymbol = element.psi.getFileSymbol()
fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol ->
private fun translateFileClassifiers(file: KtObjCExportFile) {
val resolvedFile = file.resolve()
resolvedFile.classifierSymbols.sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol ->
translateClassOrObjectSymbol(classOrObjectSymbol)
}
translateFileSymbol(fileSymbol)
}
context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileSymbol(symbol: KtFileSymbol) {
symbol.getExtensionFacades().let { extensionFacades ->
extensionFacades.forEach { facade ->
objCStubs += facade
enqueueDependencyClasses(facade)
objCClassForwardDeclarations += facade.name
}
private fun translateFileFacades(file: KtObjCExportFile) {
val resolvedFile = file.resolve()
resolvedFile.translateToObjCExtensionFacades().forEach { facade ->
objCStubs += facade
enqueueDependencyClasses(facade)
objCClassForwardDeclarations += facade.name
}
symbol.getTopLevelFacade()?.let { topLevelFacade ->
resolvedFile.translateToObjCTopLevelFacade()?.let { topLevelFacade ->
objCStubs += topLevelFacade
enqueueDependencyClasses(topLevelFacade)
}
@@ -146,15 +156,13 @@ private class KtObjCExportHeaderGenerator {
2) Super interface / superclass symbol export stubs (result of translation) have to be present in the stubs list before the
original stub
*/
symbol.getDeclaredSuperInterfaceSymbols()
.filter { it.isVisibleInObjC() }
.forEach { superInterfaceSymbol ->
translateClassOrObjectSymbol(superInterfaceSymbol)?.let {
objCProtocolForwardDeclarations += it.name
}
symbol.getDeclaredSuperInterfaceSymbols().filter { it.isVisibleInObjC() }.forEach { superInterfaceSymbol ->
translateClassOrObjectSymbol(superInterfaceSymbol)?.let {
objCProtocolForwardDeclarations += it.name
}
}
symbol.getSuperClassSymbolNotAny()?.let { superClassSymbol ->
symbol.getSuperClassSymbolNotAny()?.takeIf { it.isVisibleInObjC() }?.let { superClassSymbol ->
translateClassOrObjectSymbol(superClassSymbol)?.let {
objCClassForwardDeclarations += it.name
}
@@ -182,12 +190,10 @@ private class KtObjCExportHeaderGenerator {
* and `Array` has to be registered as forward declaration.
*/
private fun enqueueDependencyClasses(stub: ObjCExportStub) {
symbolDeque += stub.closureSequence()
classDeque += stub.closureSequence()
.flatMap { child -> child.throwsAnnotationClassIds.orEmpty() }
.map { QueueElement.Class(it) }
symbolDeque += stub.closureSequence()
classDeque += stub.closureSequence()
.mapNotNull { child ->
when (child) {
is ObjCMethod -> child.returnType
@@ -208,7 +214,6 @@ private class KtObjCExportHeaderGenerator {
if (nonNullType is ObjCProtocolType) objCProtocolForwardDeclarations += nonNullType.protocolName
}
.mapNotNull { it.originClassId }
.map(QueueElement::Class).toList()
}
/**
@@ -227,7 +232,6 @@ private class KtObjCExportHeaderGenerator {
return ObjCClassForwardDeclaration(className)
}
context(KtAnalysisSession, KtObjCExportSession)
fun buildObjCHeader(): ObjCHeader {
val hasErrorTypes = objCStubs.hasErrorTypes()
@@ -6,7 +6,6 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl
import org.jetbrains.kotlin.backend.konan.objcexport.toNameAttributes
@@ -43,18 +42,17 @@ import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrPr
*
* Where `FooKt` would be the "top level interface file facade" returned by this function.
*
* See related [getExtensionFacades]
* See related [translateToObjCExtensionFacades]
*/
context(KtAnalysisSession, KtObjCExportSession)
fun KtFileSymbol.getTopLevelFacade(): ObjCInterface? {
val extensions = getFileScope().getCallableSymbols()
fun KtResolvedObjCExportFile.translateToObjCTopLevelFacade(): ObjCInterface? {
val extensions = callableSymbols
.filter { !it.isExtension }
.toList()
.sortedWith(StableCallableOrder)
.ifEmpty { return null }
val fileName = getObjCFileClassOrProtocolName()
?: throw IllegalStateException("File '$this' cannot be translated without file name")
return ObjCInterfaceImpl(
name = fileName.objCName,
@@ -6,11 +6,10 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.psi.KtFile
internal val StableFileOrder: Comparator<KtFile>
get() = compareBy<KtFile> { file -> file.packageFqName.asString() }
.thenComparing { file -> file.name }
internal val StableFileOrder: Comparator<KtObjCExportFile>
get() = compareBy<KtObjCExportFile> { file -> file.packageFqName.asString() }
.thenComparing { file -> file.fileName }
internal val StablePropertyOrder: Comparator<KtPropertySymbol> = compareBy { it.name }
@@ -55,17 +54,6 @@ internal val StableCallableOrder: Comparator<KtCallableSymbol> = compareBy<KtCal
.thenComparing(StablePropertyOrder)
.thenComparing(StableFunctionOrder)
internal val StableSymbolOrder: Comparator<KtSymbol> = compareBy<KtSymbol> { symbol ->
when (symbol) {
is KtFileSymbol -> 0
is KtClassifierSymbol -> 1
is KtCallableSymbol -> 2
else -> Int.MAX_VALUE
}
}
.thenComparing(StableClassifierOrder)
.thenComparing(StableCallableOrder)
private inline fun <T, reified R> Comparator<T>.thenComparing(comparator: Comparator<R>): Comparator<T> where R : T {
return thenComparing { a, b ->
if (a is R && b is R) comparator.compare(a, b) else 0
@@ -41,7 +41,7 @@ object AnalysisApiHeaderGenerator : HeaderGenerator {
KtObjCExportConfiguration(
frameworkName = configuration.frameworkName,
generateBaseDeclarationStubs = configuration.generateBaseDeclarationStubs,
exportedModuleNames = setOf(defaultKotlinSourceModuleName)
exportedModuleNames = setOf(defaultKotlinSourceModuleName) + configuration.exportedDependencyModuleNames
)
) {
translateToObjCHeader(files.map { it as KtFile })
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.objcexport.tests
import org.jetbrains.kotlin.objcexport.KtObjCExportFile
import org.jetbrains.kotlin.objcexport.StableFileOrder
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.junit.jupiter.api.Test
@@ -23,8 +24,8 @@ class TranslationOrderTest(
sourceFile("C.kt", "package com.c")
}
val unsorted = listOf(files.getValue("C.kt"), files.getValue("B.kt"), files.getValue("A.kt"))
val sorted = listOf(files.getValue("A.kt"), files.getValue("B.kt"), files.getValue("C.kt"))
val unsorted = listOf(files.getValue("C.kt"), files.getValue("B.kt"), files.getValue("A.kt")).map(::KtObjCExportFile)
val sorted = listOf(files.getValue("A.kt"), files.getValue("B.kt"), files.getValue("C.kt")).map(::KtObjCExportFile)
assertNotEquals(sorted, unsorted)
assertEquals(sorted, unsorted.sortedWith(StableFileOrder))
@@ -38,8 +39,8 @@ class TranslationOrderTest(
sourceFile("B2.kt", "package com.b")
}
val unsorted = listOf(files.getValue("B2.kt"), files.getValue("A.kt"), files.getValue("B1.kt"))
val sorted = listOf(files.getValue("A.kt"), files.getValue("B1.kt"), files.getValue("B2.kt"))
val unsorted = listOf(files.getValue("B2.kt"), files.getValue("A.kt"), files.getValue("B1.kt")).map(::KtObjCExportFile)
val sorted = listOf(files.getValue("A.kt"), files.getValue("B1.kt"), files.getValue("B2.kt")).map(::KtObjCExportFile)
assertNotEquals(sorted, unsorted)
assertEquals(sorted, unsorted.sortedWith(StableFileOrder))