[ObjCExport] AA: Support additional module name prefixes

^KT-65670 Fixed
This commit is contained in:
Sebastian Sellmair
2024-02-26 16:15:03 +01:00
committed by Space Team
parent 7ee2903e15
commit 436e16efd8
14 changed files with 237 additions and 28 deletions
@@ -15,6 +15,7 @@ dependencies {
api(project(":compiler:psi"))
api(project(":native:objcexport-header-generator"))
implementation(project(":core:compiler.common.native"))
implementation(project(":kotlin-util-klib"))
testImplementation(projectTests(":native:objcexport-header-generator"))
testApi(project(":analysis:analysis-api-standalone"))
@@ -5,6 +5,9 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
data class KtObjCExportConfiguration(
/**
* Also used as top level prefix for declarations if present
@@ -28,4 +31,16 @@ data class KtObjCExportConfiguration(
* (see [org.jetbrains.kotlin.objcexport.objCBaseDeclarations]).
*/
val generateBaseDeclarationStubs: Boolean = true,
/**
* The name of modules that are to be exported in this session.
* An exported library shall be read, and its entire API surface shall be translated and presented in the
* objc header at the end.
*
* Libraries/Modules that are not listed in this [exportedModuleNames] will only export types that are used in either source code
* or other exported libraries public surface
* (see [KtLibraryModule.libraryName], [KtSourceModule.moduleName])
*/
val exportedModuleNames: Set<String> = emptySet(),
)
@@ -0,0 +1,89 @@
/*
* 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 org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.library.ToolingSingleFileKlibResolveStrategy
import org.jetbrains.kotlin.library.shortName
import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.util.DummyLogger
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
import org.jetbrains.kotlin.konan.file.File as KonanFile
interface KtObjCExportModuleNaming {
context(KtAnalysisSession)
fun getModuleName(module: KtModule): String?
companion object {
val default = KtObjCExportModuleNaming(listOf(KtKlibObjCExportModuleNaming, KtSimpleObjCExportModuleNaming))
}
}
context(KtAnalysisSession, KtObjCExportSession)
internal fun KtModule.getObjCKotlinModuleName(): String? {
return cached(GetObjCKotlinModuleNameCacheKey(this)) {
internal.moduleNaming.getModuleName(this)
}
}
private class GetObjCKotlinModuleNameCacheKey(private val module: KtModule) {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is GetObjCKotlinModuleNameCacheKey) return false
return this.module == other.module
}
override fun hashCode(): Int {
return module.hashCode()
}
}
/**
* Combines several [implementations] to a single [KtObjCExportModuleNaming].
* The order of [implementations] matters: The first implementation to resopnd with a module name will win.
*/
fun KtObjCExportModuleNaming(implementations: List<KtObjCExportModuleNaming>): KtObjCExportModuleNaming {
return KtCompositeObjCExportModuleNaming(implementations)
}
internal object KtKlibObjCExportModuleNaming : KtObjCExportModuleNaming {
context(KtAnalysisSession)
override fun getModuleName(module: KtModule): String? {
/*
In this implementation, we're actually looking into the klib file, trying to resolve
the contained manifest to get the 'shortName' or 'uniqueName'.
This information is theoretically available already (as also used by the Analysis Api), but not yet accessible.
*/
if (module !is KtLibraryModule) return null
val binaryRoot = module.getBinaryRoots().singleOrNull() ?: return null
if (!binaryRoot.isDirectory() && binaryRoot.extension != "klib") return null
val library = runCatching { ToolingSingleFileKlibResolveStrategy.tryResolve(KonanFile(binaryRoot), DummyLogger) }
.getOrElse { error -> error.printStackTrace(); return null } ?: return null
return library.shortName ?: library.uniqueName
}
}
internal object KtSimpleObjCExportModuleNaming : KtObjCExportModuleNaming {
context(KtAnalysisSession)
override fun getModuleName(module: KtModule): String? {
return when (module) {
is KtSourceModule -> module.stableModuleName ?: module.moduleName
is KtLibraryModule -> module.libraryName
else -> null
}
}
}
internal class KtCompositeObjCExportModuleNaming(private val implementations: List<KtObjCExportModuleNaming>) : KtObjCExportModuleNaming {
context(KtAnalysisSession) override fun getModuleName(module: KtModule): String? {
return implementations.firstNotNullOfOrNull { implementation -> implementation.getModuleName(module) }
}
}
@@ -12,25 +12,46 @@ sealed interface KtObjCExportSession {
val configuration: KtObjCExportConfiguration
}
/**
* Internal representation of [KtObjCExportSession].
* All *internal* accessible services shall be added here.
*/
internal sealed interface KtObjCExportSessionInternal : KtObjCExportSession {
val moduleNaming: KtObjCExportModuleNaming
}
internal val KtObjCExportSession.internal: KtObjCExportSessionInternal
get() = when (this) {
is KtObjCExportSessionInternal -> this
}
/**
* Private representation of [KtObjCExportSession].
* All *private* accessible data shall only be added here and potentially
* exposed as functions within this source file
*/
private interface KtObjCExportSessionPrivate : KtObjCExportSessionInternal {
val cache: MutableMap<Any, Any?>
}
private val KtObjCExportSession.private: KtObjCExportSessionPrivate
get() = when (this) {
is KtObjCExportSessionPrivate -> this
}
private interface KtObjCExportSessionPrivate : KtObjCExportSession {
val cache: MutableMap<Any, Any?>
}
inline fun <T> KtObjCExportSession(
configuration: KtObjCExportConfiguration,
moduleNaming: KtObjCExportModuleNaming = KtObjCExportModuleNaming.default,
block: KtObjCExportSession.() -> T,
): T {
return KtObjCExportSessionImpl(configuration, hashMapOf()).block()
return KtObjCExportSessionImpl(configuration, moduleNaming, hashMapOf()).block()
}
@PublishedApi
internal class KtObjCExportSessionImpl(
override val configuration: KtObjCExportConfiguration,
override val moduleNaming: KtObjCExportModuleNaming,
override val cache: MutableMap<Any, Any?>,
) : KtObjCExportSessionPrivate
@@ -6,10 +6,7 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.nameOrAnonymous
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportClassOrProtocolName
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
@@ -37,8 +34,11 @@ private fun KtClassLikeSymbol.getObjCName(
return containingClass.getObjCName() + objCName.capitalizeAsciiOnly()
}
// KT-65670: Append module specific prefixes?
return configuration.frameworkName.orEmpty() + objCName
return buildString {
configuration.frameworkName?.let(::append)
getObjCModuleNamePrefix()?.let(::append)
append(objCName)
}
}
context(KtAnalysisSession, KtObjCExportSession)
@@ -71,9 +71,10 @@ private fun KtClassLikeSymbol.getSwiftName(
}
}
// KT-65670: Append module specific prefixes?
return swiftName
return buildString {
getObjCModuleNamePrefix()?.let(::append)
append(swiftName)
}
}
context(KtAnalysisSession, KtObjCExportSession)
@@ -111,4 +112,28 @@ private fun KtClassLikeSymbol.canBeOuterSwift(): Boolean {
private fun mangleSwiftNestedClassName(name: String): String = when (name) {
"Type" -> "${name}_" // See https://github.com/JetBrains/kotlin-native/issues/3167
else -> name
}
context(KtAnalysisSession, KtObjCExportSession)
private fun KtSymbol.getObjCModuleNamePrefix(): String? {
val module = getContainingModule()
val moduleName = module.getObjCKotlinModuleName() ?: return null
if(moduleName == "stdlib" || moduleName == "kotlin-stdlib-common") return "Kotlin"
if (moduleName in configuration.exportedModuleNames) return null
return abbreviateModuleName(moduleName)
}
/**
* 'MyModuleName' -> 'MMN'
* 'someLibraryFoo' -> 'SLF'
*/
internal fun abbreviateModuleName(name: String): String {
val normalizedName = name
.capitalizeAsciiOnly()
.replace("[-.]".toRegex(), "_")
val uppers = normalizedName.filter { character -> character.isUpperCase() }
if (uppers.length >= 3) return uppers
return normalizedName
}
@@ -9,6 +9,7 @@ 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
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
@@ -59,7 +60,7 @@ fun KtFileSymbol.getTopLevelFacade(): ObjCInterface? {
name = fileName.objCName,
comment = null,
origin = null,
attributes = listOf(OBJC_SUBCLASSING_RESTRICTED),
attributes = listOf(OBJC_SUBCLASSING_RESTRICTED) + fileName.toNameAttributes(),
superProtocols = emptyList(),
members = extensions.mapNotNull { it.translateToObjCExportStub() },
categoryName = null,
@@ -30,6 +30,7 @@ class AnalysisApiHeaderGeneratorExtension : ParameterResolver {
object AnalysisApiHeaderGenerator : HeaderGenerator {
override fun generateHeaders(root: File, configuration: HeaderGenerator.Configuration): ObjCHeader {
val session = createStandaloneAnalysisApiSession(
kotlinSourceModuleName = defaultKotlinSourceModuleName,
kotlinFiles = root.listFiles().orEmpty().filter { it.extension == "kt" },
dependencyKlibs = configuration.dependencies
)
@@ -39,7 +40,8 @@ object AnalysisApiHeaderGenerator : HeaderGenerator {
KtObjCExportSession(
KtObjCExportConfiguration(
frameworkName = configuration.frameworkName,
generateBaseDeclarationStubs = configuration.generateBaseDeclarationStubs
generateBaseDeclarationStubs = configuration.generateBaseDeclarationStubs,
exportedModuleNames = setOf(defaultKotlinSourceModuleName)
)
) {
translateToObjCHeader(files.map { it as KtFile })
@@ -77,14 +77,14 @@ class InlineSourceCodeAnalysisExtension : ParameterResolver, AfterEachCallback {
*/
private class InlineSourceCodeAnalysisImpl(private val tempDir: File) : InlineSourceCodeAnalysis {
override fun createKtFile(@Language("kotlin") sourceCode: String): KtFile {
return createStandaloneAnalysisApiSession(tempDir, mapOf("TestSources.kt" to sourceCode))
return createStandaloneAnalysisApiSession(tempDir = tempDir, kotlinSources = mapOf("TestSources.kt" to sourceCode))
.modulesWithFiles.entries.single()
.value.single() as KtFile
}
override fun createKtFiles(builder: InlineSourceCodeAnalysis.KtModuleBuilder.() -> Unit): Map<String, KtFile> {
val sources = KtModuleBuilderImpl().also(builder).sources.toMap()
return createStandaloneAnalysisApiSession(tempDir, sources)
return createStandaloneAnalysisApiSession(tempDir = tempDir, kotlinSources = sources)
.modulesWithFiles.entries.single()
.value.map { it as KtFile }
.associateBy { it.name }
@@ -13,7 +13,9 @@ import org.jetbrains.kotlin.psi.KtElement
inline fun <T> analyzeWithObjCExport(
useSiteKtElement: KtElement,
configuration: KtObjCExportConfiguration = KtObjCExportConfiguration(),
configuration: KtObjCExportConfiguration = KtObjCExportConfiguration(
exportedModuleNames = setOf(defaultKotlinSourceModuleName)
),
action: context(KtAnalysisSession, KtObjCExportSession) () -> T,
): T = analyze(useSiteKtElement) {
KtObjCExportSession(configuration) {
@@ -20,11 +20,14 @@ import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.nameWithoutExtension
const val defaultKotlinSourceModuleName = "testModule"
/**
* Creates a standalone analysis session from Kotlin source code passed as [kotlinSources]
*/
fun createStandaloneAnalysisApiSession(
tempDir: File,
kotlinSourceModuleName: String = defaultKotlinSourceModuleName,
kotlinSources: Map</* File Name */ String, /* Source Code */ String>,
dependencyKlibs: List<Path> = emptyList(),
): StandaloneAnalysisAPISession {
@@ -36,14 +39,18 @@ fun createStandaloneAnalysisApiSession(
writeText(sourceCode)
}
}
return createStandaloneAnalysisApiSession(listOf(testModuleRoot), dependencyKlibs)
return createStandaloneAnalysisApiSession(kotlinSourceModuleName, listOf(testModuleRoot), dependencyKlibs)
}
/**
* Creates a standalone analysis session from [kotlinFiles] on disk.
* The Kotlin/Native stdlib will be provided as dependency
*/
fun createStandaloneAnalysisApiSession(kotlinFiles: List<File>, dependencyKlibs: List<Path> = emptyList()): StandaloneAnalysisAPISession {
fun createStandaloneAnalysisApiSession(
kotlinSourceModuleName: String = defaultKotlinSourceModuleName,
kotlinFiles: List<File>,
dependencyKlibs: List<Path> = emptyList(),
): StandaloneAnalysisAPISession {
val currentArchitectureTarget = HostManager.host
val nativePlatform = NativePlatforms.nativePlatformByTargets(listOf(currentArchitectureTarget))
return buildStandaloneAnalysisAPISession {
@@ -77,7 +84,7 @@ fun createStandaloneAnalysisApiSession(kotlinFiles: List<File>, dependencyKlibs:
addRegularDependency(dependencyKlibModule)
}
platform = nativePlatform
moduleName = "source"
moduleName = kotlinSourceModuleName
}
)
}
@@ -0,0 +1,42 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.objcexport.tests
import org.jetbrains.kotlin.objcexport.abbreviateModuleName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class AbbreviateModuleNameTest {
@Test
fun `test - empty string`() {
assertEquals("", abbreviateModuleName(""))
}
@Test
fun `test - simple name`() {
assertEquals("Foo", abbreviateModuleName("Foo"))
}
@Test
fun `test - simple lowercase name`() {
assertEquals("Foo", abbreviateModuleName("foo"))
}
@Test
fun `test - longer module name`() {
assertEquals("LMN", abbreviateModuleName("LongModuleName"))
}
@Test
fun `test - longer module name - starting lowercase`() {
assertEquals("LMN", abbreviateModuleName("longModuleName"))
}
@Test
fun `test - very long module name`() {
assertEquals("TIAVLMN", abbreviateModuleName("thisIsAVeryLongModuleName"))
}
}
@@ -6,6 +6,7 @@ import org.jetbrains.kotlin.backend.konan.objcexport.ObjCHeader
import org.jetbrains.kotlin.objcexport.KtObjCExportConfiguration
import org.jetbrains.kotlin.objcexport.KtObjCExportSession
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.jetbrains.kotlin.objcexport.testUtils.defaultKotlinSourceModuleName
import org.jetbrains.kotlin.objcexport.translateToObjCHeader
import org.jetbrains.kotlin.psi.KtFile
import org.junit.jupiter.api.Test
@@ -24,7 +25,7 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """
val i: Iterator<Int>
""",
protocols = setOf("Iterator"),
protocols = setOf("KotlinIterator"),
classes = emptySet()
)
}
@@ -35,8 +36,8 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """
val i: Array<Int>
""",
protocols = setOf("Iterator"),
classes = setOf("Array")
protocols = setOf("KotlinIterator"),
classes = setOf("KotlinArray")
)
}
@@ -46,8 +47,8 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """
val i: StringBuilder
""",
protocols = setOf("CharSequence", "Appendable", "Iterator"),
classes = setOf("StringBuilder", "CharArray", "CharIterator")
protocols = setOf("KotlinCharSequence", "KotlinAppendable", "KotlinIterator"),
classes = setOf("KotlinStringBuilder", "KotlinCharArray", "KotlinCharIterator")
)
}
@@ -81,7 +82,7 @@ class ForwardedClassesAndProtocolsDependenciesTest(
private fun translateClassesAndProtocols(file: KtFile): ObjCHeader {
return analyze(file) {
KtObjCExportSession(KtObjCExportConfiguration()) {
KtObjCExportSession(KtObjCExportConfiguration(exportedModuleNames = setOf(defaultKotlinSourceModuleName))) {
translateToObjCHeader(listOf(file))
}
}
@@ -83,6 +83,10 @@ fun ObjCExportClassOrProtocolName.toNameAttributes(): List<String> = listOfNotNu
swiftName.takeIf { it != objCName }?.let { swiftNameAttribute(it) }
)
fun ObjCExportFileName.toNameAttributes(): List<String> = listOfNotNull(
swiftName.takeIf { it != objCName }?.let { swiftNameAttribute(it) }
)
@InternalKotlinNativeApi
fun swiftNameAttribute(swiftName: String) = "swift_name(\"$swiftName\")"
@@ -148,7 +148,6 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
}
@Test
@TodoAnalysisApi
fun `test - functionWithErrorTypeAndFrameworkName`() {
doTest(headersTestDataDir.resolve("functionWithErrorTypeAndFrameworkName"), Configuration(frameworkName = "shared"))
}