[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