[ObjCExport] Implement isVisibleInObjC util

KT-64226
This commit is contained in:
Sebastian Sellmair
2023-12-14 13:12:17 +01:00
committed by Space Team
parent 66533141b8
commit 2f759243ba
2 changed files with 355 additions and 0 deletions
@@ -0,0 +1,171 @@
/*
* 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.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.annotationInfos
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind.*
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtAnnotatedSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtPossibleMultiplatformSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithVisibility
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.resolve.DataClassResolver
import org.jetbrains.kotlin.resolve.deprecation.DeprecationLevelValue
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
context(KtAnalysisSession)
internal fun KtCallableSymbol.isVisibleInObjC(): Boolean {
if (this is KtSymbolWithVisibility && !isPublicApi(this)) return false
if (this is KtPossibleMultiplatformSymbol && isExpect) return false
if (this.isHiddenFromObjCByDeprecation()) return false
if (this.isHiddenFromObjCByAnnotation()) return false
if (this.isSealedClassConstructor()) return false
if (this.isComponentNMethod() && this.getDirectlyOverriddenSymbols().isEmpty()) return false
return true
}
context(KtAnalysisSession)
internal fun KtClassOrObjectSymbol.isVisibleInObjC(): Boolean {
// TODO if(specialMapped()) return false
// TODO if(!defaultType.isObjCObjectType()) return false
if (this is KtSymbolWithVisibility && !isPublicApi(this)) return false
if (this.isHiddenFromObjCByDeprecation()) return false
if (this.isHiddenFromObjCByAnnotation()) return false
if (!this.classKind.isVisibleInObjC()) return false
if (this.isExpect) return false
if (this.isInlined()) return false
return true
}
/*
Private utility functions
*/
context(KtAnalysisSession)
private fun KtSymbol.isSealedClassConstructor(): Boolean {
if (this !is KtConstructorSymbol) return false
val containingSymbol = this.getContainingSymbol() as? KtSymbolWithModality ?: return false
return containingSymbol.modality == Modality.SEALED
}
context(KtAnalysisSession)
@OptIn(ExperimentalContracts::class)
private fun KtSymbol.isComponentNMethod(): Boolean {
contract {
returns(true) implies (this@isComponentNMethod is KtFunctionSymbol)
}
if (this !is KtFunctionSymbol) return false
if (!this.isOperator) return false
val containingClassSymbol = this.getContainingSymbol() as? KtNamedClassOrObjectSymbol ?: return false
if (!containingClassSymbol.isData) return false
return DataClassResolver.isComponentLike(this.name)
}
context(KtAnalysisSession)
private fun KtCallableSymbol.isHiddenFromObjCByAnnotation(): Boolean {
val overwrittenSymbols = getDirectlyOverriddenSymbols()
if (overwrittenSymbols.isNotEmpty()) return overwrittenSymbols.first().isHiddenFromObjCByAnnotation()
return this.containsHidesFromObjCAnnotation()
}
context(KtAnalysisSession)
private fun KtClassOrObjectSymbol.isHiddenFromObjCByAnnotation(): Boolean {
val containingSymbol = getContainingSymbol()
if (containingSymbol is KtClassOrObjectSymbol && containingSymbol.isHiddenFromObjCByAnnotation()) return true
return this.containsHidesFromObjCAnnotation()
}
/**
* Returns if [this] symbol is annotated with some annotation that effectively hides the symbol from ObjC.
*
* **Example: Using pre-defined 'HiddenFromObjC' annotation**
* ```kotlin
* @HiddenFromObjC
* fun foo() = Unit
* ```
*
* **Example: Using a custom 'internal api marker'**
* ```kotlin
* @HidesFromObjC
* annotation class MyInternalApi
*
* @MyInternalApi
* fun foo()
* ```
*
*/
context(KtAnalysisSession)
private fun KtAnnotatedSymbol.containsHidesFromObjCAnnotation(): Boolean {
return annotationsList.annotations.any { annotation ->
val annotationClassId = annotation.classId ?: return@any false
val annotationClassSymbol = getClassOrObjectSymbolByClassId(annotationClassId) ?: return@any false
annotationClassSymbol.annotationInfos.any { annotationAnnotation ->
annotationAnnotation.classId?.asSingleFqName() == KonanFqNames.hidesFromObjC
}
}
}
context(KtAnalysisSession)
private fun KtCallableSymbol.isHiddenFromObjCByDeprecation(): 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 (deprecationStatus?.deprecationLevel == DeprecationLevelValue.HIDDEN &&
getDirectlyOverriddenSymbols().all { overridden -> overridden.isHiddenFromObjCByDeprecation() }
) {
return true
}
val containingClassSymbol = getContainingSymbol() as? KtClassOrObjectSymbol
if (containingClassSymbol?.deprecationStatus?.deprecationLevel == DeprecationLevelValue.HIDDEN) {
return true
}
return false
}
context(KtAnalysisSession)
private fun KtClassOrObjectSymbol.isHiddenFromObjCByDeprecation(): Boolean {
if (this.deprecationStatus?.deprecationLevel == DeprecationLevelValue.HIDDEN) 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 = getSuperClassSymbolNotAny()
if (superClass != null && superClass.isHiddenFromObjCByDeprecation()) {
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 containingSymbol = getContainingSymbol()
if (containingSymbol is KtClassOrObjectSymbol && containingSymbol.isHiddenFromObjCByDeprecation()) {
return true
}
return false
}
context(KtAnalysisSession)
private fun KtClassOrObjectSymbol.isInlined(): Boolean {
if (this !is KtNamedClassOrObjectSymbol) return false
if (this.isInline) return true
// TODO: There are some native types that are 'implicitly inlined'
return false
}
private fun KtClassKind.isVisibleInObjC(): Boolean = when (this) {
CLASS, ENUM_CLASS, INTERFACE, OBJECT -> true
COMPANION_OBJECT, ANONYMOUS_OBJECT, ANNOTATION_CLASS -> false
}
@@ -0,0 +1,184 @@
/*
* 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.objcexport.analysisApiUtils.isVisibleInObjC
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.jetbrains.kotlin.objcexport.testUtils.getClassOrFail
import org.jetbrains.kotlin.objcexport.testUtils.getFunctionOrFail
import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class IsVisibleInObjCTest(
private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis,
) {
@Test
fun `test - simple public function`() {
val file = inlineSourceCodeAnalysis.createKtFile("fun foo() = Unit")
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertTrue(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - internal function`() {
val file = inlineSourceCodeAnalysis.createKtFile("internal fun foo() = Unit")
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - HiddenFromObjC function`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@kotlin.native.HiddenFromObjC
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - custom HidesFromObjC annotation function`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@kotlin.native.HidesFromObjC
annotation class MyInternalApi
@MyInternalApi
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - deprecation warning function`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@Deprecated(level = DeprecationLevel.WARNING)
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertTrue(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - deprecation hidden function`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@Deprecated(level = DeprecationLevel.HIDDEN)
fun foo() = Unit
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getFunctionOrFail("foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - public class`() {
val file = inlineSourceCodeAnalysis.createKtFile("class Foo")
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertTrue(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - internal class`() {
val file = inlineSourceCodeAnalysis.createKtFile("internal class Foo")
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - PublishedApi class`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@PublishedApi
internal class Foo
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertTrue(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - inline class`() {
val file = inlineSourceCodeAnalysis.createKtFile("inline class Foo(val x: Int)")
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - expect class`() {
val file = inlineSourceCodeAnalysis.createKtFile("expect class Foo")
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - deprecation hidden class`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@Deprecated(level = DeprecationLevel.HIDDEN)
class Foo
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
@Test
fun `test - custom HidesFromObjC annotation class`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@kotlin.native.HidesFromObjC
annotation class MyInternalApi
@MyInternalApi
class Foo
""".trimIndent()
)
analyze(file) {
val fooSymbol = file.getClassOrFail("Foo")
assertFalse(fooSymbol.isVisibleInObjC())
}
}
}