[ObjCExport] Analysis Api: Implement initial support for exporting 'MustBeDocumented' annotations

^KT-64869 Fixed
This commit is contained in:
Sebastian Sellmair
2024-01-11 08:47:27 +01:00
committed by Space Team
parent cdcb3e4dcd
commit 80d9933543
10 changed files with 318 additions and 125 deletions
@@ -0,0 +1,46 @@
/*
* 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.analysisApiUtils
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationApplicationWithArgumentsInfo
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationsList
import org.jetbrains.kotlin.analysis.api.annotations.annotationClassIds
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtAnnotatedSymbol
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.name.StandardClassIds
/**
* Returns the list of annotations which shall be documented for ObjC export.
* Usually annotations that are marked with `@MustBeDocumented` are important and shall be retained for users.
* However, some annotations e.g. [kotlin.native.ObjCName], whilst annotated with `@MustBeDocumented` shall not be exported for ObjC
* documentation.
*/
context(KtAnalysisSession)
internal fun KtAnnotatedSymbol.getObjCDocumentedAnnotations(): List<KtAnnotationApplicationWithArgumentsInfo> {
return annotationsList.getObjCDocumentedAnnotations()
}
/**
* See [getObjCDocumentedAnnotations]
*/
context(KtAnalysisSession)
internal fun KtAnnotationsList.getObjCDocumentedAnnotations(): List<KtAnnotationApplicationWithArgumentsInfo> {
return annotations
.filter { annotation ->
val annotationClassId = annotation.classId ?: return@filter false
if (annotationClassId.asSingleFqName() in mustBeDocumentedAnnotationsStopList) return@filter false
val annotationClassSymbol = getClassOrObjectSymbolByClassId(annotationClassId) ?: return@filter false
StandardClassIds.Annotations.MustBeDocumented in annotationClassSymbol.annotationClassIds
}
}
private val mustBeDocumentedAnnotationsStopList = setOf(
StandardNames.FqNames.deprecated,
KonanFqNames.objCName,
KonanFqNames.shouldRefineInSwift
)
@@ -1,13 +1,10 @@
package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationsList
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getDefaultSuperClassOrProtocolName
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getSuperClassSymbolNotAny
@@ -27,7 +24,7 @@ fun KtClassOrObjectSymbol.translateToObjCClass(): ObjCClass? {
val attributes = if (enumKind || final) listOf(OBJC_SUBCLASSING_RESTRICTED) else emptyList()
val name = getObjCClassOrProtocolName()
val comment: ObjCComment? = annotationsList.toComment()
val comment: ObjCComment? = annotationsList.translateToObjCComment()
val origin: ObjCExportStubOrigin = getObjCStubOrigin()
val superProtocols: List<String> = superProtocols()
val members: List<ObjCExportStub> = members().flatMap { it.translateToObjCExportStubs() }
@@ -59,16 +56,3 @@ private fun abbreviate(name: String): String {
return normalizedName
}
/**
* Not implemented
*/
private fun KtAnnotationsList.toComment(): ObjCComment? {
return null
}
private val mustBeDocumentedAnnotationsStopList = setOf(
StandardNames.FqNames.deprecated,
KonanFqNames.objCName,
KonanFqNames.shouldRefineInSwift
)
@@ -0,0 +1,122 @@
/*
* 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.api.annotations.KtAnnotationApplicationWithArgumentsInfo
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationsList
import org.jetbrains.kotlin.analysis.api.annotations.KtNamedAnnotationValue
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithVisibility
import org.jetbrains.kotlin.backend.konan.objcexport.MethodBridge
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCComment
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCParameter
import org.jetbrains.kotlin.backend.konan.objcexport.plus
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getObjCDocumentedAnnotations
context(KtAnalysisSession)
internal fun KtAnnotationsList.translateToObjCComment(): ObjCComment? {
val annotations = getObjCDocumentedAnnotations()
.mapNotNull { annotation -> renderAnnotation(annotation) }
if (annotations.isEmpty()) return null
return ObjCComment(listOf("@note annotations") + annotations.map { " $it" })
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.buildComment]
*/
context(KtAnalysisSession)
internal fun KtFunctionLikeSymbol.translateToObjCComment(bridge: MethodBridge, parameters: List<ObjCParameter>): ObjCComment? {
val isSuspend: Boolean = if (this is KtFunctionSymbol) this.isSuspend else false
val throwsComments = if (isSuspend || bridge.returnsError) {
val effectiveThrows = getEffectiveThrows(this).toSet()
when {
effectiveThrows.contains(StandardClassIds.Throwable) -> {
listOf("@note This method converts all Kotlin exceptions to errors.")
}
effectiveThrows.isNotEmpty() -> {
listOf(
buildString {
append("@note This method converts instances of ")
effectiveThrows.joinTo(this) { it.relativeClassName.asString() }
append(" to errors.")
},
"Other uncaught Kotlin exceptions are fatal."
)
}
else -> {
// Shouldn't happen though.
listOf("@warning All uncaught Kotlin exceptions are fatal.")
}
}
} else emptyList()
val visibilityComments = buildObjCVisibilityComment("method")
val paramComments = valueParameters.mapNotNull { parameterSymbol ->
parameters.find { parameter -> parameter.origin?.name == parameterSymbol.name }
?.renderedObjCDocumentedParamAnnotations(parameterSymbol)
}
val annotationsComments = annotationsList.translateToObjCComment()
return annotationsComments + paramComments + throwsComments + visibilityComments
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mustBeDocumentedParamAttributeList]
*/
context(KtAnalysisSession)
private fun ObjCParameter.renderedObjCDocumentedParamAnnotations(parameterSymbol: KtValueParameterSymbol): String? {
val renderedAnnotationsString = parameterSymbol.getObjCDocumentedAnnotations()
.mapNotNull { annotation -> renderAnnotation(annotation) }
.ifEmpty { return null }
.joinToString(" ")
return "@param $name annotations $renderedAnnotationsString"
}
private fun renderAnnotation(annotation: KtAnnotationApplicationWithArgumentsInfo): String? {
return renderAnnotation(annotation.classId ?: return null, annotation.arguments)
}
private fun renderAnnotation(clazz: ClassId, arguments: List<KtNamedAnnotationValue>): String {
return buildString {
append(clazz.asSingleFqName())
if (arguments.isNotEmpty()) {
append('(')
arguments.joinTo(this)
append(')')
}
}
}
/**
* Not implemented [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.getEffectiveThrows]
*/
@Suppress("UNUSED_PARAMETER")
private fun getEffectiveThrows(method: KtFunctionLikeSymbol): Sequence<ClassId> {
return emptySequence()
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.visibilityComments]
*/
private fun KtSymbol.buildObjCVisibilityComment(kind: String): ObjCComment? {
return when ((this as? KtSymbolWithVisibility)?.visibility ?: return null) {
Visibilities.Protected -> ObjCComment("@note This $kind has protected visibility in Kotlin source and is intended only for use by subclasses.")
else -> null
}
}
@@ -4,18 +4,12 @@ package org.jetbrains.kotlin.objcexport
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotationApplicationWithArgumentsInfo
import org.jetbrains.kotlin.analysis.api.annotations.annotationInfos
import org.jetbrains.kotlin.analysis.api.annotations.annotations
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.analysis.api.types.KtType
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
import org.jetbrains.kotlin.backend.konan.KonanFqNames
import org.jetbrains.kotlin.backend.konan.objcexport.*
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.objcexport.Predefined.anyMethodSelectors
@@ -62,7 +56,7 @@ internal fun KtFunctionLikeSymbol.buildObjCMethod(
val swiftName = getSwiftName(bridge)
val attributes = mutableListOf<String>()
val returnBridge = bridge.returnBridge
val comment = buildComment(this, bridge, parameters)
val comment = this.translateToObjCComment(bridge, parameters)
attributes += getSwiftPrivateAttribute() ?: swiftNameAttribute(swiftName)
@@ -109,105 +103,6 @@ internal fun String.toValidObjCSwiftIdentifier(): String {
.let { if (it == "_") "__" else it }
}
private val throwableClassId = ClassId.topLevel(StandardNames.FqNames.throwable)
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.buildComment]
*/
private fun buildComment(method: KtFunctionLikeSymbol, bridge: MethodBridge, parameters: List<ObjCParameter>): ObjCComment? {
val visibility: Visibility
val isSuspend: Boolean
if (method is KtFunctionSymbol) {
isSuspend = method.isSuspend
visibility = method.visibility
} else if (method is KtConstructorSymbol) {
isSuspend = false
visibility = Visibilities.Public //check constructor
} else {
TODO("Unsupported type for comment building: $method")
}
val throwsComments = if (isSuspend || bridge.returnsError) {
val effectiveThrows = getEffectiveThrows(method).toSet()
when {
effectiveThrows.contains(throwableClassId) -> {
listOf("@note This method converts all Kotlin exceptions to errors.")
}
effectiveThrows.isNotEmpty() -> {
listOf(
buildString {
append("@note This method converts instances of ")
effectiveThrows.joinTo(this) { it.relativeClassName.asString() }
append(" to errors.")
},
"Other uncaught Kotlin exceptions are fatal."
)
}
else -> {
// Shouldn't happen though.
listOf("@warning All uncaught Kotlin exceptions are fatal.")
}
}
} else emptyList()
val visibilityComments = visibilityComments(visibility, "method")
val paramComments = method.valueParameters.flatMap { parameterDescriptor ->
parameters.find { parameter -> parameter.origin?.name == parameterDescriptor.name }?.let { parameter ->
mustBeDocumentedParamAttributeList(parameter, descriptor = parameterDescriptor)
} ?: emptyList()
}
val annotationsComments = mustBeDocumentedAttributeList(method.annotations)
val commentLines = annotationsComments + paramComments + throwsComments + visibilityComments
return if (commentLines.isNotEmpty()) ObjCComment(commentLines) else null
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mustBeDocumentedParamAttributeList]
*/
private fun mustBeDocumentedParamAttributeList(parameter: ObjCParameter, descriptor: KtValueParameterSymbol): List<String> {
descriptor.annotationsList.annotations
val mbdAnnotations = mustBeDocumentedAnnotations(descriptor.annotations).joinToString(" ")
return if (mbdAnnotations.isNotEmpty()) listOf("@param ${parameter.name} annotations $mbdAnnotations") else emptyList()
}
/**
* Not implemented [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.mustBeDocumentedAnnotations]
*/
private fun mustBeDocumentedAnnotations(annotations: List<KtAnnotationApplicationWithArgumentsInfo>): List<String> {
return emptyList()
}
private fun mustBeDocumentedAttributeList(annotations: List<KtAnnotationApplicationWithArgumentsInfo>): List<String> {
val mustBeDocumentedAnnotations = mustBeDocumentedAnnotations(annotations)
return if (mustBeDocumentedAnnotations.isNotEmpty()) {
listOf("@note annotations") + mustBeDocumentedAnnotations.map { " $it" }
} else emptyList()
}
/**
* [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.visibilityComments]
*/
private fun visibilityComments(visibility: Visibility, kind: String): List<String> {
return when (visibility) {
Visibilities.Protected -> listOf("@note This $kind has protected visibility in Kotlin source and is intended only for use by subclasses.")
else -> emptyList()
}
}
/**
* Not implemented [org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportTranslatorImpl.getEffectiveThrows]
*/
private fun getEffectiveThrows(method: KtFunctionLikeSymbol): Sequence<ClassId> {
return emptySequence()
}
internal fun KtCallableSymbol.getSwiftPrivateAttribute(): String? =
if (isRefinedInSwift()) "swift_private" else null
@@ -28,8 +28,7 @@ fun KtClassOrObjectSymbol.translateToObjCProtocol(): ObjCProtocol? {
val members = members().flatMap { it.translateToObjCExportStubs() }
// TODO: Resolve comment
val comment: ObjCComment? = null
val comment: ObjCComment? = annotationsList.translateToObjCComment()
return ObjCProtocolImpl(
name = name.objCName,
@@ -0,0 +1,79 @@
/*
* 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.analysis.api.analyze
import org.jetbrains.kotlin.objcexport.analysisApiUtils.getObjCDocumentedAnnotations
import org.jetbrains.kotlin.objcexport.testUtils.InlineSourceCodeAnalysis
import org.jetbrains.kotlin.objcexport.testUtils.getClassOrFail
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.fail
class GetObjCDocumentedAnnotationsTest(
private val inlineSourceCodeAnalysis: InlineSourceCodeAnalysis,
) {
@Test
fun `test - no annotation present`() {
val file = inlineSourceCodeAnalysis.createKtFile("class Foo")
analyze(file) {
val foo = file.getClassOrFail("Foo")
val objCDocumentedAnnotations = foo.getObjCDocumentedAnnotations()
if (objCDocumentedAnnotations.isNotEmpty())
fail("Expected no 'ObjC Documented Annotation present, Found. $objCDocumentedAnnotations")
}
}
@Test
fun `test - simple Important annotation`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@MustBeDocumented
annotation class ImportantAnnotation
annotation class OtherAnnotation
@ImportantAnnotation @OtherAnnotation
class Foo
""".trimIndent()
)
analyze(file) {
val foo = file.getClassOrFail("Foo")
val objCDocumentedAnnotations = foo.getObjCDocumentedAnnotations()
if (objCDocumentedAnnotations.size != 1)
fail("Expected single documented annotation. Found: $objCDocumentedAnnotations")
val objCDocumentedAnnotation = objCDocumentedAnnotations.single()
assertEquals("ImportantAnnotation", objCDocumentedAnnotation.classId?.shortClassName?.asString())
}
}
@Test
fun `test - special ObjC annotations are not documented for export`() {
val file = inlineSourceCodeAnalysis.createKtFile(
"""
@MustBeDocumented
annotation class ImportantAnnotation
@kotlin.native.ObjCName("ObjCFoo")
@kotlin.native.ShouldRefineInSwift
@Deprecated("Deprecations shall also not be shown")
@ImportantAnnotation
class Foo
""".trimIndent()
)
analyze(file) {
val foo = file.getClassOrFail("Foo")
val objCDocumentedAnnotations = foo.getObjCDocumentedAnnotations()
if (objCDocumentedAnnotations.size != 1)
fail("Expected single documented annotation. Found: $objCDocumentedAnnotations")
val objCDocumentedAnnotation = objCDocumentedAnnotations.single()
assertEquals("ImportantAnnotation", objCDocumentedAnnotation.classId?.shortClassName?.asString())
}
}
}
@@ -0,0 +1,22 @@
/*
* 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.backend.konan.objcexport
import org.jetbrains.kotlin.backend.konan.InternalKotlinNativeApi
@InternalKotlinNativeApi
operator fun ObjCComment?.plus(other: ObjCComment?): ObjCComment? {
val lines = this?.contentLines.orEmpty() + other?.contentLines.orEmpty()
if (lines.isEmpty()) return null
return ObjCComment(lines)
}
@InternalKotlinNativeApi
operator fun ObjCComment?.plus(lines: Iterable<String>): ObjCComment? {
val newLines = this?.contentLines.orEmpty() + lines
if (newLines.isEmpty()) return null
return ObjCComment(newLines)
}
@@ -120,6 +120,11 @@ class ObjCExportHeaderGeneratorTest(val generator: HeaderGenerator) {
doTest(headersTestDataDir.resolve("classWithMustBeDocumentedAnnotation"))
}
@Test
fun `test - interfaceWithMustBeDocumentedAnnotation`() {
doTest(headersTestDataDir.resolve("interfaceWithMustBeDocumentedAnnotation"))
}
@Test
fun `test - functionWithMustBeDocumentedAnnotation`() {
doTest(headersTestDataDir.resolve("functionWithMustBeDocumentedAnnotation"))
@@ -0,0 +1,33 @@
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSError.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"
#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif
/**
* @note annotations
* ImportantAnnotation
* AnotherImportantAnnotation
*/
@protocol Foo
@required
@end
#pragma pop_macro("_Nullable_result")
#pragma clang diagnostic pop
NS_ASSUME_NONNULL_END
@@ -0,0 +1,8 @@
@kotlin.annotation.MustBeDocumented
annotation class ImportantAnnotation
@kotlin.annotation.MustBeDocumented
annotation class AnotherImportantAnnotation
@ImportantAnnotation @AnotherImportantAnnotation
interface Foo