[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(":compiler:psi"))
api(project(":native:objcexport-header-generator")) api(project(":native:objcexport-header-generator"))
implementation(project(":core:compiler.common.native")) implementation(project(":core:compiler.common.native"))
implementation(project(":kotlin-util-klib"))
testImplementation(projectTests(":native:objcexport-header-generator")) testImplementation(projectTests(":native:objcexport-header-generator"))
testApi(project(":analysis:analysis-api-standalone")) testApi(project(":analysis:analysis-api-standalone"))
@@ -5,6 +5,9 @@
package org.jetbrains.kotlin.objcexport package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
data class KtObjCExportConfiguration( data class KtObjCExportConfiguration(
/** /**
* Also used as top level prefix for declarations if present * Also used as top level prefix for declarations if present
@@ -28,4 +31,16 @@ data class KtObjCExportConfiguration(
* (see [org.jetbrains.kotlin.objcexport.objCBaseDeclarations]). * (see [org.jetbrains.kotlin.objcexport.objCBaseDeclarations]).
*/ */
val generateBaseDeclarationStubs: Boolean = true, 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 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 private val KtObjCExportSession.private: KtObjCExportSessionPrivate
get() = when (this) { get() = when (this) {
is KtObjCExportSessionPrivate -> this is KtObjCExportSessionPrivate -> this
} }
private interface KtObjCExportSessionPrivate : KtObjCExportSession {
val cache: MutableMap<Any, Any?>
}
inline fun <T> KtObjCExportSession( inline fun <T> KtObjCExportSession(
configuration: KtObjCExportConfiguration, configuration: KtObjCExportConfiguration,
moduleNaming: KtObjCExportModuleNaming = KtObjCExportModuleNaming.default,
block: KtObjCExportSession.() -> T, block: KtObjCExportSession.() -> T,
): T { ): T {
return KtObjCExportSessionImpl(configuration, hashMapOf()).block() return KtObjCExportSessionImpl(configuration, moduleNaming, hashMapOf()).block()
} }
@PublishedApi @PublishedApi
internal class KtObjCExportSessionImpl( internal class KtObjCExportSessionImpl(
override val configuration: KtObjCExportConfiguration, override val configuration: KtObjCExportConfiguration,
override val moduleNaming: KtObjCExportModuleNaming,
override val cache: MutableMap<Any, Any?>, override val cache: MutableMap<Any, Any?>,
) : KtObjCExportSessionPrivate ) : KtObjCExportSessionPrivate
@@ -6,10 +6,7 @@
package org.jetbrains.kotlin.objcexport package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind import org.jetbrains.kotlin.analysis.api.symbols.*
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.backend.konan.objcexport.ObjCExportClassOrProtocolName import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportClassOrProtocolName
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
@@ -37,8 +34,11 @@ private fun KtClassLikeSymbol.getObjCName(
return containingClass.getObjCName() + objCName.capitalizeAsciiOnly() return containingClass.getObjCName() + objCName.capitalizeAsciiOnly()
} }
// KT-65670: Append module specific prefixes? return buildString {
return configuration.frameworkName.orEmpty() + objCName configuration.frameworkName?.let(::append)
getObjCModuleNamePrefix()?.let(::append)
append(objCName)
}
} }
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
@@ -71,9 +71,10 @@ private fun KtClassLikeSymbol.getSwiftName(
} }
} }
// KT-65670: Append module specific prefixes? return buildString {
return swiftName getObjCModuleNamePrefix()?.let(::append)
append(swiftName)
}
} }
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
@@ -111,4 +112,28 @@ private fun KtClassLikeSymbol.canBeOuterSwift(): Boolean {
private fun mangleSwiftNestedClassName(name: String): String = when (name) { private fun mangleSwiftNestedClassName(name: String): String = when (name) {
"Type" -> "${name}_" // See https://github.com/JetBrains/kotlin-native/issues/3167 "Type" -> "${name}_" // See https://github.com/JetBrains/kotlin-native/issues/3167
else -> name 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.analysis.api.symbols.KtFileSymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterface
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl import org.jetbrains.kotlin.backend.konan.objcexport.ObjCInterfaceImpl
import org.jetbrains.kotlin.backend.konan.objcexport.toNameAttributes
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
@@ -59,7 +60,7 @@ fun KtFileSymbol.getTopLevelFacade(): ObjCInterface? {
name = fileName.objCName, name = fileName.objCName,
comment = null, comment = null,
origin = null, origin = null,
attributes = listOf(OBJC_SUBCLASSING_RESTRICTED), attributes = listOf(OBJC_SUBCLASSING_RESTRICTED) + fileName.toNameAttributes(),
superProtocols = emptyList(), superProtocols = emptyList(),
members = extensions.mapNotNull { it.translateToObjCExportStub() }, members = extensions.mapNotNull { it.translateToObjCExportStub() },
categoryName = null, categoryName = null,
@@ -30,6 +30,7 @@ class AnalysisApiHeaderGeneratorExtension : ParameterResolver {
object AnalysisApiHeaderGenerator : HeaderGenerator { object AnalysisApiHeaderGenerator : HeaderGenerator {
override fun generateHeaders(root: File, configuration: HeaderGenerator.Configuration): ObjCHeader { override fun generateHeaders(root: File, configuration: HeaderGenerator.Configuration): ObjCHeader {
val session = createStandaloneAnalysisApiSession( val session = createStandaloneAnalysisApiSession(
kotlinSourceModuleName = defaultKotlinSourceModuleName,
kotlinFiles = root.listFiles().orEmpty().filter { it.extension == "kt" }, kotlinFiles = root.listFiles().orEmpty().filter { it.extension == "kt" },
dependencyKlibs = configuration.dependencies dependencyKlibs = configuration.dependencies
) )
@@ -39,7 +40,8 @@ object AnalysisApiHeaderGenerator : HeaderGenerator {
KtObjCExportSession( KtObjCExportSession(
KtObjCExportConfiguration( KtObjCExportConfiguration(
frameworkName = configuration.frameworkName, frameworkName = configuration.frameworkName,
generateBaseDeclarationStubs = configuration.generateBaseDeclarationStubs generateBaseDeclarationStubs = configuration.generateBaseDeclarationStubs,
exportedModuleNames = setOf(defaultKotlinSourceModuleName)
) )
) { ) {
translateToObjCHeader(files.map { it as KtFile }) translateToObjCHeader(files.map { it as KtFile })
@@ -77,14 +77,14 @@ class InlineSourceCodeAnalysisExtension : ParameterResolver, AfterEachCallback {
*/ */
private class InlineSourceCodeAnalysisImpl(private val tempDir: File) : InlineSourceCodeAnalysis { private class InlineSourceCodeAnalysisImpl(private val tempDir: File) : InlineSourceCodeAnalysis {
override fun createKtFile(@Language("kotlin") sourceCode: String): KtFile { 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() .modulesWithFiles.entries.single()
.value.single() as KtFile .value.single() as KtFile
} }
override fun createKtFiles(builder: InlineSourceCodeAnalysis.KtModuleBuilder.() -> Unit): Map<String, KtFile> { override fun createKtFiles(builder: InlineSourceCodeAnalysis.KtModuleBuilder.() -> Unit): Map<String, KtFile> {
val sources = KtModuleBuilderImpl().also(builder).sources.toMap() val sources = KtModuleBuilderImpl().also(builder).sources.toMap()
return createStandaloneAnalysisApiSession(tempDir, sources) return createStandaloneAnalysisApiSession(tempDir = tempDir, kotlinSources = sources)
.modulesWithFiles.entries.single() .modulesWithFiles.entries.single()
.value.map { it as KtFile } .value.map { it as KtFile }
.associateBy { it.name } .associateBy { it.name }
@@ -13,7 +13,9 @@ import org.jetbrains.kotlin.psi.KtElement
inline fun <T> analyzeWithObjCExport( inline fun <T> analyzeWithObjCExport(
useSiteKtElement: KtElement, useSiteKtElement: KtElement,
configuration: KtObjCExportConfiguration = KtObjCExportConfiguration(), configuration: KtObjCExportConfiguration = KtObjCExportConfiguration(
exportedModuleNames = setOf(defaultKotlinSourceModuleName)
),
action: context(KtAnalysisSession, KtObjCExportSession) () -> T, action: context(KtAnalysisSession, KtObjCExportSession) () -> T,
): T = analyze(useSiteKtElement) { ): T = analyze(useSiteKtElement) {
KtObjCExportSession(configuration) { KtObjCExportSession(configuration) {
@@ -20,11 +20,14 @@ import java.nio.file.Path
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.nameWithoutExtension import kotlin.io.path.nameWithoutExtension
const val defaultKotlinSourceModuleName = "testModule"
/** /**
* Creates a standalone analysis session from Kotlin source code passed as [kotlinSources] * Creates a standalone analysis session from Kotlin source code passed as [kotlinSources]
*/ */
fun createStandaloneAnalysisApiSession( fun createStandaloneAnalysisApiSession(
tempDir: File, tempDir: File,
kotlinSourceModuleName: String = defaultKotlinSourceModuleName,
kotlinSources: Map</* File Name */ String, /* Source Code */ String>, kotlinSources: Map</* File Name */ String, /* Source Code */ String>,
dependencyKlibs: List<Path> = emptyList(), dependencyKlibs: List<Path> = emptyList(),
): StandaloneAnalysisAPISession { ): StandaloneAnalysisAPISession {
@@ -36,14 +39,18 @@ fun createStandaloneAnalysisApiSession(
writeText(sourceCode) writeText(sourceCode)
} }
} }
return createStandaloneAnalysisApiSession(listOf(testModuleRoot), dependencyKlibs) return createStandaloneAnalysisApiSession(kotlinSourceModuleName, listOf(testModuleRoot), dependencyKlibs)
} }
/** /**
* Creates a standalone analysis session from [kotlinFiles] on disk. * Creates a standalone analysis session from [kotlinFiles] on disk.
* The Kotlin/Native stdlib will be provided as dependency * 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 currentArchitectureTarget = HostManager.host
val nativePlatform = NativePlatforms.nativePlatformByTargets(listOf(currentArchitectureTarget)) val nativePlatform = NativePlatforms.nativePlatformByTargets(listOf(currentArchitectureTarget))
return buildStandaloneAnalysisAPISession { return buildStandaloneAnalysisAPISession {
@@ -77,7 +84,7 @@ fun createStandaloneAnalysisApiSession(kotlinFiles: List<File>, dependencyKlibs:
addRegularDependency(dependencyKlibModule) addRegularDependency(dependencyKlibModule)
} }
platform = nativePlatform 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.KtObjCExportConfiguration
import org.jetbrains.kotlin.objcexport.KtObjCExportSession import org.jetbrains.kotlin.objcexport.KtObjCExportSession
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.jetbrains.kotlin.objcexport.testUtils.defaultKotlinSourceModuleName
import org.jetbrains.kotlin.objcexport.translateToObjCHeader import org.jetbrains.kotlin.objcexport.translateToObjCHeader
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -24,7 +25,7 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """ code = """
val i: Iterator<Int> val i: Iterator<Int>
""", """,
protocols = setOf("Iterator"), protocols = setOf("KotlinIterator"),
classes = emptySet() classes = emptySet()
) )
} }
@@ -35,8 +36,8 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """ code = """
val i: Array<Int> val i: Array<Int>
""", """,
protocols = setOf("Iterator"), protocols = setOf("KotlinIterator"),
classes = setOf("Array") classes = setOf("KotlinArray")
) )
} }
@@ -46,8 +47,8 @@ class ForwardedClassesAndProtocolsDependenciesTest(
code = """ code = """
val i: StringBuilder val i: StringBuilder
""", """,
protocols = setOf("CharSequence", "Appendable", "Iterator"), protocols = setOf("KotlinCharSequence", "KotlinAppendable", "KotlinIterator"),
classes = setOf("StringBuilder", "CharArray", "CharIterator") classes = setOf("KotlinStringBuilder", "KotlinCharArray", "KotlinCharIterator")
) )
} }
@@ -81,7 +82,7 @@ class ForwardedClassesAndProtocolsDependenciesTest(
private fun translateClassesAndProtocols(file: KtFile): ObjCHeader { private fun translateClassesAndProtocols(file: KtFile): ObjCHeader {
return analyze(file) { return analyze(file) {
KtObjCExportSession(KtObjCExportConfiguration()) { KtObjCExportSession(KtObjCExportConfiguration(exportedModuleNames = setOf(defaultKotlinSourceModuleName))) {
translateToObjCHeader(listOf(file)) translateToObjCHeader(listOf(file))
} }
} }
@@ -83,6 +83,10 @@ fun ObjCExportClassOrProtocolName.toNameAttributes(): List<String> = listOfNotNu
swiftName.takeIf { it != objCName }?.let { swiftNameAttribute(it) } swiftName.takeIf { it != objCName }?.let { swiftNameAttribute(it) }
) )
fun ObjCExportFileName.toNameAttributes(): List<String> = listOfNotNull(
swiftName.takeIf { it != objCName }?.let { swiftNameAttribute(it) }
)
@InternalKotlinNativeApi @InternalKotlinNativeApi
fun swiftNameAttribute(swiftName: String) = "swift_name(\"$swiftName\")" fun swiftNameAttribute(swiftName: String) = "swift_name(\"$swiftName\")"
@@ -148,7 +148,6 @@ class ObjCExportHeaderGeneratorTest(private val generator: HeaderGenerator) {
} }
@Test @Test
@TodoAnalysisApi
fun `test - functionWithErrorTypeAndFrameworkName`() { fun `test - functionWithErrorTypeAndFrameworkName`() {
doTest(headersTestDataDir.resolve("functionWithErrorTypeAndFrameworkName"), Configuration(frameworkName = "shared")) doTest(headersTestDataDir.resolve("functionWithErrorTypeAndFrameworkName"), Configuration(frameworkName = "shared"))
} }