diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt new file mode 100644 index 00000000000..ad6757505bf --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/isVisibleInObjC.kt @@ -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 +} diff --git a/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/IsVisibleInObjCTest.kt b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/IsVisibleInObjCTest.kt new file mode 100644 index 00000000000..71e8fecaf22 --- /dev/null +++ b/native/objcexport-header-generator/impl/analysis-api/test/org/jetbrains/kotlin/objcexport/tests/IsVisibleInObjCTest.kt @@ -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()) + } + } +} \ No newline at end of file